diff options
| author | blasty <blasty@local> | 2026-07-26 09:30:12 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-26 09:30:12 +0200 |
| commit | 4e65c3ba16b13c3bb042830b020788a745cee9a9 (patch) | |
| tree | 791b3a94aec4e57801377943088a1dc0870704b4 /server/patch_server.py | |
| parent | listing: make every undefined byte its own row, so you can carve anywhere (diff) | |
| download | ida-tui-4e65c3ba16b13c3bb042830b020788a745cee9a9.tar.gz ida-tui-4e65c3ba16b13c3bb042830b020788a745cee9a9.tar.xz ida-tui-4e65c3ba16b13c3bb042830b020788a745cee9a9.zip | |
listing: `c` disassembles until something stops it
One instruction per keypress means pressing `c` once per opcode for the length
of a routine, which on a raw image is the whole job. IDA's `c` runs; ours now
does too.
New define_code_run tool: create instructions consecutively and report why it
stopped — 'undecodable' (bytes aren't an instruction), 'flow' (control flow ends
here), 'defined' (ran into existing code/data), 'segment' or 'limit'. It loops
inside the worker; from the client this would be one round trip per instruction,
minutes on a real image.
Stops AT a ret rather than past it: beyond the end of a routine the bytes are
usually padding or data, and running on turns a clean carve into something you
have to undo by hand. Stopping at already-defined items is the same principle —
undefining someone's existing work to keep a speculative run going isn't a trade
the user asked for.
The ret test is ida_idp.is_ret_insn, NOT canonical features: on AArch64
insn.get_canon_feature() returns 0 for RET, so a CF_STOP check silently never
fires and the run walks straight through the end of the function. Verified
against a live IDA before relying on it.
`c` on something already defined now says "already defined @ addr" instead of
claiming the instruction failed to be created — count==0 from a run means two
very different things.
Also: the result message survives the reload. Defining an item rebuilds the view,
and the reload's own cursor handler had the last word, so every edit reported
itself as "ROM @ 0x4040 [listing]". A one-shot _flash is handed to whichever
status write lands first after the edit. (Third time this clobber pattern has
turned up: split view, the no-functions hint, now this.)
Verified: nop/nop/nop/ret at 0x4040 -> "defined 4 instructions (0x4040–0x4050) —
control flow ends here", with 0x4050 left as an undefined byte. Starting on
existing code -> no-op. Random bytes -> stops at the first that won't decode.
tests: +4 blob UI (runs to the end of flow, stops at the ret, doesn't touch the
junk after it, and the status reports it). 22/0 blob, 202/0 scenarios, 30/0
project UI.
Diffstat (limited to 'server/patch_server.py')
| -rw-r--r-- | server/patch_server.py | 74 |
1 files changed, 74 insertions, 0 deletions
diff --git a/server/patch_server.py b/server/patch_server.py index 0e43ea5..4c806a7 100644 --- a/server/patch_server.py +++ b/server/patch_server.py @@ -768,6 +768,80 @@ def list_linkage( "ordinal": int(ordinal)}) return {"imports": imports, "exports": exports, "n_imports": len(imports), "n_exports": len(exports)} + +@tool +@idasync +def define_code_run( + addr: Annotated[str, "Address to start disassembling from"], + limit: Annotated[int, "Max instructions to create (safety stop)"] = 20000, +) -> dict: + """Disassemble CONSECUTIVELY from ``addr`` until something stops it, the way + IDA's 'c' does — one instruction is rarely what you want when carving a raw + image. Returns {start,end,count,stopped} where ``stopped`` says why: + 'undecodable' (bytes aren't an instruction), 'flow' (the last instruction + doesn't fall through, e.g. RET/B), 'defined' (ran into existing code/data), + 'segment' (hit the end) or 'limit'. + + Runs in-process: doing this from the client would be one round trip per + instruction, which is minutes on a real firmware image.""" + import ida_bytes + import ida_idp + import ida_segment + import ida_ua + import idaapi + + try: + ea = parse_address(addr) + except Exception as e: + return {"addr": str(addr), "error": str(e), "count": 0} + + seg = ida_segment.getseg(ea) + if not seg: + return {"addr": str(addr), "error": "no segment", "count": 0} + hi = seg.end_ea + try: + limit = max(1, min(int(limit), 200000)) + except (TypeError, ValueError): + limit = 20000 + + start, count, stopped = ea, 0, "limit" + while count < limit: + if ea >= hi: + stopped = "segment" + break + flags = ida_bytes.get_flags(ea) + if ida_bytes.is_code(flags) or ida_bytes.is_data(flags): + # Already defined: stop rather than clobber. Undefining someone's + # existing work to keep a speculative run going is not a trade the + # user asked for. + stopped = "defined" + break + n = ida_ua.create_insn(ea) + if n <= 0: + stopped = "undecodable" + break + count += 1 + # Stop where control flow stops. Past a RET the next bytes are usually + # padding or a new function's data, and running on turns a clean carve + # into a mess that has to be undone by hand. + # + # Ask ida_idp.is_ret_insn, NOT the canonical feature bits: on AArch64 + # get_canon_feature() returns 0 for RET, so a CF_STOP test silently never + # fires and the run walks straight through the end of the routine. + insn = ida_ua.insn_t() + if ida_ua.decode_insn(insn, ea) > 0: + try: + is_ret = ida_idp.is_ret_insn(insn) + except Exception: + is_ret = False + if is_ret or (insn.get_canon_feature() & idaapi.CF_STOP): + ea += n + stopped = "flow" + break + ea += n + + return {"start": hex(start), "end": hex(ea), "count": count, + "stopped": stopped} ''' SNIPPET = f"{BEGIN}\n{BODY.strip()}\n{END}\n" |
