re disasm: each [esp+N]/[ebp+N] operand is named as the argument or local it is (argN, local entry-0xN, return address), from the stack depth tracked over the function's control flow; _alloca_probe frames and known callee cleanup included, inferred cleanup marked ~

This commit is contained in:
KeelrLeek committed 2026-10-06 21:49:58 -05:00
1 parent eb948ddda1
commit 99c83bfb75
1 file changed
+140 -2
+140 -2
View File
@@ -257,6 +257,7 @@ class Reverse:
raise HarborError(f"the function is {size} bytes; pass max_bytes (at most {MAX_BYTES}) and walk it in parts") raise HarborError(f"the function is {size} bytes; pass max_bytes (at most {MAX_BYTES}) and walk it in parts")
code = self.read(start, size) code = self.read(start, size)
md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32) md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32)
md.detail = True
lines = [] lines = []
if func and not max_bytes: if func and not max_bytes:
lines.append(f"; {func[2] or 'unnamed'} 0x{func[0]:X}..0x{func[0] + func[1]:X} ({func[1]} bytes)") lines.append(f"; {func[2] or 'unnamed'} 0x{func[0]:X}..0x{func[0] + func[1]:X} ({func[1]} bytes)")
@@ -264,11 +265,148 @@ class Reverse:
if conv: if conv:
lines.append(f"; convention from the bytes: {conv['summary']}; registers in: " lines.append(f"; convention from the bytes: {conv['summary']}; registers in: "
f"{', '.join(conv['registers_in']) or 'none'}; ret pops {conv['ret_pops']}") f"{', '.join(conv['registers_in']) or 'none'}; ret pops {conv['ret_pops']}")
frames = self._stack_depths(md, func) if func else {}
if frames:
lines.append("; stack notes: argN = the Nth stack argument (entry esp + 4N), local = below the return "
"address, ~ = after a call whose cleanup was inferred from the pushes")
for ins in md.disasm(code, start): for ins in md.disasm(code, start):
note = self._annotate(ins.op_str, ins.mnemonic, start, start + size) notes = [n for n in (self._frame_note(ins, frames.get(ins.address)),
lines.append(f"{ins.address:08X} {ins.mnemonic:7} {ins.op_str}{(' ; ' + note) if note else ''}") self._annotate(ins.op_str, ins.mnemonic, start, start + size)) if n]
lines.append(f"{ins.address:08X} {ins.mnemonic:7} {ins.op_str}{(' ; ' + ', '.join(notes)) if notes else ''}")
return "\n".join(lines) return "\n".join(lines)
def _callee_pops(self, target):
"""Bytes a directly called function pops on return (its `ret N`), or None when not known."""
key = ("pops", target)
if key not in self._cache:
pops = None
try:
head = self.read(target, 5)
if head == b"\x3d\x00\x10\x00\x00": # cmp eax, 0x1000: the stack probe (_alloca_probe)
pops = "probe"
else:
conv = self.convention(target) if self.function_at(target) and \
self.function_at(target)[0] == target else None
if conv and len(conv["ret_pops"]) == 1:
pops = conv["ret_pops"][0]
except Exception: # noqa: BLE001 - unknown cleanup is reported as inferred
pops = None
self._cache[key] = pops
return self._cache[key]
def _stack_depths(self, md, func):
"""{address: (bytes pushed since entry or None, ebp's depth or None, inferred)} over the function's control
flow, so an [esp+N] or [ebp+N] operand can be named as the argument or local it is."""
import capstone
from capstone import x86
start, size = func[0], func[1]
if size > 0x8000:
return {}
insns = list(md.disasm(self.read(start, size), start))
index = {ins.address: i for i, ins in enumerate(insns)}
state = {start: (0, None, 0, False)} # depth, ebp depth, bytes pushed since the last call, inferred
work = [start]
while work:
addr = work.pop()
i = index.get(addr)
while i is not None and i < len(insns):
ins = insns[i]
d, e, p, guess = state[ins.address]
ops, m, text = ins.operands, ins.mnemonic, ins.op_str
imm = ops[1].imm if len(ops) == 2 and ops[1].type == x86.X86_OP_IMM else None
if m == "push":
d, p = (d + 4 if d is not None else None), p + 4
elif m == "pushal":
d = d + 32 if d is not None else None
elif m == "pop":
d, p = (d - 4 if d is not None else None), max(0, p - 4)
if text == "ebp":
e = None
elif m == "popal":
d = d - 32 if d is not None else None
elif m == "sub" and text.startswith("esp, ") and imm is not None:
d = d + imm if d is not None else None
elif m == "add" and text.startswith("esp, ") and imm is not None:
d, p = (d - imm if d is not None else None), max(0, p - imm)
elif m == "and" and text.startswith("esp, "):
d = None # aligned: esp no longer tracks the entry
elif m == "mov" and text == "ebp, esp":
e = d
elif m == "mov" and text == "esp, ebp":
d = e
elif m == "leave":
d, e = (e - 4 if e is not None else None), None
elif m == "lea" and text.startswith("esp, [ebp") and e is not None:
d = e - ops[1].mem.disp
elif m == "call":
pops = None
if ops and ops[0].type == x86.X86_OP_IMM:
pops = self._callee_pops(ops[0].imm)
if pops == "probe": # mov eax, N / call _alloca_probe reserves N bytes
prev = insns[i - 1] if i else None
pv = prev.operands[1].imm if prev is not None and prev.mnemonic == "mov" and \
prev.op_str.startswith("eax, ") and prev.operands[1].type == x86.X86_OP_IMM else None
d = d + pv if d is not None and pv is not None else None
elif pops is not None:
d = d - pops if d is not None else None
else:
nxt = insns[i + 1] if i + 1 < len(insns) else None
cleaned = nxt is not None and nxt.mnemonic == "add" and nxt.op_str.startswith("esp, ")
if not cleaned and p:
d, guess = (d - p if d is not None else None), True # the callee popped what was pushed
p = 0
succ = []
if ins.group(capstone.CS_GRP_RET):
pass
elif m == "jmp":
if ops and ops[0].type == x86.X86_OP_IMM and ops[0].imm in index:
succ.append(ops[0].imm)
else:
if ins.group(capstone.CS_GRP_JUMP) and ops and ops[0].type == x86.X86_OP_IMM \
and ops[0].imm in index:
succ.append(ops[0].imm)
if i + 1 < len(insns):
succ.append(insns[i + 1].address)
fall = insns[i + 1].address if i + 1 < len(insns) else None
i = None
for s in succ:
if s in state:
continue
state[s] = (d, e, p, guess)
if s == fall:
i = index[s]
else:
work.append(s)
return state
@staticmethod
def _frame_note(ins, frame):
"""'esp+0x10 = arg2' style notes for the stack operands of one instruction."""
if not frame:
return ""
from capstone import x86
d, e, _, guess = frame
notes = []
for op in ins.operands:
if op.type != x86.X86_OP_MEM or op.mem.index:
continue
base = ins.reg_name(op.mem.base) if op.mem.base else ""
if base == "esp" and d is not None:
off = op.mem.disp - d
elif base == "ebp" and e is not None:
off = op.mem.disp - e
else:
continue
if off == 0:
what = "return address"
elif off >= 4:
k, r = divmod(off - 4, 4)
what = f"arg{k + 1}" + (f"+{r}" if r else "")
else:
what = f"local entry-0x{-off:X}"
notes.append(("~" if guess else "") + what)
return ", ".join(notes)
def _annotate(self, op_str, mnemonic, lo, hi): def _annotate(self, op_str, mnemonic, lo, hi):
notes = [] notes = []
constant = re.search(r"(dword|qword) ptr \[0x([0-9a-f]{6,8})\]", op_str) if mnemonic.startswith("f") else None constant = re.search(r"(dword|qword) ptr \[0x([0-9a-f]{6,8})\]", op_str) if mnemonic.startswith("f") else None