aboutsummaryrefslogtreecommitdiffstats
path: root/server/patch_server.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-26 20:49:30 +0200
committerblasty <blasty@local>2026-07-26 20:49:30 +0200
commit781c5eff4218d7ffdc9905f652ca08ce10953e43 (patch)
treeff240cd5d515ac9b64c3c0578d85307a4f293601 /server/patch_server.py
parentdecomp: say the FIX, not the diagnosis (diff)
downloadida-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.py120
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"