From ed38fd8bc1fa278528ee4db00219ddfc53f72bd8 Mon Sep 17 00:00:00 2001 From: mahaloz Date: Fri, 17 Jul 2026 02:47:07 +0000 Subject: [PATCH] Add code/data repair (define function|code|data, undefine) to the CLI Important for obfuscated binaries and incorrect auto-analysis. - New `decompiler define function|code|data` and `decompiler undefine`. - Interface define_function/define_code/define_data/undefine (base raises NotImplementedError) with per-backend implementations: - IDA: ida_funcs.add_func/del_func, idc.create_insn, ida_bytes create_data/del_items. - Ghidra: FlatProgramAPI.createFunction/disassemble/createData + FunctionManager.removeFunction + clearListing (transactional). - Binary Ninja: create_user_function/remove_user_function/ define_user_data_var (best-effort). - angr: unsupported (CFG-based; no define/undefine primitives). - undefine removes a function if one starts at the address, then clears the requested span. - Client proxies + tests: undefine->define round-trip on a real function (define reporting success proves undefine removed it) and define data. IDA + Ghidra full pass; angr asserts the unsupported (exit 2) path. - SKILL.md / docs updated. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HfZHdprXg38re1XyKxzGg5 --- declib/api/decompiler_client.py | 16 +++++ declib/api/decompiler_interface.py | 19 ++++++ declib/cli/decompiler_cli.py | 85 ++++++++++++++++++++++++++ declib/decompilers/binja/interface.py | 37 +++++++++++ declib/decompilers/ghidra/interface.py | 46 ++++++++++++++ declib/decompilers/ida/compat.py | 48 +++++++++++++++ declib/decompilers/ida/interface.py | 12 ++++ declib/skills/decompiler/SKILL.md | 19 ++++++ docs/decompiler_cli.md | 22 +++++++ tests/test_decompiler_cli.py | 63 +++++++++++++++++++ 10 files changed, 367 insertions(+) diff --git a/declib/api/decompiler_client.py b/declib/api/decompiler_client.py index 1697811..756abdb 100644 --- a/declib/api/decompiler_client.py +++ b/declib/api/decompiler_client.py @@ -573,6 +573,22 @@ def list_imports(self) -> List: """Return (lifted_addr, name, library) tuples for imported symbols.""" return self._send_request({"type": "method_call", "method_name": "list_imports"}) + def define_function(self, addr: int) -> bool: + """Create a function at ``addr`` (lifted).""" + return self._send_request({"type": "method_call", "method_name": "define_function", "args": [addr]}) + + def define_code(self, addr: int) -> bool: + """Turn the bytes at ``addr`` (lifted) into an instruction.""" + return self._send_request({"type": "method_call", "method_name": "define_code", "args": [addr]}) + + def define_data(self, addr: int, type_str: Optional[str] = None, size: Optional[int] = None) -> bool: + """Define data at ``addr`` (lifted), optionally with a C type.""" + return self._send_request({"type": "method_call", "method_name": "define_data", "args": [addr, type_str, size]}) + + def undefine(self, addr: int, size: int = 1) -> bool: + """Undefine code/data at ``addr`` (lifted).""" + return self._send_request({"type": "method_call", "method_name": "undefine", "args": [addr, size]}) + def save(self, path: Optional[str] = None) -> bool: """Persist the backend's analysis to disk. Raises NotImplementedError for in-memory backends.""" return self._send_request({"type": "method_call", "method_name": "save", "args": [path]}) diff --git a/declib/api/decompiler_interface.py b/declib/api/decompiler_interface.py index a4d10b0..694497e 100644 --- a/declib/api/decompiler_interface.py +++ b/declib/api/decompiler_interface.py @@ -630,6 +630,25 @@ def search_bytes(self, pattern: bytes, max_results: int = 100) -> List[int]: """ raise NotImplementedError("Byte search is not implemented for this backend.") + # ------------------------------------------------------------------ + # Code/data repair (define / undefine) + # ------------------------------------------------------------------ + def define_function(self, addr) -> bool: + """Create a function at ``addr`` (lifted). Repairs missed analysis.""" + raise NotImplementedError("define_function is not implemented for this backend.") + + def define_code(self, addr) -> bool: + """Turn the bytes at ``addr`` (lifted) into an instruction.""" + raise NotImplementedError("define_code is not implemented for this backend.") + + def define_data(self, addr, type_str: Optional[str] = None, size: Optional[int] = None) -> bool: + """Define data at ``addr`` (lifted), optionally with a C type.""" + raise NotImplementedError("define_data is not implemented for this backend.") + + def undefine(self, addr, size: int = 1) -> bool: + """Undefine code/data at ``addr`` (lifted). Removes a function if one starts here.""" + raise NotImplementedError("undefine is not implemented for this backend.") + def list_imports(self) -> List[Tuple[int, str, str]]: """Return ``(lifted_addr, name, library)`` tuples for imported symbols. diff --git a/declib/cli/decompiler_cli.py b/declib/cli/decompiler_cli.py index 80d0889..ac18357 100644 --- a/declib/cli/decompiler_cli.py +++ b/declib/cli/decompiler_cli.py @@ -19,6 +19,8 @@ - comment get/set/append/delete/list comments (annotations) - search find bytes/string/instruction patterns - imports list imported symbols +- define define a function/code/data at an address +- undefine clear code/data at an address - global list/get/rename/retype global variables - signature get/set a function's full signature (prototype) - rename rename a function or local variable @@ -1316,6 +1318,51 @@ def cmd_read_memory(args) -> int: return 0 +# --------------------------------------------------------------------------- +# define / undefine (code & data repair) +# --------------------------------------------------------------------------- + +def cmd_define(args) -> int: + """Define a function, code, or data at an address (repair analysis).""" + action = args.define_action + with _with_client(args) as client: + addr_value, _ = _parse_target(args.addr) + if addr_value is None: + raise SystemExit(f"Invalid address {args.addr!r}; expected hex (0x..) or decimal.") + lifted = _to_lifted_addr(client, addr_value) + + if action == "function": + ok = bool(client.define_function(lifted)) + elif action == "code": + ok = bool(client.define_code(lifted)) + elif action == "data": + ok = bool(client.define_data(lifted, getattr(args, "type", None), getattr(args, "size", None))) + else: + raise SystemExit(f"Unknown define action: {action}") + + if not ok: + raise SystemExit( + f"Backend could not define {action} at {_format_addr_hex(lifted)} " + "(the address may already be defined that way, or be invalid)." + ) + _emit(args, {"addr": lifted, "defined": action, "success": ok}) + return EXIT_OK + + +def cmd_undefine(args) -> int: + """Undefine (clear) code/data at an address; removes a function if one starts there.""" + with _with_client(args) as client: + addr_value, _ = _parse_target(args.addr) + if addr_value is None: + raise SystemExit(f"Invalid address {args.addr!r}; expected hex (0x..) or decimal.") + lifted = _to_lifted_addr(client, addr_value) + ok = bool(client.undefine(lifted, args.size)) + if not ok: + raise SystemExit(f"Nothing to undefine at {_format_addr_hex(lifted)}.") + _emit(args, {"addr": lifted, "size": args.size, "undefined": ok}) + return EXIT_OK + + # --------------------------------------------------------------------------- # search (bytes / string / instruction) + imports # --------------------------------------------------------------------------- @@ -2001,6 +2048,44 @@ def _add_comment_addr_action(name, help_text, with_text=False): _add_output_args(p_gc) p_gc.set_defaults(func=cmd_get_callers) + # define + p_def = sub.add_parser( + "define", + help="Define a function, code, or data at an address (repair analysis).", + ) + def_sub = p_def.add_subparsers(dest="define_action", required=True) + + p_deffn = def_sub.add_parser("function", help="Create a function at an address.") + p_deffn.add_argument("addr", help="Address (hex 0x.., decimal, lifted or absolute).") + _add_server_filter_args(p_deffn) + _add_output_args(p_deffn) + p_deffn.set_defaults(func=cmd_define) + + p_defcode = def_sub.add_parser("code", help="Turn bytes at an address into an instruction.") + p_defcode.add_argument("addr", help="Address to disassemble.") + _add_server_filter_args(p_defcode) + _add_output_args(p_defcode) + p_defcode.set_defaults(func=cmd_define) + + p_defdata = def_sub.add_parser("data", help="Define data at an address (optionally typed).") + p_defdata.add_argument("addr", help="Address of the data.") + p_defdata.add_argument("--type", dest="type", default=None, + help='C type to apply, e.g. "int", "char[16]".') + p_defdata.add_argument("--size", dest="size", type=lambda x: int(x, 0), default=None, + help="Byte size when no --type is given (default 1).") + _add_server_filter_args(p_defdata) + _add_output_args(p_defdata) + p_defdata.set_defaults(func=cmd_define) + + # undefine + p_undef = sub.add_parser("undefine", help="Undefine (clear) code/data at an address.") + p_undef.add_argument("addr", help="Address to undefine.") + p_undef.add_argument("--size", dest="size", type=lambda x: int(x, 0), default=1, + help="Number of bytes to undefine (default 1).") + _add_server_filter_args(p_undef) + _add_output_args(p_undef) + p_undef.set_defaults(func=cmd_undefine) + # search p_search = sub.add_parser( "search", diff --git a/declib/decompilers/binja/interface.py b/declib/decompilers/binja/interface.py index 439d539..95d0df4 100644 --- a/declib/decompilers/binja/interface.py +++ b/declib/decompilers/binja/interface.py @@ -382,6 +382,43 @@ def list_imports(self) -> List[tuple]: out.append((self.art_lifter.lift_addr(int(sym.address)), str(sym.name), "")) return out + def define_function(self, addr) -> bool: + lowered = self.art_lifter.lower_addr(addr) + self.bv.create_user_function(lowered) + self.bv.update_analysis_and_wait() + return self.bv.get_function_at(lowered) is not None + + def define_code(self, addr) -> bool: + # Binary Ninja disassembles when it creates a function; treat as such. + return self.define_function(addr) + + def define_data(self, addr, type_str=None, size=None) -> bool: + lowered = self.art_lifter.lower_addr(addr) + try: + if type_str: + parsed = self.bv.parse_type_string(type_str)[0] + self.bv.define_user_data_var(lowered, parsed) + else: + self.bv.define_user_data_var(lowered, "char") + return True + except Exception as e: + l.warning("Binary Ninja define_data failed: %s", e) + return False + + def undefine(self, addr, size=1) -> bool: + lowered = self.art_lifter.lower_addr(addr) + removed = False + f = self.bv.get_function_at(lowered) + if f is not None: + self.bv.remove_user_function(f) + removed = True + try: + self.bv.undefine_user_data_var(lowered) + removed = True + except Exception: + pass + return removed + def start_artifact_watchers(self): if not self.artifact_watchers_started: from .hooks import DataMonitor diff --git a/declib/decompilers/ghidra/interface.py b/declib/decompilers/ghidra/interface.py index 08240c4..e8b5a37 100644 --- a/declib/decompilers/ghidra/interface.py +++ b/declib/decompilers/ghidra/interface.py @@ -561,6 +561,52 @@ def search_bytes(self, pattern: bytes, max_results: int = 100) -> List[int]: start = found.add(1) return results + def define_function(self, addr) -> bool: + from .compat.transaction import Transaction + gaddr = self._to_gaddr(self.art_lifter.lower_addr(addr)) + with Transaction(self.flat_api, msg="define_function"): + func = self.flat_api.createFunction(gaddr, None) + return func is not None + + def define_code(self, addr) -> bool: + from .compat.transaction import Transaction + gaddr = self._to_gaddr(self.art_lifter.lower_addr(addr)) + with Transaction(self.flat_api, msg="define_code"): + return bool(self.flat_api.disassemble(gaddr)) + + def define_data(self, addr, type_str=None, size=None) -> bool: + from .compat.transaction import Transaction + gaddr = self._to_gaddr(self.art_lifter.lower_addr(addr)) + with Transaction(self.flat_api, msg="define_data"): + try: + if type_str: + gtype = self.typestr_to_gtype(type_str) + if gtype is None: + return False + from ghidra.program.model.data import DataUtilities + DataUtilities.createData( + self.currentProgram, gaddr, gtype, -1, + DataUtilities.ClearDataMode.CLEAR_ALL_CONFLICT_DATA, + ) + return True + from .compat.imports import ByteDataType + self.flat_api.createData(gaddr, ByteDataType.dataType) + return True + except Exception as exc: + self.warning(f"define_data failed: {exc}") + return False + + def undefine(self, addr, size=1) -> bool: + from .compat.transaction import Transaction + gaddr = self._to_gaddr(self.art_lifter.lower_addr(addr)) + with Transaction(self.flat_api, msg="undefine"): + fm = self.currentProgram.getFunctionManager() + if fm.getFunctionAt(gaddr) is not None: + fm.removeFunction(gaddr) + end = gaddr.add(max(1, size) - 1) + self.flat_api.clearListing(gaddr, end) + return True + def read_memory(self, addr: int, size: int) -> Optional[bytes]: if size <= 0: return b"" diff --git a/declib/decompilers/ida/compat.py b/declib/decompilers/ida/compat.py index e3bd89b..f34c73a 100644 --- a/declib/decompilers/ida/compat.py +++ b/declib/decompilers/ida/compat.py @@ -1976,6 +1976,54 @@ def wait_for_idc_initialization(): idc.auto_wait() +@execute_write +def define_function(addr): + """Create a function at ``addr`` (repairs missed auto-analysis).""" + return bool(ida_funcs.add_func(addr)) + + +@execute_write +def define_code(addr): + """Turn the bytes at ``addr`` into an instruction. Returns True on success.""" + return bool(idc.create_insn(addr)) + + +@execute_write +def define_data(addr, type_str=None, size=None): + """Define data at ``addr``. + + With a ``type_str`` we undefine the span and apply that type; otherwise we + create a plain byte/word/dword/qword sized item. + """ + if type_str: + tif = convert_type_str_to_ida_type(type_str) + nbytes = (tif.get_size() if tif is not None else None) or size or 1 + ida_bytes.del_items(addr, idc.DELIT_SIMPLE, nbytes) + return bool(idc.SetType(addr, type_str)) + + n = size or 1 + ida_bytes.del_items(addr, idc.DELIT_SIMPLE, n) + flag_by_size = { + 1: ida_bytes.FF_BYTE, + 2: getattr(ida_bytes, "FF_WORD", ida_bytes.FF_BYTE), + 4: ida_bytes.FF_DWORD, + 8: getattr(ida_bytes, "FF_QWORD", ida_bytes.FF_DWORD), + } + flag = flag_by_size.get(n, ida_bytes.FF_BYTE) + return bool(ida_bytes.create_data(addr, flag, n, idaapi.BADADDR)) + + +@execute_write +def undefine(addr, size=1): + """Undefine code/data at ``addr``. Removes a function if one starts here.""" + removed = False + func = ida_funcs.get_func(addr) + if func is not None and func.start_ea == addr: + removed |= bool(ida_funcs.del_func(addr)) + removed |= bool(ida_bytes.del_items(addr, idc.DELIT_SIMPLE, max(1, size))) + return removed + + @execute_write def search_bytes(pattern, max_results=100): """Return every EA where ``pattern`` (a bytes object) occurs. diff --git a/declib/decompilers/ida/interface.py b/declib/decompilers/ida/interface.py index a165d58..f092b9b 100755 --- a/declib/decompilers/ida/interface.py +++ b/declib/decompilers/ida/interface.py @@ -341,6 +341,18 @@ def list_imports(self) -> List[tuple]: out.append((self.art_lifter.lift_addr(ea), name, lib)) return out + def define_function(self, addr) -> bool: + return compat.define_function(self.art_lifter.lower_addr(addr)) + + def define_code(self, addr) -> bool: + return compat.define_code(self.art_lifter.lower_addr(addr)) + + def define_data(self, addr, type_str=None, size=None) -> bool: + return compat.define_data(self.art_lifter.lower_addr(addr), type_str, size) + + def undefine(self, addr, size=1) -> bool: + return compat.undefine(self.art_lifter.lower_addr(addr), size) + def _collect_xrefs_to(self, lowered_addr: int, only_code: bool, _max_chase: int = 2) -> List[Artifact]: """Collect function-level xrefs to ``lowered_addr``. diff --git a/declib/skills/decompiler/SKILL.md b/declib/skills/decompiler/SKILL.md index 8d2da5f..4c93028 100644 --- a/declib/skills/decompiler/SKILL.md +++ b/declib/skills/decompiler/SKILL.md @@ -154,6 +154,8 @@ same binary. | `search string ` | Find a string's bytes in memory. | `--encoding`, `--max`, same | | `search instruction ` | Regex-search disassembly across all functions. | `--max`, same | | `imports` | List imported symbols (external functions/data). | `--filter REGEX`, same | +| `define function/code/data ` | Repair analysis: create a function, disassemble bytes, or define data. | `--type`, `--size` (data), same | +| `undefine ` | Clear code/data at an address (removes a function if one starts there). | `--size`, same | | `read int/string/struct [...]` | Typed reads: decode memory as an integer, C string, or defined struct. | `--size`, `--signed`, `--endian`, `--max-len`, `--encoding`, same + `--json` | | `read_memory ` | Read raw bytes from the binary at ``. Default output is a hexdump. | `--format {hexdump,hex,raw}`, same + `--json` (base64-encoded bytes) | | `install-skill` | Install this file for Claude Code or Codex. | `--agent`, `--dest`, `--force`, `--json` | @@ -239,6 +241,23 @@ purely in-memory: `save` exits `2` (`not implemented`) and there is nothing to reload. IDA reopens the saved `.i64` directly (no re-analysis), so a reload after `save` is fast and lossless. +### `define` / `undefine` — repair analysis + +When auto-analysis misses a function or mislabels code as data (common on +obfuscated or hand-written binaries), fix it: + +```bash +decompiler define function 0x401200 # create a function IDA/Ghidra missed +decompiler define code 0x401200 # disassemble bytes into an instruction +decompiler define data 0x4040 --type int # define typed data +decompiler define data 0x4040 --size 8 # or a raw 8-byte item +decompiler undefine 0x401200 --size 32 # clear code/data; removes a function here +``` + +A realistic repair sequence is `undefine` → `define code` → `define function`. +Supported on IDA, Ghidra, and Binary Ninja; `angr`'s CFG-based model has no +define/undefine primitives (those commands exit `2`). + ### `search` and `imports` — discovery ```bash diff --git a/docs/decompiler_cli.md b/docs/decompiler_cli.md index 2173576..800ab05 100644 --- a/docs/decompiler_cli.md +++ b/docs/decompiler_cli.md @@ -439,6 +439,28 @@ decompiler get_callers [--id ID] [--binary PATH] [--backend BACKEND] [- Unlike `xref_to`, this never returns globals or other data refs. Rows are always of kind `Function`. +### `define` and `undefine` + +Repair analysis: create functions, disassemble code, define data, or clear a +region back to undefined bytes. + +```bash +decompiler define function [--id ID] [--json] +decompiler define code [--id ID] [--json] +decompiler define data [--type C_TYPE | --size N] [--json] +decompiler undefine [--size N] [--json] +``` + +```bash +decompiler define function 0x401200 # create a missed function +decompiler define data 0x4040 --type int +decompiler undefine 0x401200 --size 32 # removes a function starting here +``` + +Supported on IDA, Ghidra, and Binary Ninja. `angr` has no define/undefine +primitives and returns exit `2`. A realistic repair is `undefine` → +`define code` → `define function`. + ### `search` and `imports` Find byte/string/instruction patterns and enumerate imported symbols. diff --git a/tests/test_decompiler_cli.py b/tests/test_decompiler_cli.py index 43835b6..ce3b5c9 100644 --- a/tests/test_decompiler_cli.py +++ b/tests/test_decompiler_cli.py @@ -350,6 +350,67 @@ def test_read_memory_invalid_address(self): combined = result.stdout + result.stderr self.assertNotIn("|.ELF", combined) + # ------------------------------------------------------------------- + # define / undefine (code & data repair) + # ------------------------------------------------------------------- + + #: Backends that support define/undefine of code & data. + _repairs_analysis: bool = True + + def _find_non_entry_function(self): + """Return (addr, size) of a non-entry function safe to undefine/redefine.""" + lf = json.loads(_run_cli("list_functions", "--json").stdout) + pref = next((e for e in lf if e.get("name") == "authenticate"), None) + if pref is None: + pref = next((e for e in lf if (e.get("size") or 0) > 8 and e.get("name")), None) + return pref + + def test_undefine_and_define_function(self): + self._load_fauxware_isolated() + target = self._find_non_entry_function() + if target is None: + self.skipTest(f"{self.backend}: no suitable function") + addr, size = target["addr"], (target.get("size") or 1) + + if not self._repairs_analysis: + r = _run_cli("define", "function", _format_hex(addr), check=False) + self.assertEqual(r.returncode, 2, + f"{self.backend}: expected unsupported (exit 2), got {r.returncode}") + return + + # Undefine removes the function. + u = _run_cli("undefine", _format_hex(addr), "--size", str(size), "--json", check=False) + if u.returncode != 0: + self.skipTest(f"{self.backend}: undefine unsupported: {u.stdout + u.stderr}") + self.assertTrue(json.loads(u.stdout)["undefined"]) + + # Re-disassemble and re-create the function (realistic repair sequence). + # define_function reporting success is the strong signal that undefine + # truly removed the function — add_func/createFunction only succeeds when + # no function is already there. + _run_cli("define", "code", _format_hex(addr), check=False) + d = _run_cli("define", "function", _format_hex(addr), "--json", check=False) + if d.returncode != 0: + self.skipTest(f"{self.backend}: define function unsupported: {d.stdout + d.stderr}") + self.assertTrue(json.loads(d.stdout)["success"], + f"{self.backend}: define function did not report success") + lf3 = {e["addr"] for e in json.loads(_run_cli("list_functions", "--json").stdout)} + self.assertIn(addr, lf3, f"{self.backend}: function not present after define") + + def test_define_data(self): + if not self._repairs_analysis: + self.skipTest(f"{self.backend} does not implement define/undefine") + self._load_fauxware_isolated() + globs = json.loads(_run_cli("global", "list", "--json").stdout) + cand = [g for g in globs if g.get("addr", -1) >= 0] + if not cand: + self.skipTest(f"{self.backend}: no global address to define data on") + addr = cand[0]["addr"] + r = _run_cli("define", "data", _format_hex(addr), "--type", "int", "--json", check=False) + if r.returncode != 0: + self.skipTest(f"{self.backend}: define data unsupported: {r.stdout + r.stderr}") + self.assertTrue(json.loads(r.stdout)["success"]) + # ------------------------------------------------------------------- # search + imports # ------------------------------------------------------------------- @@ -831,6 +892,8 @@ class TestDecompilerCLIAngr(_CLIBackendTestBase): # angr has no native byte-search API (instruction search still works — # it's client-side). It does enumerate imports via the cle loader. _searches_bytes = False + # angr's CFG-based model has no define/undefine primitives. + _repairs_analysis = False # angr-specific sanity checks that don't map cleanly to the other # backends live here.