El resumen y el C2
La muestra es un PE32 .NET, no código nativo: todo lo que sigue ocurre en IL y en metadatos CLR, no en instrucciones x86. Oculta un AgentTesla a través de cinco etapas, y cada salto usa una técnica distinta: constantes LZMA de ConfuserEx, un encoding hexadecimal invertido hecho a mano, esteganografía en los píxeles de un PNG, .NET Reactor con aplanamiento de flujo, QuickLZ y un XOR posicional.
El payload final exfiltra por SMTP, con las credenciales dentro del binario. Recolecta contraseñas de 26 navegadores Chromium y 39 aplicaciones más, registra pulsaciones de teclado, vigila el portapapeles y captura la pantalla.
Configuración de mando y control
- Servidor SMTP
- mail.scottbyscott.com
- Puerto
- 587 EnableSsl = false
- Usuario
- ideshow@eflownutrition.com
- Contraseña
- ngo•••••• enmascarada
- Destinatario
- harrisonjeff401@gmail.com
La contraseña va enmascarada a propósito. eflownutrition.com aparenta ser una
empresa legítima cuyo buzón se está usando como relay, así que publicar la credencial completa
expondría esa cuenta a cualquiera que lea esto. El resto son indicadores de publicación habitual.
Esa elección de canal no es casual: en esta familia SMTP domina precisamente porque al operador le basta una sola cuenta de correo comprometida. No necesita infraestructura propia, ni dominio, ni servidor que mantener.
La cadena de cinco etapas
Dropper — CorrectionAlgorit.exe
VB.NET protegido con ConfuserEx. Se camufla como un proyecto escolar llamado «SpellingBee», con 13 formularios WinForms funcionales. Diez clases con nombres hechos de caracteres Unicode invisibles implementan el range decoder de LZMA.
Las rutas internas delatan el origen del proyecto reutilizado:
\\file-home1\TomOye161$\Computing\Year 2\Spelling Bee\. Alguien robó el
trabajo de un estudiante para usarlo de envoltorio.
Se dispara desde Admin_page::InitializeComponent, al construir el formulario.
Loader — TimeSpan.dll
Escondido entre clases del tema WinForms open-source «Pico2012». Duerme entre 39 y 61 segundos antes de actuar, para agotar los tiempos de análisis de sandbox.
Evasión de firmas por troceado de cadenas:
"Pico2012".Replace("Pico","Inv").Replace("2012","oke") produce
"Invoke" en tiempo de ejecución, así que el nombre nunca aparece en el binario.
Stub — SinkProvider.dll
Protegido con .NET Reactor. Trae anti-VM, anti-Wine, evasión de Defender, persistencia y el inyector. No es el stealer: es el envoltorio del crypter.
Camuflado con la librería FastColoredTextBox, cuyos regex de T-SQL y HTML
componen la mayoría de sus 237 cadenas cifradas.
Contenedor de recursos
Un assembly .NET con metadata de 524 bytes y sin un solo método: existe únicamente para alojar recursos. El nombre de assembly es un GUID.
El truco: los recursos de la etapa anterior se mueven a un assembly aparte que se carga en
memoria con Assembly.Load, de modo que el análisis estático del stub no los
encuentra. Uno de ellos se llama SinkProvider.g.resources y delata la relación.
AgentTesla payload
Ejecutable con punto de entrada, 84 tipos y 1.347 métodos. Referencia
System.Management, System.Security y System.Xml.
Aquí vive todo: credenciales, keylogger, portapapeles, capturas y exfiltración.
Las capas de ofuscación
Constantes de ConfuserEx
El dropper guarda sus cadenas en un array embebido de 11.040 enteros de 32 bits. Un PRNG xorshift
sembrado con -794857484 genera 16 claves que se aplican por bloques, evolucionando tras
cada uno. El resultado se descomprime con LZMA.
x ^= x >>> 12; x ^= x << 25; x ^= x >>> 27 // 16 claves
for (i = 0; i < 11040; i += 16) {
for (j = 0; j < 16; j++) {
v = data[i+j] ^ key[j];
escribir_le32(salida, v);
key[j] ^= v; // la clave evoluciona
}
}
salida = LZMA_descomprimir(salida) // 215.920 B de cadenas
Un encoding hexadecimal inventado
Dentro de esas constantes hay una cadena de 212.461 caracteres con un alfabeto de solo 19
símbolos: los 16 dígitos hexadecimales más el espacio, el guion y <. Es hexadecimal
leído al revés, separado por espacios, donde los dos símbolos extra comprimen las tiradas de bytes
nulos.
| Elemento | Significado | Ejemplo |
|---|---|---|
| La cadena completa | Invertida, con StrReverse | …15 17 65 45 → 54 56 71 51 |
| Espacio | Separador entre pares hexadecimales | 54 56 71 51 = "TVqQ" |
- | Una A de base64, es decir un byte nulo | --4D → AAM |
< | Tres A de base64 | <<<51 → AAAAAAAAAQ |
El reparto se confirma contra el header DOS canónico de un binario .NET, que es fijo y conocido: los 92.844 caracteres base64 resultantes decodifican a la etapa 2.
Esteganografía en los píxeles
La etapa 3 viaja dentro de un PNG de 354×354 guardado como recurso Bitmap bajo el
nombre TaskSchedulerAwaitTaskContinuati.
"pHw" en UTF-16, y la semilla sale del ultimo byte del propio bloque: sin ese byte no se puede descifrar nada.El keystream tiene periodo 6: 9a ea 9a d2 9a ed. Contra el header DOS conocido sale
la clave de inmediato.
.NET Reactor
La etapa 3 cifra sus 237 cadenas con AES, indexadas por desplazamiento, mediante un descifrador
de 3.517 instrucciones IL aplanadas en una máquina de estados con unos 370 destinos
y predicados opacos. Dos métodos auxiliares resultaron ser constantes: uno devuelve siempre
1 y el otro siempre 0, lo que explica 61 de las ramificaciones.
Los stubs nativos de NecroBit suman unos 70 bytes en total: dos trampolines x86 y x64 del tipo
mov rax, imm64; jmp rax. No protegen al descifrador de cadenas, y eso fue determinante
para resolverlo estáticamente.
QuickLZ, y dos XOR hechos a mano
El paso a la etapa 4 usa un descompresor propio con magic QCLZ escrito con
aritmética de punteros. El último salto no comprime nada: son dos capas de XOR
encadenadas, verificadas contra el binario real.
// iNi: XOR con semilla derivada del ultimo byte + clave repetida
clave = Encoding.BigEndianUnicode("OpWegUbUUQ") // cicla sobre 10 caracteres
k1 = d[ultimo] ^ 0x70
out[i] = d[i] ^ k1 ^ clave[i % 10] // se descarta el ultimo byte
// ONb: XOR autoclavado -- la cabecera es su propia clave
k = d[0:16]
out[i] = d[16+i] ^ k[i % 16]
// 221.713 B -> iNi -> 221.712 B -> ONb -> 221.696 B = AgentTesla
Y aquí está el detalle que más dice del autor: iNi es la misma primitiva
que descifra el stego del PNG dos etapas antes. Idéntica estructura —semilla ^ 0x70
sobre el último byte, clave repetida sobre la longitud de la cadena—, cambiando únicamente el
encoding con que la clave pasa a bytes. Es la misma rutina reutilizada, y explica por qué la
constante 0x70 reaparece en etapas distintas. Como firma de familia es excelente.
Qué hace el stub
Antes de soltar el payload, la etapa 3 comprueba dónde está.
| Categoría | Qué busca |
|---|---|
| Hipervisores | Registro de VirtualBox Guest Additions y VMware Tools, DEVICEMAP\Scsi, QEMU, y por WMI SELECT * FROM Win32_VideoController contra VirtualBox Graphics Adapter, VMware SVGA II y S3 Trio32/64 |
| Sandboxes | SbieDll.dll de Sandboxie, y nombres de usuario señuelo: SANDBOX, VIRUS, MALWARE, SCHMIDTI, SAMPLE, C:\file.exe |
| Wine | GetProcAddress(GetModuleHandle("kernel32.dll"), "wine_get_unix_file_name") — si ese export existe, no estás en Windows |
| Defender | powershell Add-MpPreference -ExclusionPath sobre su propia ruta |
| Inyección | ZwUnmapViewOfSection, VirtualAllocEx, WriteProcessMemory, CreateProcessA, Get/SetThreadContext y variantes Wow64, en MSBuild.exe, vbc.exe o RegSvcs.exe — binarios firmados del .NET Framework |
La persistencia, observada en ejecución
Con los valores reales de configuración, la secuencia que ejecuta es:
GetFolderPath(ApplicationData) -> %APPDATA%
File.Exists('%APPDATA%\GIxVTvvWqR.exe') -> si no existe:
DirectorySecurity.SetAccessRuleProtection(...) corta la herencia de ACL
FileSystemSecurity.AddAccessRule(...) x9 ACL endurecida
FileSystemInfo.set_Attributes(...) x2 oculto / sistema
File.Copy(muestra.exe -> %APPDATA%\GIxVTvvWqR.exe) autocopia
WindowsIdentity.GetCurrent().Name usuario para el XML
Path.GetTempFileName() + String.Replace x2 sustituye [LOCATION] y [USERID]
File.WriteAllText(temp) escribe el XML de la tarea
Process.Start('schtasks.exe', '/Create /TN "Updates\..." /XML "temp"')
File.Delete(temp) borra el XML
La plantilla del XML no estaba en las cadenas: vive en un recurso del contenedor de la etapa 4, que mi primer extractor se había saltado por tamaño. Sus parámetros dicen bastante:
| Elemento | Valor | Implicación |
|---|---|---|
| LogonTrigger | [USERID] | Arranca en cada inicio de sesión |
| RunLevel | LeastPrivilege | No pide UAC; funciona como usuario estándar |
| ExecutionTimeLimit | PT0S | Sin límite de ejecución |
| AllowHardTerminate | false | Resiste la terminación por el planificador |
| Hidden | false | Visible en el Programador de tareas — detección gratis |
| Date | 2014-10-25 | Fecha falsa, señuelo |
Qué roba, y cómo
El punto de entrada fija TLS 1.0–1.3, construye el identificador de víctima a partir del nombre de usuario y del equipo, y arma un temporizador de 30 segundos. El constructor estático instala el hook de teclado antes que nada.
HttpWebRequest no habla SOCKS — pero su URL de destino quedo como marcador sin sustituir. Que el canal activo sea SMTP encaja con lo
documentado de la familia: al operador le basta una sola cuenta de correo comprometida, y es la razon
por la que ese canal domina.La infraestructura que trae dentro
Lo que más llama la atención del payload no es lo que roba, sino que no depende de nada del sistema para hacerlo.
| Componente | Qué es |
|---|---|
| parser SQLite | Recorre páginas B-tree a mano, 1.301 instrucciones. Sin System.Data.SQLite: no necesita DLL nativa y elude la firma más evidente de un ladrón de credenciales de navegador |
| parser DER | 895 instrucciones de decodificación TLV recursiva de ASN.1, sin una sola API externa |
| Berkeley DB | Para key3.db, el formato que Firefox usaba antes de key4.db |
| AES-GCM | Vía BCrypt, para los valores v10/v11 de Chromium moderno |
| 3DES por reflexión | Configura Key, Mode, IV y Padding con LateSet, por nombre. El IL nunca referencia estáticamente las APIs de criptografía: no aparecen en la tabla de referencias del binario |
| keylogger | WH_KEYBOARD_LL: captura también Alt y teclas de sistema, con el título de la ventana en primer plano |
| portapapeles | Cadena de visores clásica, con SetClipboardViewer |
| proxy HTTP→SOCKS | Porque HttpWebRequest no habla SOCKS y el canal Tor lo necesita |
Ese 3DES por reflexión me resolvió un cabo suelto que llevaba horas arrastrando: unas cadenas
sueltas —Key, Mode, IV, Padding— que no
encajaban en ningún sitio. No eran restos: son los nombres con los que invoca la criptografía para
no declararla.
Las dos rutas de descifrado
nss3.dll ni a System.Data.SQLite, que serian las firmas mas evidentes de un ladron de credenciales de navegador.Los objetivos
Navegadores Chromium, 26 enumerados: Opera, Yandex, Iridium, Chromium, 7Star, Torch, Cool Novo, Kometa, Amigo, Brave, CentBrowser, Chedot, Orbitum, Sputnik, Comodo Dragon, Vivaldi, Citrio, 360, Uran, Liebao, Elements, Epic Privacy, Coccoc, Sleipnir 6, QIP Surf y Coowon. UC Browser, Edge y QQ tienen módulo propio.
Safari para Windows merece mención aparte: no implementa un parser de plist
binario, sino que invoca plutil.exe de Apple —un binario firmado de terceros— para
convertir el keychain a XML antes de parsearlo. Living-off-the-land con herramienta ajena.
Cookies tienen lista propia y más amplia, con 12 navegadores Gecko vía
cookies.sqlite. K-Meleon, WaterFox, BlackHawk y CyberFox solo se tocan para cookies, no
para credenciales.
Y 39 módulos de aplicación, cada uno aislado en un On Error de
Visual Basic para que el fallo de uno no interrumpa la cadena:
| Categoría | Aplicaciones | Técnica predominante |
|---|---|---|
| Correo | Outlook, Foxmail, Opera Mail, eM Client, Mailbird, Becky!, The Bat!, IncrediMail, PocoMail, Eudora, Claws Mail, Thunderbird, Postbox, SeaMonkey | Registro y DPAPI; 3DES con alfabetos de sustitución propios en The Bat! y Opera Mail; PBKDF2 con clave fija en eM Client |
| FTP y SFTP | FileZilla, WinSCP, SmartFTP, FlashFXP, WS_FTP, FTP Navigator, FTP Commander, FTPGetter, CoreFTP | XML, INI y deobfuscación hexadecimal propia en WinSCP |
| VPN | NordVPN, OpenVPN GUI, Private Internet Access | DPAPI, con entropía adicional en OpenVPN |
| Mensajería | Pidgin, Trillian, Psi/Psi+, Paltalk | XML en claro y DPAPI |
| Escritorio remoto y BD | Windows RDP, RealVNC 3.x y 4.x, TightVNC, MySQL Workbench | Registro y DPAPI sobre blobs de credenciales |
| Otros | JDownloader, DynDNS Updater, NO-IP, Windows Credential Manager | Claves fijas embebidas y vaultcli.dll |
Lo que venía apagado
Varias capacidades están compiladas pero inertes. Lo interesante es que se puede demostrar: en los dos casos el interruptor tiene un único escritor —el constructor estático, que lo pone a cero— y los lectores lo usan como guarda.
| Función | Estado |
|---|---|
Segunda persistencia, con %startupfolder%\%insfolder%\%insname% | muerta El punto de entrada salta el bloque entero. Esos marcadores nunca se alcanzan, así que la única persistencia real es la del stub |
Línea IP Address: del informe | muerta Bloquea tanto la consulta como la emisión de la línea. Y aunque estuviera activa, el obtenedor usa WebRequest.Create('%'): el marcador sin sustituir |
| Exfiltración por Tor sobre HTTP POST | inactiva Completamente implementada, proxy incluido, pero su URL de destino quedó como plantilla |
Las tres comparten causa: esta compilación salió del constructor con varias opciones sin configurar. Es un recordatorio útil de que «el malware tiene la capacidad X» y «el malware ejecuta X» son afirmaciones distintas, y que la diferencia a veces son dos bytes en un constructor estático.
Detección
Seis reglas YARA, de lo más genérico a lo más específico, ancladas en bytes verificados sobre los binarios y no en conjeturas:
| Regla | Ancla | Etapas |
|---|---|---|
| Xor70_Primitive | IL 1F 70 61, la primitiva compartida. Exactamente una aparición en cada etapa que la usa | 2, 3 |
| Dropper_ConfuserEx_Stego | Nombre del recurso PNG, más de 50 apariciones de U+200B en UTF-8 y el señuelo | 0 |
| Loader_IExpando | Namespace más las piezas del truco Pico2012 → Invoke | 2 |
| Stub_NETReactor | Centinelas de .NET Reactor, el delegado nativo y el señuelo | 3 |
| Payload_XorStringTable | IL del bucle tabla[i] ^= i ^ K, con tokens y constante comodinados | 5 |
| Campaign | Nombres de recurso aleatorios de esta build concreta | 3, 4 |
Las cinco etapas quedan cubiertas, y los controles negativos —el PNG, el blob cifrado, el XML, binarios nativos— no disparan ninguna.
Limitación honesta: no las probé contra un corpus de binarios .NET benignos, así que el riesgo de falso positivo está estimado por construcción y no medido. La regla de la primitiva compartida son tres bytes de IL: sirve como señal de apoyo, nunca sola. Y la etapa 5 no tiene ni una cadena en claro, de modo que su detección depende por completo del patrón de IL.
Lo que no pude cerrar
Dos cosas, y ninguna es un detalle.
Nada de esto se observó en un Windows real. Extraje los bytes de las cinco etapas y cada una es un PE estructuralmente válido, y la configuración SMTP sale del propio código que la consume: esa parte es evidencia fuerte. Pero la inyección, la tarea programada, la exclusión de Defender y el tráfico SMTP los leí en el código, no los vi ocurrir. La distinción importa.
El pivoting no se hizo, no salió negativo. Sin acceso a una API de inteligencia de amenazas, me limité a búsqueda web, que no indexa ese tipo de bases. No hubo aciertos ni para el hash ni para el dominio, pero eso no significa «sin actividad conocida»: significa «no consultado». Un pivoting real necesitaría VirusTotal o MalwareBazaar, DNS pasivo sobre el dominio, y correlación por nombres de recurso contra un repositorio de muestras.
Queda también un bloque opaco de unas 900 instrucciones sin una sola cadena, y ocho cadenas de la etapa 3 cuyo desplazamiento se calcula en ejecución.