Files
BinaryHardening-cfgrip/cfg/builder.cpp
T
x86byte 6c2dca78ab 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
2026-06-30 10:04:56 +01:00

846 lines
27 KiB
C++

#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, bool 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)
{
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))
{
t = skipEndbr64(t);
block.successors.push_back(t);
if (m_prologs.count(t))
m_funcs.insert(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))
{
t = skipEndbr64(t);
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;
}
resolved = skipEndbr64(resolved);
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)
{
resolved = skipEndbr64(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;
addr_t actual = skipEndbr64(start);
queue<addr_t> q;
q.push(actual);
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);
}
// 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;
}
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 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;
// 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 {
for (const auto& b : f.blocks)
if (!b.instructions.empty()) return false;
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)
{
addr = skipEndbr64(addr);
if (built.count(addr)) return;
built.insert(addr);
auto func = buildFunction(addr, name);
if (isEmpty(func)) return;
setEndAddress(func);
cfg.functions.push_back(func);
scanCallAndJmpTargets(func);
for (auto d : m_funcs)
if (!built.count(d)) pending.push(d);
};
{
addr_t ep = skipEndbr64(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);
}
if (!m_subs_only)
{
for (const auto& exp : m_bin->getExportedFunctions())
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;
for (const auto& imp : m_bin->getImportedFunctions())
imp_map[imp.address] = imp.name;
while (!pending.empty())
{
addr_t a = pending.front(); pending.pop();
a = skipEndbr64(a);
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;
}
auto func = buildFunction(a, name);
if (isEmpty(func)) continue;
setEndAddress(func);
cfg.functions.push_back(func);
scanCallAndJmpTargets(func);
if (!m_subs_only)
{
for (auto p : m_prologs)
if (!built.count(p)) pending.push(p);
}
}
// 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;
return cfg;
}
static int64_t stackDelta(const Instruction& inst)
{
if (inst.mnemonic == "push") return -8;
if (inst.mnemonic == "pop") return 8;
if (inst.mnemonic == "call") return 0;
if (inst.mnemonic == "ret" || inst.mnemonic == "retf") return 8;
if (inst.mnemonic == "sub")
{
size_t p = inst.operands.find("rsp");
if (p == string::npos) p = inst.operands.find("esp");
if (p != string::npos && (p == 0 || inst.operands[p-1] == ' ' || inst.operands[p-1] == ','))
{
size_t x = inst.operands.rfind("0x");
if (x != string::npos && x > p)
{
try { return -(int64_t)stoull(inst.operands.substr(x), nullptr, 16); }
catch (...) {}
}
}
}
if (inst.mnemonic == "add")
{
size_t p = inst.operands.find("rsp");
if (p == string::npos) p = inst.operands.find("esp");
if (p != string::npos && (p == 0 || inst.operands[p-1] == ' ' || inst.operands[p-1] == ','))
{
size_t x = inst.operands.rfind("0x");
if (x != string::npos && x > p)
{
try { return (int64_t)stoull(inst.operands.substr(x), nullptr, 16); }
catch (...) {}
}
}
}
return 0;
}
static void computeStackDeltas(CFG& cfg)
{
for (auto& func : cfg.functions)
{
for (auto& block : func.blocks)
{
int64_t delta = 0;
for (auto& inst : block.instructions)
{
inst.stack_offset = delta;
delta += stackDelta(inst);
}
}
}
}
static void jumpThread(CFG& cfg)
{
map<addr_t, BasicBlock*> addr_to_block;
map<addr_t, Function*> addr_to_func;
for (auto& func : cfg.functions)
{
for (auto& block : func.blocks)
{
addr_to_block[block.address] = &block;
addr_to_func[block.address] = &func;
}
}
for (auto& func : cfg.functions)
{
bool changed = true;
while (changed)
{
changed = false;
for (auto& block : func.blocks)
{
if (block.successors.size() != 1) continue;
addr_t t = block.successors[0];
auto it = addr_to_block.find(t);
if (it == addr_to_block.end()) continue;
if (addr_to_func[t] != &func) continue;
BasicBlock* target = it->second;
if (!target->instructions.empty() &&
target->instructions.back().mnemonic == "jmp" &&
target->successors.size() == 1)
{
addr_t redirect = target->successors[0];
if (redirect != block.address && redirect != t)
{
block.successors[0] = redirect;
changed = true;
}
}
}
}
}
}
static void pruneDeadBlocks(CFG& cfg)
{
for (auto& func : cfg.functions)
{
if (func.blocks.empty()) continue;
set<addr_t> reachable;
queue<addr_t> q;
reachable.insert(func.blocks[0].address);
q.push(func.blocks[0].address);
map<addr_t, BasicBlock*> addr_map;
for (auto& b : func.blocks) addr_map[b.address] = &b;
while (!q.empty())
{
addr_t cur = q.front(); q.pop();
auto it = addr_map.find(cur);
if (it == addr_map.end()) continue;
for (auto s : it->second->successors)
if (reachable.insert(s).second) q.push(s);
}
vector<BasicBlock> alive;
for (auto& b : func.blocks)
if (reachable.count(b.address))
alive.push_back(move(b));
func.blocks = move(alive);
}
}
static void computeXrefs(CFG& cfg)
{
map<addr_t, vector<pair<addr_t, string>>> xmap;
for (const auto& func : cfg.functions)
{
for (const auto& block : func.blocks)
{
for (const auto& inst : block.instructions)
{
if (inst.mnemonic == "call" || inst.mnemonic == "jmp" ||
(inst.mnemonic.size() == 2 && inst.mnemonic[0] == 'j' && inst.mnemonic != "jn"))
{
size_t pos = inst.operands.find("0x");
if (pos != string::npos)
{
try
{
addr_t target = stoull(inst.operands.substr(pos), nullptr, 16);
string type = inst.mnemonic;
xmap[target].push_back({inst.address, type});
}
catch (...) {}
}
}
}
}
}
for (auto& kv : xmap)
{
XrefEntry xe;
xe.target = kv.first;
xe.callers = move(kv.second);
cfg.xrefs.push_back(move(xe));
}
}
void CFGBuilder::clean(CFG& cfg)
{
jumpThread(cfg);
pruneDeadBlocks(cfg);
computeStackDeltas(cfg);
cfg.has_stack_offsets = true;
computeXrefs(cfg);
}