La repetición mata la creatividad, y en este oficio, también mata la paciencia. Cuando tienes enfrente un montón de muestras de malware (como las del famoso set Bazaar 2020.02 con el que estuve peleándome), la rutina se vuelve tu peor enemiga. Ponerte a configurar el entorno, abrir el desensamblador, buscar el main, y repetir el proceso para 50 archivos idénticos… bueno, cansa bastante. Es la receta perfecta para el “burnout” del analista.

Llegó un punto en julio de 2024 en el que dije “basta”. No podía seguir perdiendo 15 minutos por muestra solo en tareas de triviales.

Por eso escribi analystty. Básicamente, es un proyecto con la intencion de automatizar todo ese trabajo sucio inicial por mi cuenta.

¿Qué hace? (Detectando TTPs y Capacidades)

Mirar la tabla de importaciones (IAT) está bien, es lo primero que nos enseñan. Pero seamos honestos: ver una lista plana de 200 funciones no te dice mucho a menos que tengas una memoria enciclopédica. ¡Entenderla es mejor!

Usando la librería pefile para parsear las cabeceras y una base de datos personalizada inspirada en MalApi (que vive en un fichero malapi.json bastante completo), la herramienta no solo me lista las funciones, sino que me dice para qué sirven. Le da semántica al código muerto.

analystty cruza cada importación con categorías predefinidas. Por ejemplo:

  • Si el malware trae VirtualProtect, WriteProcessMemory o CreateRemoteThread, analystty me grita al instante: ¡Ojo, esto es Injection!.
  • Si ve IsDebuggerPresent o CheckRemoteDebuggerPresent, lo marca como Evasion o Anti-Debugging.
  • Si aparecen CreateToolhelp32Snapshot o EnumProcesses, sabemos que está en fase de Enumeration (reconocimiento).

Así ya sé a qué atenerme antes de abrir nada más. Es la diferencia entre entrar a una habitación a oscuras o entrar con visión nocturna.

Detección de APIs de Malware

El dolor de cabeza de x64: RIP-Relative Addressing

Si vienes de analizar binarios de 32 bits (x86), seguro recuerdas que la vida era más sencilla: las llamadas a las APIs solían ser absolutas o fáciles de seguir. Veías un call 0x401000 y sabías exactamente a dónde ibas.

Pero en x64, la cosa se complica —y mucho— con el direccionamiento relativo al puntero de instrucción (RIP-Relative Addressing). Esto se hace para facilitar el Position Independent Code (PIC), pero para nosotros es un dolor de muelas.

Ahora, en lugar de una dirección clara, ves cosas como call [rip + 0x6e58]. Esto significa: “salta a la dirección donde estoy ahora + 0x6e58 bytes”. A simple vista, eso no te dice absolutamente nada. No sabes si está llamando a Sleep, a Wannacry o a la función de imprimir por pantalla.

Para no tener que sacar la calculadora hexadecimal cada dos por tres (o confiar ciegamente en que el desensamblador acierte siempre), escribí una lógica en Python dentro de analystty.py que lo resuelve estáticamente:

  • Desarma el código binario usando el motor de capstone.
  • Busca patrones de instrucciones específicas (CALL o JMP) que usen RIP como base.

Así es como se ve el “dump” crudo que procesa la herramienta. Capstone nos permite iterar sobre cada instrucción, dándonos los opcodes y mnemónicos necesarios para identificar dónde ocurren estos saltos relativos: Disassembly raw output showing opcodes

  • Calcula la dirección real. La fórmula mágica es: Dirección de la siguiente instrucción + Offset.
Output offsets

Mira cómo el script hace la “magia” con expresiones regulares para encontrar el offset y resolver el destino:

if mnemonic in ['call', 'jmp']:
    # Estas instrucciones pueden usar direccionamiento relativo a RIP.
    # Necesitamos identificar el destino para un posible análisis posterior (ej. resolución de llamadas).
    if operands == 'rax':
        # Caso especial: El operando es 'rax' (registro), pero la instrucción previa pudo haber cargado 
        # una dirección relativa a RIP en 'rax' (por ejemplo, 'mov rax, [rip + 0x...]').
        # Intentamos extraer el offset de RIP de los operadores de la instrucción anterior.
        match = re.search(r'\[rip\s*([\+\-])\s*0x([0-9a-fA-F]+)\]', previous['OPERATORS'])
    else:
        # Caso general: El operando actual es una dirección relativa a RIP directa.
        # ¿Es una dirección relativa fea tipo [rip + 0x123]? let's go!
        # Usamos regex para capturar el signo (+/-) y el valor hexadecimal.
        match = re.search(r'\[rip\s*([\+\-])\s*0x([0-9a-fA-F]+)\]', operands)
    if match:
        # Si encontramos un offset relativo a RIP: Calculamos la dirección física real del destino.
        # match.group(1) es el signo (+ o -), match.group(2) es el valor hexadecimal.
        offset = int(match.group(1) + match.group(2), 16)   
        # ¡Bingo! Aquí está la dirección destino absoluta en el archivo.
        # 'address' es la dirección de la instrucción actual, 'offset' es cuánto nos movemos.
        # Sumamos el offset a la dirección actual para obtener el destino absoluto.
        address = hex(int(address, 16) + offset)

¿El resultado? Una tabla preciosa donde [rip + 0x6e58] se convierte mágicamente en WinExec o Sleep. ¡Adiós a los cálculos manuales y a adivinar qué hace el código! Esto me permite trazar el flujo de ejecución estáticamente con una precisión que antes solo tenía ejecutándolo.

Tabla limpia

Conectando con x64dbg (en desarrollo)

Todo este análisis estático está genial para informes, pero la verdadera diversión empieza en el debugging dinámico. Aquí es donde analystty intenta cerrar el círculo, aunque aviso importante: esta feature aún está en el horno y no funciona al 100%. Es un trabajo en progreso, al igual que otras funcionalidades avanzadas que sigo puliendo.

Implementé una clase llamada medbg (dentro del módulo analystty.py) que actúa como puente experimental. La meta es dejar de copiar y pegar direcciones manualmente de mi terminal al debugger.

La visión es que la herramienta haga esto automáticamente (y ya lo logra en entornos controlados):

  • Conexión: Se conecta a una instancia de x64dbg que ya tengas corriendo mediante un socket o interfaz de plugin.
  • Cálculo de ASLR: Aquí viene lo interesante. El binario en disco tiene una ImageBase (por defecto suele ser 0x140000000), pero en memoria, Windows lo carga donde le da la gana por seguridad (ASLR). analystty calcula la diferencia (rebase) y ajusta todas las direcciones que encontró en el paso anterior.
    • Fórmula: Dirección Dinámica = (Dirección Estática - ImageBase Estática) + ImageBase Dinámica.
  • Breakpoints Automáticos: Recorre la lista de APIs de interés detectadas en el paso 1 (esa inyección de código que vimos antes) y le dice al debugger: “Pon un breakpoint aquí, aquí y allá”.

Para poder poner estos breakpoints, el script primero genera una matriz que ordena las funciones por las importaciones utilizadas. Esto permite saber exactamente en qué dirección de memoria se llama a WriteFile o Sleep, agrupándolas para facilitar la automatización:

Functions ordered by imports used Tabla resultante para el MalApi

Básicamente, lanzo el script y cuando miro el debugger, la idea es estar parado justo antes de que el malware intente inyectarse o conectarse a internet. Aún tiene sus fallos y casos borde, pero cuando funciona, te saltas todo el proceso del principio.

Sigo trabajando en esto y puliendo cosas (la lista de malapi.json siempre puede crecer y la detección de patrones puede mejorar).