aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/patch_server.py406
1 files changed, 405 insertions, 1 deletions
diff --git a/server/patch_server.py b/server/patch_server.py
index f42cc3a..6862785 100644
--- a/server/patch_server.py
+++ b/server/patch_server.py
@@ -1,6 +1,10 @@
#!/usr/bin/env python3
"""Inject idatui's extra ida-pro-mcp tools into the installed server package.
+DEPRECATED along with the ida-pro-mcp transport: the default backend is now the
+idalib worker (idatui/worker.py), which registers these same tools in-process and
+needs no patching. Kept only for `--backend mcp`; slated for removal.
+
ida-pro-mcp lacks a few tools idatui needs. Rather than vendor/fork the server,
we keep the tool source here and inject it (idempotently) into the installed
``api_types.py``. That module is imported by every worker
@@ -42,6 +46,25 @@ def _idatui_lv_get(x):
@tool
@idasync
+def resolve_names(
+ queries: Annotated[list, "Symbol name(s) to resolve to their OWN address"],
+) -> list:
+ """Resolve named locations (functions, labels like loc_/locret_, data) to the
+ exact address the NAME denotes, via get_name_ea. Unlike lookup_funcs, a
+ mid-function label resolves to the label's address, not the containing
+ function's entry."""
+ import idaapi
+ qs = queries if isinstance(queries, list) else [queries]
+ out = []
+ for q in qs:
+ q = str(q).strip()
+ ea = idaapi.get_name_ea(idaapi.BADADDR, q)
+ out.append({"query": q, "ea": (hex(ea) if ea != idaapi.BADADDR else None)})
+ return out
+
+
+@tool
+@idasync
def del_type(
name: Annotated[str, "Local type name to delete (struct/union/enum/typedef)"],
) -> dict:
@@ -175,9 +198,390 @@ def file_regions() -> dict:
fo = -1
if fo < 0 or fo >= (1 << 48):
fo = -1
- out.append({"start": hex(seg.start_ea), "end": hex(seg.end_ea), "file_off": fo})
+ try:
+ nm = ida_segment.get_segm_name(seg) or ""
+ except Exception:
+ nm = ""
+ out.append({"start": hex(seg.start_ea), "end": hex(seg.end_ea),
+ "file_off": fo, "name": nm})
seg = ida_segment.get_next_seg(seg.start_ea)
return {"regions": out}
+
+
+@tool
+@idasync
+def make_string(
+ addr: Annotated[str, "Address of the string start"],
+ length: Annotated[int, "Length in bytes (0 = auto-detect to the terminator)"] = 0,
+ kind: Annotated[str, "String kind: c | c16 | c32 | pascal"] = "c",
+) -> dict:
+ """Create a string literal at ``addr`` (IDA's 'A'). ``length`` 0 auto-detects
+ to the terminator. Undefines any items in the way first, like the UI does.
+ Returns the created byte size and the decoded contents."""
+ import ida_bytes
+ import ida_nalt
+
+ ea = parse_address(addr)
+ strtype = {
+ "c": ida_nalt.STRTYPE_C,
+ "c16": ida_nalt.STRTYPE_C_16,
+ "c32": ida_nalt.STRTYPE_C_32,
+ "pascal": ida_nalt.STRTYPE_PASCAL,
+ }.get(str(kind).lower(), ida_nalt.STRTYPE_C)
+ n = max(int(length), 0)
+ # Free any existing item(s) so create_strlit can carve the literal.
+ ida_bytes.del_items(ea, ida_bytes.DELIT_SIMPLE, n if n > 0 else 1)
+ ok = bool(ida_bytes.create_strlit(ea, n, strtype))
+ if not ok:
+ return {"addr": addr, "ok": False, "error": "create_strlit failed"}
+ size = int(ida_bytes.get_item_size(ea))
+ try:
+ raw = ida_bytes.get_strlit_contents(ea, -1, strtype)
+ text = raw.decode("utf-8", "replace") if raw else ""
+ except Exception:
+ text = ""
+ return {"addr": addr, "ok": True, "size": size, "text": text}
+
+
+@tool
+@idasync
+def read_raw(
+ addr: Annotated[str, "Start address (hex or name)"],
+ size: Annotated[int, "Number of bytes to read"],
+) -> dict:
+ """Read ``size`` bytes at ``addr`` as ONE contiguous lowercase hex string
+ (no per-byte '0x'/spaces). The hot path for the hex view and disasm opcode
+ bytes.
+
+ Fast: does a single bulk ``ida_bytes.get_bytes`` (C-speed) instead of the
+ per-byte read_bytes_bss_safe loop (2 IDA calls/byte). Unloaded bytes come
+ back from IDA as the 0xFF sentinel, so we only re-check is_loaded for the
+ (usually sparse) 0xFF bytes and zero the genuinely-unloaded ones — matching
+ get_bytes' bss semantics without paying per-byte for the whole range.
+
+ Encoding is compact hex (~2.5x smaller than get_bytes' '0x..'-with-spaces)
+ and, unlike get_bytes, does not truncate on large reads."""
+ import ida_bytes
+
+ ea = parse_address(addr)
+ n = max(int(size), 0)
+ if n == 0:
+ return {"addr": addr, "hex": "", "n": 0}
+ raw = ida_bytes.get_bytes(ea, n)
+ if raw is None or len(raw) < n: # nothing (or not all) mapped
+ base = bytearray(raw or b"")
+ base.extend(b"\\xff" * (n - len(base)))
+ raw = bytes(base)
+ ba = bytearray(raw)
+ # Only unloaded bytes read as 0xFF; correct just those to 0 (bss => zero).
+ i = ba.find(0xFF)
+ while i != -1:
+ if not ida_bytes.is_loaded(ea + i):
+ ba[i] = 0
+ i = ba.find(0xFF, i + 1)
+ return {"addr": addr, "hex": bytes(ba).hex(), "n": len(ba)}
+
+
+def _idatui_head_row(ea):
+ """One flat-listing row for the head at ``ea``: kind (code/data/unknown),
+ byte size, rendered text, and any symbol name."""
+ import ida_bytes
+ import ida_lines
+ import ida_name
+
+ f = ida_bytes.get_flags(ea)
+ if ida_bytes.is_code(f):
+ kind = "code"
+ elif ida_bytes.is_data(f):
+ kind = "data"
+ else:
+ kind = "unknown"
+ line = ida_lines.generate_disasm_line(ea, 0)
+ text = ida_lines.tag_remove(line) if line else ""
+ text = " ".join(text.split()) # collapse IDA's column padding
+ row = {
+ "ea": hex(ea),
+ "kind": kind,
+ "size": int(ida_bytes.get_item_size(ea)),
+ "text": text,
+ }
+ nm = ida_name.get_ea_name(ea)
+ if nm:
+ row["name"] = nm
+ return row
+
+
+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
+ collapses to ``db N dup(?)`` so a big .bss/gap doesn't explode into millions
+ of one-byte rows."""
+ import ida_name
+
+ if size <= 1:
+ return _idatui_head_row(ea)
+ row = {"ea": hex(ea), "kind": "unknown", "size": int(size),
+ "text": f"db {size} dup(?)"}
+ nm = ida_name.get_ea_name(ea)
+ if nm:
+ row["name"] = nm
+ return row
+
+
+def _idatui_struct_member_rows(ea):
+ """Indented member rows for a struct-typed data item at ``ea`` (expansion),
+ or [] if it isn't a struct. Top-level fields only."""
+ import ida_nalt
+ import ida_typeinf
+ import idaapi
+
+ tif = ida_typeinf.tinfo_t()
+ if not (ida_nalt.get_tinfo(tif, ea) and tif.is_udt()):
+ return []
+ udt = ida_typeinf.udt_type_data_t()
+ if not tif.get_udt_details(udt):
+ return []
+ rows = []
+ for m in udt:
+ off = m.begin() // 8
+ try:
+ mtype = m.type._print() or ""
+ except Exception:
+ mtype = ""
+ try:
+ sz = int(m.type.get_size())
+ if sz == idaapi.BADSIZE:
+ sz = 0
+ except Exception:
+ sz = 0
+ name = m.name or ""
+ text = f"+{off:X} {name}" + (f" {mtype}" if mtype else "")
+ rows.append({"ea": hex(ea + off), "kind": "member", "size": sz,
+ "text": text})
+ return rows
+
+
+def _idatui_func_header_rows(ea):
+ """IDA-style subroutine banner rows shown just before a function's entry."""
+ import ida_funcs
+
+ name = ida_funcs.get_func_name(ea) or "sub_%X" % ea
+ bar = "=" * 15 + " S U B R O U T I N E " + "=" * 15
+ return [
+ {"ea": hex(ea), "kind": "sep", "size": 0, "text": ""},
+ {"ea": hex(ea), "kind": "sep", "size": 0, "text": "; " + bar},
+ {"ea": hex(ea), "kind": "funchdr", "size": 0,
+ "text": name + " proc", "name": name},
+ ]
+
+
+def _idatui_func_footer_rows(ea, func):
+ """End-of-function marker shown just after a function's last item."""
+ import ida_funcs
+
+ name = ida_funcs.get_func_name(func.start_ea) or "sub_%X" % func.start_ea
+ return [
+ {"ea": hex(ea), "kind": "funchdr", "size": 0,
+ "text": name + " endp", "name": name},
+ {"ea": hex(ea), "kind": "sep", "size": 0, "text": "; " + "-" * 60},
+ ]
+
+
+@tool
+@idasync
+def heads(
+ addr: Annotated[str, "Start address or name to walk from"],
+ count: Annotated[int, "Max heads to return (default 200, max 2000)"] = 200,
+ offset: Annotated[int, "Skip first N heads from addr (default 0)"] = 0,
+ end: Annotated[str, "Optional exclusive end address; default = segment end"] = "",
+ back: Annotated[bool, "Walk backwards: return the count heads ENDING just before addr, in forward order"] = False,
+ annotate: Annotated[bool, "Emit IDA-style function boundary banner rows (kind sep/funchdr)"] = False,
+) -> dict:
+ """Walk item heads from ``addr`` as a flat listing: every head is rendered
+ (code OR data OR undefined) via generate_disasm_line and stepped with
+ next_head/prev_head. Unlike ``disasm`` (code-only, bails at the first data
+ byte) this shows db/dw/dd/... lines for data and undefined regions — IDA's
+ real disassembly view. Address-paged: page forward by re-calling with
+ ``addr`` = the returned cursor.next; page up with ``back=true``."""
+ import ida_bytes
+ import ida_segment
+ import idaapi
+
+ count = 2000 if count > 2000 else (1 if count < 1 else count)
+ offset = max(int(offset), 0)
+ try:
+ start = parse_address(addr)
+ except Exception as e:
+ return {"addr": str(addr), "error": str(e), "heads": [], "cursor": {"done": True}}
+ seg = ida_segment.getseg(start)
+ if not seg:
+ return {"addr": str(addr), "error": "no segment", "heads": [], "cursor": {"done": True}}
+ lo, hi = seg.start_ea, seg.end_ea
+ if end:
+ try:
+ hi = min(hi, parse_address(end))
+ except Exception:
+ pass
+
+ rows = []
+ if back:
+ # Collect up to (count+offset) heads strictly before `start`, then take
+ # the window closest to `start`, returned in forward order.
+ walk = []
+ cur = ida_bytes.prev_head(start, lo)
+ while cur != idaapi.BADADDR and cur >= lo and len(walk) < count + offset:
+ walk.append(cur)
+ cur = ida_bytes.prev_head(cur, lo)
+ walk.reverse()
+ chosen = walk[: len(walk) - offset] if offset else walk
+ chosen = chosen[-count:]
+ rows = [_idatui_head_row(e) for e in chosen]
+ first = chosen[0] if chosen else start
+ pea = ida_bytes.prev_head(first, lo)
+ cursor = {"done": True} if pea == idaapi.BADADDR or pea < lo else {"prev": hex(pea)}
+ return {"addr": str(addr), "heads": rows, "cursor": cursor}
+
+ # Walk by item END (not next_head): next_head SKIPS undefined bytes, but a
+ # flat listing must show them (IDA renders undefined as `db ?` lines, and
+ # navigating to an unmarked address must land ON it). Defined items advance
+ # by get_item_end; a run of undefined bytes is COLLAPSED into one row (its
+ # end found in O(1) via next_head, which skips undefined) so a large .bss or
+ # gap doesn't explode into millions of one-byte rows.
+ def _is_unknown(e):
+ f = ida_bytes.get_flags(e)
+ return not (ida_bytes.is_code(f) or ida_bytes.is_data(f))
+
+ def _run_end(e):
+ """End (exclusive) of the undefined run starting at ``e``."""
+ nh = ida_bytes.next_head(e, hi)
+ return nh if (nh != idaapi.BADADDR and e < nh <= hi) else hi
+
+ def _advance(e):
+ if _is_unknown(e):
+ return _run_end(e)
+ nxt = ida_bytes.get_item_end(e)
+ return nxt if nxt > e else e + 1
+
+ def _rows_for(e):
+ if _is_unknown(e):
+ return [_idatui_unknown_row(e, _run_end(e) - e)]
+ func = idaapi.get_func(e) if annotate else None
+ at_start = func is not None and func.start_ea == e
+ out = []
+ if at_start:
+ out.extend(_idatui_func_header_rows(e))
+ row = _idatui_head_row(e)
+ if at_start:
+ row = dict(row)
+ row["name"] = None # the name is shown on the proc header line
+ elif annotate and row.get("kind") == "code" and row.get("name"):
+ # A code label (loc_XXX/jump target) gets its OWN line at depth 0,
+ # like IDA; strip it from the instruction row below.
+ nm = row["name"]
+ out.append({"ea": hex(e), "kind": "label", "size": 0,
+ "text": nm + ":", "name": nm})
+ row = dict(row)
+ row["name"] = None
+ out.append(row)
+ if row.get("kind") == "data":
+ out.extend(_idatui_struct_member_rows(e)) # expand struct fields
+ if func is not None and ida_bytes.get_item_end(e) >= func.end_ea:
+ out.extend(_idatui_func_footer_rows(e, func))
+ return out
+
+ ea = ida_bytes.get_item_head(start)
+ for _ in range(offset):
+ if ea >= hi or ea == idaapi.BADADDR:
+ break
+ ea = _advance(ea)
+ more = False
+ while ea != idaapi.BADADDR and ea < hi:
+ if len(rows) >= count:
+ more = True
+ break
+ rows.extend(_rows_for(ea)) # a struct head expands into member rows
+ ea = _advance(ea)
+ cursor = {"next": hex(ea)} if more else {"done": True}
+ return {"addr": str(addr), "heads": rows, "cursor": cursor}
+
+
+@tool
+@idasync
+def xref_types(
+ queries: Annotated[list, "[{addr, direction:'to'|'from'|'both', include_fn, dedup, count}]"],
+) -> dict:
+ """Like xref_query, but every row carries a fine-grained ``kind`` derived from
+ the IDA xref type \u2014 call/jump/flow for code, read/write/offset/text/info for
+ data \u2014 alongside the coarse ``type`` (code/data). Feeds the xref dialog's
+ r/w/call badges. Same query/envelope shape as xref_query."""
+ import idaapi, idautils, ida_funcs, ida_bytes, ida_xref
+ code_kind = {ida_xref.fl_CF: "call", ida_xref.fl_CN: "call",
+ ida_xref.fl_JF: "jump", ida_xref.fl_JN: "jump",
+ ida_xref.fl_F: "flow"}
+ data_kind = {ida_xref.dr_O: "offset", ida_xref.dr_W: "write",
+ ida_xref.dr_R: "read", ida_xref.dr_T: "text", ida_xref.dr_I: "info"}
+
+ def _kind(xr):
+ table = code_kind if xr.iscode else data_kind
+ return table.get(xr.type, "code" if xr.iscode else "data")
+
+ def _fn(ea):
+ f = ida_funcs.get_func(ea)
+ if not f:
+ return None
+ return {"addr": hex(f.start_ea), "name": ida_funcs.get_func_name(f.start_ea)}
+
+ def _resolve(raw):
+ raw = str(raw).strip()
+ try:
+ return int(raw, 16) # handles '0x2490' and '2490'
+ except ValueError:
+ return idaapi.get_name_ea(idaapi.BADADDR, raw)
+
+ qs = queries if isinstance(queries, list) else [queries]
+ result = []
+ for q in qs:
+ q = q if isinstance(q, dict) else {"addr": q}
+ raw = str(q.get("addr", "")).strip()
+ direction = str(q.get("direction", "to") or "to").lower()
+ include_fn = bool(q.get("include_fn", True))
+ dedup = bool(q.get("dedup", True))
+ try:
+ count = int(q.get("count", 2000) or 2000)
+ except (TypeError, ValueError):
+ count = 2000
+ target = _resolve(raw)
+ rows = []
+ if target is not None and target != idaapi.BADADDR and ida_bytes.is_mapped(target):
+ if direction in ("to", "both"):
+ for xr in idautils.XrefsTo(target, 0):
+ row = {"direction": "to", "addr": hex(xr.frm), "from": hex(xr.frm),
+ "to": hex(target), "type": "code" if xr.iscode else "data",
+ "kind": _kind(xr)}
+ if include_fn:
+ row["fn"] = _fn(xr.frm)
+ rows.append(row)
+ if direction in ("from", "both"):
+ for xr in idautils.XrefsFrom(target, 0):
+ row = {"direction": "from", "addr": hex(xr.to), "from": hex(target),
+ "to": hex(xr.to), "type": "code" if xr.iscode else "data",
+ "kind": _kind(xr)}
+ if include_fn:
+ row["fn"] = _fn(xr.to)
+ rows.append(row)
+ if dedup:
+ seen = set()
+ deduped = []
+ for r in rows:
+ k = (r["direction"], r["from"], r["to"], r["kind"])
+ if k in seen:
+ continue
+ seen.add(k)
+ deduped.append(r)
+ rows = deduped
+ rows = rows[:count]
+ result.append({"query": raw, "data": rows, "next_offset": None})
+ return {"result": result}
'''
SNIPPET = f"{BEGIN}\n{BODY.strip()}\n{END}\n"