Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions declib/api/decompiler_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]})
Expand Down
19 changes: 19 additions & 0 deletions declib/api/decompiler_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
85 changes: 85 additions & 0 deletions declib/cli/decompiler_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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",
Expand Down
37 changes: 37 additions & 0 deletions declib/decompilers/binja/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions declib/decompilers/ghidra/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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""
Expand Down
48 changes: 48 additions & 0 deletions declib/decompilers/ida/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions declib/decompilers/ida/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
Expand Down
19 changes: 19 additions & 0 deletions declib/skills/decompiler/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ same binary.
| `search string <text>` | Find a string's bytes in memory. | `--encoding`, `--max`, same |
| `search instruction <regex>` | Regex-search disassembly across all functions. | `--max`, same |
| `imports` | List imported symbols (external functions/data). | `--filter REGEX`, same |
| `define function/code/data <addr>` | Repair analysis: create a function, disassemble bytes, or define data. | `--type`, `--size` (data), same |
| `undefine <addr>` | Clear code/data at an address (removes a function if one starts there). | `--size`, same |
| `read int/string/struct <addr> [...]` | Typed reads: decode memory as an integer, C string, or defined struct. | `--size`, `--signed`, `--endian`, `--max-len`, `--encoding`, same + `--json` |
| `read_memory <addr> <size>` | Read raw bytes from the binary at `<addr>`. 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` |
Expand Down Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions docs/decompiler_cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,28 @@ decompiler get_callers <target> [--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 <addr> [--id ID] [--json]
decompiler define code <addr> [--id ID] [--json]
decompiler define data <addr> [--type C_TYPE | --size N] [--json]
decompiler undefine <addr> [--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.
Expand Down
Loading