Saltar a contenido

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)

  1. Ejecutar el binario en x64dbg o IDA Debugger
  2. Trasear (F7) hasta la zona ofuscada
  3. Observar cómo el código "se arregla" automáticamente al ejecutarse
  4. 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:

  1. Cambiar a vista de texto (barra espaciadora en IDA)
  2. Activar visualización de bytes: Options > General > Number of opcode bytes
  3. Identificar el salto: jnz short loc_102C+1
  4. Ubicar el byte destino (102D)
  5. Presionar U en la dirección 102C para "undefinir" el retn
  6. Ahora la dirección 102D existe
  7. Presionar C en 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:

85 ED 75 01 C2    ; test ebp, ebp; jnz +1; retn

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:

85 ED 75 01 C2    ; Común (5 veces)

Y variantes que requieren corrección manual:

85 ED 75 01 C3    ; Usa retn en vez de retn 0
85 ED 75 03 C3    ; Salta +3 bytes en vez de +1

Bytes Residuales (0x48)

Después de aplicar el script, suelen quedar bytes 0x48 sin desensamblar. Solución:

  • Presionar C sobre 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 carpeta flair/)

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:

Properties > C/C++ > Code Generation > Runtime Library

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:

  1. Intel C++ Compiler (para el challenge)
  2. Visual Studio Compiler (para Bindiff posterior)

Paso 3: Convertir IDB a Archivo .pat

En IDA Pro/Classroom:

  1. Abrir el ejecutable compilado con símbolos (archivo .pdb)
  2. Verificar que los nombres estén correctos: Options > Demangle names
  3. Ejecutar: File > Script file > idb2pat.py
  4. 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:

sigmake.exe [-n nombre] <archivo.pat> <archivo.sig>

Problema común: Colisiones

Si sigmake dice:

console_app.exc: 5 collision(s)

Solución:

  1. Se generó un archivo .exc con las colisiones
  2. Abrir console_app.exc con Notepad++
  3. Comentar las líneas con ; al principio:
;00401234 05 memcpy....
;00401240 06 strlen....
  1. 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):

  1. File > Load file > FLIRT signature file
  2. Seleccionar console_app.sig
  3. IDA aplica las firmas automáticamente

Antes:

sub_401234
sub_401250
sub_401ABC

Después:

std::basic_string::operator<<
printf
std::allocator::allocate

Limitaciones y Casos Especiales

Problema: Compilador Intel y Name Mangling

Síntoma: Los nombres en Release con Intel C++ salen así:

??$operator@_W@std@@blah___

En vez de:

std::operator<<(std::string&, char)

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:

  1. Abrir el ejecutable en DIE
  2. Ver "Compiler" y "Linker"

Ejemplo:

Compiler: Microsoft Visual C++ 2022 (19.34)
Linker: Microsoft Linker 14.34

Con Intel C++:

Compiler: Intel C++ (info incomplete)
Linker: Microsoft Linker (no version)

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:
??$operator@___W@std@@blah___
  • El FLIRT generado no funciona en el challenge porque los bytes no coinciden exactamente

Solución: Compilar dos versiones del mismo código:

  1. Con Intel C++ (sin símbolos legibles)
  2. 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:

2. Instalar normalmente:

C:\Program Files\BinDiff\

3. Descargar librerías parcheadas:

Opción A (GitHub - cfox):

https://github.com/google/bindiff/issues/XX
(Buscar: "IDA 9 support")

Bajar los archivos:

  • zynamics_binexport_12_ida64.dll
  • zynamics_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):

C:\Users\[Usuario]\AppData\Roaming\Hex-Rays\IDA Pro\plugins\

Archivos a reemplazar:

  • zynamics_binexport_12_ida64.dll
  • zynamics_bindiff_10_ida64.dll

Ubicación 2 (instalación de Bindiff):

C:\Program Files\BinDiff\plugins\

¡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:

Edit > Plugins > Bindiff ✓
Edit > Plugins > Binexport ✓

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:

IDA Free > Edit > Plugins > 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:

Edit > Plugins > Bindiff > Add existing diff results

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:

  1. Abrir console_app_vs.exe (con símbolos correctos gracias a /MD y optimizaciones desactivadas)
  2. Options > Demangle names → Verificar que los nombres sean claramente visibles
  3. Cerrar

  4. Abrir console_app_intel.exe (compilado con Intel C++)

  5. Options > Demangle names → Ver que los nombres pueden no estar tan claros
  6. No cerrar este

Paso 3: Ejecutar Bindiff

Con console_app_intel.exe abierto:

Edit > Plugins > Bindiff > Diff database...

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:

  1. En la pestaña Matched Functions
  2. Ctrl+A (seleccionar todas)
  3. Clic derecho → Import Symbol & Comments

Alternativamente (selectivo):

  • Filtrar por similitud > 0.90
  • Importar solo las de alta confianza

IDA aplica los cambios:

Primary antes:     ??$operator@_W@std@@___
Primary después:   std::basic_string::operator<<

Cerrar Bindiff: File > Close

Paso 6: Verificar en IDA

En la ventana de IDA (Intel C++):

View > Open subviews > Functions

Antes:

??$operator@_W@std@@___
??allocator@std@@___
sub_401234

Después:

std::basic_string::operator<<
std::allocator::allocate
printf

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:

Clic derecho en función > Matched Function > View

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:

Structures: 65 (solo las básicas de Windows)

Necesitamos:

Structures: 706 (incluyendo toda la STL)

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):

File > Produce file > Dump typeinfo to IDC file...

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:

File > Script file... > Seleccionar structures.idc

IDA ejecuta el script silenciosamente.

Verificar:

View > Open subviews > Local Types (Shift+F1)

Antes: 65 estructuras
Después: 706 estructuras

Buscar std:::

std::string
std::allocator<char>
std::char_traits<char>
std::vector<int>
...

Aplicación de Estructuras en el Código

Identificar std::string en el Stack

Código desensamblado:

lea     rcx, [rsp+68h+username]   ; RCX = puntero al stack
call    std::string::basic_string ; Constructor

Pseudocódigo (antes de aplicar estructura):

char username[32];  // IDA asume array de bytes

¿Cómo saber que es std::string?

  1. Se llama al constructor std::string::basic_string
  2. El tamaño en el stack es 0x20 bytes (32 decimal)
  3. En Local Types, std::string tiene tamaño 0x20

Verificar tamaño de std::string:

Local Types > std::string > Clic derecho > Edit
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:

username = byte ptr -48h

Pasos:

  1. Posicionarse en username
  2. Presionar Y (cambiar tipo)
  3. Escribir: std::string
  4. Enter

Ahora muestra:

username = std::string ptr -48h

Pseudocódigo (después):

std::string username;
std::string password;

Repetir para password:

password = byte ptr -28h

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:

    const char* user_cstr = username.c_str();
    char first_char = user_cstr[0];

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:

Dirección destino = main + 0xBB + (int)username[0]

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:

0x68 = 'h'
0x69 = 'i'
0x6A = 'j'  ← Valid
0x6B = 'k'  ← Valid

Pero hay un problema...

Stack Alignment y Crashes

Problema: Call sin Return

El código hace:

call    rbx    ; Salta a dirección calculada

Pero NO hay:

ret            ; Para volver

Consecuencia:

  1. El call pushea la dirección de retorno al stack
  2. RSP se decrementa en 8 bytes
  3. El stack se desalinea

Stack antes del call:

RSP = 0x...F0  (termina en 0, alineado a 16 bytes)

Stack después del call:

RSP = 0x...E8  (termina en 8, desalineado)

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:

Destino = 0x401000 + 0xBB + 0x7B = 0x401136

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:

and    rsp, 0FFFFFFFFFFFFFFF0h  ; Alinear a 16 bytes

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:

Posicionarse en cada offset → Presionar N → Escribir nombre

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:

  1. Presionar Y en el nombre de la función
  2. Aparece el editor de tipo:
void nivel_7(int case_num, int arg2, int arg3, void* arg4, char* arg5);
  1. Eliminar argumentos extras:
void nivel_7(int case_num);
  1. Enter para aplicar

Resultado en el decompiler:

// Antes:
nivel_7(7, v5, v6, ptr, str);

// Después:
nivel_7(7);

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:

[SP+98h] warning: SP value is incorrect

Causa: El call rbx (salto calculado) confunde al análisis de stack de IDA.

Activar Vista de Stack Pointer

Opción de IDA:

Options > General > Stack pointer

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:

  1. Posicionarse en call rbx
  2. Clic derecho → Change stack pointer
  3. Ingresar: 0 (delta = 0, no cambia RSP)

Antes:

00401055: call    rbx           ; [sp+A0h]
0040105A: add     rsp, 10h      ; [sp+B0h]

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:

  1. Posicionarse en el byte problemático (ej: C2 en offset 102)
  2. Presionar Edit > Patch program > Change byte
  3. Cambiar C2 por 90 (NOP)
  4. Presionar C para 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:

File > Take database snapshot...
Nombre: "limpio_para_estatico"

¿Por qué?

  • Los parches (90 en vez de C2) 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:

  1. Snapshot "limpio_para_estatico": Para análisis estático en decompiler
  2. IDB original sin parches: Para debugging dinámico con x64dbg/WinDbg

Casos Especiales Encontrados

Función con Puntero + Tamaño

Prototipo incorrecto:

void mystery_func(void* ptr, int arg2, int arg3, int arg4, ...);

Análisis del desensamblado:

lea     rcx, [rbp-48h]  ; Primer argumento
mov     edx, 1000h      ; 4096 decimal
call    mystery_func

Prototipo correcto:

void mystery_func(void* buffer, size_t size);

Aplicar con Y:

void mystery_func(void* buffer, size_t size);

Código al final de funciones:

mov     rcx, [rbp+8]
xor     rcx, rbp
call    __security_check_cookie

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:

Clic derecho en expresión confusa > Deobfuscate > Simplify arithmetic

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++:

  1. Detección del compilador:
  2. Usar Detect It Easy
  3. Identificar: MSVC, Intel C++, MinGW, Clang, etc.

  4. Desofuscación (si aplica):

  5. Identificar patrones de ofuscación
  6. Crear script IDAPython
  7. Aplicar y hacer snapshot del IDB

  8. FLIRT básico:

  9. Intentar cargar firmas estándar de IDA
  10. Ver cuántas funciones se detectan

  11. FLIRT personalizado (si falla el estándar):

  12. Generar código de prueba con ChatGPT
  13. Compilar con mismo compilador: /MD + sin optimizaciones (/Od)
  14. idb2pat → sigmake → .sig
  15. Aplicar al challenge

  16. Bindiff (si los nombres siguen rotos):

  17. Compilar versión con Visual Studio
  18. Ejecutar Bindiff entre Intel y VS
  19. Importar símbolos

  20. Importación de estructuras:

  21. Exportar typeinfo del IDB con símbolos
  22. Importar en el challenge
  23. Aplicar estructuras al stack frame

  24. Análisis manual:

  25. Renombrar variables
  26. Entender lógica
  27. 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 .i64 a 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 .idc se 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.