Clic, bum, y tu red queda comprometida. Un hacker solo necesita un exploit exitoso para que tengas un día muy malo. Hace poco, descubrimos un artefacto que queremos analizar y mostrar. Aquí nos meteremos de lleno en los detalles técnicos, así que ponte tus protectores auditivos y vamos a recorrer el campo de tiro.
Nota del editor: esta publicación se publicó originalmente en Threatpost.
La prueba irrefutable
Hace poco, el equipo del SOC de Huntress descubrió un artefacto de malware que quiero analizar y mostrar.
Aunque a primera vista parece un galimatías, podemos desarmarlo y entender qué sucede realmente. Vamos a avanzar por el código de forma ordenada, tomando una línea a la vez y entendiendo la sintaxis.
Lo primero que hay que notar es que esto tomó la forma de un script "batch" de Windows, un archivo con extensión .bat. Los scripts batch son interpretados y ejecutados por el símbolo del sistema de Windows, o el programa "cmd.exe". cmd.exe es el intérprete de línea de comandos predeterminado para los sistemas operativos Windows, pero es una utilidad antigua que se remonta a DOS (o Disk Operating System). En el mundo en el que vivimos ahora, los desarrolladores y profesionales de seguridad prefieren trabajar en PowerShell, un shell y lenguaje de línea de comandos mucho más moderno.
PowerShell se presentará aquí en un momento, pero primero tenemos que hablar de las diferencias de sintaxis. Las variables en PowerShell se denotan con la sintaxis "$varname", con el nombre de la variable precedido por un signo de dólar. En los scripts batch de cmd.exe, las variables se indican como %varname%, con el nombre de la variable rodeado de signos de porcentaje a ambos lados. En este caso, vemos que se hace referencia a una variable de entorno, %COMSPEC%. Su valor es:
C:\Windows\System32\cmd.exe
Ese valor se colocará donde está la sintaxis %COMSPEC%. Al ejecutarse, iniciará cmd.exe con los parámetros y argumentos que siguen. En nuestra analogía "armada", podemos llamar a estas primeras piezas del payload, el gatillo.
El gatillo
El argumento /b de cmd.exe significa "iniciar la aplicación sin crear una nueva ventana", así que nuestro hacker está tratando de ocultarse. /c significa "ejecutar un solo comando y salir", lo que explica que el resto de este código realmente se ejecutará.
El comando start que sigue lanzará un nuevo programa, de nuevo con el /b para asegurar que no se cree ninguna ventana. El argumento /min parece agregarse solo como medida extra: la aplicación se iniciaría minimizada (si, por alguna razón, se llegara a crear una ventana con el argumento /b).
A continuación, vemos que powershell.exe es la aplicación que se inicia. También incluye varios argumentos, como -nop (no cargar con un perfil de inicio), -w hidden (de nuevo, no crear una ventana), -noni (no ejecutar en modo interactivo) y finalmente -c (ejecutar un solo comando y salir).
Llegados a este punto, finalmente entramos a la cadena de código que se pasa a PowerShell. Esto realiza algunas verificaciones para asegurar que el payload usado para el objetivo sea el adecuado.
La mira
Al comienzo mismo de la sintaxis de PowerShell, vemos:
Este condicional if es interesante porque verifica si el tamaño del tipo de dato "integer pointer" es igual al número 4. Esto puede parecer una verificación aleatoria, pero en realidad es un método ingenioso para determinar la arquitectura del sistema objetivo. Una computadora de 64 bits tendría un tamaño de IntPtr de 8, refiriéndose a la longitud de las direcciones de memoria. Un sistema de 32 bits tendría un tamaño de IntPtr de 4, así que el código determina la ruta de PowerShell según la arquitectura.
La variable $b se crea como una variable de PowerShell para contener la ruta del ejecutable de PowerShell.
Justo después de ese condicional if, vemos el siguiente fragmento de código:
Esto crea otra variable de PowerShell, $s, esta vez definida como un nuevo objeto. En este caso, el objeto creado es un nuevo proceso, con el nombre de archivo configurado como $b (que ahora sabemos que es la ruta de PowerShell) con argumentos como los que ya hemos visto. De nuevo, estamos generando otra instancia de PowerShell, sin perfil y con una ventana oculta.
La bala
Para el comando ejecutado por la nueva instancia de PowerShell, la más interna, vemos esta sintaxis:
La llamada [scriptblock]::create define el nuevo código a ejecutar. El New-Object IO.StreamReader nos permite leer el código "sobre la marcha", extraído de los datos pasados. Los datos que vemos están envueltos en estas funciones: IO.Compression.GzipStream, IO.MemoryStream y [Convert]::FromBase64String, con GzipStream usando una bandera Decompress.
Esto indica que el gran bloque de caracteres que parece un galimatías sin sentido en realidad son datos GZIP codificados en Base64.
Base64 es un esquema de codificación que simplemente representa los datos en un formato diferente. Decodificar los datos es trivial: solo hay que hacer la operación inversa. Los datos GZIP están comprimidos, archivados, prácticamente igual que un archivo .ZIP que podrías ver en tu computadora. Por suerte, podemos realizar la operación inversa en ese gran bloque de datos para entender mejor qué está haciendo.
Pero primero, terminemos el análisis del resto del código.
El silenciador y el tirador
Justo después del bloque de Base64, vemos estas líneas de código:
Le llamo en broma a este segmento "el silenciador" porque, de nuevo, trata de enmascarar y ocultar la nueva instancia de PowerShell. Ese $s es nuestro nuevo proceso, con valores de configuración definidos para ocultar la ventana, no crear la ventana, y no llevar registro de la salida estándar ni invocar un nuevo shell.
Y, por supuesto, justo después de este fragmento vemos lo que realmente dispara el arma.
$p=[System.Diagnostics.Process]::Start($s)
Esta línea iniciará nuestro nuevo proceso y se ejecutará el código decodificado y descomprimido dentro del bloque Base64. Ahora que entendemos mejor cómo funciona esto, podemos enfocarnos en ese bloque de datos.
Dentro de la munición
La verdadera sustancia de este lanzador proviene del bloque codificado en Base64 y comprimido en GZIP que se extrae y ejecuta sobre la marcha. Ese es este fragmento:
Podemos realizar las operaciones inversas con cualquier herramienta que queramos, ya sea en la línea de comandos, en Python, o incluso con CyberChef.
Por conveniencia, podemos hacer esto con CyberChef.
Esto devuelve, como era de esperarse, más código de PowerShell. Como ya sabemos, esto será ejecutado por el launcher. El volcado de salida se ve así:
Evidentemente, hay mucho que analizar aquí. Este código de PowerShell es al menos algo legible, ya que tiene saltos de línea y espacios claros, pero los nombres de variables y parte de la lógica siguen ofuscados. Lo entenderemos pieza por pieza.
Examinando la pólvora
La primera función que vemos definida en este código de PowerShell se llama sOH, lo cual no es muy descriptivo. Todos estos nombres de funciones y variables parecen ser aleatorios y ofuscados, pero podemos entenderlos leyendo la definición de la función.
La función soH toma dos parámetros. Usa una técnica para buscar “reflexivamente” la dirección de llamadas a la API de Win32, de modo que PowerShell tenga la capacidad de ejecutar estos procedimientos internos y fundamentales conocidos por los sistemas operativos de más bajo nivel. En el contexto actual, busca dónde podría estar cargado System.dll y lo usa para encontrar el nombre de una función deseada dentro de otras DLL que luego podría ejecutar. El nombre de la DLL a la que pertenece esta función, y la función de la API de Win32 que debe llamarse, son los dos valores que se pasan como parámetros a esta función sOH. Todo esto se hace mediante “reflexión”, la capacidad que le permite a PowerShell hacer introspección y buscar procedimientos ya definidos.
En definitiva, esto le da a PowerShell mucho más poder. Obtener acceso para ejecutar las funciones de la API de Win32 le permite hacer cosas como asignar memoria, copiar y mover memoria, u otras cosas particulares que veremos en el código muy pronto.
Para nuestro propio entendimiento, deberíamos mentalmente renombrar esta función a algo como:
Hasta ahora, lo que conocíamos como la función sOH agrega una parte de esta nueva capacidad. Si los hackers quieren usar esta técnica para invocar llamadas a funciones de la API de Win32 dentro de PowerShell, también necesitan la funcionalidad para trabajar con “delegados”. La siguiente función, b9MW, completa el código “repetitivo” necesario para poder hacer esto.
Como puedes ver, esto está repleto de necesidades internas de Windows y detalles adicionales que hacen que esto funcione. No haremos un análisis muy profundo de este código, explicando cada línea y variable, pero esta función ahora brinda la funcionalidad para interpretar parámetros de funciones de la API de Win32 y valores de retorno.
Como nuestro hacker está construyendo la funcionalidad para poder llamar funciones de la API de Win32 con PowerShell, necesitaba este procedimiento sOH para poder encontrar y localizar las funciones, y este procedimiento b9MW para suministrar parámetros y entender los valores de retorno de las funciones.
Con estas dos funciones en su lugar, el código ahora tiene los elementos primitivos para llamar libremente a cualquier función de la API de Win32 que desee. A continuación, veremos esto en acción.
El explosivo
Después de esas definiciones de funciones, este fragmento de PowerShell define un arreglo de bytes, extraído al decodificar más Base64 codificado.
Decodificar este Base64 lamentablemente nos da muchos caracteres no imprimibles.
Podemos llegar a decir que esto es shellcode, o instrucciones de procesador como opcodes que se ejecutarán. Como esto son datos binarios, no podemos entenderlos rápidamente, pero sabemos que este malware termina usando shellcode.
Justo debajo de esto vemos:
Ahora entra en juego una variable $sC6US, que llama a la función GetDelegateForFunctionPointer, con nuestras funciones sOH y b9MW recién definidas. Recuerda, estas funciones le permitieron al hacker cargar funciones de la API de Win32, y en este caso, podemos ver que extrajeron la función VirtualAlloc.
Esta función VirtualAlloc le indica al sistema operativo que asigne memoria. Como podemos ver en los parámetros de la función, invoca esta función para asignar suficiente memoria para la longitud del arreglo de bytes $bUMJ (¡el shellcode!). El 0x3000 indica “reservar y confirmar esta memoria”, y el 0x40 indica “esta memoria debe ser legible, escribible y ejecutable”.
En este punto, el espacio de memoria asignado se almacena en esa variable $sC6US. Luego, vemos una función Copy llamada para llenar ese espacio de memoria con el arreglo de bytes del shellcode, $bUMJ.
El script malicioso ya asignó memoria para el shellcode, y podemos adivinar fácilmente qué harán a continuación. Ejecutar el shellcode.
A continuación, se crea una variable $t6Y, que de nuevo recurre a una llamada a la API de Win32, esta vez específicamente CreateThread. Esta llamada a CreateThread se invoca con la dirección de memoria $sC6US, que, como ya sabemos, contiene el shellcode. En última instancia, esto ejecuta el shellcode.
Después de eso, vemos una llamada más a la función de la API de Win32 WaitForSingleObject. Esto “bloqueará” la ejecución y esperará pacientemente a que el shellcode termine de ejecutarse. Puedes ver que incluye la variable $t6Y (que es el nuevo hilo que ejecuta el shellcode) y el 0xFFFFFFFF indica “esperar indefinidamente”.
Finalmente, después de todas estas capas anidadas, la ofuscación y las abstracciones, el malware cargó el shellcode en la memoria y lo ejecutó. La siguiente pregunta es: ¿qué hace exactamente este shellcode?
Como analistas de seguridad, todavía tenemos trabajo por hacer. Podemos monitorear el comportamiento de este malware y observar si crea nuevos archivos o se comunica con algún otro endpoint externo. El shellcode en sí parece muy pequeño, por lo que tal vez sea un stub para cargar más malware. Aunque este artículo se centró únicamente en entender el lanzador de PowerShell, tal vez el próximo analice el shellcode dentro de un depurador como scdbg u observe el malware ejecutándose en un sandbox contenido.
Nos adentramos en los detalles técnicos para entender mejor qué hicieron los hackers y cómo funcionó su payload. Aprender de la ofensiva es la mejor manera de tener una defensa más sólida. Algunas tácticas de mitigación, como habilitar AppLocker o el modo de lenguaje restringido de PowerShell, al menos bloquearían la ejecución de este lanzador inicial, y los hackers tendrían que esforzarse más. Al final del día, ese es nuestro objetivo: hacer que los hackers se ganen cada centímetro de su acceso.
¿Quieres profundizar en los detalles técnicos y ponerte un poco turbio con nosotros? Únete a nosotros en Tradecraft Tuesday para escuchar análisis de amenazas en vivo y comentarios de nuestro equipo de expertos en ciberseguridad.