David Rosas — investigación
10 oct 2026·AgentTesla·Ingeniería inversa

Cinco etapas, un PNG y una contraseña en claro.

Un dropper de 604 KB esconde un infostealer tras ConfuserEx, esteganografía en los píxeles de una imagen, .NET Reactor y dos capas de XOR escritas a mano. Cinco etapas hasta el payload, que exfiltra por SMTP con las credenciales dentro del binario.

Muestra ffdc0c9d0453f714bfc0b1d98141c21d Compilada 17 jul 2021 Familia AgentTesla Objetivo Windows · PE32 .NET

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

0 · CorrectionAlgorit.exe dropper VB.NET · 604 KB ConfuserEx: 11.040 u32, XOR con 16 claves xorshift → LZMA → hex invertido → base64 2 · TimeSpan.dll loader · 69.632 B Stego: PNG 354×354, pixeles BGRA XOR semilla ^0x70 + clave «pHw» 3 · SinkProvider.dll stub .NET Reactor · 497.664 B QuickLZ (magic QCLZ) 4 · contenedor de recursos metadata de 524 B, sin codigo · 231.424 B XOR BE16(«OpWegUbUUQ») + XOR autoclavado de 16 B 5 · AgentTesla stealer · 221.696 B
Cada salto usa una tecnica distinta. Las etapas 0 y 3 son las unicas protegidas por un ofuscador comercial; el resto de las transformaciones son del propio autor del crypter. La etapa 4 no contiene codigo: existe solo para alojar recursos que el analisis estatico de la etapa 3 no encontraria.

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.

604 KB · md5 ffdc0c9d0453f714bfc0b1d98141c21d

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.

69.632 B · md5 ecff0ff8f308ad9a7ab4e8d8c6632f1f

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.

497.664 B · md5 56f0eb08635393c5052221a18b8e4488

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.

231.424 B · md5 f3553fc66fa0655005350c4e91e447b5

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.

221.696 B · md5 7a5999bf2197cd4e208e32e67e04bead

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.

ElementoSignificadoEjemplo
La cadena completaInvertida, con StrReverse…15 17 65 45 → 54 56 71 51
EspacioSeparador entre pares hexadecimales54 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.

1. recorrido por COLUMNAS 354 × 354 RGBA 2. cada pixel → 4 bytes Color.ToArgb() → int32 BitConverter.GetBytes, little-endian B G R A el orden ARGB se invierte al serializar 3. el flujo resultante 4 B longitud 497.665 B de datos cifrados relleno se descartan los 4 primeros bytes; la longitud sale de ahi 4. salida[i] = datos[i] ⊕ (datos[ultimo] ⊕ 0x70) ⊕ clave[i mod 6] PE etapa 3
El prefijo de longitud ocupa el primer pixel, de modo que el payload empieza en el segundo. La clave de 6 bytes es "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íaQué busca
HipervisoresRegistro 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
SandboxesSbieDll.dll de Sandboxie, y nombres de usuario señuelo: SANDBOX, VIRUS, MALWARE, SCHMIDTI, SAMPLE, C:\file.exe
WineGetProcAddress(GetModuleHandle("kernel32.dll"), "wine_get_unix_file_name") — si ese export existe, no estás en Windows
Defenderpowershell Add-MpPreference -ExclusionPath sobre su propia ruta
InyecciónZwUnmapViewOfSection, 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:

ElementoValorImplicación
LogonTrigger[USERID]Arranca en cada inicio de sesión
RunLevelLeastPrivilegeNo pide UAC; funciona como usuario estándar
ExecutionTimeLimitPT0SSin límite de ejecución
AllowHardTerminatefalseResiste la terminación por el planificador
HiddenfalseVisible en el Programador de tareas — detección gratis
Date2014-10-25Fecha 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.

RECOLECTORES PW · contrasenas 26 navegadores + 39 apps KL · pulsaciones hook WH_KEYBOARD_LL CO · cookies Chromium + 12 Gecko SC · captura pantalla completa, JPEG informe HTML cabecera: equipo, usuario, CPU, RAM uno por informe temporizador de 30 s activo CANALES SMTP mail.scottbyscott.com : 587 EnableSsl = false · sin TLS a harrisonjeff401@gmail.com HTTP POST sobre Tor cuerpo p=<datos>, 3DES de .C .j puente HTTP→SOCKS, IPAddress.Any destino %PostURL% — sin sustituir compilado, pero inactivo
Los cuatro informes se envian por separado, cada uno con su propio correo y su etiqueta de asunto. El canal alternativo esta completamente implementado — incluido un proxy HTTP a SOCKS propio, porque 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.

ComponenteQué es
parser SQLiteRecorre 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 DER895 instrucciones de decodificación TLV recursiva de ASN.1, sin una sola API externa
Berkeley DBPara key3.db, el formato que Firefox usaba antes de key4.db
AES-GCMVía BCrypt, para los valores v10/v11 de Chromium moderno
3DES por reflexiónConfigura 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
keyloggerWH_KEYBOARD_LL: captura también Alt y teclas de sistema, con el título de la ventana en primer plano
portapapelesCadena de visores clásica, con SetClipboardViewer
proxy HTTP→SOCKSPorque 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

CHROMIUM — bifurca por el prefijo del valor Local State encrypted_key → DPAPI clave Login Data SQLite propio (.h) password_value v10/v11 sin prefijo BCrypt AES-GCM (.i) Chromium ≥ 80 DPAPI directo anterior a v80 texto claro FIREFOX / NSS — bifurca por el identificador de objeto del DER key4.db SQLite propio (.h) key3.db Berkeley DB (.p) parser DER .Q (895 insns) modelo .q .s · 3DES por reflexion OID …1.12.5.1.3 · legado .r · AES-256-CBC OID …2.9 + PBKDF2 · moderno texto claro Las dos rutas de cada familia estan implementadas: el payload descifra credenciales tanto de instalaciones actuales como de versiones de hace una decada. Resaltada en color, la ruta que aplica a un equipo actualizado.
Ninguna de las dos rutas depende de bibliotecas del sistema: el payload trae su propio lector de SQLite, su propio parser de Berkeley DB y su propio decodificador ASN.1. Por eso no aparece ninguna referencia a 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íaAplicacionesTécnica predominante
CorreoOutlook, Foxmail, Opera Mail, eM Client, Mailbird, Becky!, The Bat!, IncrediMail, PocoMail, Eudora, Claws Mail, Thunderbird, Postbox, SeaMonkeyRegistro y DPAPI; 3DES con alfabetos de sustitución propios en The Bat! y Opera Mail; PBKDF2 con clave fija en eM Client
FTP y SFTPFileZilla, WinSCP, SmartFTP, FlashFXP, WS_FTP, FTP Navigator, FTP Commander, FTPGetter, CoreFTPXML, INI y deobfuscación hexadecimal propia en WinSCP
VPNNordVPN, OpenVPN GUI, Private Internet AccessDPAPI, con entropía adicional en OpenVPN
MensajeríaPidgin, Trillian, Psi/Psi+, PaltalkXML en claro y DPAPI
Escritorio remoto y BDWindows RDP, RealVNC 3.x y 4.x, TightVNC, MySQL WorkbenchRegistro y DPAPI sobre blobs de credenciales
OtrosJDownloader, DynDNS Updater, NO-IP, Windows Credential ManagerClaves 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ónEstado
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 informemuerta 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 POSTinactiva 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:

ReglaAnclaEtapas
Xor70_PrimitiveIL 1F 70 61, la primitiva compartida. Exactamente una aparición en cada etapa que la usa2, 3
Dropper_ConfuserEx_StegoNombre del recurso PNG, más de 50 apariciones de U+200B en UTF-8 y el señuelo0
Loader_IExpandoNamespace más las piezas del truco Pico2012 → Invoke2
Stub_NETReactorCentinelas de .NET Reactor, el delegado nativo y el señuelo3
Payload_XorStringTableIL del bucle tabla[i] ^= i ^ K, con tokens y constante comodinados5
CampaignNombres de recurso aleatorios de esta build concreta3, 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.