Stadeo es un conjunto de herramientas desarrolladas principalmente para facilitar el análisis de Stantinko, una botnet que realiza fraude de clics, inyección de anuncios, fraude en redes sociales, ataques de robo de contraseñas y criptominería.

Stadeo se presentó por primera vez en Black Hat USA 2020 y luego se publicó para uso de forma gratuita.

Los scripts, escritos completamente en Python, se ocupan de las técnicas de ofuscación de strings y de aplanamiento de control de flujo (CFF, por sus siglas en inglés) de Stantinko descritas en nuestra publicación de marzo de 2020. Además, se pueden utilizar para otros fines: por ejemplo, ya hemos ampliado nuestro enfoque para soportar la desofuscación del CFF presentado en Emotet –un troyano que roba credenciales bancarias y descarga payloads adicionales como ransomware.

Nuestros métodos de desofuscación utilizan IDA, que es una herramienta estándar en la industria, y Miasm, un framework de código abierto que nos proporciona varios análisis de flujo de datos, un motor de ejecución simbólica, un motor de ejecución simbólica dinámica y los medios para reensamblar funciones modificadas.

Puedes encontrar Stadeo aquí.

Ejemplos de cómo usarlo

Para trabajar con Stadeo, primero necesitamos configurar un servidor RPyC (Remote Python Call) dentro de IDA, que nos permite acceder a la API de IDA desde un intérprete de Python arbitrario. Puedes usar este script para abrir un servidor RPyC en IDA.

En todos los ejemplos que siguen configuramos un servidor RPyC que escucha en 10.1.40.164:4455 (siendo 4455 el puerto predeterminado) y luego nos comunicamos con el servidor desde una consola Python.

Usaremos dos clases de Stadeo:

  • CFFStrategies para la desofuscación CFF
  • StringRevealer para la desofuscación de strings

Ambas clases se pueden inicializar tanto para una arquitectura de 32 como de 64 bits.

Desofuscando una sola función

SHA-1 de la muestra: 791ad58d9bb66ea08465aad4ea968656c81d0b8e

El siguiente código desofusca la función en 0x1800158B0 con el parámetro en el registro R9 establecido en 0x567C y escribe su versión desofuscada en la dirección 0x18008D000.

El parámetro R9 es una variable de control para la fusión de funciones del bucle CFF (variable de fusión) y debe especificarse mediante expresiones Miasm, que están documentadas aquí; tenga en cuenta que uno tiene que usar RSP_init/ESP_init en lugar de RSP/ESP para referirse a los parámetros de la pila. Por ejemplo, usaríamos ExprMem(ExprId(“ESP_init”, 32 ) + ExprInt (4, 32), 32) para apuntar al primer parámetro de pila en una arquitectura de 32 bits.

from stadeo.cff.cff_strategies import CFFStrategies
from miasm.expression.expression import ExprId, ExprInt

cs = CFFStrategies(64)
cs.solve_loop(0x1800158B0,
              0x18008D000,
              context={ExprId("R9", 64): ExprInt(0x567c, 64)},
              ip='10.1.40.164')

Figura 1. Llamada a la función ofuscada

Figura 2. CFG ofuscado (izquierda) y desofuscado (derecha)

Procesamiento de funciones accesibles

SHA-1 de la muestra: e0087a763929dee998deebbcfa707273380f05ca

El siguiente código reconoce solo las funciones ofuscadas accesibles desde 0x1002DC50 y busca candidatos para fusionar variables. Las funciones reconocidas y desofuscadas con éxito comienzan en 0x10098000.

from stadeo.cff.cff_strategies import CFFStrategies
strat = CFFStrategies(32)
res = strat.process_merging(0x1002DC50,
                            0x10098000,
                            ip="10.1.40.164")

 

Salida parcial:

skipping 0x1002dc50 with val None: 0xbadf00d
mapping: 0x1001ffc0 -> 0x10098000 with val @32[ESP_init + 0x8]: 0x6cef
skipping 0x100010f0 with val None: 0xbadf00d
mapping: 0x1000f0c0 -> 0x100982b7 with val @32[ESP_init + 0x8]: 0x2012
mapping: 0x1003f410 -> 0x10098c8a with val @32[ESP_init + 0x4]: 0x21a4
mapping: 0x1003f410 -> 0x10098f3d with val @32[ESP_init + 0x4]: 0x772a
skipping 0x1004ee79 (library func)
...

Las funciones mapeadas se desofuscaron correctamente y las omitidas no se consideraron ofuscadas. El formato de las líneas de mapeo en la salida es:

mapping: %obfuscated_function_address% -> %deobfuscated_function_address% with val %merging_variable%: % merging_variable_value%

El valor predeterminado de merging_variable_value es 0x0BADF00D. Las líneas de omisión siguen el mismo patrón, pero no hay una dirección_función_desofuscada. Las funciones de biblioteca reconocidas por IDA simplemente se omiten sin más procesamiento.

Procesando todas las funciones

SHA-1 de la muestra: e575f01d3df0b38fc9dc7549e6e762936b9cc3c3

Usamos el siguiente código solo para tratar con el CFF presentado en Emotet, cuya implementación del CFF se ajusta a la descripción de aplanamiento de control flujo aquí.

Preferimos este enfoque porque el método CFFStrategies.process_all() no intenta reconocer las variables de fusión que no están presentes en Emotet y busca solo un bucle CFF por función; por lo que es más eficiente.

Las funciones reconocidas y desofuscadas con éxito se escriben secuencialmente desde 0x0040B000. El formato de la salida es el mismo que en el método process_merging usado en el ejemplo de Procesamiento de funciones accesibles, pero naturalmente no habrá ninguna variable fusionada.

from stadeo.cff.cff_strategies import CFFStrategies
strat = CFFStrategies(32)
res = strat.process_all(0x0040b000,
                        ip="10.1.40.164")

 

Salida parcial:

mapping: 0x00401020 -> 0x0040b000 with val None: 0xbadf00d
skipping 0x00401670 with val None: 0xbadf00d
mapping: 0x00401730 -> 0x0040b656 with val None: 0xbadf00d
...

Figura 3. Ejemplo de función ofuscada (izquierda) y desofuscada (derecha) en Emotet

Redirigiendo referencias a funciones desofuscadas

Stadeo no actualiza automáticamente las referencias a las funciones después de su desofuscación. En el ejemplo a continuación, demostramos cómo parchear una llamada a la función en la Figura 1 desde el ejemplo Desofuscando una sola función. La referencia parcheada se muestra en la Figura 4.

Usamos el siguiente código para parchear llamadas, cuyo cuarto parámetro es 0x567C, a la función ofuscada en 0x1800158B0 con la desofuscada en 0x18008D000. Tenga en cuenta que uno debe asegurarse de que IDA haya reconocido correctamente los parámetros de la función y posiblemente corregirlos.

from stadeo.utils.xref_patcher import patch_xrefs
patch_xrefs(0x1800158B0,
            0x18008D000,
            {3: 0x567c},
            ip='10.1.40.164')

Figura 4. Referencia parcheada

Revelando strings ofuscadas en una función

La función StringRevealer.process_funcs() revela strings ofuscadas en la función especificada y devuelve un mapa de strings desofuscadas y sus direcciones.

Tenga en cuenta que el flujo de control de la función de destino ya debe haberse desofuscado.

En el siguiente ejemplo, desofuscamos las strings de la función en 0x100982B7, que se muestra en la Figura 5. La función en sí fue desofuscada en el ejemplo anterior de Procesamiento de funciones alcanzables.

from stadeo.string.string_revealer import StringRevealer
sr = StringRevealer(32)
strings = sr.process_funcs([0x100982B7],
                           ip="10.1.40.164")

Figura 5. Parte de la función en 0x100982B7 que claramente ensambla una string

El contenido de la variable strings después de la ejecución es:

{0x100982B7: {'SeLockMemoryPrivilege'}}

Esperamos que las herramientas de Stadeo y esta explicación de su uso le resulten útiles. Si tiene alguna consulta, comuníquese con nosotros en threatintel[at]eset.com o abra un problema en https://github.com/eset/stadeo.