diff options
Diffstat (limited to 'server/patch_server.py')
| -rw-r--r-- | server/patch_server.py | 364 |
1 files changed, 364 insertions, 0 deletions
diff --git a/server/patch_server.py b/server/patch_server.py index 4c806a7..160b15b 100644 --- a/server/patch_server.py +++ b/server/patch_server.py @@ -305,12 +305,137 @@ def _idatui_head_row(ea): "size": int(ida_bytes.get_item_size(ea)), "text": text, } + if line: + # Keep IDA's own token classification for syntax highlighting. Built from + # the SAME line as `text`, then whitespace-collapsed identically so the + # two never disagree about what the row says. + spans = _idatui_spans(line) + joined = "".join(t for _k, t in spans) + if " ".join(joined.split()) == text: + row["spans"] = spans nm = ida_name.get_ea_name(ea) if nm: row["name"] = nm return row +#: IDA colour tag -> the semantic kind the TUI styles. IDA already classifies +#: every token in a disassembly line, for every processor it supports, so there +#: is nothing to lex: generate_disasm_line emits \x01<tag>text\x02<tag> and the +#: tag says what the text IS. A pygments assembly lexer would be a worse guess at +#: this and would need one dialect per architecture. +_IDATUI_SPAN_KINDS = { + "insn": ("SCOLOR_INSN", "SCOLOR_KEYWORD", "SCOLOR_ASMDIR", "SCOLOR_MACRO"), + "reg": ("SCOLOR_REG",), + "num": ("SCOLOR_NUMBER", "SCOLOR_CHAR", "SCOLOR_BINPREF"), + "str": ("SCOLOR_STRING",), + # NB the real constant names: DATNAME/CODNAME, not "DNAME". Guessing here + # fails silently — an unmapped tag renders as plain body text, so symbols + # just quietly aren't blue and nothing tells you why. + "name": ("SCOLOR_DATNAME", "SCOLOR_CODNAME", "SCOLOR_LOCNAME", + "SCOLOR_IMPNAME", "SCOLOR_DEMNAME", "SCOLOR_LIBNAME", + "SCOLOR_CNAME", "SCOLOR_DNAME", + "SCOLOR_CREF", "SCOLOR_DREF", "SCOLOR_CREFTAIL", "SCOLOR_DREFTAIL"), + "seg": ("SCOLOR_SEGNAME",), + "cmt": ("SCOLOR_AUTOCMT", "SCOLOR_REGCMT", "SCOLOR_RPTCMT", "SCOLOR_VOIDOP"), + "punct": ("SCOLOR_SYMBOL", "SCOLOR_ALTOP", "SCOLOR_HIDNAME"), + "err": ("SCOLOR_ERROR",), +} + + +def _idatui_tag_map(): + """{tag character: kind}, built once from whatever this IDA actually has.""" + import ida_lines + out = {} + for kind, names in _IDATUI_SPAN_KINDS.items(): + for n in names: + v = getattr(ida_lines, n, None) + if isinstance(v, str) and v: + out[v[0]] = kind + elif isinstance(v, int): + out[chr(v)] = kind + return out + + +_IDATUI_TAGS = None + + +def _idatui_spans(line): + """A tagged disasm line as [[kind, text], ...], colour tags resolved. + + Unknown tags become 'text' rather than being dropped: a processor module can + emit a colour we don't classify, and losing the characters would corrupt the + line.""" + global _IDATUI_TAGS + import ida_lines + if _IDATUI_TAGS is None: + _IDATUI_TAGS = _idatui_tag_map() + on, off, esc = "\x01", "\x02", "\x03" + addr_tag = chr(getattr(ida_lines, "COLOR_ADDR", 0x28)) + addr_len = int(getattr(ida_lines, "COLOR_ADDR_SIZE", 16)) + spans, stack, buf = [], [], [] + i, n = 0, len(line) + + def flush(): + if buf: + spans.append([stack[-1] if stack else "text", "".join(buf)]) + del buf[:] + + while i < n: + ch = line[i] + if ch == on and i + 1 < n: + tag = line[i + 1] + if tag == addr_tag: + # An embedded target address, not display text: 16 hex digits + # that must not reach the screen. + i += 2 + addr_len + continue + flush() + stack.append(_IDATUI_TAGS.get(tag, "text")) + i += 2 + continue + if ch == off and i + 1 < n: + flush() + if stack: + stack.pop() + i += 2 + continue + if ch == esc and i + 1 < n: # escaped literal + buf.append(line[i + 1]) + i += 2 + continue + buf.append(ch) + i += 1 + flush() + # Collapse IDA's column padding EXACTLY as the plain text does. A run of + # spaces can straddle two spans, so this walks characters rather than + # collapsing each span on its own — otherwise the spans and `text` disagree + # about the line and the row silently loses its highlighting. + out, prev_space = [], False + for kind, txt in spans: + acc = [] + for ch in txt: + if ch.isspace(): + if prev_space: + continue + acc.append(" ") + prev_space = True + else: + acc.append(ch) + prev_space = False + if acc: + out.append([kind, "".join(acc)]) + while out and out[0][1] == " ": + out.pop(0) + while out and out[-1][1] == " ": + out.pop() + if out and out[0][1].startswith(" "): + out[0][1] = out[0][1].lstrip() + if out and out[-1][1].endswith(" "): + out[-1][1] = out[-1][1].rstrip() + return [[k, t] for k, t in out if t] + + def _idatui_unknown_row(ea, size): """One collapsed row for a run of ``size`` undefined bytes starting at ``ea``. A single byte is rendered normally (shows its value); a longer run @@ -842,6 +967,245 @@ def define_code_run( return {"start": hex(start), "end": hex(ea), "count": count, "stopped": stopped} + +@tool +@idasync +def set_thumb( + addr: Annotated[str, "Address to change the ARM decoding mode at"], + mode: Annotated[str, "'toggle', 'on' (Thumb) or 'off' (ARM)"] = "toggle", + end: Annotated[str, "Optional exclusive end address (default: this item)"] = "", +) -> dict: + """Switch ARM/Thumb decoding at ``addr`` (IDA's T segment register). + + Thumb is not a property of the bytes, it's a mode the CPU is in, so a raw + image gives IDA no way to know: at a Thumb entry point it decodes 16-bit + instructions as 32-bit ARM and produces confident nonsense + (``push {r3,lr}`` reads as ``SVCLT 0xBF00``). + + Also forces the segment to 32-bit when turning Thumb ON. Thumb does not + exist in AArch64, and a headerless blob loaded with -parm defaults to + 64-bit — so setting T alone changes nothing and looks broken. Asking for + Thumb IS asking for ARM32.""" + import ida_bytes + import ida_idp + import ida_segment + import ida_segregs + + try: + ea = parse_address(addr) + except Exception as e: + return {"addr": str(addr), "error": str(e)} + treg = ida_idp.str2reg("T") + if treg is None or treg < 0: + return {"addr": hex(ea), "error": "no T register (not an ARM database)"} + seg = ida_segment.getseg(ea) + if not seg: + return {"addr": hex(ea), "error": "no segment"} + + import ida_ida + db64 = ida_ida.inf_get_app_bitness() == 64 + cur = ida_segregs.get_sreg(ea, treg) + cur = 0 if cur in (None, 0xFFFFFFFF, -1) else int(cur) + want = {"on": 1, "off": 0}.get(str(mode).lower(), 0 if cur else 1) + + changed_bits = False + if want and seg.bitness != 1: + ida_segment.set_segm_addressing(seg, 1) + changed_bits = True + + try: + stop = parse_address(end) if end else 0 + except Exception: + stop = 0 + size = max(int(stop) - ea, 0) or max(ida_bytes.get_item_size(ea), 2) + # The bytes are currently decoded in the OLD mode; leaving that item defined + # pins the wrong instruction length and the new mode has nothing to apply to. + ida_bytes.del_items(ea, 0, size) + ok = bool(ida_segregs.split_sreg_range(ea, treg, want, ida_segregs.SR_user)) + now = ida_segregs.get_sreg(ea, treg) + return {"addr": hex(ea), "thumb": bool(now), "was": bool(cur), "ok": ok, + "bitness": ida_segment.getseg(ea).bitness, + "forced_32bit": changed_bits, + # The DATABASE's bitness is fixed at load and can't be corrected + # here (setting it post-hoc makes the decompiler INTERR). In a + # 64-bit database a 32-bit function disassembles but Hex-Rays + # refuses it outright, so say so instead of leaving the user to + # 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( + addr: Annotated[str, "Entry point of the function to create"], +) -> dict: + """Create a function at ``addr``, working out its end if IDA can't. + + ida_funcs.add_func(ea) asks IDA to find the end itself, and on hand-carved + code it often can't — a run that ends in a tail call, or whose last + instruction isn't recognised as a return, simply fails with no reason given. + You then have a disassembled routine that refuses to become a function, and + F5 has nothing to work with. + + So: try IDA's way, and if that fails, use the end of the contiguous + instruction run starting at ``addr``.""" + import ida_bytes + import ida_funcs + import ida_segment + import idaapi + + try: + ea = parse_address(addr) + except Exception as e: + return {"addr": str(addr), "error": str(e), "ok": False} + fn = idaapi.get_func(ea) + 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"} + auto = ida_funcs.add_func(ea) + if not auto and not _idatui_add_func(ea): + return {"addr": hex(ea), "ok": False, + "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": "auto" if auto else "explicit-end"} + +@tool +@idasync +def decomp_error( + addr: Annotated[str, "Address of the function that failed to decompile"], +) -> dict: + """Why Hex-Rays refused this function, in its own words. + + The plain decompile tool reports "Decompilation failed at 0x0" and drops the + reason, which is the only useful part. Hex-Rays fills in a hexrays_failure_t + saying things like "only 64-bit functions can be decompiled in the current + database" — that one is unfixable in place (the database's bitness is set at + load), so a user who can't see it has no way to know they must reload.""" + import ida_funcs + import ida_hexrays + import ida_ida + + try: + ea = parse_address(addr) + except Exception as e: + return {"addr": str(addr), "error": str(e)} + out = {"addr": hex(ea), "bitness": ida_ida.inf_get_app_bitness()} + fn = ida_funcs.get_func(ea) + if fn is None: + out["reason"] = "no function here" + return out + try: + if not ida_hexrays.init_hexrays_plugin(): + out["reason"] = "the decompiler is not available for this processor" + return out + hf = ida_hexrays.hexrays_failure_t() + cf = ida_hexrays.decompile_func(fn, hf) + if cf is not None: + out["reason"] = "" # it decompiles now + return out + out["reason"] = hf.desc() or f"error {hf.code}" + out["code"] = int(hf.code) + out["errea"] = hex(hf.errea) + 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" |
