cfgrip: standalone CFG extraction tool from PE/ELF binaries

This commit is contained in:
x86byte
2026-06-29 11:48:20 +01:00
commit 5fab3a419c
21 changed files with 153465 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
build/
+31
View File
@@ -0,0 +1,31 @@
cmake_minimum_required(VERSION 3.14)
project(cfgrip)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
FetchContent_Declare(
capstone
GIT_REPOSITORY https://github.com/capstone-engine/capstone.git
GIT_TAG 5.0.7
)
set(CAPSTONE_BUILD_TESTS OFF)
set(CAPSTONE_BUILD_SHARED OFF)
set(CAPSTONE_BUILD_CSTOOL OFF)
set(CAPSTONE_BUILD_CSVIEW OFF)
FetchContent_MakeAvailable(capstone)
add_executable(cfgrip
main.cpp
loader/binary.cpp
loader/pe.cpp
loader/elf.cpp
disasm/engine.cpp
cfg/builder.cpp
)
target_include_directories(cfgrip PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_include_directories(cfgrip PRIVATE ${capstone_SOURCE_DIR}/include)
target_link_libraries(cfgrip capstone_static)
+14
View File
@@ -0,0 +1,14 @@
# code of conduct
## the rule
Don't be an asshole.
## what that means
- No personal attacks, no harassment, no discrimination.
- Assume good faith. People make mistakes, including you.
- If someone asks you to stop, stop.
- If you see shitty behavior, call it out.
That's it. No long document. Just don't be an asshole.
+39
View File
@@ -0,0 +1,39 @@
# contributing
Pull requests are welcome. If you're planning something big, open an issue first so we don't step on each other.
## building
```sh
cmake -B build
cmake --build build --config Release
```
Capstone is fetched automatically via FetchContent. You don't need to install it.
## what needs work
- **More architectures** — right now it's x86/x86-64 only. ARM, AArch64, etc would be nice.
- **Better indirect call resolution** — the register tracer is basic. It could be smarter.
- **More binary formats** — Mach-O isn't supported yet.
## style
- No tabs. 4 spaces.
- No comments unless the code is genuinely confusing.
- Match the existing style. If you're changing something, make it look like it belongs.
- Keep it simple. This is not a framework.
## committing
Write short commit messages. Use `git rebase` to keep the history clean. No merge commits.
## testing
There's a test binary in `tests/`. Run cfgrip on it and make sure the output is valid JSON:
```sh
./build/cfgrip tests/example1.exe
```
If your change affects the output format, update `tests/example1.exe.cfg` too.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 BinaryHardening
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+419
View File
@@ -0,0 +1,419 @@
# cfgrip
PE/ELF x86/x64 CFG extractor. Takes a binary in, disassembles it, resolves every jump and call (GOT, jump tables, register tracing), and exports the full control flow graph as structured JSON.
## why
You need to know exactly where every branch goes. Not for reading — for patching. Feed the JSON into Zydis or AsmJit, locate the exact instruction you need to hook or modify, and write back. Anti-cheat teams use it to map out game binaries. RE people use it to lift code into their own analysis pipelines. Software analysts trace execution paths without running the binary.
cfgrip gives you the map. What you do with it is up to you.
## what it extracts
For every binary cfgrip processes, it produces:
- **Function list** with addresses and names (entry point, exports, discovered via `call` instructions)
- **Basic blocks** per function — instruction sequences terminated by branches, calls, returns, or traps
- **Control flow edges** — successors of each block (direct branches, fall-through, indirect targets)
- **Import table** — resolved library imports with addresses
- **Indirect targets** — GOT-resolved calls, jump table entries, register-traced branches, and unresolved ones (marked as such)
It handles indirect branches by:
1. Checking the **GOT** (Global Offset Table) for known imports
2. Scanning backward for **LEA instructions** to locate jump tables, then reading table entries
3. **Tracing registers** backward through `mov`/`lea` chains to find concrete addresses
## usage
```
cfgrip <binary>
```
That's it. Feed it a binary, get `<binary>.cfg` as output.
---
Example:
```
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: 422
indirect targets: 1372
cfg written to: tests\example1.exe.cfg
```
---
## the output format
The `.cfg` file is structured JSON. Here's what it looks like:
```
{
"binary": "tests\\example1.exe",
"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"
]
},
...
...
...
```
## building
Requires CMake and a C++17 compiler. Capstone is fetched automatically.
```
cmake -B build
cmake --build build --config Release
./build/cfgrip <binary>
```
Or on Windows with Visual Studio:
```
cmake -B build -S .
cmake --build build --config Release
.\build\Release\cfgrip.exe <binary>
```
---
<img width="855" height="976" alt="image" src="https://github.com/user-attachments/assets/69018295-e3a7-4e86-a66a-4a9de448ad00" />
<img width="774" height="654" alt="image" src="https://github.com/user-attachments/assets/a2918f3b-caa9-4bbc-be19-bb06d74bd177" />
---
## what it supports
| | | |
|---|---|:-:|
| **Formats** | PE (32/64-bit) | YES |
| | ELF (64-bit) | YES |
| **Architectures** | x86 | YES |
| | x86-64 | YES |
| **Indirect calls** | GOT resolution | YES |
| | Jump table detection | YES |
| | Backward register tracing | YES |
| **Function discovery** | Entry point | YES |
| | Exports | YES |
| | `call` targets | YES |
| | Prolog scanning (`push rbp`, CET `endbr64`) | YES |
+7
View File
@@ -0,0 +1,7 @@
# security
This tool parses untrusted binaries. There's always a risk of bugs in the parsers (PE, ELF) or the disassembler that could crash or do worse.
If you find something, don't open a public issue. Email me directly or reach out on the repo's security tab.
If you're integrating cfgrip into something that processes untrusted files, consider sandboxing it.
+530
View File
@@ -0,0 +1,530 @@
#include "cfg/builder.hpp"
#include <queue>
#include <algorithm>
static bool isMemOp(const string& op)
{
return op.find('[') != string::npos || op.find('+') != string::npos;
}
static bool regMatch(const string& op, const string& target)
{
auto norm = [](const string& s) -> string {
if (s.size() >= 3 && s[0] == 'r' && isdigit(s[1])) return s.substr(1);
if (s.size() >= 3 && s[0] == 'e' && isdigit(s[1])) return s.substr(1);
return s;
};
return norm(op) == norm(target) || op == target;
}
CFGBuilder::CFGBuilder(Binary* binary, Disassembler* disasm)
: m_bin(binary), m_dis(disasm) {}
bool CFGBuilder::isVisited(addr_t addr)
{
return m_blocks.count(addr) > 0;
}
void CFGBuilder::markVisited(addr_t addr)
{
m_blocks.insert(addr);
}
void CFGBuilder::buildGotMap()
{
auto imports = m_bin->getImportedFunctions();
for (const auto& imp : imports)
{
if (imp.address)
m_got[imp.address] = {imp.name, imp.library};
}
}
addr_t CFGBuilder::resolveGOT(const Instruction& inst)
{
if (!m_dis->isIndirectBranch(inst)) return 0;
int64_t disp = m_dis->getRIPDisp(inst);
if (disp == 0) return 0;
addr_t got = inst.address + inst.size + disp;
auto it = m_got.find(got);
if (it != m_got.end())
{
if (!it->second.first.empty()) return got;
}
auto bytes = m_bin->readBytes(got, 8);
if (bytes.size() < 4) return 0;
uint64_t target = 0;
if (bytes.size() >= 8)
{
target = bytes[0] | ((uint64_t)bytes[1] << 8) | ((uint64_t)bytes[2] << 16) |
((uint64_t)bytes[3] << 24) | ((uint64_t)bytes[4] << 32) |
((uint64_t)bytes[5] << 40) | ((uint64_t)bytes[6] << 48) |
((uint64_t)bytes[7] << 56);
}
else
{
target = bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24);
}
return target;
}
addr_t CFGBuilder::resolveJumpTable(const vector<Instruction>& block, const Instruction& inst)
{
size_t n = block.size();
if (n < 3) return 0;
int idx = -1;
for (int i = (int)n - 1; i >= 0; i--)
{
if (block[i].address == inst.address) { idx = i; break; }
}
if (idx < 2) return 0;
for (int i = idx - 1; i >= max(0, idx - 8); i--)
{
if (block[i].mnemonic == "lea")
{
int64_t disp = m_dis->getRIPDisp(block[i]);
if (disp == 0) continue;
addr_t table = block[i].address + block[i].size + disp;
int64_t bound = -1;
for (int j = i - 1; j >= max(0, i - 4); j--)
{
if (block[j].mnemonic == "cmp")
{
size_t pos = block[j].operands.find("0x");
if (pos != string::npos)
{
try { bound = stoll(block[j].operands.substr(pos), nullptr, 16); }
catch (...) {}
}
break;
}
}
if (bound <= 0) bound = 32;
for (int64_t e = 0; e < bound && e < 256; e++)
{
auto ent = m_bin->readBytes(table + e * 8, 8);
if (ent.size() < 4) break;
uint64_t entry = 0;
if (ent.size() >= 8)
entry = ent[0] | ((uint64_t)ent[1] << 8) | ((uint64_t)ent[2] << 16) |
((uint64_t)ent[3] << 24) | ((uint64_t)ent[4] << 32) |
((uint64_t)ent[5] << 40) | ((uint64_t)ent[6] << 48) | ((uint64_t)ent[7] << 56);
else
entry = ent[0] | (ent[1] << 8) | (ent[2] << 16) | (ent[3] << 24);
if (entry == 0 || entry > 0x100000000ULL) break;
auto ranges = m_bin->getExecutableRanges();
bool ok = false;
for (const auto& r : ranges)
if (entry >= r.first && entry < r.second) { ok = true; break; }
if (ok) return table;
else break;
}
}
}
return 0;
}
addr_t CFGBuilder::traceReg(const string& reg, const vector<Instruction>& block, size_t idx)
{
if (idx >= block.size() || idx == 0) return 0;
string cur = reg;
for (int i = (int)idx - 1; i >= 0; i--)
{
const auto& inst = block[i];
if (inst.mnemonic == "nop" || inst.mnemonic == "cmp" || inst.mnemonic == "test")
continue;
size_t comma = inst.operands.find(',');
string dst = comma != string::npos ? inst.operands.substr(0, comma) : inst.operands;
while (!dst.empty() && dst[0] == ' ') dst = dst.substr(1);
while (!dst.empty() && dst.back() == ' ') dst.pop_back();
if (inst.mnemonic == "mov" || inst.mnemonic == "lea")
{
if (dst == cur || dst == "r" + cur.substr(1) || regMatch(dst, cur))
{
if (inst.mnemonic == "mov" && !inst.operands.empty())
{
size_t pos = inst.operands.rfind("0x");
if (pos != string::npos)
{
try { return stoull(inst.operands.substr(pos), nullptr, 16); }
catch (...) {}
}
}
if (inst.mnemonic == "lea")
{
int64_t disp = m_dis->getRIPDisp(inst);
if (disp != 0) return inst.address + inst.size + disp;
}
if (comma != string::npos)
{
string src = inst.operands.substr(comma + 1);
while (!src.empty() && src[0] == ' ') src = src.substr(1);
while (!src.empty() && src.back() == ' ') src.pop_back();
if (!src.empty() && src[0] != '0' && !isMemOp(src))
{
cur = src;
continue;
}
}
return 0;
}
}
if (dst == cur || regMatch(dst, cur)) return 0;
}
return 0;
}
void CFGBuilder::recordIndirect(const Instruction& inst, addr_t target, bool ok, const string& name, const string& lib)
{
IndirectTarget it;
it.instruction_addr = inst.address;
it.resolved_target = ok ? target : 0;
it.resolved = ok;
it.name = name;
it.library = lib;
m_targets.push_back(it);
}
BasicBlock CFGBuilder::disassembleBlock(addr_t start, addr_t& next)
{
BasicBlock block;
block.address = start;
block.is_prolog = false;
block.is_epilog = false;
addr_t cur = start;
auto ranges = m_bin->getExecutableRanges();
auto inExec = [&](addr_t a) -> bool {
for (const auto& r : ranges)
if (a >= r.first && a < r.second) return true;
return false;
};
for (size_t i = 0; i < 5000; ++i)
{
if (!inExec(cur)) { next = 0; break; }
auto bytes = m_bin->readBytes(cur, 15);
if (bytes.empty()) { next = 0; break; }
auto insts = m_dis->disassemble(bytes, cur, 1);
if (insts.empty()) { next = 0; break; }
auto& inst = insts[0];
block.instructions.push_back(inst);
if (i <= 3 && !block.is_prolog && m_dis->isProlog(block.instructions))
block.is_prolog = true;
if (m_dis->isEpilog(inst))
block.is_epilog = true;
if (m_dis->isReturn(inst)) { next = 0; break; }
if (m_dis->isTrap(inst)) { next = 0; break; }
if (m_dis->isUnconditionalBranch(inst))
{
addr_t t = m_dis->getBranchTarget(inst);
if (t && !m_dis->isIndirectBranch(inst))
block.successors.push_back(t);
else if (m_dis->isIndirectBranch(inst))
{
addr_t resolved = resolveGOT(inst);
if (!resolved)
{
resolved = resolveJumpTable(block.instructions, inst);
if (resolved)
{
for (int j = (int)block.instructions.size() - 2; j >= 0; j--)
{
if (block.instructions[j].mnemonic == "lea")
{
int64_t ld = m_dis->getRIPDisp(block.instructions[j]);
if (ld != 0)
{
addr_t tbl = block.instructions[j].address + block.instructions[j].size + ld;
for (int64_t e = 0; e < 64; e++)
{
auto eb = m_bin->readBytes(tbl + e * 8, 8);
if (eb.size() < 4) break;
uint64_t entry = 0;
if (eb.size() >= 8)
entry = eb[0] | ((uint64_t)eb[1] << 8) | ((uint64_t)eb[2] << 16) |
((uint64_t)eb[3] << 24) | ((uint64_t)eb[4] << 32) |
((uint64_t)eb[5] << 40) | ((uint64_t)eb[6] << 48) | ((uint64_t)eb[7] << 56);
else
entry = eb[0] | (eb[1] << 8) | (eb[2] << 16) | (eb[3] << 24);
if (entry == 0 || entry > 0x100000000ULL) break;
bool ok = false;
for (const auto& r : ranges)
if (entry >= r.first && entry < r.second) { ok = true; break; }
if (ok) block.successors.push_back(entry);
else break;
}
resolved = tbl;
break;
}
}
}
}
}
if (!resolved)
{
string reg = inst.operands;
while (!reg.empty() && reg[0] == ' ') reg = reg.substr(1);
resolved = traceReg(reg, block.instructions, block.instructions.size() - 1);
if (resolved)
{
block.successors.push_back(resolved);
recordIndirect(inst, resolved, true, "traced", "");
}
else
{
recordIndirect(inst, 0, false, "unresolved", "");
}
}
else
{
auto git = m_got.find(resolved);
string nm = git != m_got.end() ? git->second.first : "";
string lb = git != m_got.end() ? git->second.second : "";
block.successors.push_back(resolved);
recordIndirect(inst, resolved, true, nm, lb);
}
}
next = 0; break;
}
if (m_dis->isConditionalBranch(inst))
{
addr_t t = m_dis->getBranchTarget(inst);
if (t && !m_dis->isIndirectBranch(inst))
block.successors.push_back(t);
block.successors.push_back(cur + inst.size);
next = cur + inst.size; break;
}
if (m_dis->isCall(inst))
{
addr_t t = m_dis->getBranchTarget(inst);
if (t && !m_dis->isIndirectBranch(inst))
{
m_funcs.insert(t);
recordIndirect(inst, t, true, "", "");
}
else if (m_dis->isIndirectBranch(inst))
{
addr_t resolved = resolveGOT(inst);
if (resolved)
{
auto git = m_got.find(resolved);
string nm = git != m_got.end() ? git->second.first : "";
string lb = git != m_got.end() ? git->second.second : "";
auto gb = m_bin->readBytes(resolved, 8);
if (gb.size() >= 8)
{
uint64_t target = gb[0] | ((uint64_t)gb[1] << 8) |
((uint64_t)gb[2] << 16) | ((uint64_t)gb[3] << 24) |
((uint64_t)gb[4] << 32) | ((uint64_t)gb[5] << 40) |
((uint64_t)gb[6] << 48) | ((uint64_t)gb[7] << 56);
if (target && target < 0x100000000ULL) resolved = target;
}
m_funcs.insert(resolved);
recordIndirect(inst, resolved, true, nm, lb);
}
else
{
string reg = inst.operands;
while (!reg.empty() && reg[0] == ' ') reg = reg.substr(1);
resolved = traceReg(reg, block.instructions, block.instructions.size() - 1);
if (resolved)
{
m_funcs.insert(resolved);
recordIndirect(inst, resolved, true, "traced", "");
}
else
{
recordIndirect(inst, 0, false, "unresolved", "");
}
}
}
}
cur += inst.size;
next = cur;
}
return block;
}
set<addr_t> CFGBuilder::findPrologs()
{
set<addr_t> cand;
auto ranges = m_bin->getExecutableRanges();
for (const auto& r : ranges)
{
addr_t a = r.first;
while (a < r.second)
{
auto bytes = m_bin->readBytes(a, 15);
if (bytes.empty()) { a += 1; continue; }
auto insts = m_dis->disassemble(bytes, a, 1);
if (insts.empty()) { a += 1; continue; }
auto& inst = insts[0];
if (inst.mnemonic == "endbr64")
{
auto nb = m_bin->readBytes(a + inst.size, 15);
auto ni = m_dis->disassemble(nb, a + inst.size, 2);
if (!ni.empty() && m_dis->isProlog(ni)) cand.insert(a);
}
else
{
vector<Instruction> tmp{inst};
if (m_dis->isProlog(tmp)) cand.insert(a);
}
a += inst.size;
}
}
return cand;
}
Function CFGBuilder::buildFunction(addr_t start, const string& name)
{
Function func;
func.address = start;
func.name = name;
queue<addr_t> q;
q.push(start);
while (!q.empty())
{
addr_t a = q.front(); q.pop();
if (isVisited(a)) continue;
markVisited(a);
addr_t nxt = a;
auto block = disassembleBlock(a, nxt);
for (auto s : block.successors)
if (!isVisited(s)) q.push(s);
func.blocks.push_back(block);
}
return func;
}
CFG CFGBuilder::build()
{
CFG cfg;
cfg.binary_path = m_bin->getPath();
cfg.entry_point = m_bin->getEntryPoint();
switch (m_bin->getArch())
{
case Arch::X86: cfg.arch_str = "x86"; break;
case Arch::X64: cfg.arch_str = "x86-64"; break;
default: cfg.arch_str = "unknown";
}
switch (m_bin->getFormat())
{
case BinaryFormat::PE: cfg.format_str = "PE"; break;
case BinaryFormat::ELF: cfg.format_str = "ELF"; break;
default: cfg.format_str = "unknown";
}
cfg.imports = m_bin->getImportedFunctions();
buildGotMap();
m_prologs = findPrologs();
set<addr_t> built;
queue<addr_t> pending;
auto process = [&](addr_t addr, const string& name)
{
if (built.count(addr)) return;
built.insert(addr);
auto func = buildFunction(addr, name);
cfg.functions.push_back(func);
for (const auto& b : func.blocks)
for (const auto& inst : b.instructions)
if (m_dis->isCall(inst))
{
addr_t t = m_dis->getBranchTarget(inst);
if (t && !m_dis->isIndirectBranch(inst))
m_funcs.insert(t);
}
for (auto d : m_funcs)
if (!built.count(d)) pending.push(d);
};
{
addr_t ep = m_bin->getEntryPoint();
string ep_name = "entry";
for (const auto& exp : m_bin->getExportedFunctions())
if (exp.first == ep) { ep_name = exp.second; break; }
process(ep, ep_name);
}
for (const auto& exp : m_bin->getExportedFunctions())
pending.push(exp.first);
map<addr_t, string> imp_map;
for (const auto& imp : m_bin->getImportedFunctions())
imp_map[imp.address] = imp.name;
while (!pending.empty())
{
addr_t a = pending.front(); pending.pop();
if (built.count(a)) continue;
string name;
for (const auto& exp : m_bin->getExportedFunctions())
if (exp.first == a) { name = exp.second; break; }
if (name.empty())
{
auto it = imp_map.find(a);
if (it != imp_map.end()) name = it->second;
}
built.insert(a);
auto func = buildFunction(a, name);
cfg.functions.push_back(func);
for (const auto& b : func.blocks)
for (const auto& inst : b.instructions)
if (m_dis->isCall(inst))
{
addr_t t = m_dis->getBranchTarget(inst);
if (t && !m_dis->isIndirectBranch(inst))
if (!built.count(t)) pending.push(t);
}
for (auto p : m_prologs)
if (!built.count(p)) pending.push(p);
}
cfg.indirect_targets = m_targets;
return cfg;
}
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include "types.hpp"
#include "loader/binary.hpp"
#include "disasm/engine.hpp"
#include <map>
#include <set>
#include <vector>
class CFGBuilder
{
public:
CFGBuilder(Binary* binary, Disassembler* disasm);
CFG build();
private:
void buildGotMap();
Function buildFunction(addr_t start, const string& name);
BasicBlock disassembleBlock(addr_t start, addr_t& next);
set<addr_t> findPrologs();
void markVisited(addr_t addr);
bool isVisited(addr_t addr);
addr_t resolveGOT(const Instruction& inst);
addr_t resolveJumpTable(const vector<Instruction>& block, const Instruction& inst);
addr_t traceReg(const string& reg, const vector<Instruction>& block, size_t idx);
void recordIndirect(const Instruction& inst, addr_t target, bool ok, const string& name, const string& lib);
Binary* m_bin;
Disassembler* m_dis;
set<addr_t> m_funcs;
set<addr_t> m_blocks;
set<addr_t> m_prologs;
map<addr_t, pair<string, string>> m_got;
vector<IndirectTarget> m_targets;
};
+163
View File
@@ -0,0 +1,163 @@
#include "disasm/engine.hpp"
#include <cstring>
Disassembler::Disassembler() : m_handle(0), m_initialized(false) {}
Disassembler::~Disassembler()
{
if (m_initialized) cs_close(&m_handle);
}
bool Disassembler::init(Arch arch)
{
cs_arch a;
cs_mode m;
if (arch == Arch::X64) { a = CS_ARCH_X86; m = CS_MODE_64; }
else if (arch == Arch::X86) { a = CS_ARCH_X86; m = CS_MODE_32; }
else return false;
if (cs_open(a, m, &m_handle) != CS_ERR_OK) return false;
cs_option(m_handle, CS_OPT_DETAIL, CS_OPT_ON);
m_initialized = true;
return true;
}
vector<Instruction> Disassembler::disassemble(const vector<uint8_t>& bytes, addr_t base, size_t count)
{
vector<Instruction> result;
if (!m_initialized || bytes.empty()) return result;
const uint8_t* code = bytes.data();
size_t size = bytes.size();
addr_t addr = base;
size_t n = count ? count : size;
cs_insn* insn = cs_malloc(m_handle);
if (!insn) return result;
while (cs_disasm_iter(m_handle, &code, &size, &addr, insn))
{
Instruction inst;
inst.address = insn->address;
inst.mnemonic = insn->mnemonic;
inst.operands = insn->op_str;
inst.size = static_cast<uint8_t>(insn->size);
inst.bytes.assign(insn->bytes, insn->bytes + insn->size);
result.push_back(inst);
if (count && result.size() >= count) break;
}
cs_free(insn, 1);
return result;
}
bool Disassembler::isProlog(const vector<Instruction>& insts) const
{
for (const auto& inst : insts)
{
if (inst.mnemonic == "push" && (inst.operands == "rbp" || inst.operands == "ebp"))
return true;
}
return false;
}
bool Disassembler::isEpilog(const Instruction& inst) const
{
return inst.mnemonic == "leave" || inst.mnemonic == "ret" || inst.mnemonic == "retf";
}
bool Disassembler::isReturn(const Instruction& inst) const
{
return inst.mnemonic == "ret" || inst.mnemonic == "retf" || inst.mnemonic == "iret" || inst.mnemonic == "iretd";
}
bool Disassembler::isTrap(const Instruction& inst) const
{
return inst.mnemonic == "hlt" || inst.mnemonic == "ud2" || inst.mnemonic == "int3" || inst.mnemonic == "ud0";
}
bool Disassembler::isCall(const Instruction& inst) const
{
return inst.mnemonic == "call";
}
bool Disassembler::isUnconditionalBranch(const Instruction& inst) const
{
return inst.mnemonic == "jmp" || inst.mnemonic == "ljmp";
}
bool Disassembler::isConditionalBranch(const Instruction& inst) const
{
return inst.mnemonic.size() == 2 && inst.mnemonic[0] == 'j' && inst.mnemonic != "jmp";
}
bool Disassembler::isIndirectBranch(const Instruction& inst) const
{
const string& ops = inst.operands;
if (inst.mnemonic == "call" || inst.mnemonic == "jmp")
{
if (ops.find('[') != string::npos) return true;
if (ops.size() >= 3 && ops[0] == 'r' && isdigit(ops[1])) return true;
if (ops.size() >= 4 && ops[0] == 'r' && ops[1] == '1' && isdigit(ops[2])) return true;
if (ops.size() >= 3 && ops[0] == 'e' && isdigit(ops[1])) return true;
if (ops[0] == '*') return true;
}
return false;
}
addr_t Disassembler::getBranchTarget(const Instruction& inst) const
{
const string& ops = inst.operands;
if (ops.empty()) return 0;
size_t pos = string::npos;
if (ops[0] == '0' && ops.size() > 2 && ops[1] == 'x')
pos = 0;
else if (ops[0] >= '0' && ops[0] <= '9')
pos = 0;
else
pos = ops.find("0x");
if (pos == string::npos) return 0;
try {
return stoull(ops.substr(pos), nullptr, 16);
} catch (...) {
return 0;
}
}
int64_t Disassembler::getRIPDisp(const Instruction& inst) const
{
if (!m_initialized) return 0;
auto bytes = inst.bytes;
if (bytes.empty()) return 0;
cs_insn* insn = cs_malloc(m_handle);
if (!insn) return 0;
const uint8_t* code = bytes.data();
size_t size = bytes.size();
addr_t addr = inst.address;
int64_t disp = 0;
if (cs_disasm_iter(m_handle, &code, &size, &addr, insn))
{
if (insn->detail)
{
for (int i = 0; i < insn->detail->x86.op_count; i++)
{
const auto& op = insn->detail->x86.operands[i];
if (op.type == X86_OP_MEM && op.mem.base == X86_REG_RIP)
{
disp = op.mem.disp;
break;
}
}
}
}
cs_free(insn, 1);
return disp;
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include "types.hpp"
#include "loader/binary.hpp"
#include <capstone/capstone.h>
class Disassembler
{
public:
Disassembler();
~Disassembler();
bool init(Arch arch);
vector<Instruction> disassemble(const vector<uint8_t>& bytes, addr_t base, size_t count = 0);
bool isProlog(const vector<Instruction>& insts) const;
bool isEpilog(const Instruction& inst) const;
bool isReturn(const Instruction& inst) const;
bool isTrap(const Instruction& inst) const;
bool isCall(const Instruction& inst) const;
bool isUnconditionalBranch(const Instruction& inst) const;
bool isConditionalBranch(const Instruction& inst) const;
bool isIndirectBranch(const Instruction& inst) const;
addr_t getBranchTarget(const Instruction& inst) const;
int64_t getRIPDisp(const Instruction& inst) const;
private:
csh m_handle;
bool m_initialized;
};
+15
View File
@@ -0,0 +1,15 @@
#include "loader/binary.hpp"
#include "loader/elf.hpp"
#include "loader/pe.hpp"
#include <memory>
unique_ptr<Binary> Binary::create(const string& path)
{
auto elf = make_unique<ELFLoader>();
if (elf->load(path)) return elf;
auto pe = make_unique<PELoader>();
if (pe->load(path)) return pe;
return nullptr;
}
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include "types.hpp"
enum class BinaryFormat
{
PE,
ELF,
UNKNOWN
};
enum class Arch
{
X86,
X64,
UNKNOWN
};
class Binary
{
public:
virtual ~Binary() = default;
virtual bool load(const string& path) = 0;
virtual BinaryFormat getFormat() const = 0;
virtual Arch getArch() const = 0;
virtual addr_t getEntryPoint() const = 0;
virtual addr_t getImageBase() const = 0;
virtual vector<uint8_t> readBytes(addr_t vaddr, size_t size) const = 0;
virtual vector<pair<addr_t, addr_t>> getExecutableRanges() const = 0;
virtual vector<pair<addr_t, string>> getExportedFunctions() const = 0;
virtual vector<ImportEntry> getImportedFunctions() const
{
return {};
}
virtual const string& getPath() const = 0;
static unique_ptr<Binary> create(const string& path);
};
+332
View File
@@ -0,0 +1,332 @@
#include "loader/elf.hpp"
#include <fstream>
#include <algorithm>
static const uint64_t EI_NIDENT = 16;
static const uint32_t PT_LOAD = 1;
static const uint32_t PT_DYNAMIC = 2;
static const uint32_t DT_NULL = 0;
static const uint32_t DT_NEEDED = 1;
static const uint32_t DT_INIT = 12;
static const uint32_t DT_FINI = 13;
static const uint32_t DT_INIT_ARRAY = 25;
static const uint32_t DT_FINI_ARRAY = 26;
static const uint32_t DT_INIT_ARRAYSZ = 27;
static const uint32_t DT_FINI_ARRAYSZ = 28;
static const uint32_t DT_STRTAB = 5;
static const uint32_t DT_SYMTAB = 6;
static const uint32_t DT_STRSZ = 10;
static const uint32_t SHT_SYMTAB = 2;
static const uint32_t SHT_DYNSYM = 11;
ELFLoader::ELFLoader()
: m_arch(Arch::X64), m_entry(0),
m_init_array(0), m_init_array_size(0),
m_fini_array(0), m_fini_array_size(0),
m_init(0), m_fini(0) {}
ELFLoader::~ELFLoader() {}
bool ELFLoader::load(const string& path)
{
m_path = path;
ifstream f(path, ios::binary);
if (!f) return false;
f.seekg(0, ios::end);
size_t sz = f.tellg();
f.seekg(0, ios::beg);
m_data.resize(sz);
f.read((char*)m_data.data(), sz);
f.close();
if (m_data.size() < EI_NIDENT) return false;
if (m_data[0] != 0x7F || m_data[1] != 'E' || m_data[2] != 'L' || m_data[3] != 'F')
return false;
return parseHeader();
}
static uint16_t r16(const vector<uint8_t>& d, size_t off)
{
if (off + 2 > d.size()) return 0;
return d[off] | (d[off+1] << 8);
}
static uint32_t r32(const vector<uint8_t>& d, size_t off)
{
if (off + 4 > d.size()) return 0;
return d[off] | (d[off+1] << 8) | (d[off+2] << 16) | (d[off+3] << 24);
}
static uint64_t r64(const vector<uint8_t>& d, size_t off)
{
if (off + 8 > d.size()) return 0;
return (uint64_t)d[off] | ((uint64_t)d[off+1] << 8) |
((uint64_t)d[off+2] << 16) | ((uint64_t)d[off+3] << 24) |
((uint64_t)d[off+4] << 32) | ((uint64_t)d[off+5] << 40) |
((uint64_t)d[off+6] << 48) | ((uint64_t)d[off+7] << 56);
}
struct Elf64_Ehdr
{
uint8_t ident[16];
uint16_t type;
uint16_t machine;
uint32_t version;
uint64_t entry;
uint64_t phoff;
uint64_t shoff;
uint32_t flags;
uint16_t ehsize;
uint16_t phentsize;
uint16_t phnum;
uint16_t shentsize;
uint16_t shnum;
uint16_t shstrndx;
};
struct Elf64_Phdr
{
uint32_t type;
uint32_t flags;
uint64_t offset;
uint64_t vaddr;
uint64_t paddr;
uint64_t filesz;
uint64_t memsz;
uint64_t align;
};
struct Elf64_Shdr
{
uint32_t name;
uint32_t type;
uint64_t flags;
uint64_t addr;
uint64_t offset;
uint64_t size;
uint32_t link;
uint32_t info;
uint64_t addralign;
uint64_t entsize;
};
struct Elf64_Dyn
{
int64_t d_tag;
uint64_t d_val;
};
bool ELFLoader::parseHeader()
{
if (m_data.size() < 64) return false;
uint8_t cls = m_data[4];
bool is_64 = (cls == 2);
if (is_64)
{
m_arch = Arch::X64;
if (m_data.size() < 64) return false;
uint16_t type = r16(m_data, 16);
m_entry = r64(m_data, 24);
uint64_t phoff = r64(m_data, 32);
uint64_t shoff = r64(m_data, 40);
uint16_t phnum = r16(m_data, 56);
uint16_t shnum = r16(m_data, 60);
uint16_t shstrndx = r16(m_data, 62);
for (uint16_t i = 0; i < phnum; i++)
{
size_t off = phoff + i * 56;
if (off + 56 > m_data.size()) break;
uint32_t ptype = r32(m_data, off);
uint32_t pflags = r32(m_data, off + 4);
uint64_t poffset = r64(m_data, off + 8);
uint64_t pvaddr = r64(m_data, off + 16);
uint64_t pfilesz = r64(m_data, off + 32);
uint64_t pmemsz = r64(m_data, off + 40);
if (ptype == PT_LOAD && (pflags & 1))
{
m_exec_ranges.push_back({pvaddr, pvaddr + pmemsz});
}
if (ptype == PT_DYNAMIC)
{
addr_t dyn_va = pvaddr;
size_t dyn_sz = pmemsz;
size_t dyn_off = poffset;
size_t end = dyn_off + dyn_sz;
for (size_t j = dyn_off; j + 16 <= end; j += 16)
{
int64_t tag = (int64_t)r64(m_data, j);
uint64_t val = r64(m_data, j + 8);
if (tag == DT_NULL) break;
if (tag == DT_NEEDED && val)
{
uint64_t strtab = 0;
for (size_t k = dyn_off; k + 16 <= end; k += 16)
{
int64_t t2 = (int64_t)r64(m_data, k);
uint64_t v2 = r64(m_data, k + 8);
if (t2 == DT_STRTAB) { strtab = v2; break; }
}
if (strtab)
{
size_t name_off = strtab + val;
if (name_off < m_data.size())
{
string lib;
for (size_t c = name_off; c < m_data.size() && m_data[c]; c++)
lib += (char)m_data[c];
ImportEntry e;
e.address = 0;
e.name = lib;
e.library = lib;
m_imports.push_back(e);
}
}
}
if (tag == DT_INIT) m_init = val;
if (tag == DT_FINI) m_fini = val;
if (tag == DT_INIT_ARRAY) m_init_array = val;
if (tag == DT_INIT_ARRAYSZ) m_init_array_size = val;
if (tag == DT_FINI_ARRAY) m_fini_array = val;
if (tag == DT_FINI_ARRAYSZ) m_fini_array_size = val;
}
}
}
for (uint16_t i = 0; i < shnum; i++)
{
size_t off = shoff + i * 64;
if (off + 64 > m_data.size()) break;
uint32_t stype = r32(m_data, off + 4);
if (stype == SHT_DYNSYM || stype == SHT_SYMTAB)
{
uint64_t sym_off = r64(m_data, off + 24);
uint64_t sym_sz = r64(m_data, off + 32);
uint32_t link = r32(m_data, off + 40);
uint64_t entsize = r64(m_data, off + 56);
if (entsize == 0) entsize = 24;
uint64_t strtab_off = 0;
uint64_t strtab_sz = 0;
for (uint16_t k = 0; k < shnum; k++)
{
size_t soff = shoff + k * 64;
if (soff + 64 > m_data.size()) break;
if (r32(m_data, soff + 4) == SHT_SYMTAB || r32(m_data, soff + 4) == SHT_DYNSYM)
continue;
if ((uint32_t)k == link)
{
strtab_off = r64(m_data, soff + 24);
strtab_sz = r64(m_data, soff + 32);
break;
}
}
size_t nsym = sym_sz / entsize;
for (size_t j = 0; j < nsym; j++)
{
size_t sym_ent = sym_off + j * entsize;
if (sym_ent + 16 > m_data.size()) break;
uint32_t sym_name_off = r32(m_data, sym_ent);
uint8_t sym_info = m_data[sym_ent + 4];
uint64_t sym_val = r64(m_data, sym_ent + 8);
uint8_t sym_type = sym_info & 0xF;
uint8_t sym_bind = sym_info >> 4;
if (sym_val && sym_type == 2 && sym_name_off && strtab_off)
{
size_t name_pos = strtab_off + sym_name_off;
if (name_pos < m_data.size())
{
string sym_name;
for (size_t c = name_pos; c < m_data.size() && m_data[c]; c++)
sym_name += (char)m_data[c];
if (!sym_name.empty())
{
m_symbols[sym_val] = sym_name;
if (sym_bind == 0) m_exports.push_back({sym_val, sym_name});
}
}
}
}
}
if (stype == SHT_DYNSYM) break;
}
}
else
{
m_arch = Arch::X86;
}
return true;
}
bool ELFLoader::parseDynamic()
{
return true;
}
bool ELFLoader::parseSections()
{
return true;
}
bool ELFLoader::parseSymbols()
{
return true;
}
vector<uint8_t> ELFLoader::readBytes(addr_t vaddr, size_t size) const
{
vector<uint8_t> result;
for (const auto& r : m_exec_ranges)
{
if (vaddr >= r.first && vaddr < r.second)
{
addr_t offset = vaddr;
for (const auto& r2 : m_exec_ranges)
{
if (offset >= r2.first && offset < r2.second)
{
addr_t file_off = 0;
size_t max_read = min(size, (size_t)(r2.second - offset));
size_t copy_start = offset - r2.first;
if (copy_start < m_data.size())
{
size_t to_copy = min(max_read, m_data.size() - copy_start);
result.assign(m_data.begin() + copy_start, m_data.begin() + copy_start + to_copy);
}
return result;
}
}
}
}
return result;
}
vector<pair<addr_t, addr_t>> ELFLoader::getExecutableRanges() const
{
return m_exec_ranges;
}
vector<pair<addr_t, string>> ELFLoader::getExportedFunctions() const
{
return m_exports;
}
vector<ImportEntry> ELFLoader::getImportedFunctions() const
{
return m_imports;
}
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include "loader/binary.hpp"
#include <vector>
#include <string>
#include <map>
class ELFLoader : public Binary
{
public:
ELFLoader();
~ELFLoader() override;
bool load(const string& path) override;
BinaryFormat getFormat() const override { return BinaryFormat::ELF; }
Arch getArch() const override { return m_arch; }
addr_t getEntryPoint() const override { return m_entry; }
addr_t getImageBase() const override { return 0; }
vector<uint8_t> readBytes(addr_t vaddr, size_t size) const override;
vector<pair<addr_t, addr_t>> getExecutableRanges() const override;
vector<pair<addr_t, string>> getExportedFunctions() const override;
vector<ImportEntry> getImportedFunctions() const override;
const string& getPath() const override { return m_path; }
addr_t getInitArray() const { return m_init_array; }
addr_t getFiniArray() const { return m_fini_array; }
size_t getInitArraySize() const { return m_init_array_size; }
size_t getFiniArraySize() const { return m_fini_array_size; }
addr_t getInit() const { return m_init; }
addr_t getFini() const { return m_fini; }
private:
bool parseHeader();
bool parseDynamic();
bool parseSections();
bool parseSymbols();
string m_path;
Arch m_arch;
addr_t m_entry;
vector<uint8_t> m_data;
vector<pair<addr_t, addr_t>> m_exec_ranges;
vector<pair<addr_t, string>> m_exports;
vector<ImportEntry> m_imports;
map<addr_t, string> m_symbols;
addr_t m_init_array;
size_t m_init_array_size;
addr_t m_fini_array;
size_t m_fini_array_size;
addr_t m_init;
addr_t m_fini;
};
+276
View File
@@ -0,0 +1,276 @@
#include "loader/pe.hpp"
#include <fstream>
#include <cstring>
PELoader::PELoader() : m_arch(Arch::X64), m_entry(0), m_image_base(0) {}
PELoader::~PELoader() {}
bool PELoader::load(const string& path)
{
m_path = path;
ifstream f(path, ios::binary);
if (!f) return false;
f.seekg(0, ios::end);
size_t sz = f.tellg();
f.seekg(0, ios::beg);
m_data.resize(sz);
f.read((char*)m_data.data(), sz);
f.close();
return parseHeaders();
}
static uint16_t r16(const vector<uint8_t>& d, size_t off)
{
if (off + 2 > d.size()) return 0;
return d[off] | (d[off+1] << 8);
}
static uint32_t r32(const vector<uint8_t>& d, size_t off)
{
if (off + 4 > d.size()) return 0;
return d[off] | (d[off+1] << 8) | (d[off+2] << 16) | (d[off+3] << 24);
}
static uint64_t r64(const vector<uint8_t>& d, size_t off)
{
if (off + 8 > d.size()) return 0;
return (uint64_t)d[off] | ((uint64_t)d[off+1] << 8) |
((uint64_t)d[off+2] << 16) | ((uint64_t)d[off+3] << 24) |
((uint64_t)d[off+4] << 32) | ((uint64_t)d[off+5] << 40) |
((uint64_t)d[off+6] << 48) | ((uint64_t)d[off+7] << 56);
}
bool PELoader::parseHeaders()
{
if (m_data.size() < 64) return false;
if (r16(m_data, 0) != 0x5A4D) return false;
uint32_t pe_off = r32(m_data, 0x3C);
if (pe_off + 4 > m_data.size()) return false;
if (r32(m_data, pe_off) != 0x00004550) return false;
uint16_t machine = r16(m_data, pe_off + 4);
uint16_t num_sections = r16(m_data, pe_off + 6);
if (machine == 0x8664) m_arch = Arch::X64;
else if (machine == 0x14C) m_arch = Arch::X86;
else return false;
uint16_t opt_hdr_size = r16(m_data, pe_off + 20);
uint32_t opt_hdr_off = pe_off + 24;
if (opt_hdr_off + 2 > m_data.size()) return false;
uint16_t magic = r16(m_data, opt_hdr_off);
if (magic == 0x10B)
{
m_arch = Arch::X86;
m_image_base = r32(m_data, opt_hdr_off + 28);
m_entry = r32(m_data, opt_hdr_off + 16) + m_image_base;
}
else if (magic == 0x20B)
{
m_arch = Arch::X64;
m_image_base = r64(m_data, opt_hdr_off + 24);
m_entry = r32(m_data, opt_hdr_off + 16) + m_image_base;
}
else return false;
uint16_t section_hdr_size = 40;
uint32_t sections_off = opt_hdr_off + opt_hdr_size;
struct Section
{
string name;
addr_t vaddr;
uint32_t vsize;
uint32_t raw_ptr;
uint32_t raw_size;
uint32_t characteristics;
};
vector<Section> sections;
for (uint16_t i = 0; i < num_sections; i++)
{
uint32_t off = sections_off + i * section_hdr_size;
if (off + section_hdr_size > m_data.size()) break;
char name_buf[9] = {};
memcpy(name_buf, &m_data[off], 8);
Section s;
s.name = name_buf;
s.vsize = r32(m_data, off + 8);
s.vaddr = r32(m_data, off + 12) + m_image_base;
s.raw_size = r32(m_data, off + 16);
s.raw_ptr = r32(m_data, off + 20);
s.characteristics = r32(m_data, off + 36);
sections.push_back(s);
}
for (const auto& s : sections)
{
if (s.characteristics & 0x20000000)
{
addr_t start = s.vaddr;
addr_t end = start + (s.vsize ? s.vsize : s.raw_size);
m_exec_ranges.push_back({start, end});
}
}
bool is_64 = (m_arch == Arch::X64);
uint32_t import_rva = 0;
uint32_t data_dir_off = (magic == 0x10B) ? opt_hdr_off + 96 : opt_hdr_off + 112;
import_rva = r32(m_data, data_dir_off + 8);
auto rvaToOffset = [&](uint32_t rva) -> uint32_t {
for (const auto& s : sections)
{
addr_t va = rva + m_image_base;
if (va >= s.vaddr && va < s.vaddr + (s.vsize ? s.vsize : s.raw_size))
return s.raw_ptr + static_cast<uint32_t>(va - s.vaddr);
}
return 0;
};
auto readString = [&](uint32_t off) -> string {
string s;
for (size_t c = off; c < m_data.size() && m_data[c]; c++)
s += (char)m_data[c];
return s;
};
if (import_rva)
{
uint32_t desc_off = rvaToOffset(import_rva);
for (uint32_t i = 0; ; i++)
{
uint32_t desc = desc_off + i * 20;
if (desc + 20 > m_data.size()) break;
uint32_t oft_rva = r32(m_data, desc);
if (oft_rva == 0) break;
uint32_t name_rva = r32(m_data, desc + 12);
string dll_name;
if (name_rva)
{
uint32_t name_off = rvaToOffset(name_rva);
if (name_off) dll_name = readString(name_off);
}
if (dll_name.empty()) continue;
uint32_t thunk_rva = r32(m_data, desc + 16);
if (thunk_rva == 0) thunk_rva = oft_rva;
uint32_t thunk_off = rvaToOffset(thunk_rva);
if (!thunk_off) continue;
size_t entry_size = is_64 ? 8 : 4;
uint64_t ordinal_flag = is_64 ? 0x8000000000000000ULL : 0x80000000;
for (uint32_t j = 0; ; j++)
{
uint32_t entry = static_cast<uint32_t>(thunk_off + j * entry_size);
if (entry + entry_size > m_data.size()) break;
uint64_t val = is_64 ? r64(m_data, entry) : r32(m_data, entry);
if (val == 0) break;
string func_name;
if (val & ordinal_flag)
{
func_name = "ord_" + to_string(val & 0xFFFF);
}
else
{
addr_t hint_va = val + m_image_base;
uint32_t hint_off = rvaToOffset(static_cast<uint32_t>(val));
if (hint_off && hint_off + 2 < m_data.size())
func_name = readString(hint_off + 2);
}
addr_t iat_addr = thunk_rva + m_image_base + j * entry_size;
ImportEntry e;
e.address = iat_addr;
e.name = func_name;
e.library = dll_name;
m_imports.push_back(e);
}
}
}
return true;
}
bool PELoader::parseImportTable()
{
return true;
}
bool PELoader::parseExportTable()
{
return true;
}
vector<uint8_t> PELoader::readBytes(addr_t vaddr, size_t size) const
{
vector<uint8_t> result;
uint32_t off = 0;
uint32_t raw_off = 0;
uint32_t raw_end = 0;
uint16_t num_sections = r16(m_data, r32(m_data, 0x3C) + 6);
uint32_t pe_off = r32(m_data, 0x3C);
uint16_t opt_sz = r16(m_data, pe_off + 20);
uint32_t sections_off = pe_off + 24 + opt_sz;
for (uint16_t i = 0; i < num_sections; i++)
{
uint32_t so = sections_off + i * 40;
if (so + 40 > m_data.size()) break;
addr_t sva = r32(m_data, so + 12) + m_image_base;
uint32_t svs = r32(m_data, so + 8);
uint32_t srs = r32(m_data, so + 16);
uint32_t srp = r32(m_data, so + 20);
addr_t end = sva + (svs ? svs : srs);
if (vaddr >= sva && vaddr < end)
{
off = srp + static_cast<uint32_t>(vaddr - sva);
raw_off = srp;
raw_end = srp + srs;
break;
}
}
if (!off) return result;
size_t available = (raw_end > off) ? (raw_end - off) : 0;
size_t to_read = min(size, available);
if (off + to_read > m_data.size())
to_read = m_data.size() - off;
if (to_read > 0)
result.assign(m_data.begin() + off, m_data.begin() + off + to_read);
return result;
}
vector<pair<addr_t, addr_t>> PELoader::getExecutableRanges() const
{
return m_exec_ranges;
}
vector<pair<addr_t, string>> PELoader::getExportedFunctions() const
{
return m_exports;
}
vector<ImportEntry> PELoader::getImportedFunctions() const
{
return m_imports;
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include "loader/binary.hpp"
#include <vector>
#include <string>
#include <map>
class PELoader : public Binary
{
public:
PELoader();
~PELoader() override;
bool load(const string& path) override;
BinaryFormat getFormat() const override { return BinaryFormat::PE; }
Arch getArch() const override { return m_arch; }
addr_t getEntryPoint() const override { return m_entry; }
addr_t getImageBase() const override { return m_image_base; }
vector<uint8_t> readBytes(addr_t vaddr, size_t size) const override;
vector<pair<addr_t, addr_t>> getExecutableRanges() const override;
vector<pair<addr_t, string>> getExportedFunctions() const override;
vector<ImportEntry> getImportedFunctions() const override;
const string& getPath() const override { return m_path; }
private:
bool parseHeaders();
bool parseImportTable();
bool parseExportTable();
string m_path;
Arch m_arch;
addr_t m_entry;
addr_t m_image_base;
vector<uint8_t> m_data;
vector<pair<addr_t, addr_t>> m_exec_ranges;
vector<pair<addr_t, string>> m_exports;
vector<ImportEntry> m_imports;
};
+74
View File
@@ -0,0 +1,74 @@
#include "types.hpp"
#include "loader/binary.hpp"
#include "cfg/builder.hpp"
#include "disasm/engine.hpp"
int main(int argc, char* argv[])
{
if (argc < 2)
{
cerr << "usage: cfgrip <binary>" << endl;
return 1;
}
string path = argv[1];
auto binary = Binary::create(path);
if (!binary)
{
cerr << "failed to load binary: " << path << endl;
return 1;
}
cout << "format: ";
switch (binary->getFormat())
{
case BinaryFormat::PE: cout << "PE"; break;
case BinaryFormat::ELF: cout << "ELF"; break;
default: cout << "unknown";
}
cout << endl;
cout << "arch: ";
switch (binary->getArch())
{
case Arch::X86: cout << "x86"; break;
case Arch::X64: cout << "x86-64"; break;
default: cout << "unknown";
}
cout << endl;
cout << "entry: 0x" << hex << binary->getEntryPoint() << dec << endl;
cout << "imports: " << binary->getImportedFunctions().size() << endl;
for (const auto& imp : binary->getImportedFunctions())
{
cout << " 0x" << hex << imp.address << " " << imp.name;
if (!imp.library.empty()) cout << " (" << imp.library << ")";
cout << dec << endl;
}
Disassembler dis;
if (!dis.init(binary->getArch()))
{
cerr << "failed to init capstone" << endl;
return 1;
}
CFGBuilder builder(binary.get(), &dis);
CFG cfg = builder.build();
cout << "functions: " << cfg.functions.size() << endl;
cout << "indirect targets: " << cfg.indirect_targets.size() << endl;
string out = path + ".cfg";
ofstream f(out);
if (f)
{
f << cfg.toJSON();
f.close();
cout << "cfg written to: " << out << endl;
}
return 0;
}
Binary file not shown.
+151183
View File
File diff suppressed because it is too large Load Diff
+169
View File
@@ -0,0 +1,169 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include <sstream>
#include <iomanip>
#include <iostream>
#include <fstream>
#include <memory>
using namespace std;
using addr_t = uint64_t;
struct ImportEntry
{
addr_t address;
string name;
string library;
};
struct IndirectTarget
{
addr_t instruction_addr;
addr_t resolved_target;
string name;
string library;
bool resolved;
};
struct Instruction
{
addr_t address;
vector<uint8_t> bytes;
string mnemonic;
string operands;
uint8_t size;
};
struct BasicBlock
{
addr_t address;
vector<Instruction> instructions;
vector<addr_t> successors;
bool is_prolog;
bool is_epilog;
};
struct Function
{
addr_t address;
string name;
vector<BasicBlock> blocks;
};
struct CFG
{
string binary_path;
string arch_str;
string format_str;
addr_t entry_point;
vector<Function> functions;
vector<ImportEntry> imports;
vector<IndirectTarget> indirect_targets;
static string escapeJSON(const string& s)
{
ostringstream os;
for (char c : s)
{
switch (c)
{
case '"': os << "\\\""; break;
case '\\': os << "\\\\"; break;
case '\n': os << "\\n"; break;
case '\r': os << "\\r"; break;
case '\t': os << "\\t"; break;
default: os << c;
}
}
return os.str();
}
string toJSON() const
{
ostringstream os;
os << "{\n";
os << " \"binary\": \"" << escapeJSON(binary_path) << "\",\n";
os << " \"arch\": \"" << escapeJSON(arch_str) << "\",\n";
os << " \"format\": \"" << escapeJSON(format_str) << "\",\n";
os << " \"entry_point\": \"0x" << hex << entry_point << "\",\n";
os << " \"imports\": [\n";
for (size_t i = 0; i < imports.size(); ++i) {
os << " {\n";
os << " \"address\": \"0x" << hex << imports[i].address << "\",\n";
os << " \"name\": \"" << escapeJSON(imports[i].name) << "\",\n";
os << " \"library\": \"" << escapeJSON(imports[i].library) << "\"\n";
os << " }";
if (i < imports.size() - 1) os << ",";
os << "\n";
}
os << " ],\n";
os << " \"indirect_targets\": [\n";
for (size_t i = 0; i < indirect_targets.size(); ++i) {
const auto& it = indirect_targets[i];
os << " {\n";
os << " \"instruction\": \"0x" << hex << it.instruction_addr << "\",\n";
if (it.resolved) {
os << " \"resolved_target\": \"0x" << it.resolved_target << "\",\n";
} else {
os << " \"resolved_target\": null,\n";
}
os << " \"name\": \"" << escapeJSON(it.name) << "\",\n";
os << " \"library\": \"" << escapeJSON(it.library) << "\"\n";
os << " }";
if (i < indirect_targets.size() - 1) os << ",";
os << "\n";
}
os << " ],\n";
os << " \"functions\": [\n";
for (size_t i = 0; i < functions.size(); ++i) {
const auto& f = functions[i];
os << " {\n";
os << " \"address\": \"0x" << hex << f.address << "\",\n";
os << " \"name\": \"" << escapeJSON(f.name) << "\",\n";
os << " \"blocks\": [\n";
for (size_t j = 0; j < f.blocks.size(); ++j) {
const auto& b = f.blocks[j];
os << " {\n";
os << " \"address\": \"0x" << hex << b.address << "\",\n";
os << " \"size\": " << dec << b.instructions.size() << ",\n";
os << " \"is_prolog\": " << (b.is_prolog ? "true" : "false") << ",\n";
os << " \"is_epilog\": " << (b.is_epilog ? "true" : "false") << ",\n";
os << " \"instructions\": [\n";
for (size_t k = 0; k < b.instructions.size(); ++k) {
const auto& inst = b.instructions[k];
os << " {\n";
os << " \"address\": \"0x" << hex << inst.address << "\",\n";
os << " \"size\": " << dec << (int)inst.size << ",\n";
os << " \"mnemonic\": \"" << escapeJSON(inst.mnemonic) << "\",\n";
os << " \"operands\": \"" << escapeJSON(inst.operands) << "\"\n";
os << " }";
if (k < b.instructions.size() - 1) os << ",";
os << "\n";
}
os << " ],\n";
os << " \"successors\": [\n";
for (size_t k = 0; k < b.successors.size(); ++k) {
os << " \"0x" << hex << b.successors[k] << "\"";
if (k < b.successors.size() - 1) os << ",";
os << "\n";
}
os << " ]\n";
os << " }";
if (j < f.blocks.size() - 1) os << ",";
os << "\n";
}
os << " ]\n";
os << " }";
if (i < functions.size() - 1) os << ",";
os << "\n";
}
os << " ]\n";
os << "}\n";
return os.str();
}
};