
Desensambla binarios PE/ELF x86/x64, resuelve ramas indirectas a través de GOT, tablas de saltos y rastreo de registros, y exporta el grafo de flujo de control completo como JSON estructurado para análisis estático y parcheo.
Extractor de CFG para PE/ELF x86/x64. Toma un binario, lo desensambla, resuelve cada salto y llamada (GOT, tablas de saltos, trazado de registros), y exporta el grafo de flujo de control completo como JSON estructurado.
Necesitas saber exactamente a dónde va cada rama. No para leer — para parchar. Alimenta el JSON en Zydis o AsmJit, localiza la instrucción exacta que necesitas enganchar o modificar, y escribe de vuelta. Los equipos anti-trampas lo usan para mapear binarios de juegos. Los analistas de RE lo usan para elevar código a sus propios pipelines de análisis. Los analistas de software trazan rutas de ejecución sin ejecutar el binario.
cfgrip te da el mapa. Lo que hagas con él depende de ti.
Para cada binario que cfgrip procesa, produce:
.pdata), nombres (punto de entrada, exportaciones, descubiertas), y etiquetado de thunk (stubs PLT).pdata, punteros a funciones en sección de datos) para máxima coberturaCon --clean, adicionalmente:
jmp → jmp en aristas directasstack_offset (delta de RSP desde la entrada de la función)xrefs que mapea cada destino de llamada/salto de vuelta a sus llamantesManeja ramas indirectas mediante:
mov/lea para encontrar direcciones concretascfgrip [--subs-only] [--clean] <binary>
Aliméntalo con un binario, obtén <binary>.cfg como salida.
Usa --subs-only para extraer solo funciones alcanzables desde el punto de entrada a través del grafo de llamadas — omite exportaciones no alcanzables y candidatos de prólogo.
Usa --clean para aplicar encadenamiento de saltos, poda de bloques muertos, seguimiento de desplazamientos de pila y análisis de referencias cruzadas.
Ejemplo:
cfgrip.exe tests\example1.exe
format: PE
arch: x86-64
entry: 0x1400054bc
imports: 85
0x140020000 EncodePointer (KERNEL32.dll)
0x140020008 DecodePointer (KERNEL32.dll)
0x140020010 EnterCriticalSection (KERNEL32.dll)
0x140020018 LeaveCriticalSection (KERNEL32.dll)
0x140020020 InitializeCriticalSectionEx (KERNEL32.dll)
0x140020028 DeleteCriticalSection (KERNEL32.dll)
0x140020030 MultiByteToWideChar (KERNEL32.dll)
0x140020038 WideCharToMultiByte (KERNEL32.dll)
0x140020040 LCMapStringEx (KERNEL32.dll)
0x140020048 GetStringTypeW (KERNEL32.dll)
0x140020050 GetCPInfo (KERNEL32.dll)
0x140020058 RtlCaptureContext (KERNEL32.dll)
0x140020060 RtlLookupFunctionEntry (KERNEL32.dll)
0x140020068 RtlVirtualUnwind (KERNEL32.dll)
0x140020070 UnhandledExceptionFilter (KERNEL32.dll)
0x140020078 SetUnhandledExceptionFilter (KERNEL32.dll)
0x140020080 GetCurrentProcess (KERNEL32.dll)
0x140020088 TerminateProcess (KERNEL32.dll)
0x140020090 IsProcessorFeaturePresent (KERNEL32.dll)
0x140020098 QueryPerformanceCounter (KERNEL32.dll)
0x1400200a0 GetCurrentProcessId (KERNEL32.dll)
0x1400200a8 GetCurrentThreadId (KERNEL32.dll)
0x1400200b0 GetSystemTimeAsFileTime (KERNEL32.dll)
0x1400200b8 InitializeSListHead (KERNEL32.dll)
0x1400200c0 IsDebuggerPresent (KERNEL32.dll)
0x1400200c8 GetStartupInfoW (KERNEL32.dll)
0x1400200d0 GetModuleHandleW (KERNEL32.dll)
0x1400200d8 WriteConsoleW (KERNEL32.dll)
0x1400200e0 RtlPcToFileHeader (KERNEL32.dll)
0x1400200e8 RaiseException (KERNEL32.dll)
0x1400200f0 RtlUnwindEx (KERNEL32.dll)
0x1400200f8 GetLastError (KERNEL32.dll)
0x140020100 SetLastError (KERNEL32.dll)
0x140020108 InitializeCriticalSectionAndSpinCount (KERNEL32.dll)
0x140020110 TlsAlloc (KERNEL32.dll)
0x140020118 TlsGetValue (KERNEL32.dll)
0x140020120 TlsSetValue (KERNEL32.dll)
0x140020128 TlsFree (KERNEL32.dll)
0x140020130 FreeLibrary (KERNEL32.dll)
0x140020138 GetProcAddress (KERNEL32.dll)
0x140020140 LoadLibraryExW (KERNEL32.dll)
0x140020148 GetStdHandle (KERNEL32.dll)
0x140020150 WriteFile (KERNEL32.dll)
0x140020158 GetModuleFileNameW (KERNEL32.dll)
0x140020160 ExitProcess (KERNEL32.dll)
0x140020168 GetModuleHandleExW (KERNEL32.dll)
0x140020170 GetCommandLineA (KERNEL32.dll)
0x140020178 GetCommandLineW (KERNEL32.dll)
0x140020180 HeapAlloc (KERNEL32.dll)
0x140020188 HeapFree (KERNEL32.dll)
0x140020190 FlsAlloc (KERNEL32.dll)
0x140020198 FlsGetValue (KERNEL32.dll)
0x1400201a0 FlsSetValue (KERNEL32.dll)
0x1400201a8 FlsFree (KERNEL32.dll)
0x1400201b0 VirtualProtect (KERNEL32.dll)
0x1400201b8 CompareStringW (KERNEL32.dll)
0x1400201c0 LCMapStringW (KERNEL32.dll)
0x1400201c8 GetLocaleInfoW (KERNEL32.dll)
0x1400201d0 IsValidLocale (KERNEL32.dll)
0x1400201d8 GetUserDefaultLCID (KERNEL32.dll)
0x1400201e0 EnumSystemLocalesW (KERNEL32.dll)
0x1400201e8 GetFileType (KERNEL32.dll)
0x1400201f0 CloseHandle (KERNEL32.dll)
0x1400201f8 FlushFileBuffers (KERNEL32.dll)
0x140020200 GetConsoleOutputCP (KERNEL32.dll)
0x140020208 GetConsoleMode (KERNEL32.dll)
0x140020210 ReadFile (KERNEL32.dll)
0x140020218 GetFileSizeEx (KERNEL32.dll)
0x140020220 SetFilePointerEx (KERNEL32.dll)
0x140020228 ReadConsoleW (KERNEL32.dll)
0x140020230 HeapReAlloc (KERNEL32.dll)
0x140020238 FindClose (KERNEL32.dll)
0x140020240 FindFirstFileExW (KERNEL32.dll)
0x140020248 FindNextFileW (KERNEL32.dll)
0x140020250 IsValidCodePage (KERNEL32.dll)
0x140020258 GetACP (KERNEL32.dll)
0x140020260 GetOEMCP (KERNEL32.dll)
0x140020268 GetEnvironmentStringsW (KERNEL32.dll)
0x140020270 FreeEnvironmentStringsW (KERNEL32.dll)
0x140020278 SetEnvironmentVariableW (KERNEL32.dll)
0x140020280 SetStdHandle (KERNEL32.dll)
0x140020288 GetProcessHeap (KERNEL32.dll)
0x140020290 HeapSize (KERNEL32.dll)
0x140020298 CreateFileW (KERNEL32.dll)
0x1400202a0 RtlUnwind (KERNEL32.dll)
functions: 1975
indirect targets: 3453
cfg written to: tests\example1.exe.cfg
El archivo .cfg es JSON estructurado. Esto es lo que parece:
{
"binary": "tests\\example1.exe",
"mode": "full",
"arch": "x86-64",
"format": "PE",
"entry_point": "0x1400054bc",
"imports": [
{
"address": "0x140020000",
"name": "EncodePointer",
"library": "KERNEL32.dll"
},
{
"address": "0x140020008",
"name": "DecodePointer",
"library": "KERNEL32.dll"
},
{
"address": "0x140020010",
"name": "EnterCriticalSection",
"library": "KERNEL32.dll"
},
{
"address": "0x140020018",
"name": "LeaveCriticalSection",
"library": "KERNEL32.dll"
},
{
"address": "0x140020020",
"name": "InitializeCriticalSectionEx",
"library": "KERNEL32.dll"
},
...
...
...
"functions": [
{
"address": "0x1400054bc",
"name": "entry",
"blocks": [
{
"address": "0x1400054bc",
"size": 4,
"is_prolog": false,
"is_epilog": false,
"instructions": [
{
"address": "0x1400054bc",
"size": 4,
"mnemonic": "sub",
"operands": "rsp, 0x28"
},
{
"address": "0x1400054c0",
"size": 5,
"mnemonic": "call",
"operands": "0x140005d30"
},
{
"address": "0x1400054c5",
"size": 4,
"mnemonic": "add",
"operands": "rsp, 0x28"
},
{
"address": "0x1400054c9",
"size": 5,
"mnemonic": "jmp",
"operands": "0x140005340"
}
],
"successors": [
"0x140005340"
]
},
{
"address": "0x140005340",
"size": 8,
"is_prolog": false,
"is_epilog": false,
"instructions": [
{
"address": "0x140005340",
"size": 5,
"mnemonic": "mov",
"operands": "qword ptr [rsp + 8], rbx"
},
{
"address": "0x140005345",
"size": 5,
"mnemonic": "mov",
"operands": "qword ptr [rsp + 0x10], rsi"
},
{
"address": "0x14000534a",
"size": 1,
"mnemonic": "push",
"operands": "rdi"
},
{
"address": "0x14000534b",
"size": 4,
"mnemonic": "sub",
"operands": "rsp, 0x30"
},
{
"address": "0x14000534f",
"size": 5,
"mnemonic": "mov",
"operands": "ecx, 1"
},
{
"address": "0x140005354",
"size": 5,
"mnemonic": "call",
"operands": "0x14000550c"
},
{
"address": "0x140005359",
"size": 2,
"mnemonic": "test",
"operands": "al, al"
},
{
"address": "0x14000535b",
"size": 6,
"mnemonic": "je",
"operands": "0x140005497"
}
],
"successors": [
"0x140005497",
"0x140005361"
]
},
{
"address": "0x140005497",
"size": 15,
"is_prolog": false,
"is_epilog": false,
"instructions": [
{
"address": "0x140005497",
"size": 5,
"mnemonic": "mov",
"operands": "ecx, 7"
},
{
"address": "0x14000549c",
"size": 5,
"mnemonic": "call",
"operands": "0x140005e44"
},
{
"address": "0x1400054a1",
"size": 1,
"mnemonic": "nop",
"operands": ""
},
{
"address": "0x1400054a2",
"size": 5,
"mnemonic": "mov",
"operands": "ecx, 7"
},
{
"address": "0x1400054a7",
"size": 5,
"mnemonic": "call",
"operands": "0x140005e44"
},
{
"address": "0x1400054ac",
"size": 2,
"mnemonic": "mov",
"operands": "ecx, ebx"
},
{
"address": "0x1400054ae",
"size": 5,
"mnemonic": "call",
"operands": "0x14000ec14"
},
{
"address": "0x1400054b3",
"size": 1,
"mnemonic": "nop",
"operands": ""
},
{
"address": "0x1400054b4",
"size": 2,
"mnemonic": "mov",
"operands": "ecx, ebx"
},
{
"address": "0x1400054b6",
"size": 5,
"mnemonic": "call",
"operands": "0x14000ebcc"
},
{
"address": "0x1400054bb",
"size": 1,
"mnemonic": "nop",
"operands": ""
},
{
"address": "0x1400054bc",
"size": 4,
"mnemonic": "sub",
"operands": "rsp, 0x28"
},
{
"address": "0x1400054c0",
"size": 5,
"mnemonic": "call",
"operands": "0x140005d30"
},
{
"address": "0x1400054c5",
"size": 4,
"mnemonic": "add",
"operands": "rsp, 0x28"
},
{
"address": "0x1400054c9",
"size": 5,
"mnemonic": "jmp",
"operands": "0x140005340"
}
],
"successors": [
"0x140005340"
]
},
...
...
...
Cada función ahora incluye campos opcionales:
end_address — final preciso de la función cuando esté disponible (desde la tabla de excepciones .pdata de PE), de lo contrario se calcula como la dirección de instrucción máxima entre todos los bloquesis_thunk — true para stubs PLT y thunks de importación (funciones que solo redirigen a otra dirección)--subs-only{
"binary": "C:\\binaries\\target.exe",
"mode": "subs-only",
"arch": "x86-64",
"format": "PE",
"entry_point": "0x1400054bc",
"imports": [ ... ],
"indirect_targets": [ ... ],
"functions": [
{
"address": "0x1400054bc",
"name": "entry",
"blocks": [ ... ]
},
...
]
}
El campo "mode": "subs-only" indica a las herramientas posteriores que este CFG solo contiene funciones alcanzables desde el punto de entrada. Las exportaciones no alcanzables y los candidatos de prólogo se excluyen — menos funciones, superficie de análisis más limpia.
--clean{
"binary": "C:\\binaries\\target.exe",
"mode": "full+clean",
"arch": "x86-64",
"format": "PE",
"entry_point": "0x1400054bc",
"imports": [ ... ],
"indirect_targets": [ ... ],
"functions": [
{
"address": "0x1400054bc",
"name": "entry",
"blocks": [
{
"address": "0x1400054bc",
"size": 4,
"is_prolog": false,
"is_epilog": false,
"instructions": [
{
"address": "0x1400054bc",
"size": 4,
"mnemonic": "sub",
"operands": "rsp, 0x28",
"stack_offset": 0
},
{
"address": "0x1400054c0",
"size": 5,
"mnemonic": "call",
"operands": "0x140005d30",
"stack_offset": -40
},
{
"address": "0x1400054c5",
"size": 4,
"mnemonic": "add",
"operands": "rsp, 0x28",
"stack_offset": -40
},
{
"address": "0x1400054c9",
"size": 5,
"mnemonic": "jmp",
"operands": "0x140005340",
"stack_offset": 0
}
],
"successors": [ "0x140005340" ]
},
...
]
},
...
],
"xrefs": [
{
"target": "0x140011b00",
"callers": [
{ "address": "0x14001a30f", "type": "call" },
{ "address": "0x140019ec1", "type": "call" },
...
]
},
{
"target": "0x140007394",
"callers": [
{ "address": "0x14001a31a", "type": "call" },
...
]
},
...
]
}
Cada instrucción en el modo --clean incluye stack_offset — el delta de RSP desde la entrada de la función en ese punto de instrucción. La sección xrefs mapea cada destino de llamada/salto de vuelta a cada instrucción que lo referencia.
--subs-only --clean{
"binary": "C:\\binaries\\target.exe",
"mode": "subs-only+clean",
...
}
Misma estructura que --clean, pero con "mode": "subs-only+clean" para indicar que se aplicaron ambos filtros. El recuento de funciones se reduce solo a funciones alcanzables desde el punto de entrada, y las funciones restantes tienen desplazamientos de pila y referencias cruzadas.
cfgrip descubre funciones mediante múltiples pasadas de detección:
| Pase | Qué detecta | Cubre |
|---|---|---|
| Escaneo de prólogo | push rbp, push r15/r14/r13/r12/rbx/rdi/rsi, sub rsp, >=0x20, enter | MSVC x64, GCC, funciones hoja, CET (endbr64) |
| Destinos de llamada | Cada destino de instrucción call es un inicio de función | Llamadas directas e indirectas resueltas por GOT |
| Llamadas de cola | Instrucciones jmp que apuntan a candidatos de prólogo | Cadenas de llamadas de cola optimizadas |
.pdata (PE) | Entradas de función de tiempo de ejecución desde la tabla de manejadores de excepciones | Inicio/fin preciso para cada función x64 PE |
| Punteros de datos | Valores de 8 bytes en .rdata/.data que apuntan a código ejecutable | Punteros a funciones, tablas virtuales, callbacks |
Las funciones con is_thunk: true son stubs PLT o thunks de importación — funciones de un solo bloque que redirigen a otra dirección.
Requiere CMake y un compilador C++17. Capstone se obtiene automáticamente.
cmake -B build
cmake --build build --config Release
./build/cfgrip <binary>
O en Windows con Visual Studio:
cmake -B build -S .
cmake --build build --config Release
.\build\Release\cfgrip.exe <binary>

| Formatos | PE (32/64 bits) | YES |
| ELF (64 bits) | YES | |
| Arquitecturas | x86 | YES |
| x86-64 | YES | |
| Llamadas indirectas | Resolución GOT | YES |
| Detección de tablas de saltos | YES | |
| Trazado de registros hacia atrás | YES | |
| Descubrimiento de funciones | Punto de entrada | YES |
| Exportaciones | YES | |
Destinos call | YES | |
Escaneo de prólogo (MSVC x64, GCC, CET endbr64) | YES | |
Detección de llamadas de cola (jmp → función) | YES | |
PE .pdata (tabla de manejadores de excepciones) | YES | |
| Escaneo de punteros a funciones en sección de datos | YES | |
| Detección de thunks | Stubs PLT / thunks de importación (is_thunk) | YES |
| Límites de funciones | end_address desde .pdata o instrucción máxima | YES |
| Modo solo subfunciones | Indicador --subs-only | YES |
| Limpieza de CFG | --clean (encadenamiento de saltos, poda de bloques muertos, deltas de pila, referencias cruzadas) | YES |
La detección de límites de funciones en esta herramienta se basa en el enfoque descrito en "Function Boundary Detection in Stripped Binaries" (Alves-Foss & Song, 2019), que introduce un algoritmo multiheurístico para localizar inicios y fines de funciones en binarios despojados x86/x64.
El artículo está disponible en papers/Function_Boundary_Detection_in_Stripped_Binaries.pdf.
Cómo se mapea nuestra implementación con las heurísticas del artículo:
| Heurística | Descripción del artículo | Nuestra implementación |
|---|---|---|
| H1–H4 | Firmas de prólogo (push rbp, registros preservados por callee, sub de pila, enter) | isProlog() en disasm/engine.cpp — detecta push rbp, push r15..rbx, sub rsp >= 0x20, enter |
| H5 | Siembra de destinos de llamada | Cada destino directo de call es un inicio de función |
| H6 | Detección de salto-a-función (llamada de cola) | jmp a candidatos de prólogo añade el destino a la cola de funciones |
| H7 | Análisis de tabla de excepciones | Las entradas RUNTIME_FUNCTION de PE .pdata dan inicio/fin precisos |
| H8 | Análisis de referencias de datos | scanDataPointers() recorre secciones de datos en busca de punteros a código |
La idea clave del artículo es que las heurísticas algorítmicas — sin aprendizaje automático — pueden lograr alta precisión en binarios despojados. Nuestra implementación sigue esta filosofía, utilizando un enfoque de múltiples pasadas donde cada pasada captura funciones que las otras podrían pasar por alto.