diff options
| author | blasty <blasty@local> | 2026-07-26 20:49:30 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-26 20:49:30 +0200 |
| commit | 781c5eff4218d7ffdc9905f652ca08ce10953e43 (patch) | |
| tree | ff240cd5d515ac9b64c3c0578d85307a4f293601 /server/patch_server.py | |
| parent | decomp: say the FIX, not the diagnosis (diff) | |
| download | ida-tui-781c5eff4218d7ffdc9905f652ca08ce10953e43.tar.gz ida-tui-781c5eff4218d7ffdc9905f652ca08ce10953e43.tar.xz ida-tui-781c5eff4218d7ffdc9905f652ca08ce10953e43.zip | |
arm: find Thumb entry points from a vector table (Shift+T)
An ARM function pointer carries the mode in bit 0: odd means Thumb. A Cortex-M
vector table is therefore a list of Thumb entry points, and IDA won't follow them
on a headerless image because nothing tells it those words are pointers at all.
Shift+T scans forward from the cursor and marks them.
0 functions -> 3 Thumb entries found, 3 disassembled
A word only counts when it is odd, lands in a loaded segment, and its target is
executable and not already data. The even words in a vector table — the initial
stack pointer — fail the first test, which is the point: marking a data word as
code corrupts the listing, so a false positive costs more than a miss. The
fixture includes an even in-range word and an odd OUT-of-range word to keep that
honest.
A note on how this started: I recommended this feature, then probed
experiments/fibonacci.bin for the signal and found ZERO odd in-range pointers —
it's a flat code blob, not a firmware image. Rather than build a detector I
couldn't test, I wrote experiments/cortexm.bin: a real vector table pointing at
small self-contained Thumb handlers. The first version of that fixture aimed its
handlers into the middle of copied code, so two "entries" were really inside one
function — the tool was right and the fixture was wrong, which is worth stating
because I nearly filed it as a bug.
Function creation goes through one _idatui_add_func helper now, shared with
define_func_run: add_func(ea) alone fails on freshly-marked code (IDA can't find
the end), and the scan hit exactly the same wall `p` did.
Status precedence, fixed properly this time. An action's result kept being
overwritten by the reload it triggered — cursor moved, filter re-applied,
functions re-counted. I patched that at FIVE separate call sites before
admitting it's one problem. _status(text, priority=True) now marks a result: it
holds the bar for 8s or until the next keypress, and routine chatter can't
outrank it. The per-site special cases are gone.
tests: +4 thumb (20) — a bare vector table gives IDA nothing, scanning finds
exactly the three handlers, the non-pointer words are ignored, and the result
survives both the reload and the reindex. 209/0 scenarios, 30/0 blob, 30/0
project UI.
Diffstat (limited to 'server/patch_server.py')
| -rw-r--r-- | server/patch_server.py | 120 |
1 files changed, 101 insertions, 19 deletions
diff --git a/server/patch_server.py b/server/patch_server.py index 2de852f..160b15b 100644 --- a/server/patch_server.py +++ b/server/patch_server.py @@ -1033,6 +1033,31 @@ def set_thumb( # discover that F5 does nothing. "db_64bit": bool(db64 and want)} +def _idatui_add_func(ea): + """add_func at ``ea``, falling back to an explicit end. + + ida_funcs.add_func(ea) asks IDA to find the end and on carved or + freshly-marked code it often can't, failing with no reason given.""" + import ida_bytes + import ida_funcs + import ida_segment + import idaapi + + if idaapi.get_func(ea) is not None: + return True + if ida_funcs.add_func(ea): + return True + seg = ida_segment.getseg(ea) + hi = seg.end_ea if seg else ea + end = ea + while end < hi and ida_bytes.is_code(ida_bytes.get_flags(end)): + nxt = ida_bytes.get_item_end(end) + if nxt <= end: + break + end = nxt + return bool(end > ea and ida_funcs.add_func(ea, end)) + + @tool @idasync def define_func_run( @@ -1061,28 +1086,15 @@ def define_func_run( if fn is not None and fn.start_ea == ea: return {"addr": hex(ea), "ok": True, "start": hex(fn.start_ea), "end": hex(fn.end_ea), "how": "existed"} - if ida_funcs.add_func(ea): - f = idaapi.get_func(ea) - return {"addr": hex(ea), "ok": True, "start": hex(f.start_ea), - "end": hex(f.end_ea), "how": "auto"} - - seg = ida_segment.getseg(ea) - hi = seg.end_ea if seg else ea - end = ea - while end < hi and ida_bytes.is_code(ida_bytes.get_flags(end)): - nxt = ida_bytes.get_item_end(end) - if nxt <= end: - break - end = nxt - if end <= ea: + auto = ida_funcs.add_func(ea) + if not auto and not _idatui_add_func(ea): return {"addr": hex(ea), "ok": False, - "error": "no code here to make a function from"} - if not ida_funcs.add_func(ea, end): - return {"addr": hex(ea), "ok": False, - "error": f"IDA refused a function at {ea:#x}..{end:#x}"} + "error": f"IDA refused a function at {ea:#x}"} f = idaapi.get_func(ea) + if f is None: + return {"addr": hex(ea), "ok": False, "error": "function did not stick"} return {"addr": hex(ea), "ok": True, "start": hex(f.start_ea), - "end": hex(f.end_ea), "how": "explicit-end"} + "end": hex(f.end_ea), "how": "auto" if auto else "explicit-end"} @tool @idasync @@ -1124,6 +1136,76 @@ def decomp_error( except Exception as e: # noqa: BLE001 out["reason"] = f"{type(e).__name__}: {e}" return out + +@tool +@idasync +def thumb_scan( + start: Annotated[str, "Start of the range to scan for entry pointers"] = "", + end: Annotated[str, "Exclusive end of the range (default: 1KB from start)"] = "", + apply: Annotated[bool, "Mark the targets as Thumb and disassemble them"] = True, + limit: Annotated[int, "Max entries to act on"] = 512, +) -> dict: + """Find Thumb entry points from ODD pointers, e.g. a Cortex-M vector table. + + An ARM function pointer carries the mode in bit 0: odd means Thumb. A vector + table is therefore a list of Thumb entry points that IDA won't follow on a + headerless image, because nothing tells it those words are pointers at all. + + Being wrong here is expensive — marking a data word as code corrupts the + listing — so a word only counts when it is odd, lands inside a loaded + segment, and its target is EXECUTABLE and not already defined as data. The + even words in a vector table (the initial stack pointer) fail the first test, + which is the point.""" + import ida_bytes + import ida_funcs + import ida_idp + import ida_segment + import ida_segregs + import ida_ua + + seg0 = ida_segment.getseg(parse_address(start)) if start else None + if seg0 is None: + seg0 = ida_segment.getnseg(0) + if seg0 is None: + return {"error": "no segments", "found": [], "applied": 0} + try: + lo = parse_address(start) if start else seg0.start_ea + hi = parse_address(end) if end else min(lo + 0x400, seg0.end_ea) + except Exception as e: + return {"error": str(e), "found": [], "applied": 0} + + treg = ida_idp.str2reg("T") + found, applied = [], 0 + ea = lo + while ea + 4 <= hi and len(found) < limit: + w = ida_bytes.get_dword(ea) + ea += 4 + if not (w & 1): + continue # even: not a Thumb pointer + tgt = w & ~1 + seg = ida_segment.getseg(tgt) + if seg is None or not (seg.perm & ida_segment.SEGPERM_EXEC or seg.perm == 0): + continue # points outside the image, or at data + f = ida_bytes.get_flags(tgt) + if ida_bytes.is_data(f): + continue # already something else; don't fight it + rec = {"at": hex(ea - 4), "value": hex(w), "target": hex(tgt), + "was_code": bool(ida_bytes.is_code(f))} + found.append(rec) + if not apply: + continue + if treg is not None and treg >= 0: + ida_segregs.split_sreg_range(tgt, treg, 1, ida_segregs.SR_user) + if not ida_bytes.is_code(ida_bytes.get_flags(tgt)): + ida_bytes.del_items(tgt, 0, 2) + if ida_ua.create_insn(tgt) <= 0: + rec["decoded"] = False + continue + rec["decoded"] = True + rec["function"] = _idatui_add_func(tgt) + applied += 1 + return {"start": hex(lo), "end": hex(hi), "found": found, + "applied": applied, "n": len(found)} ''' SNIPPET = f"{BEGIN}\n{BODY.strip()}\n{END}\n" |
