mirror of
https://gitlab.com/BinaryHardening/cfgrip
synced 2026-07-26 12:41:08 +00:00
Function boundary detection: multi-pass prolog, tail-call, .pdata, data pointers
- Extended prolog patterns: MSVC x64 callee-saves, sub rsp >= 0x20, enter - Tail-call detection: jmp to prolog candidates seeds new functions - endbr64/endbr32 CET skipping at function starts - PE .pdata exception table parsing for precise start/end boundaries - Data-section function pointer scanning (vtables, callbacks) - PLT stub detection with is_thunk tag - end_address and is_thunk fields in Function struct and JSON output - getDataRanges() and getRuntimeFunctions() in Binary interface - README docs for all new features + research paper reference
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
build/
|
build/
|
||||||
|
build_wsl/
|
||||||
tests/*.subs.cfg
|
tests/*.subs.cfg
|
||||||
tests/*.clean.cfg
|
tests/*.clean.cfg
|
||||||
tests/*.subs.clean.cfg
|
tests/*.subs.clean.cfg
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ cfgrip gives you the map. What you do with it is up to you.
|
|||||||
|
|
||||||
For every binary cfgrip processes, it produces:
|
For every binary cfgrip processes, it produces:
|
||||||
|
|
||||||
- **Function list** with addresses and names (entry point, exports, discovered via `call` instructions)
|
- **Function list** with addresses, optional end address (from `.pdata`), names (entry point, exports, discovered), and thunk tagging (PLT stubs)
|
||||||
- **Basic blocks** per function — instruction sequences terminated by branches, calls, returns, or traps
|
- **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)
|
- **Control flow edges** — successors of each block (direct branches, fall-through, indirect targets)
|
||||||
- **Import table** — resolved library imports with addresses
|
- **Import table** — resolved library imports with addresses
|
||||||
- **Indirect targets** — GOT-resolved calls, jump table entries, register-traced branches, and unresolved ones (marked as such)
|
- **Indirect targets** — GOT-resolved calls, jump table entries, register-traced branches, and unresolved ones (marked as such)
|
||||||
|
- **Function boundaries** — multiple detection passes (prolog patterns, call targets, tail calls, `.pdata` entries, data-section function pointers) for maximum coverage
|
||||||
|
|
||||||
With `--clean`, it additionally:
|
With `--clean`, it additionally:
|
||||||
|
|
||||||
@@ -133,8 +134,8 @@ imports: 85
|
|||||||
0x140020290 HeapSize (KERNEL32.dll)
|
0x140020290 HeapSize (KERNEL32.dll)
|
||||||
0x140020298 CreateFileW (KERNEL32.dll)
|
0x140020298 CreateFileW (KERNEL32.dll)
|
||||||
0x1400202a0 RtlUnwind (KERNEL32.dll)
|
0x1400202a0 RtlUnwind (KERNEL32.dll)
|
||||||
functions: 414
|
functions: 1975
|
||||||
indirect targets: 1372
|
indirect targets: 3453
|
||||||
cfg written to: tests\example1.exe.cfg
|
cfg written to: tests\example1.exe.cfg
|
||||||
```
|
```
|
||||||
---
|
---
|
||||||
@@ -384,6 +385,10 @@ The `.cfg` file is structured JSON. Here's what it looks like:
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Each function now includes optional fields:
|
||||||
|
- `end_address` — precise function end when available (from PE `.pdata` exception table), otherwise computed as the maximum instruction address across all blocks
|
||||||
|
- `is_thunk` — `true` for PLT stubs and import thunks (functions that only redirect to another address)
|
||||||
|
|
||||||
### `--subs-only` output
|
### `--subs-only` output
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -501,6 +506,20 @@ Every instruction in `--clean` mode includes `stack_offset` — the RSP delta fr
|
|||||||
|
|
||||||
Same structure as `--clean`, but with `"mode": "subs-only+clean"` to indicate both filters were applied. Function count is reduced to only entry-point-reachable functions, and remaining functions have stack offsets and xrefs.
|
Same structure as `--clean`, but with `"mode": "subs-only+clean"` to indicate both filters were applied. Function count is reduced to only entry-point-reachable functions, and remaining functions have stack offsets and xrefs.
|
||||||
|
|
||||||
|
## function boundary detection
|
||||||
|
|
||||||
|
cfgrip discovers functions through multiple detection passes:
|
||||||
|
|
||||||
|
| Pass | What it detects | Covers |
|
||||||
|
|---|---|---|
|
||||||
|
| **Prolog scanning** | `push rbp`, `push r15`/`r14`/`r13`/`r12`/`rbx`/`rdi`/`rsi`, `sub rsp, >=0x20`, `enter` | MSVC x64, GCC, leaf functions, CET (`endbr64`) |
|
||||||
|
| **Call targets** | Every `call` instruction target is a function start | Direct and GOT-resolved indirect calls |
|
||||||
|
| **Tail calls** | `jmp` instructions targeting prolog candidates | Optimized tail-call chains |
|
||||||
|
| **`.pdata` (PE)** | Runtime function entries from the exception handler table | Precise start/end for every x64 PE function |
|
||||||
|
| **Data pointers** | 8-byte values in `.rdata`/`.data` pointing into executable code | Function pointers, vtables, callbacks |
|
||||||
|
|
||||||
|
Functions with `is_thunk: true` are PLT stubs or import thunks — single-block functions that redirect to another address.
|
||||||
|
|
||||||
## building
|
## building
|
||||||
|
|
||||||
Requires CMake and a C++17 compiler. Capstone is fetched automatically.
|
Requires CMake and a C++17 compiler. Capstone is fetched automatically.
|
||||||
@@ -541,8 +560,31 @@ cmake --build build --config Release
|
|||||||
| **Function discovery** | Entry point | YES |
|
| **Function discovery** | Entry point | YES |
|
||||||
| | Exports | YES |
|
| | Exports | YES |
|
||||||
| | `call` targets | YES |
|
| | `call` targets | YES |
|
||||||
| | Prolog scanning (`push rbp`, CET `endbr64`) | YES |
|
| | Prolog scanning (MSVC x64, GCC, CET `endbr64`) | YES |
|
||||||
|
| | Tail-call detection (`jmp` → function) | YES |
|
||||||
|
| | PE `.pdata` (exception handler table) | YES |
|
||||||
|
| | Data-section function pointer scan | YES |
|
||||||
|
| **Thunk detection** | PLT stubs / import thunks (`is_thunk`) | YES |
|
||||||
|
| **Function boundaries** | `end_address` from `.pdata` or max instruction | YES |
|
||||||
| **Subs-only mode** | `--subs-only` flag | YES |
|
| **Subs-only mode** | `--subs-only` flag | YES |
|
||||||
| **CFG cleaning** | `--clean` (jump-thread, dead-block prune, stack deltas, xrefs) | YES |
|
| **CFG cleaning** | `--clean` (jump-thread, dead-block prune, stack deltas, xrefs) | YES |
|
||||||
|
|
||||||
|
## research
|
||||||
|
|
||||||
|
Function boundary detection in this tool is based on the approach described in **"Function Boundary Detection in Stripped Binaries"** (Alves-Foss & Song, 2019), which introduces a multi-heuristic algorithm for locating function starts and ends in stripped x86/x64 binaries.
|
||||||
|
|
||||||
|
The paper is available at `papers/Function_Boundary_Detection_in_Stripped_Binaries.pdf`.
|
||||||
|
|
||||||
|
**How our implementation maps to the paper's heuristics:**
|
||||||
|
|
||||||
|
| Heuristic | Paper description | Our implementation |
|
||||||
|
|---|---|---|
|
||||||
|
| H1–H4 | Prolog signatures (`push rbp`, callee-saved regs, stack sub, `enter`) | `isProlog()` in `disasm/engine.cpp` — detects `push rbp`, `push r15..rbx`, `sub rsp >= 0x20`, `enter` |
|
||||||
|
| H5 | Call-target seeding | Every direct `call` target is a function start |
|
||||||
|
| H6 | Jump-to-function (tail call) detection | `jmp` to prolog candidates adds target to function queue |
|
||||||
|
| H7 | Exception table parsing | PE `.pdata` RUNTIME_FUNCTION entries give precise start/end |
|
||||||
|
| H8 | Data reference analysis | `scanDataPointers()` walks data sections for code pointers |
|
||||||
|
|
||||||
|
The paper's key insight is that algorithmic heuristics — without machine learning — can achieve high accuracy on stripped binaries. Our implementation follows this philosophy, using a multi-pass approach where each pass catches functions the others might miss.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+150
-22
@@ -20,6 +20,20 @@ static bool regMatch(const string& op, const string& target)
|
|||||||
CFGBuilder::CFGBuilder(Binary* binary, Disassembler* disasm, bool subsOnly)
|
CFGBuilder::CFGBuilder(Binary* binary, Disassembler* disasm, bool subsOnly)
|
||||||
: m_bin(binary), m_dis(disasm), m_subs_only(subsOnly) {}
|
: m_bin(binary), m_dis(disasm), m_subs_only(subsOnly) {}
|
||||||
|
|
||||||
|
addr_t CFGBuilder::skipEndbr64(addr_t addr)
|
||||||
|
{
|
||||||
|
auto bytes = m_bin->readBytes(addr, 4);
|
||||||
|
if (bytes.size() >= 4)
|
||||||
|
{
|
||||||
|
// endbr64: F3 0F 1E FA
|
||||||
|
// endbr32: F3 0F 1E FB
|
||||||
|
if (bytes[0] == 0xF3 && bytes[1] == 0x0F && bytes[2] == 0x1E &&
|
||||||
|
(bytes[3] == 0xFA || bytes[3] == 0xFB))
|
||||||
|
return addr + 4;
|
||||||
|
}
|
||||||
|
return addr;
|
||||||
|
}
|
||||||
|
|
||||||
bool CFGBuilder::isVisited(addr_t addr)
|
bool CFGBuilder::isVisited(addr_t addr)
|
||||||
{
|
{
|
||||||
return m_blocks.count(addr) > 0;
|
return m_blocks.count(addr) > 0;
|
||||||
@@ -239,7 +253,12 @@ BasicBlock CFGBuilder::disassembleBlock(addr_t start, addr_t& next)
|
|||||||
{
|
{
|
||||||
addr_t t = m_dis->getBranchTarget(inst);
|
addr_t t = m_dis->getBranchTarget(inst);
|
||||||
if (t && !m_dis->isIndirectBranch(inst))
|
if (t && !m_dis->isIndirectBranch(inst))
|
||||||
|
{
|
||||||
|
t = skipEndbr64(t);
|
||||||
block.successors.push_back(t);
|
block.successors.push_back(t);
|
||||||
|
if (m_prologs.count(t))
|
||||||
|
m_funcs.insert(t);
|
||||||
|
}
|
||||||
else if (m_dis->isIndirectBranch(inst))
|
else if (m_dis->isIndirectBranch(inst))
|
||||||
{
|
{
|
||||||
addr_t resolved = resolveGOT(inst);
|
addr_t resolved = resolveGOT(inst);
|
||||||
@@ -322,6 +341,7 @@ BasicBlock CFGBuilder::disassembleBlock(addr_t start, addr_t& next)
|
|||||||
addr_t t = m_dis->getBranchTarget(inst);
|
addr_t t = m_dis->getBranchTarget(inst);
|
||||||
if (t && !m_dis->isIndirectBranch(inst))
|
if (t && !m_dis->isIndirectBranch(inst))
|
||||||
{
|
{
|
||||||
|
t = skipEndbr64(t);
|
||||||
m_funcs.insert(t);
|
m_funcs.insert(t);
|
||||||
recordIndirect(inst, t, true, "", "");
|
recordIndirect(inst, t, true, "", "");
|
||||||
}
|
}
|
||||||
@@ -342,6 +362,7 @@ BasicBlock CFGBuilder::disassembleBlock(addr_t start, addr_t& next)
|
|||||||
((uint64_t)gb[6] << 48) | ((uint64_t)gb[7] << 56);
|
((uint64_t)gb[6] << 48) | ((uint64_t)gb[7] << 56);
|
||||||
if (target && target < 0x100000000ULL) resolved = target;
|
if (target && target < 0x100000000ULL) resolved = target;
|
||||||
}
|
}
|
||||||
|
resolved = skipEndbr64(resolved);
|
||||||
m_funcs.insert(resolved);
|
m_funcs.insert(resolved);
|
||||||
recordIndirect(inst, resolved, true, nm, lb);
|
recordIndirect(inst, resolved, true, nm, lb);
|
||||||
}
|
}
|
||||||
@@ -352,6 +373,7 @@ BasicBlock CFGBuilder::disassembleBlock(addr_t start, addr_t& next)
|
|||||||
resolved = traceReg(reg, block.instructions, block.instructions.size() - 1);
|
resolved = traceReg(reg, block.instructions, block.instructions.size() - 1);
|
||||||
if (resolved)
|
if (resolved)
|
||||||
{
|
{
|
||||||
|
resolved = skipEndbr64(resolved);
|
||||||
m_funcs.insert(resolved);
|
m_funcs.insert(resolved);
|
||||||
recordIndirect(inst, resolved, true, "traced", "");
|
recordIndirect(inst, resolved, true, "traced", "");
|
||||||
}
|
}
|
||||||
@@ -411,8 +433,10 @@ Function CFGBuilder::buildFunction(addr_t start, const string& name)
|
|||||||
func.address = start;
|
func.address = start;
|
||||||
func.name = name;
|
func.name = name;
|
||||||
|
|
||||||
|
addr_t actual = skipEndbr64(start);
|
||||||
|
|
||||||
queue<addr_t> q;
|
queue<addr_t> q;
|
||||||
q.push(start);
|
q.push(actual);
|
||||||
|
|
||||||
while (!q.empty())
|
while (!q.empty())
|
||||||
{
|
{
|
||||||
@@ -428,9 +452,66 @@ Function CFGBuilder::buildFunction(addr_t start, const string& name)
|
|||||||
func.blocks.push_back(block);
|
func.blocks.push_back(block);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compute end_address as max instruction address across all blocks
|
||||||
|
for (const auto& b : func.blocks)
|
||||||
|
for (const auto& inst : b.instructions)
|
||||||
|
if (inst.address + inst.size > func.end_address)
|
||||||
|
func.end_address = inst.address + inst.size;
|
||||||
|
|
||||||
|
// Tag PLT stubs
|
||||||
|
if (isPLTStub(func)) func.is_thunk = true;
|
||||||
|
|
||||||
return func;
|
return func;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CFGBuilder::isPLTStub(const Function& func) const
|
||||||
|
{
|
||||||
|
if (func.blocks.size() != 1) return false;
|
||||||
|
const auto& block = func.blocks[0];
|
||||||
|
if (block.instructions.empty()) return false;
|
||||||
|
const auto& first = block.instructions[0];
|
||||||
|
if (first.mnemonic == "jmp" && first.operands.find('[') != string::npos)
|
||||||
|
{
|
||||||
|
int64_t disp = m_dis->getRIPDisp(first);
|
||||||
|
if (disp != 0)
|
||||||
|
{
|
||||||
|
addr_t target = first.address + first.size + disp;
|
||||||
|
if (m_got.count(target)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CFGBuilder::scanDataPointers()
|
||||||
|
{
|
||||||
|
auto ranges = m_bin->getDataRanges();
|
||||||
|
auto exec = m_bin->getExecutableRanges();
|
||||||
|
|
||||||
|
auto isInExec = [&](addr_t addr) -> bool {
|
||||||
|
for (const auto& r : exec)
|
||||||
|
if (addr >= r.first && addr < r.second) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& r : ranges)
|
||||||
|
{
|
||||||
|
addr_t a = r.first;
|
||||||
|
// Align to 8 bytes
|
||||||
|
if (a & 7) a = (a + 7) & ~7ULL;
|
||||||
|
for (; a + 8 <= r.second; a += 8)
|
||||||
|
{
|
||||||
|
auto bytes = m_bin->readBytes(a, 8);
|
||||||
|
if (bytes.size() < 8) continue;
|
||||||
|
uint64_t val = 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);
|
||||||
|
if (val && isInExec(val))
|
||||||
|
m_funcs.insert(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CFG CFGBuilder::build()
|
CFG CFGBuilder::build()
|
||||||
{
|
{
|
||||||
CFG cfg;
|
CFG cfg;
|
||||||
@@ -458,36 +539,67 @@ CFG CFGBuilder::build()
|
|||||||
set<addr_t> built;
|
set<addr_t> built;
|
||||||
queue<addr_t> pending;
|
queue<addr_t> pending;
|
||||||
|
|
||||||
|
// Apply .pdata end_address to functions
|
||||||
|
auto pdata = m_bin->getRuntimeFunctions();
|
||||||
|
auto setEndAddress = [&](Function& func) {
|
||||||
|
for (const auto& rf : pdata)
|
||||||
|
if (rf.first == func.address)
|
||||||
|
{
|
||||||
|
func.end_address = rf.second;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
auto isEmpty = [](const Function& f) -> bool {
|
auto isEmpty = [](const Function& f) -> bool {
|
||||||
for (const auto& b : f.blocks)
|
for (const auto& b : f.blocks)
|
||||||
if (!b.instructions.empty()) return false;
|
if (!b.instructions.empty()) return false;
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
auto scanCallAndJmpTargets = [&](const Function& 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))
|
||||||
|
{
|
||||||
|
t = skipEndbr64(t);
|
||||||
|
m_funcs.insert(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_dis->isUnconditionalBranch(inst))
|
||||||
|
{
|
||||||
|
addr_t t = m_dis->getBranchTarget(inst);
|
||||||
|
if (t && !m_dis->isIndirectBranch(inst))
|
||||||
|
{
|
||||||
|
t = skipEndbr64(t);
|
||||||
|
if (m_prologs.count(t))
|
||||||
|
m_funcs.insert(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
auto process = [&](addr_t addr, const string& name)
|
auto process = [&](addr_t addr, const string& name)
|
||||||
{
|
{
|
||||||
|
addr = skipEndbr64(addr);
|
||||||
if (built.count(addr)) return;
|
if (built.count(addr)) return;
|
||||||
built.insert(addr);
|
built.insert(addr);
|
||||||
|
|
||||||
auto func = buildFunction(addr, name);
|
auto func = buildFunction(addr, name);
|
||||||
if (isEmpty(func)) return;
|
if (isEmpty(func)) return;
|
||||||
|
setEndAddress(func);
|
||||||
cfg.functions.push_back(func);
|
cfg.functions.push_back(func);
|
||||||
|
scanCallAndJmpTargets(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)
|
for (auto d : m_funcs)
|
||||||
if (!built.count(d)) pending.push(d);
|
if (!built.count(d)) pending.push(d);
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
addr_t ep = m_bin->getEntryPoint();
|
addr_t ep = skipEndbr64(m_bin->getEntryPoint());
|
||||||
string ep_name = "entry";
|
string ep_name = "entry";
|
||||||
for (const auto& exp : m_bin->getExportedFunctions())
|
for (const auto& exp : m_bin->getExportedFunctions())
|
||||||
if (exp.first == ep) { ep_name = exp.second; break; }
|
if (exp.first == ep) { ep_name = exp.second; break; }
|
||||||
@@ -497,7 +609,10 @@ CFG CFGBuilder::build()
|
|||||||
if (!m_subs_only)
|
if (!m_subs_only)
|
||||||
{
|
{
|
||||||
for (const auto& exp : m_bin->getExportedFunctions())
|
for (const auto& exp : m_bin->getExportedFunctions())
|
||||||
pending.push(exp.first);
|
pending.push(skipEndbr64(exp.first));
|
||||||
|
for (const auto& rf : pdata)
|
||||||
|
if (rf.first != m_bin->getEntryPoint())
|
||||||
|
pending.push(skipEndbr64(rf.first));
|
||||||
}
|
}
|
||||||
|
|
||||||
map<addr_t, string> imp_map;
|
map<addr_t, string> imp_map;
|
||||||
@@ -507,6 +622,7 @@ CFG CFGBuilder::build()
|
|||||||
while (!pending.empty())
|
while (!pending.empty())
|
||||||
{
|
{
|
||||||
addr_t a = pending.front(); pending.pop();
|
addr_t a = pending.front(); pending.pop();
|
||||||
|
a = skipEndbr64(a);
|
||||||
if (built.count(a)) continue;
|
if (built.count(a)) continue;
|
||||||
|
|
||||||
string name;
|
string name;
|
||||||
@@ -518,19 +634,11 @@ CFG CFGBuilder::build()
|
|||||||
if (it != imp_map.end()) name = it->second;
|
if (it != imp_map.end()) name = it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
built.insert(a);
|
|
||||||
auto func = buildFunction(a, name);
|
auto func = buildFunction(a, name);
|
||||||
if (isEmpty(func)) continue;
|
if (isEmpty(func)) continue;
|
||||||
|
setEndAddress(func);
|
||||||
cfg.functions.push_back(func);
|
cfg.functions.push_back(func);
|
||||||
|
scanCallAndJmpTargets(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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_subs_only)
|
if (!m_subs_only)
|
||||||
{
|
{
|
||||||
@@ -539,6 +647,26 @@ CFG CFGBuilder::build()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add data-section function pointers as seeds (if not subs-only)
|
||||||
|
if (!m_subs_only)
|
||||||
|
{
|
||||||
|
scanDataPointers();
|
||||||
|
for (auto d : m_funcs)
|
||||||
|
if (!built.count(d)) pending.push(d);
|
||||||
|
while (!pending.empty())
|
||||||
|
{
|
||||||
|
addr_t a = pending.front(); pending.pop();
|
||||||
|
a = skipEndbr64(a);
|
||||||
|
if (built.count(a)) continue;
|
||||||
|
|
||||||
|
auto func = buildFunction(a, "");
|
||||||
|
if (isEmpty(func)) continue;
|
||||||
|
setEndAddress(func);
|
||||||
|
cfg.functions.push_back(func);
|
||||||
|
scanCallAndJmpTargets(func);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cfg.indirect_targets = m_targets;
|
cfg.indirect_targets = m_targets;
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ class CFGBuilder
|
|||||||
Function buildFunction(addr_t start, const string& name);
|
Function buildFunction(addr_t start, const string& name);
|
||||||
BasicBlock disassembleBlock(addr_t start, addr_t& next);
|
BasicBlock disassembleBlock(addr_t start, addr_t& next);
|
||||||
set<addr_t> findPrologs();
|
set<addr_t> findPrologs();
|
||||||
|
void scanDataPointers();
|
||||||
|
addr_t skipEndbr64(addr_t addr);
|
||||||
|
bool isPLTStub(const Function& func) const;
|
||||||
void markVisited(addr_t addr);
|
void markVisited(addr_t addr);
|
||||||
bool isVisited(addr_t addr);
|
bool isVisited(addr_t addr);
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,32 @@ bool Disassembler::isProlog(const vector<Instruction>& insts) const
|
|||||||
{
|
{
|
||||||
if (inst.mnemonic == "push" && (inst.operands == "rbp" || inst.operands == "ebp"))
|
if (inst.mnemonic == "push" && (inst.operands == "rbp" || inst.operands == "ebp"))
|
||||||
return true;
|
return true;
|
||||||
|
if (inst.mnemonic == "push")
|
||||||
|
{
|
||||||
|
const string& ops = inst.operands;
|
||||||
|
if (ops == "r15" || ops == "r14" || ops == "r13" || ops == "r12" ||
|
||||||
|
ops == "rbx" || ops == "rdi" || ops == "rsi")
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (inst.mnemonic == "sub")
|
||||||
|
{
|
||||||
|
const string& ops = inst.operands;
|
||||||
|
size_t p = ops.find("rsp");
|
||||||
|
if (p == string::npos) p = ops.find("esp");
|
||||||
|
if (p != string::npos && (p == 0 || ops[p-1] == ' ' || ops[p-1] == ','))
|
||||||
|
{
|
||||||
|
size_t x = ops.rfind("0x");
|
||||||
|
if (x != string::npos && x > p)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
uint64_t val = stoull(ops.substr(x), nullptr, 16);
|
||||||
|
if (val >= 0x20) return true;
|
||||||
|
} catch (...) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inst.mnemonic == "enter")
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ class Binary
|
|||||||
virtual addr_t getImageBase() const = 0;
|
virtual addr_t getImageBase() const = 0;
|
||||||
virtual vector<uint8_t> readBytes(addr_t vaddr, size_t size) 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, addr_t>> getExecutableRanges() const = 0;
|
||||||
|
virtual vector<pair<addr_t, addr_t>> getDataRanges() const
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
virtual vector<pair<addr_t, addr_t>> getRuntimeFunctions() const
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
virtual vector<pair<addr_t, string>> getExportedFunctions() const = 0;
|
virtual vector<pair<addr_t, string>> getExportedFunctions() const = 0;
|
||||||
virtual vector<ImportEntry> getImportedFunctions() const
|
virtual vector<ImportEntry> getImportedFunctions() const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -290,6 +290,19 @@ bool ELFLoader::parseHeader()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collect data section ranges (allocatable, non-executable)
|
||||||
|
for (uint16_t i = 0; i < shnum; i++)
|
||||||
|
{
|
||||||
|
uint64_t sh_flags = 0;
|
||||||
|
size_t so = shoff + i * 64;
|
||||||
|
if (so + 64 > m_data.size()) continue;
|
||||||
|
sh_flags = r64(m_data, so + 8);
|
||||||
|
bool is_exec = (sh_flags & 0x4) != 0;
|
||||||
|
bool is_alloc = (sh_flags & 0x2) != 0;
|
||||||
|
if (is_alloc && !is_exec && sections[i].addr && sections[i].size)
|
||||||
|
m_data_ranges.push_back({sections[i].addr, sections[i].addr + sections[i].size});
|
||||||
|
}
|
||||||
|
|
||||||
for (uint16_t i = 0; i < shnum; i++)
|
for (uint16_t i = 0; i < shnum; i++)
|
||||||
{
|
{
|
||||||
string sname = sectionName(sections[i].name_idx);
|
string sname = sectionName(sections[i].name_idx);
|
||||||
@@ -391,6 +404,11 @@ vector<uint8_t> ELFLoader::readBytes(addr_t vaddr, size_t size) const
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vector<pair<addr_t, addr_t>> ELFLoader::getDataRanges() const
|
||||||
|
{
|
||||||
|
return m_data_ranges;
|
||||||
|
}
|
||||||
|
|
||||||
vector<pair<addr_t, addr_t>> ELFLoader::getExecutableRanges() const
|
vector<pair<addr_t, addr_t>> ELFLoader::getExecutableRanges() const
|
||||||
{
|
{
|
||||||
return m_exec_ranges;
|
return m_exec_ranges;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class ELFLoader : public Binary
|
|||||||
addr_t getEntryPoint() const override { return m_entry; }
|
addr_t getEntryPoint() const override { return m_entry; }
|
||||||
addr_t getImageBase() const override { return 0; }
|
addr_t getImageBase() const override { return 0; }
|
||||||
vector<uint8_t> readBytes(addr_t vaddr, size_t size) const override;
|
vector<uint8_t> readBytes(addr_t vaddr, size_t size) const override;
|
||||||
|
vector<pair<addr_t, addr_t>> getDataRanges() const override;
|
||||||
vector<pair<addr_t, addr_t>> getExecutableRanges() const override;
|
vector<pair<addr_t, addr_t>> getExecutableRanges() const override;
|
||||||
vector<pair<addr_t, string>> getExportedFunctions() const override;
|
vector<pair<addr_t, string>> getExportedFunctions() const override;
|
||||||
vector<ImportEntry> getImportedFunctions() const override;
|
vector<ImportEntry> getImportedFunctions() const override;
|
||||||
@@ -39,6 +40,7 @@ class ELFLoader : public Binary
|
|||||||
addr_t m_entry;
|
addr_t m_entry;
|
||||||
vector<uint8_t> m_data;
|
vector<uint8_t> m_data;
|
||||||
vector<pair<addr_t, addr_t>> m_exec_ranges;
|
vector<pair<addr_t, addr_t>> m_exec_ranges;
|
||||||
|
vector<pair<addr_t, addr_t>> m_data_ranges;
|
||||||
vector<tuple<uint64_t, addr_t, uint64_t>> m_file_map;
|
vector<tuple<uint64_t, addr_t, uint64_t>> m_file_map;
|
||||||
vector<pair<addr_t, string>> m_exports;
|
vector<pair<addr_t, string>> m_exports;
|
||||||
vector<ImportEntry> m_imports;
|
vector<ImportEntry> m_imports;
|
||||||
|
|||||||
+60
-1
@@ -120,9 +120,58 @@ bool PELoader::parseHeaders()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collect data section ranges (non-executable readable)
|
||||||
|
for (const auto& s : sections)
|
||||||
|
{
|
||||||
|
if (s.raw_ptr == 0) continue;
|
||||||
|
bool is_exec = (s.characteristics & 0x20000000) != 0;
|
||||||
|
bool is_read = (s.characteristics & 0x40000000) != 0;
|
||||||
|
if (!is_exec && is_read)
|
||||||
|
{
|
||||||
|
addr_t start = s.vaddr;
|
||||||
|
addr_t end = start + (s.vsize ? s.vsize : s.raw_size);
|
||||||
|
m_data_ranges.push_back({start, end});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse .pdata (exception handler table) for precise function boundaries
|
||||||
|
uint32_t pdata_rva = 0;
|
||||||
|
uint32_t pdata_size = 0;
|
||||||
|
uint32_t data_dir_off = (magic == 0x10B) ? opt_hdr_off + 96 : opt_hdr_off + 112;
|
||||||
|
if (magic == 0x20B)
|
||||||
|
{
|
||||||
|
pdata_rva = r32(m_data, data_dir_off + 24);
|
||||||
|
pdata_size = r32(m_data, data_dir_off + 28);
|
||||||
|
}
|
||||||
|
if (pdata_rva && pdata_size >= 12)
|
||||||
|
{
|
||||||
|
uint32_t pdata_off = 0;
|
||||||
|
for (const auto& s : sections)
|
||||||
|
{
|
||||||
|
addr_t va = pdata_rva + m_image_base;
|
||||||
|
if (va >= s.vaddr && va < s.vaddr + (s.vsize ? s.vsize : s.raw_size))
|
||||||
|
{
|
||||||
|
pdata_off = s.raw_ptr + static_cast<uint32_t>(va - s.vaddr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pdata_off)
|
||||||
|
{
|
||||||
|
size_t nentries = pdata_size / 12;
|
||||||
|
for (size_t i = 0; i < nentries; i++)
|
||||||
|
{
|
||||||
|
uint32_t off = pdata_off + i * 12;
|
||||||
|
if (off + 12 > m_data.size()) break;
|
||||||
|
uint32_t begin_rva = r32(m_data, off);
|
||||||
|
uint32_t end_rva = r32(m_data, off + 4);
|
||||||
|
if (begin_rva && end_rva && end_rva > begin_rva)
|
||||||
|
m_pdata.push_back({begin_rva + m_image_base, end_rva + m_image_base});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool is_64 = (m_arch == Arch::X64);
|
bool is_64 = (m_arch == Arch::X64);
|
||||||
uint32_t import_rva = 0;
|
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);
|
import_rva = r32(m_data, data_dir_off + 8);
|
||||||
|
|
||||||
auto rvaToOffset = [&](uint32_t rva) -> uint32_t {
|
auto rvaToOffset = [&](uint32_t rva) -> uint32_t {
|
||||||
@@ -260,11 +309,21 @@ vector<uint8_t> PELoader::readBytes(addr_t vaddr, size_t size) const
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vector<pair<addr_t, addr_t>> PELoader::getDataRanges() const
|
||||||
|
{
|
||||||
|
return m_data_ranges;
|
||||||
|
}
|
||||||
|
|
||||||
vector<pair<addr_t, addr_t>> PELoader::getExecutableRanges() const
|
vector<pair<addr_t, addr_t>> PELoader::getExecutableRanges() const
|
||||||
{
|
{
|
||||||
return m_exec_ranges;
|
return m_exec_ranges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vector<pair<addr_t, addr_t>> PELoader::getRuntimeFunctions() const
|
||||||
|
{
|
||||||
|
return m_pdata;
|
||||||
|
}
|
||||||
|
|
||||||
vector<pair<addr_t, string>> PELoader::getExportedFunctions() const
|
vector<pair<addr_t, string>> PELoader::getExportedFunctions() const
|
||||||
{
|
{
|
||||||
return m_exports;
|
return m_exports;
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ class PELoader : public Binary
|
|||||||
addr_t getEntryPoint() const override { return m_entry; }
|
addr_t getEntryPoint() const override { return m_entry; }
|
||||||
addr_t getImageBase() const override { return m_image_base; }
|
addr_t getImageBase() const override { return m_image_base; }
|
||||||
vector<uint8_t> readBytes(addr_t vaddr, size_t size) const override;
|
vector<uint8_t> readBytes(addr_t vaddr, size_t size) const override;
|
||||||
|
vector<pair<addr_t, addr_t>> getDataRanges() const override;
|
||||||
vector<pair<addr_t, addr_t>> getExecutableRanges() const override;
|
vector<pair<addr_t, addr_t>> getExecutableRanges() const override;
|
||||||
|
vector<pair<addr_t, addr_t>> getRuntimeFunctions() const override;
|
||||||
vector<pair<addr_t, string>> getExportedFunctions() const override;
|
vector<pair<addr_t, string>> getExportedFunctions() const override;
|
||||||
vector<ImportEntry> getImportedFunctions() const override;
|
vector<ImportEntry> getImportedFunctions() const override;
|
||||||
const string& getPath() const override { return m_path; }
|
const string& getPath() const override { return m_path; }
|
||||||
@@ -32,6 +34,8 @@ class PELoader : public Binary
|
|||||||
addr_t m_image_base;
|
addr_t m_image_base;
|
||||||
vector<uint8_t> m_data;
|
vector<uint8_t> m_data;
|
||||||
vector<pair<addr_t, addr_t>> m_exec_ranges;
|
vector<pair<addr_t, addr_t>> m_exec_ranges;
|
||||||
|
vector<pair<addr_t, addr_t>> m_data_ranges;
|
||||||
|
vector<pair<addr_t, addr_t>> m_pdata;
|
||||||
vector<pair<addr_t, string>> m_exports;
|
vector<pair<addr_t, string>> m_exports;
|
||||||
vector<ImportEntry> m_imports;
|
vector<ImportEntry> m_imports;
|
||||||
};
|
};
|
||||||
|
|||||||
Binary file not shown.
+322252
-79879
File diff suppressed because it is too large
Load Diff
@@ -56,8 +56,10 @@ struct BasicBlock
|
|||||||
struct Function
|
struct Function
|
||||||
{
|
{
|
||||||
addr_t address;
|
addr_t address;
|
||||||
|
addr_t end_address = 0;
|
||||||
string name;
|
string name;
|
||||||
vector<BasicBlock> blocks;
|
vector<BasicBlock> blocks;
|
||||||
|
bool is_thunk = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct CFG
|
struct CFG
|
||||||
@@ -135,7 +137,11 @@ struct CFG
|
|||||||
const auto& f = functions[i];
|
const auto& f = functions[i];
|
||||||
os << " {\n";
|
os << " {\n";
|
||||||
os << " \"address\": \"0x" << hex << f.address << "\",\n";
|
os << " \"address\": \"0x" << hex << f.address << "\",\n";
|
||||||
|
if (f.end_address)
|
||||||
|
os << " \"end_address\": \"0x" << hex << f.end_address << "\",\n";
|
||||||
os << " \"name\": \"" << escapeJSON(f.name) << "\",\n";
|
os << " \"name\": \"" << escapeJSON(f.name) << "\",\n";
|
||||||
|
if (f.is_thunk)
|
||||||
|
os << " \"is_thunk\": true,\n";
|
||||||
os << " \"blocks\": [\n";
|
os << " \"blocks\": [\n";
|
||||||
for (size_t j = 0; j < f.blocks.size(); ++j) {
|
for (size_t j = 0; j < f.blocks.size(); ++j) {
|
||||||
const auto& b = f.blocks[j];
|
const auto& b = f.blocks[j];
|
||||||
|
|||||||
Reference in New Issue
Block a user