Ghidra para análisis de malware, del log al código del binario

Centro de operaciones de ciberseguridad analizando malware con Ghidra mediante ingeniería inversa

Usar Ghidra para análisis de malware significa recurrir a la suite de ingeniería inversa de código abierto de la NSA para desensamblar ejecutables sospechosos de Windows y decompilarlos a pseudocódigo C. Con ella el analista deja de mirar solo el rastro que dejan los logs y empieza a leer la intención escrita en el propio binario.

Ese salto cambia por completo la manera de investigar una amenaza. A lo largo de este artículo veremos cómo preparar un laboratorio seguro, qué revelan los imports y las cadenas, cómo seguir la lógica maliciosa en el decompilador y de qué manera los scripts aceleran la desofuscación. Los fragmentos de código son ejemplos ilustrativos, pensados para enseñar el método y adaptables a cada muestra.

Cómo desensamblar un ejecutable sospechoso de Windows con Ghidra

El uso de Ghidra para análisis de malware es, ante todo, un ejercicio de análisis estático, porque la muestra se estudia sin llegar a ejecutarla. Así se evita que el código malicioso se active durante la investigación y se pueden examinar también las rutas que quizá no se activarían en una ejecución controlada.

El método más eficaz avanza de fuera hacia dentro, lo que aquí llamamos triaje en capas. Primero se observa la estructura del fichero, después se buscan pistas de comportamiento y solo al final se baja al detalle de cada función. Ghidra acompaña todo ese recorrido porque reúne en un mismo entorno el desensamblador, el decompilador, los grafos de control y el motor de scripting, y funciona tanto en modo interactivo como automatizado.

El análisis estático permite estudiar la lógica del binario sin ejecutarlo, y se complementa con análisis dinámico cuando hay empaquetado, cifrado u otras limitaciones

Laboratorio aislado y triaje del ejecutable PE

Antes de abrir cualquier muestra en Ghidra, el entorno de análisis tiene que estar aislado. La opción más extendida es una máquina virtual Windows preparada con FLARE-VM, el conjunto de scripts de Mandiant que instala herramientas de ingeniería inversa mediante Chocolatey y Boxstarter.

Para que ese laboratorio sea realmente seguro, conviene cumplir estas condiciones:

  • Solo en máquina virtual: FLARE-VM está pensada para instalarse exclusivamente en una VM, nunca en el equipo de trabajo.
  • Snapshot antes y después: Una instantánea previa a la instalación y otra con el entorno configurado permiten volver a un estado limpio tras cada muestra.
  • Instalar con red y analizar sin ella: FLARE-VM descarga herramientas durante la instalación, así que se monta con conexión y antes de meter ninguna muestra; para analizar, se desconecta la red.
  • Aislar la red si hace falta: Si el análisis necesita una red de laboratorio, hay que aislarla sabiendo que el modo host-only deja comunicación con el equipo anfitrión.
  • Cerrar los canales con el anfitrión: Conviene desactivar las carpetas compartidas, el portapapeles compartido y el arrastrar y soltar entre la máquina virtual y el equipo real.
  • Protecciones del sistema desactivadas: La instalación exige desactivar Tamper Protection, el antimalware y las actualizaciones de Windows, por eso el aislamiento es imprescindible.

Con el laboratorio listo, el triaje empieza al importar el binario en un proyecto de Ghidra. La herramienta identifica el formato Portable Executable y propone la arquitectura, que en la mayoría de muestras de Windows será x86 de 32 bits o x64 de 64 bits. Al abrirlo en el CodeBrowser, Ghidra ofrece lanzar el Auto Analysis para identificar funciones, referencias cruzadas y tipos de datos.

Sus opciones merecen una revisión antes de pulsar Analyze. Aggressive Instruction Finder viene desactivado de serie y ayuda con binarios sin símbolos, a cambio de más tiempo y algún falso positivo. Decompiler Parameter ID, en cambio, puede venir activado en los ejecutables PE pequeños y desactivado en otros casos, y su ejecución puede alargar el análisis.

Cuando llegan varias muestras a la vez, el mismo proceso puede lanzarse sin interfaz gráfica con el modo headless de Ghidra:

C:\ghidra_12.1.4_PUBLIC\support\analyzeHeadless.bat ^
  C:\Analisis\Proyectos CasoMalware ^
  -import C:\Muestras\muestra.exe ^
  -analysisTimeoutPerFile 300 ^
  -log C:\Analisis\import.log

Con el análisis inicial terminado, toca escuchar lo que el propio fichero cuenta de sí mismo.

Cabecera PE y secciones

La cabecera PE es la carta de presentación del ejecutable, y Ghidra la muestra completa en el Listing a partir de la dirección base de la imagen. Todo empieza con la firma MZ del stub de MS-DOS, cuyo campo e_lfanew, situado en el desplazamiento 0x3C, apunta a la firma PE\0\0 y a la cabecera COFF.

Las cabeceras COFF y opcional aportan los primeros datos del triaje. Machine indica la arquitectura, NumberOfSections el número de secciones y TimeDateStamp la fecha de compilación, aunque este último valor se falsifica con facilidad. En la cabecera opcional, AddressOfEntryPoint marca dónde empieza la ejecución, ImageBase la dirección de carga preferida y Subsystem si el programa es de consola o gráfico.

Las secciones se revisan en la ventana Memory Map de Ghidra, que muestra sus permisos de lectura, escritura y ejecución. Estas son las más habituales y lo que conviene vigilar en cada una:

Sección Contenido habitual Permisos Señal de alerta
.text Código ejecutable R-X Permiso de escritura o punto de entrada fuera de esta sección
.rdata Datos de solo lectura y tablas de importación R-- Permiso de ejecución
.data Variables globales inicializadas RW- Permiso de ejecución
.rsrc Iconos, diálogos e información de versión R-- Tamaño desproporcionado que puede ocultar una carga útil
UPX0, UPX1 u otros nombres anómalos Código empaquetado y rutina de descompresión RWX Indicio directo de un packer

Otra pista aparece cuando VirtualSize supera con mucho a SizeOfRawData. Esa diferencia señala una sección que ocupa más en memoria que en disco, algo propio de un desempaquetador, pero también de una sección legítima de datos sin inicializar como .bss, que Windows rellena con ceros. Por sí sola no lo confirma, así que el siguiente paso es medir el desorden de los bytes.

Entropía y detección de packers

La entropía de Shannon mide el grado de aleatoriedad de los bytes de un fichero y es un indicador útil de un posible empaquetado o cifrado. Se calcula a partir de la frecuencia con la que aparece cada uno de los 256 valores posibles de un byte, por lo que el resultado oscila entre 0 y 8 bits por byte.

Entropía de Shannon H = −∑x=0255 p(x) · log2 p(x)

En la fórmula, p(x) es la proporción de bytes con valor x dentro del bloque analizado. El código máquina habitual repite patrones y opcodes, mientras que los datos comprimidos o cifrados reparten los valores de forma casi uniforme y empujan la entropía hacia valores cercanos a 8. Un valor superior a 7 sirve como referencia orientativa, que hay que leer junto con las importaciones, los permisos y la estructura de las secciones, no como una frontera exacta entre código normal y empaquetado.

Ghidra permite visualizar esta métrica sin salir del CodeBrowser gracias a la barra de entropía del margen de vista general, que colorea cada zona del binario según su nivel de desorden. Una sección ejecutable con entropía alta suele venir acompañada de otros síntomas:

  • Importaciones mínimas: La tabla se reduce a unas pocas funciones como LoadLibraryA, GetProcAddress o VirtualProtect, suficientes para reconstruir el resto en memoria.
  • Cadenas casi inexistentes: La ventana Defined Strings apenas muestra texto legible porque está comprimido o cifrado.
  • Pocas funciones reconocidas: El Auto Analysis identifica muy poco código, normalmente un bucle de descompresión que termina en un salto hacia otra región de memoria.

Una entropía alta no confirma por sí sola que la muestra sea maliciosa, porque los instaladores y los recursos comprimidos legítimos también la presentan. Cuando el packer es conocido, como UPX, puede revertirse con la opción -d del propio UPX, siempre que el atacante no lo haya modificado. Una vez desempaquetado el binario, sus importaciones y cadenas empiezan a contar la historia real.

Imports, strings y referencias cruzadas

Las funciones que un ejecutable importa de la API de Windows delatan buena parte de su comportamiento antes de leer una sola línea de código. En Ghidra aparecen en la carpeta Imports del Symbol Tree, agrupadas por DLL, y cada una enlaza con su entrada en la tabla de direcciones de importación.

La ventana Defined Strings completa ese primer retrato. URLs, rutas del registro, nombres de mutex o notas de rescate son cadenas que orientan la investigación hacia la red, la persistencia o el cifrado.

El paso decisivo llega con las referencias cruzadas. Desde cualquier import o cadena, la opción Show References to lista las instrucciones que lo utilizan, y un doble clic lleva a la función que lo invoca. Así se pasa de una API sospechosa al código que la usa sin recorrer el binario entero.

Estas son las combinaciones de APIs que más conviene rastrear y la técnica de MITRE ATT&CK con la que se relacionan:

APIs de Windows Comportamiento Técnica MITRE ATT&CK
VirtualAllocEx + WriteProcessMemory + CreateRemoteThread Reserva memoria en otro proceso, escribe código en ella y lo ejecuta con un hilo remoto T1055 Process Injection
RegSetValueEx en claves Run o RunOnce Configura la ejecución al iniciar sesión, recurrente con Run y una sola vez con RunOnce T1547.001 Registry Run Keys / Startup Folder
IsDebuggerPresent Comprueba si hay un depurador para cambiar o detener su ejecución T1622 Debugger Evasion
Funciones de WinHTTP o WinINet Permiten comunicaciones HTTP o HTTPS, que cuando sirven para mando y control encajan en esta técnica T1071.001 Web Protocols

Que una API no figure entre las importaciones no significa que el malware no la use. Muchas muestras cargan las DLL con LoadLibrary y obtienen cada función con GetProcAddress en tiempo de ejecución, de modo que su nombre solo aparece como cadena. Buscar esos nombres en Defined Strings y seguir sus referencias cruzadas destapa las llamadas ocultas y lleva directamente a la lógica que toca leer en el decompilador.

Triaje en capas, de fuera hacia dentro

Decompilador y lectura de la lógica maliciosa

El decompilador de Ghidra traduce el ensamblador de cada función a pseudocódigo C sincronizado con el Listing. Al seleccionar una línea en cualquiera de las dos vistas, Ghidra resalta su equivalente en la otra, de modo que la lógica se lee en alto nivel sin perder la referencia a las instrucciones reales.

El recorrido empieza en la función entry, el punto de entrada que marca la cabecera PE. En binarios compilados con Visual C++, esa rutina atraviesa el código de inicialización del runtime antes de llegar a main o WinMain, donde arranca la lógica del autor. Si una función tiene muchas bifurcaciones, la ventana Function Graph la representa como un diagrama de bloques y saltos.

Así aparece recién decompilada, en un ejemplo ilustrativo, la función a la que lleva la referencia cruzada de VirtualAllocEx:

undefined4 FUN_00401a30(DWORD param_1)
{
  HANDLE pvVar1;
  LPVOID pvVar2;

  pvVar1 = OpenProcess(0x1fffff, 0, param_1);
  pvVar2 = VirtualAllocEx(pvVar1, (LPVOID)0x0,
                          0x1000, 0x3000, 0x40);
  WriteProcessMemory(pvVar1, pvVar2, &DAT_00403000,
                     0x1000, (SIZE_T *)0x0);
  CreateRemoteThread(pvVar1, (LPSECURITY_ATTRIBUTES)0x0, 0,
                     (LPTHREAD_START_ROUTINE)pvVar2,
                     (LPVOID)0x0, 0, (LPDWORD)0x0);
  return 1;
}

El mismo código cobra sentido tras aplicar estas técnicas de anotación:

  • Renombrar (L): Sustituye nombres genéricos como FUN_00401a30 o param_1 por otros que describan su papel.
  • Retipar variables (Ctrl+L): Asigna tipos de Windows como HANDLE o LPVOID para que el decompilador propague esa información.
  • Equates (E): Convierte constantes numéricas en su nombre simbólico, como PAGE_EXECUTE_READWRITE en lugar de 0x40.
  • Comentarios y marcadores (; y Ctrl+D): Documentan hallazgos y señalan direcciones clave para volver a ellas.
BOOL InyectarShellcode(DWORD pidObjetivo)
{
  HANDLE hProceso;
  LPVOID memRemota;

  hProceso = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pidObjetivo);
  memRemota = VirtualAllocEx(hProceso, NULL, 0x1000,
                             MEM_COMMIT | MEM_RESERVE,
                             PAGE_EXECUTE_READWRITE);
  WriteProcessMemory(hProceso, memRemota, &shellcode,
                     0x1000, NULL);
  CreateRemoteThread(hProceso, NULL, 0,
                     (LPTHREAD_START_ROUTINE)memRemota,
                     NULL, 0, NULL);
  return TRUE;
}

Anotar convierte horas de lectura en conocimiento reutilizable. Pero cuando el mismo patrón se repite en decenas de muestras, el trabajo manual se queda corto y llega el momento de automatizar.

Automatización y desofuscación con scripts

El Script Manager de Ghidra reúne una amplia colección de scripts organizados por categorías y permite crear otros propios desde la misma ventana. Pueden escribirse en Java o en Python. Desde Ghidra 12.0, los scripts de Python se ejecutan por defecto con PyGhidra, que aporta CPython 3 nativo y acceso a cualquier librería del ecosistema Python, mientras que Jython pasó a ser una extensión opcional en la versión 12.1.

Para usar estos scripts de Python desde la interfaz gráfica, Ghidra se arranca con support\pyghidraRun.bat en Windows. Hace falta una versión de Python compatible y el paquete PyGhidra, y si no está, el propio lanzador ofrece instalarlo con sus dependencias.

La extracción de IOCs es uno de los usos más inmediatos. Este script recorre los datos definidos del binario y extrae las URLs, direcciones IP y rutas del registro que aparecen en sus cadenas:

# Extrae posibles IOCs de las cadenas definidas
# @category Malware.IOC
# @runtime PyGhidra
import re

PATRON = re.compile(
    r"https?://[^\s\"']+"
    r"|\b(?:\d{1,3}\.){3}\d{1,3}\b"
    r"|(?:HKEY_[A-Z_]+|SOFTWARE)\\[^\s\"']+",
    re.IGNORECASE)

for dato in currentProgram.listing.getDefinedData(True):
    if dato.hasStringValue():
        cadena = str(dato.getValue())
        for ioc in PATRON.findall(cadena):
            print(f"{currentProgram.name}\t{dato.address}\t{ioc}")

La automatización alcanza su verdadero valor al combinarse con el modo headless. Si se lanza con pyghidraRun y la opción -H, el analizador acepta los mismos parámetros vistos en el triaje, importa una carpeta completa de muestras con -recursive y aplica el script a cada una mediante -postScript:

C:\ghidra_12.1.4_PUBLIC\support\pyghidraRun.bat -H ^
  C:\Analisis\Proyectos LoteMuestras ^
  -import C:\Muestras ^
  -recursive ^
  -scriptPath C:\Analisis\scripts ^
  -postScript ExtraerIOCs.py

El resultado es un inventario de indicadores por muestra, que conviene validar y depurar antes de incorporarlo al SIEM o a una plataforma de threat intelligence. Algunas muestras, sin embargo, esconden incluso los nombres de las funciones que utilizan.

El reversing no se aprende leyendo, se aprende abriendo binarios. Ese es el salto que marca la diferencia

API hashing y cadenas cifradas

El API hashing sustituye los nombres de las funciones por un valor numérico para que no aparezcan ni en la tabla de importaciones ni entre las cadenas. El código malicioso recorre en memoria el directorio de exportaciones de cada DLL, calcula el hash de cada nombre y lo compara con el valor que lleva incrustado hasta dar con la función que necesita. Uno de los algoritmos más extendidos es ROR13, popularizado por Metasploit, que rota el hash 13 bits a la derecha y le suma cada carácter del nombre:

Hash ROR13 h = ROR13(h) + c

Al no ser un hash criptográfico, basta con precalcular los valores de las APIs más habituales para revertirlo. Este script resuelve una variante simple de ROR13, aplicada solo al nombre de la función, y añade como comentario el nombre junto a cada instrucción que contiene uno de esos hashes. No cubre las variantes que incorporan el nombre del módulo u otros elementos, como la que usa Metasploit:

# Resuelve hashes ROR13 de APIs de Windows
# @category Malware.Deobfuscation
# @runtime PyGhidra

def ror13(nombre):
    h = 0
    for c in nombre.encode():
        h = ((h >> 13) | (h << 19)) & 0xFFFFFFFF
        h = (h + c) & 0xFFFFFFFF
    return h

APIS = ["LoadLibraryA", "GetProcAddress", "VirtualAlloc",
        "VirtualProtect", "CreateThread", "WinExec"]
TABLA = {ror13(api): api for api in APIS}

for ins in currentProgram.listing.getInstructions(True):
    for i in range(ins.getNumOperands()):
        valor = ins.getScalar(i)
        if valor and valor.getUnsignedValue() in TABLA:
            setEOLComment(ins.address,
                          TABLA[valor.getUnsignedValue()])

La misma lógica sirve para las cadenas cifradas. Una vez localizada en el decompilador la rutina que las descifra, como un XOR con clave fija, un script que la reproduzca devuelve el texto original y lo deja anotado en el propio binario.

Dominar Ghidra para análisis de malware, desde el triaje del ejecutable PE hasta la desofuscación automatizada, es lo que permite pasar de leer alertas a entender amenazas. Quien entiende el binario deja de perseguir al malware y empieza a anticiparlo. El Máster en Análisis de Malware y Reversing del Campus Ciberseguridad, avalado por la UCAM, profundiza en el reversing en Windows, el análisis estático y dinámico y las técnicas de empaquetado y ofuscación para convertir ese conocimiento en una especialización profesional.

Los atacantes cuentan con que nadie mire dentro. Demuéstrales lo contrario.

¿Qué diferencia a Ghidra de IDA Pro?

Ghidra es gratuito y de código abierto bajo licencia Apache 2.0, mientras que IDA Pro es una herramienta comercial de pago. Los dos desensamblan y decompilan, pero Ghidra incluye su decompilador sin coste y permite trabajar en equipo sobre un mismo proyecto. La elección entre uno y otro depende del presupuesto y del tipo de binario que se analice con más frecuencia.

¿Qué conocimientos necesito para empezar con Ghidra?

Conviene manejar algo de ensamblador x86, entender cómo funciona la API de Windows y tener nociones de C, ya que el decompilador traduce a pseudocódigo C. No hace falta ser programador experto, pero sí comprender cómo se organiza un proceso en memoria. El resto llega analizando muestras reales dentro de un laboratorio aislado.

¿Cuándo conviene pasar del análisis estático al dinámico?

El análisis estático revela la lógica sin ejecutar la muestra, pero algunos binarios solo muestran su comportamiento al correr, cuando se desempaquetan o descifran su configuración en memoria. En esos casos se recurre a un depurador como x64dbg para seguir la ejecución paso a paso. Lo habitual es combinar ambos, el estático para entender y el dinámico para confirmar.

¿Ghidra sirve solo para malware de Windows?

No. Ghidra funciona en Windows, macOS y Linux, y reconoce numerosas arquitecturas y formatos de fichero. Eso permite aplicar el mismo método a binarios ELF de Linux, a aplicaciones móviles o al firmware de dispositivos, lo que lo convierte en una herramienta válida para casi cualquier plataforma.