Reversing Avanzado con IDA Pro: FLIRT, Bindiff y Desofuscación¶
Introducción¶
Este documento compila técnicas avanzadas de reversing aplicadas a binarios Windows, específicamente enfocado en:
- Desofuscación estática mediante scripts IDAPython
- Creación de firmas FLIRT personalizadas para librerías embebidas
- Reconstrucción de símbolos C++ usando Bindiff
- Importación de estructuras STL (std::string, std::vector, etc.)
- Análisis de calling conventions y stack alignment en x64
Estas técnicas fueron aplicadas en el análisis del challenge "Binary Echo" del curso de Binary Exploitation de Gecko Academy, compilado con Intel C++ Compiler y con múltiples capas de ofuscación.
Parte 1: Desofuscación Estática¶
Introducción al Problema¶
El binario presenta ofuscación mediante saltos manipulados que rompen el desensamblado automático de IDA Pro. El patrón típico es:
test ebp, ebp
jnz short loc_102C+1 ; Salta a 102D (dentro de una instrucción)
retn 0 ; 0xC2 - byte que se saltea
nop ; 0x90 - destino real
Problema: IDA ve el retn 0 (C2 00) como una instrucción válida y no desensambla desde el byte siguiente (90), dejando código "roto".
Métodos de Corrección¶
Método 1: Corrección Dinámica (Debugging)¶
- Ejecutar el binario en x64dbg o IDA Debugger
- Trasear (F7) hasta la zona ofuscada
- Observar cómo el código "se arregla" automáticamente al ejecutarse
- El problema: trabajo de mono si hay miles de instancias
Ventaja: Funciona siempre
Desventaja: Extremadamente lento para binarios grandes
Método 2: Corrección Manual¶
Para cada instancia ofuscada:
- Cambiar a vista de texto (barra espaciadora en IDA)
- Activar visualización de bytes:
Options > General > Number of opcode bytes - Identificar el salto:
jnz short loc_102C+1 - Ubicar el byte destino (102D)
- Presionar
Uen la dirección 102C para "undefinir" elretn - Ahora la dirección 102D existe
- Presionar
Cen 102D para crear código
Ejemplo visual:
Antes:
102C: jnz short loc_102C+1 ; Salta a 102D (no existe)
102C: retn 0 ; C2 00
102E: nop ; 90
Después:
102C: jnz short loc_102D ; Ahora salta a dirección válida
102C: db 0xC2 ; Byte indefinido
102D: nop ; Código válido
Método 3: Script IDAPython (Recomendado)¶
Paso 1: Identificar el patrón de bytes
Buscar la secuencia repetida:
En IDA: Search > Sequence of bytes > 85 ED 75 01 C2
Paso 2: Extraer direcciones
Copiar resultados al Notepad, extraer direcciones con Alt+selección de columna.
Paso 3: Aplicar script
import ida_bytes
import ida_auto
import idaapi
# Direcciones donde está el patrón
addrs = [
0x00401028,
0x0040108C,
0x00401095,
0x00401142,
0x004011F3
]
for i in addrs:
# Paso 1: Undefinir el byte C2 (offset +4 desde el inicio del patrón)
ida_bytes.del_items(i + 4, ida_bytes.DELIT_SIMPLE, 1)
# Paso 2: Crear código desde el byte siguiente (offset +5)
for ea in range(i + 5, idaapi.ida_inf_get_max_ea()):
ida_ua.create_insn(ea)
break
# Paso 3: Re-analizar (opcional, puede comentarse)
ida_auto.auto_wait()
print("Desofuscación completada")
Ejecución: File > Script command > Python > Pegar script > Run
Patrones de Ofuscación Encontrados¶
En el challenge había 5 instancias del patrón principal:
Y variantes que requieren corrección manual:
Bytes Residuales (0x48)¶
Después de aplicar el script, suelen quedar bytes 0x48 sin desensamblar. Solución:
- Presionar
Csobre el byte 0x48 - IDA lo reconocerá como parte de una instrucción válida
Notas Importantes¶
- Analizar todo al final puede romper correcciones: Comentar
ida_auto.auto_wait()si causa problemas - Backups: Hacer snapshots de IDA (
Take database snapshot) antes de aplicar scripts masivos - Patrones únicos: Si cada ofuscación es diferente, buscar sub-patrones comunes
- Automatización vs. Manual: Si son <10 instancias, manual es viable; si son >100, script obligatorio
Parte 2: Creación de Firmas FLIRT Personalizadas¶
Contexto: ¿Por Qué Necesitamos FLIRT Personalizado?¶
FLIRT (Fast Library Identification and Recognition Technology): Sistema de IDA para reconocer funciones de librerías mediante patrones de bytes.
Problema en el challenge:
- Binario compilado con static linking (DLLs embebidas)
- Funciones como
printf,scanf, operadores de C++ (operator<<) no aparecen en la IAT - FLIRT por defecto de IDA no reconoce estas funciones embebidas
- Compilador: Intel C++ (no Visual Studio estándar)
Resultado: IDA muestra sub_401234 en vez de std::basic_string::operator<<, haciendo el análisis incomprensible.
Prerequisitos¶
Herramientas necesarias:
- IDA Pro o IDA Classroom (con soporte IDAPython)
- IDA Free NO soporta IDAPython (limitación)
- Compilador Intel C++ integrado en Visual Studio 2022
- Scripts:
idb2pat.py(convierte IDB a archivo .pat)sigmake.exe(viene con IDA, en carpetaflair/)
Proceso Completo: Paso a Paso¶
Paso 1: Generar Código de Prueba con Símbolos¶
Objetivo: Crear un ejecutable que use todas las funciones que queremos detectar, con símbolos claros para IDA Pro.
Método recomendado: Pedirle a ChatGPT/Claude:
Prompt: "Genera un programa en C++ que use:
- printf, scanf, fgets, strcmp
- std::string, std::vector
- Operadores << y >> de iostream
- Constructores y destructores de STL
Usa la mayor cantidad posible de funciones estándar."
Ejemplo de código generado:
#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
#include <cstring>
int main() {
std::string str = "Hello";
std::cout << str << std::endl;
std::vector<int> vec = {1, 2, 3};
char buffer[100];
scanf("%s", buffer);
printf("You entered: %s\n", buffer);
return 0;
}
Paso 2: Compilar con Configuraciones Específicas¶
Configuración crítica en Visual Studio:
Configuración correcta:
Multi-threaded DLL (/MD)→ Use this para que IDA Pro pueda ver símbolos correctamente
Nota importante: A diferencia de otros escenarios, para que FLIRT funcione bien y IDA detecte nombres de funciones y variables, es necesario usar /MD (DLL linking) en vez de /MT (static linking).
Optimizaciones:
- Desactivar TODAS las optimizaciones (
/Od- Disabled) - No usar Release con optimizaciones (
/O2) - Razón: Sin optimizaciones, IDA Pro puede ver claramente los nombres de funciones, variables y estructuras de STL
Compiladores a probar:
- Intel C++ Compiler (para el challenge)
- Visual Studio Compiler (para Bindiff posterior)
Paso 3: Convertir IDB a Archivo .pat¶
En IDA Pro/Classroom:
- Abrir el ejecutable compilado con símbolos (archivo .pdb)
- Verificar que los nombres estén correctos:
Options > Demangle names - Ejecutar:
File > Script file > idb2pat.py - Guardar como:
console_app.pat
¿Qué hace idb2pat.py?
- Lee el IDB (base de datos de IDA)
- Extrae patrones de bytes de cada función
- Asocia cada patrón con su nombre simbólico
- Genera archivo
.pat(intermedio)
Paso 4: Generar Archivo .sig con sigmake¶
Abrir terminal (PowerShell/CMD):
cd "C:\Program Files\IDA Pro 9.0\flair\bin\win"
.\sigmake.exe -n "MyLibrary" "C:\path\to\console_app.pat" "C:\path\to\console_app.sig"
Sintaxis:
Problema común: Colisiones
Si sigmake dice:
Solución:
- Se generó un archivo
.exccon las colisiones - Abrir
console_app.exccon Notepad++ - Comentar las líneas con
;al principio:
- Guardar y volver a ejecutar
sigmake
Ahora sí genera: console_app.sig
Paso 5: Aplicar la Firma al Binario Target¶
En IDA (con el challenge abierto):
File > Load file > FLIRT signature file- Seleccionar
console_app.sig - IDA aplica las firmas automáticamente
Antes:
Después:
Limitaciones y Casos Especiales¶
Problema: Compilador Intel y Name Mangling¶
Síntoma: Los nombres en Release con Intel C++ salen así:
En vez de:
Causa: El compilador Intel no incluye los símbolos completos en Release mode.
Solución: Usar Bindiff (ver Parte 3).
Múltiples Versiones de FLIRT¶
Recomendación: Mantener una colección de .sig:
flirt_signatures/
├── msvc_2019_x64_mt.sig
├── msvc_2022_x64_mt.sig
├── intel_cpp_2023_x64.sig
├── mingw_gcc_x64.sig
└── clang_x64.sig
Probar varias: Si una no detecta nada, probar otra. No hay riesgo de "romper" el IDB.
Detectar el Compilador: Detect It Easy¶
Tool: Detect It Easy (DIE)
Uso:
- Abrir el ejecutable en DIE
- Ver "Compiler" y "Linker"
Ejemplo:
Con Intel C++:
Nota: Intel C++ es difícil de detectar porque usa el linker de MSVC.
Parte 3: Reconstrucción de Símbolos con Bindiff¶
Contexto: El Problema del Intel C++ Compiler¶
Escenario:
- Compilaste con Intel C++ en Release
- Los símbolos en IDA salen ofuscados:
- El FLIRT generado no funciona en el challenge porque los bytes no coinciden exactamente
Solución: Compilar dos versiones del mismo código:
- Con Intel C++ (sin símbolos legibles)
- Con Visual Studio (con símbolos correctos)
Luego usar Bindiff para:
- Comparar ambos ejecutables
- Machear funciones similares
- Importar los nombres del ejecutable de VS al de Intel
Instalación de Bindiff para IDA 9¶
Problema: Incompatibilidad Oficial¶
Estado actual (2025):
- Bindiff oficial NO soporta IDA 9
- Issue abierto en GitHub sin solución oficial
- Solución: Librerías parcheadas por la comunidad
Instalación Paso a Paso¶
1. Descargar Bindiff:
- Página oficial
- Descargar el instalador
.msipara Windows
2. Instalar normalmente:
3. Descargar librerías parcheadas:
Opción A (GitHub - cfox):
Bajar los archivos:
zynamics_binexport_12_ida64.dllzynamics_bindiff_10_ida64.dll
Opción B (Moodle del curso):
Archivo bindiff_ida9_dlls.zip con las DLLs ya parcheadas.
4. Reemplazar DLLs:
Ubicación 1 (plugins de IDA):
Archivos a reemplazar:
zynamics_binexport_12_ida64.dllzynamics_bindiff_10_ida64.dll
Ubicación 2 (instalación de Bindiff):
¡Importante! Hay que reemplazar en ambos lugares, porque Bindiff restaura las DLLs originales al arrancar.
Solución definitiva: Borrar las DLLs de la instalación de Bindiff para evitar que restaure las viejas.
5. Verificar instalación:
Abrir IDA Pro:
Si aparecen, la instalación fue exitosa.
Bindiff en IDA Free: Workaround¶
Problema: Bindiff en IDA Free dice "database is open in another instance" aunque no lo esté.
Solución:
1. Exportar ambos ejecutables con Binexport:
Genera: executable.BinExport
2. Usar Bindiff desde línea de comandos:
cd "C:\Program Files\BinDiff\bin"
.\bindiff.exe `
--primary "C:\path\to\intel.BinExport" `
--secondary "C:\path\to\vs.BinExport" `
--output "."
3. Cargar resultados en IDA Free:
Seleccionar el archivo .bindiff generado.
Proceso: Matcheo de Símbolos¶
Paso 1: Compilar Dos Versiones¶
Versión 1: Intel C++ Compiler
Properties > General > Platform Toolset: Intel C++ Compiler
Properties > C/C++ > Code Generation > Runtime Library: Multi-threaded DLL (/MD)
Properties > C/C++ > Optimization: Disabled (/Od)
Output: console_app_intel.exe + console_app_intel.pdb
Versión 2: Visual Studio
Properties > General > Platform Toolset: Visual Studio 2022 (v143)
Properties > C/C++ > Code Generation > Runtime Library: Multi-threaded DLL (/MD)
Properties > C/C++ > Optimization: Disabled (/Od)
Output: console_app_vs.exe + console_app_vs.pdb
Importante: Ambos deben:
- Usar mismo código fuente
- Compilar en Release
- Usar dynamic linking (/MD) para que IDA vea los símbolos
- Desactivar todas las optimizaciones (
/Od) - Tener símbolos (.pdb disponibles)
Paso 2: Abrir Ejecutables en IDA¶
En IDA Pro:
- Abrir
console_app_vs.exe(con símbolos correctos gracias a/MDy optimizaciones desactivadas) Options > Demangle names→ Verificar que los nombres sean claramente visibles-
Cerrar
-
Abrir
console_app_intel.exe(compilado con Intel C++) Options > Demangle names→ Ver que los nombres pueden no estar tan claros- No cerrar este
Paso 3: Ejecutar Bindiff¶
Con console_app_intel.exe abierto:
Seleccionar: console_app_vs.i64 (el IDB del ejecutable de VS)
Importante: El .i64 de VS debe estar cerrado (no abierto en otra instancia de IDA).
Bindiff comienza a analizar:
Analyzing functions...
Matching algorithms:
- Address matching
- String references
- Call graph matching
- Manual matches
Resultado: Ventana de Bindiff con matches.
Paso 4: Interpretar Resultados¶
Pestañas en Bindiff:
| Pestaña | Descripción |
|---|---|
| Matched Functions | Funciones que Bindiff considera iguales (high confidence) |
| Unmatched Primary | Funciones solo en Intel (no machean) |
| Unmatched Secondary | Funciones solo en VS (no machean) |
| Statistics | Métricas de similitud |
Columnas importantes:
Primary Name: operator@@___W@std (nombre roto de Intel)
Secondary Name: std::operator<<(std::string&) (nombre correcto de VS)
Similarity: 0.95 (95% de código similar)
Confidence: High
Ejemplo visual:
Matched Functions:
Primary (Intel) Secondary (VS) Similarity
??$operator@_W@std@@___ std::basic_string::operator<< 0.98
sub_401234 printf 1.00
??allocator@std@@___ std::allocator::allocate 0.95
Paso 5: Importar Símbolos¶
Seleccionar funciones a importar:
- En la pestaña Matched Functions
Ctrl+A(seleccionar todas)- Clic derecho → Import Symbol & Comments
Alternativamente (selectivo):
- Filtrar por similitud > 0.90
- Importar solo las de alta confianza
IDA aplica los cambios:
Cerrar Bindiff: File > Close
Paso 6: Verificar en IDA¶
En la ventana de IDA (Intel C++):
Antes:
Después:
Ahora sí: Los nombres están legibles y se puede generar el FLIRT correctamente.
Casos de Uso Adicionales de Bindiff¶
1. Diffing de versiones de malware:
- Comparar v1.0 vs v1.1 de un malware
- Identificar qué funciones cambiaron
2. Patch analysis:
- Comparar ejecutable antes/después de un parche de seguridad
- Ver exactamente qué se modificó
3. Funciones parcialmente similares:
- Bindiff muestra gráficos de flujo lado a lado
- Ver bloques básicos que difieren
Visualización gráfica:
Muestra dos grafos lado a lado con colores:
- Verde: Bloques idénticos
- Amarillo: Bloques similares
- Rojo: Bloques diferentes
Parte 4: Importación de Estructuras STL y Análisis del Challenge¶
Contexto: El Binario Ya Tiene Símbolos, Pero...¶
Después de aplicar FLIRT + Bindiff:
Tenemos: Nombres de funciones correctos
No tenemos: Definiciones de estructuras STL
Problema visible en IDA:
// Sin estructuras:
char username[32]; // IDA muestra "db 20h dup(?)"
char password[32];
// Con estructuras:
std::string username; // IDA muestra "std::string"
std::string password;
Local Types muestra:
Necesitamos:
Importación de Estructuras desde Otro IDB¶
Paso 1: Exportar Estructuras del Ejecutable con Símbolos¶
En el IDB de Visual Studio (con símbolos completos):
Guardar como: structures.idc
¿Qué contiene este archivo?
- Definiciones de todas las estructuras en formato IDC
- Incluye:
std::string,std::vector,std::allocator, etc.
Paso 2: Importar en el Challenge¶
En el IDB del challenge:
IDA ejecuta el script silenciosamente.
Verificar:
Antes: 65 estructuras
Después: 706 estructuras
Buscar std:::
Aplicación de Estructuras en el Código¶
Identificar std::string en el Stack¶
Código desensamblado:
Pseudocódigo (antes de aplicar estructura):
¿Cómo saber que es std::string?
- Se llama al constructor
std::string::basic_string - El tamaño en el stack es 0x20 bytes (32 decimal)
- En
Local Types,std::stringtiene tamaño 0x20
Verificar tamaño de std::string:
struct std::string {
char* _Ptr; // 8 bytes (puntero a buffer)
size_t _Size; // 8 bytes (longitud actual)
size_t _Res; // 8 bytes (capacidad reservada)
// Total: 24 bytes (0x18)
// + alignment = 32 bytes (0x20)
};
Aplicar Estructura en el Stack Frame¶
En la vista de desensamblado:
Pasos:
- Posicionarse en
username - Presionar
Y(cambiar tipo) - Escribir:
std::string - Enter
Ahora muestra:
Pseudocódigo (después):
Repetir para password:
Aplicar tipo: std::string
Análisis del Challenge: Flujo Principal¶
Parte 1: Inicialización¶
Código desensamblado:
sub_401000 proc near
; Prólogo
push rbp
mov rbp, rsp
sub rsp, 60h ; Reservar stack (96 bytes)
; Crear std::string username (vacío)
lea rcx, [rbp-48h] ; RCX = &username
call std::string::basic_string
; Crear std::string password (vacío)
lea rcx, [rbp-28h] ; RCX = &password
call std::string::basic_string
Pseudocódigo:
void main() {
std::string username; // Constructor en rbp-48h
std::string password; // Constructor en rbp-28h
Parte 2: Input del Usuario¶
Código desensamblado:
; Imprimir "Enter username: "
lea rdx, aEnterUsername ; RDX = "Enter username: "
lea rcx, std::cout ; RCX = &cout (this)
call std::operator<< ; cout << "Enter username: "
; Leer username
lea rdx, [rbp-48h] ; RDX = &username
lea rcx, std::cin ; RCX = &cin (this)
call std::getline ; getline(cin, username)
; Imprimir "Enter password: "
lea rdx, aEnterPassword
lea rcx, std::cout
call std::operator<<
; Leer password
lea rdx, [rbp-28h] ; RDX = &password
lea rcx, std::cin
call std::getline
Calling convention (Windows x64 - FastCall):
RCX = this (primer parámetro, objeto implícito)
RDX = segundo parámetro
R8 = tercer parámetro
R9 = cuarto parámetro
Pseudocódigo:
std::cout << "Enter username: ";
std::getline(std::cin, username);
std::cout << "Enter password: ";
std::getline(std::cin, password);
Parte 3: Extracción de C-String¶
Código desensamblado:
; Obtener puntero a la C-string interna de username
lea rcx, [rbp-48h] ; RCX = &username
call std::string::c_str ; RAX = puntero a "char*"
; Leer primer carácter
mov al, byte ptr [rax] ; AL = username[0]
movzx eax, al ; EAX = (int)username[0]
¿Qué hace c_str()?
- Devuelve puntero al buffer interno de
std::string - Equivalente a:
const char* ptr = username.c_str();
Pseudocódigo:
Parte 4: Cálculo de Dirección de Salto¶
Código desensamblado:
; Cargar dirección base (main)
mov rdi, offset main ; RDI = dirección de main
; Sumar offset fijo (0xBB)
add rdi, 0BBh ; RDI = main + 0xBB
; Sumar primer carácter del username
add rdi, rax ; RDI = main + 0xBB + username[0]
; Guardar dirección calculada
mov [rbp-8], rdi ; Guardar en variable local
; Cargar en RBX y saltar
mov rbx, [rbp-8]
call rbx ; Saltar a la dirección calculada
Fórmula del salto:
Ejemplo:
Si main = 0x401000
Si username[0] = 'j' (ASCII 0x6A = 106)
Destino = 0x401000 + 0xBB + 0x6A
= 0x401000 + 0x125
= 0x401125
Pseudocódigo:
void* jump_addr = (void*)((size_t)main + 0xBB + (size_t)first_char);
((void(*)())jump_addr)(); // Call function pointer
Parte 5: Destinos Válidos¶
Análisis del código:
Si queremos saltar al inicio de la validación (donde debería ir normalmente):
00401123: nop ; Alineamiento
00401124: nop
00401125: mov dword ptr [rbp-4], 101h ; Código de validación
Dirección objetivo: 0x401125
Cálculo inverso:
0x401125 = main + 0xBB + username[0]
0x401125 = 0x401000 + 0xBB + username[0]
username[0] = 0x401125 - 0x401000 - 0xBB
username[0] = 0x125 - 0xBB
username[0] = 0x6A
Caracteres ASCII:
Pero hay un problema...
Stack Alignment y Crashes¶
Problema: Call sin Return¶
El código hace:
Pero NO hay:
Consecuencia:
- El
callpushea la dirección de retorno al stack - RSP se decrementa en 8 bytes
- El stack se desalinea
Stack antes del call:
Stack después del call:
Por Qué Importa el Alignment¶
Windows x64 ABI requiere:
- Stack alineado a 16 bytes al entrar a una función
- Las APIs de Windows asumen este alineamiento
- Instrucciones SSE/AVX requieren alineamiento
¿Qué pasa si no está alineado?
- Windows 10: Funciona (por suerte)
- Windows 11: Crash en llamadas a APIs (
printf,ExitProcess, etc.) - Algunas funciones STL: Crash inmediato
Solución 1: Ajustar el Salto para Evitar el Call¶
En vez de saltar a 0x401125, saltar después del prólogo de la función destino (donde el stack ya está alineado).
No viable: Rompe el flujo esperado del programa.
Solución 2: Caracteres Alternativos (Trampa del Challenge)¶
Descubrimiento: Algunos caracteres saltan a lugares donde sí funciona.
Ejemplos encontrados:
username[0] = '{' (0x7B) → Salta a código válido
username[0] = '}' (0x7D) → Salta a código válido
username[0] = '~' (0x7E) → Funciona parcialmente
Calculando:
En 0x401136:
00401136: sub rsp, 28h ; Ajusta el stack
00401138: nop
00401139: nop
0040113A: mov dword ptr [rbp-4], 101h ; Código de validación
¡Funciona! Porque el sub rsp, 28h realinea el stack.
Solución 3: Parchear el Binario (No Recomendado)¶
Agregar instrucción de alineamiento:
Razón para no hacerlo: El challenge pide keygen, no parche.
Variables Locales del Challenge¶
Stack frame completo:
rbp-68h: username (std::string, 32 bytes)
rbp-48h: password (std::string, 32 bytes)
rbp-28h: n (int, valor calculado = 0x100 o 0x101)
rbp-8h: jump_address (void*, dirección calculada)
Renombrar variables:
Resultado:
std::string username; // rbp-68h
std::string password; // rbp-48h
int n = 0x100; // rbp-28h, cambia según input
void* jump_addr; // rbp-8h
Parte 5: Limpieza del Decompiler y Corrección del Stack Pointer¶
Contexto: Argumentos Fantasma por Ofuscación¶
Después de aplicar todas las técnicas anteriores, el decompiler de IDA muestra argumentos incorrectos en las funciones debido a la ofuscación.
Problema visible:
// Función con argumentos fantasma:
void nivel_7(int case_num, int arg2, int arg3, void* arg4, char* arg5, ...);
// Debería ser:
void nivel_7(int case_num);
Causa: La ofuscación rompió el análisis de la calling convention, y IDA asume que hay más argumentos de los que realmente existen.
Corrección de Prototipos de Funciones¶
Paso 1: Identificar Argumentos Reales¶
Comparar con otros cases del switch:
; Case 1, 2, 3, etc.:
mov ecx, 1 ; Solo un argumento
call nivel_1
; Case 7 (ofuscado):
mov ecx, 7
; ... código ofuscado ...
call nivel_7 ; Debería tener solo un argumento
Todos los cases del switch tienen un solo argumento (el número de case en rcx).
Paso 2: Editar el Prototipo¶
En la función nivel_7:
- Presionar
Yen el nombre de la función - Aparece el editor de tipo:
- Eliminar argumentos extras:
- Enter para aplicar
Resultado en el decompiler:
Paso 3: Corregir Función de Validación¶
Problema similar en validate_credentials:
// Incorrecto:
bool validate_credentials(std::string& user, std::string& pass,
int arg3, void* arg4, int arg5);
// Correcto:
bool validate_credentials(std::string& user, std::string& pass);
Aplicar el mismo proceso: Y → Editar → Eliminar argumentos extras
Corrección Manual del Stack Pointer¶
Problema: RSP Tracking Incorrecto¶
Síntoma en IDA:
Causa: El call rbx (salto calculado) confunde al análisis de stack de IDA.
Activar Vista de Stack Pointer¶
Opción de IDA:
Marcar: "Show stack pointer"
Vista en desensamblador:
00401000: push rbp ; [sp+0]
00401001: mov rbp, rsp ; [sp+8]
00401004: sub rsp, 98h ; [sp+98h] ← Aquí debe quedarse
...
00401050: call some_func ; [sp+98h] ← Correcto
00401055: call rbx ; [sp+A0h] ← ¡INCORRECTO!
IDA asume que el call rbx cambia RSP, pero en nuestro caso sabemos que es un salto controlado.
Forzar el Valor Correcto¶
En la instrucción problemática:
- Posicionarse en
call rbx - Clic derecho → Change stack pointer
- Ingresar: 0 (delta = 0, no cambia RSP)
Antes:
Después:
00401055: call rbx ; [sp+98h] ← Forzado a 98h
0040105A: add rsp, 10h ; [sp+A8h] ← Ahora es consistente
Repetir para todos los call ofuscados donde IDA se confunde.
Limpieza de Jump-Outs y Código Muerto¶
Identificar Código Inalcanzable¶
Patrón típico:
00401100: jnz short loc_102D
00401102: retn 0 ; C2 00 - Nunca se ejecuta
00401104: nop ; Código real
El retn 0 (byte C2 00) es código muerto que solo confunde a IDA.
Parchear Temporalmente para Análisis Estático¶
¡IMPORTANTE! Estos parches son solo para análisis estático, no para ejecución.
Proceso:
- Posicionarse en el byte problemático (ej:
C2en offset 102) - Presionar Edit > Patch program > Change byte
- Cambiar
C2por90(NOP) - Presionar
Cpara recrear código
Ejemplo:
; Antes:
00401102: db 0C2h ; Basura
00401103: db 00h
00401104: nop
; Después (parcheado):
00401102: nop ; 90
00401103: nop ; 90
00401104: nop
Resultado: Los "jump-outs" (flechas rojas en el grafo) desaparecen.
Script para Parchear Múltiples Bytes¶
Si hay muchos bytes C2 o 48 sueltos:
import ida_bytes
# Direcciones con bytes basura
basura = [0x401102, 0x401156, 0x401234, 0x401ABC]
for addr in basura:
ida_bytes.patch_byte(addr, 0x90) # Cambiar a NOP
ida_ua.create_insn(addr) # Recrear código
print("Limpieza completada")
Ejecución: File > Script command > Python
Renombrado Final y Documentación¶
Variables y Funciones Clave¶
Renombrar con N:
| Nombre Original | Nombre Descriptivo |
|---|---|
sub_401000 |
nivel_7 |
sub_401500 |
validate_credentials |
off_403000 |
ptr_nivel_7 |
var_48 |
username |
var_28 |
password |
var_8 |
jump_address |
Agregar comentarios con ;:
; Calcular dirección de salto basada en username[0]
mov rdi, offset nivel_7
add rdi, 0BBh
add rdi, rax ; rax = username[0]
Estado Final del Pseudocódigo¶
Antes de limpieza:
void __fastcall sub_401000(int a1, int a2, int a3, void *a4,
char *a5, int a6, __int64 a7) {
char v8[20];
char v9[20];
// ... código confuso ...
}
Después de limpieza:
void __fastcall nivel_7(int case_num) {
std::string username;
std::string password;
void* jump_addr;
std::cout << "User Authentication System\n";
std::cout << "Enter username: ";
std::getline(std::cin, username);
std::cout << "Enter password: ";
std::getline(std::cin, password);
// Calcular salto basado en primer carácter
jump_addr = (void*)((size_t)&nivel_7 + 0xBB + username.c_str()[0]);
// Saltar (desalineado, pero ya lo corregimos)
((void(*)())jump_addr)();
// Validación
if (validate_credentials(username, password)) {
std::cout << "Access granted!\n";
} else {
std::cout << "Authentication failed!\n";
}
}
Mucho más legible que el código ofuscado original.
Snapshot para Preservar el Trabajo¶
¡Crítico! Antes de continuar con más análisis:
¿Por qué?
- Los parches (
90en vez deC2) no son para ejecución - Si guardamos y ejecutamos el binario, crasheará
- El snapshot preserva el estado "limpio para análisis estático"
Flujo de trabajo:
- Snapshot "limpio_para_estatico": Para análisis estático en decompiler
- IDB original sin parches: Para debugging dinámico con x64dbg/WinDbg
Casos Especiales Encontrados¶
Función con Puntero + Tamaño¶
Prototipo incorrecto:
Análisis del desensamblado:
Prototipo correcto:
Aplicar con Y:
Security Cookie Check¶
Código al final de funciones:
Si falla: Salta a __report_gsfailure y llama a ExitProcess.
Nota: No es parte de la lógica del challenge, es protección del compilador. Ignorar en el análisis.
Desofuscar Expresiones Automáticamente¶
IDA tiene una función útil:
Ejemplo:
// Antes:
v5 = ((unsigned __int64)(v3 + 0x1234) ^ 0x5678) - 0x9ABC;
// Después:
v5 = v3 + 0xABCD; // Simplificado
No siempre funciona, pero vale la pena intentar.
Parte 6: Notas Finales y Workflow Recomendado¶
Orden de Aplicación de Técnicas¶
Workflow completo para un binario ofuscado con C++:
- Detección del compilador:
- Usar Detect It Easy
-
Identificar: MSVC, Intel C++, MinGW, Clang, etc.
-
Desofuscación (si aplica):
- Identificar patrones de ofuscación
- Crear script IDAPython
-
Aplicar y hacer snapshot del IDB
-
FLIRT básico:
- Intentar cargar firmas estándar de IDA
-
Ver cuántas funciones se detectan
-
FLIRT personalizado (si falla el estándar):
- Generar código de prueba con ChatGPT
- Compilar con mismo compilador:
/MD+ sin optimizaciones (/Od) - idb2pat → sigmake → .sig
-
Aplicar al challenge
-
Bindiff (si los nombres siguen rotos):
- Compilar versión con Visual Studio
- Ejecutar Bindiff entre Intel y VS
-
Importar símbolos
-
Importación de estructuras:
- Exportar typeinfo del IDB con símbolos
- Importar en el challenge
-
Aplicar estructuras al stack frame
-
Análisis manual:
- Renombrar variables
- Entender lógica
- Escribir keygen
Comandos y Atajos Esenciales¶
IDA Pro:
| Atajo | Función |
|---|---|
Space |
Toggle vista Graph/Text |
Shift+F12 |
Ver strings |
Alt+M |
Crear bookmark |
N |
Renombrar variable/función |
Y |
Cambiar tipo de variable |
U |
Undefinir (convertir a bytes) |
C |
Crear código desde bytes |
H |
Toggle hex/decimal |
; |
Agregar comentario |
X |
Ver referencias cruzadas (xrefs) |
G |
Saltar a dirección |
Shift+F1 |
Abrir Local Types |
IDAPython (consola o script):
import ida_bytes
import ida_ua
import ida_auto
import idaapi
# Undefinir byte
ida_bytes.del_items(addr, ida_bytes.DELIT_SIMPLE, size)
# Crear instrucción
ida_ua.create_insn(addr)
# Analizar desde inicio hasta fin
ida_auto.auto_wait()
# Get min/max address
min_ea = idaapi.ida_inf_get_min_ea()
max_ea = idaapi.ida_inf_get_max_ea()
Errores Comunes y Soluciones¶
1. "Bindiff dice que el IDB está abierto"
- Cerrar todas las instancias de IDA
- Si persiste: Copiar el
.i64a otra carpeta y probar de nuevo - En IDA Free: Usar workaround con Binexport + línea de comandos
2. "FLIRT no detecta nada o ve pocos símbolos"
- Verificar que el compilador coincida
- Verificar que las optimizaciones están desactivadas (
/Od) - Verificar que usas
/MD(dynamic linking) - Verificar que el binario tiene símbolos (.pdb)
3. "Símbolos importados con Bindiff están incompletos"
- Filtrar por similitud > 0.90
- Verificar que ambos ejecutables usen el mismo código fuente
4. "Script IDAPython da error al ejecutar"
- Verificar sintaxis de Python 3
- Usar
print()para debugging - IDA Free no soporta IDAPython
5. "Estructuras importadas no aparecen"
- Verificar que el script
.idcse ejecutó sin errores - Recargar IDA si es necesario
- Asegurarse de que el IDB origen tenga las estructuras
Recursos Adicionales¶
Herramientas:
Resumen Final¶
✅ Desofuscación estática con scripts IDAPython automatizados
✅ Creación de FLIRT personalizado para detectar funciones embebidas de C++
✅ Uso de Bindiff para reconstruir símbolos desde ejecutables con compiladores diferentes
✅ Importación de estructuras STL para mejorar la legibilidad del pseudocódigo
✅ Análisis de stack alignment y problemas de calling convention en x64
Lección clave: El reversing profesional requiere automatización y herramientas personalizadas. Un script de 10 líneas puede ahorrar horas de trabajo manual.