From a56851f994fc9b2f2063c244a915430c1be2f519 Mon Sep 17 00:00:00 2001 From: MaddoScientisto Date: Sat, 21 Mar 2026 09:44:35 +0100 Subject: [PATCH] Enhance CLI functionality and improve common utilities - Added new commands to the CLI for dumping regions, renaming functions by address, and setting various types of comments. - Implemented JSON output formatting for CLI commands. - Introduced functions for decompiling and disassembling functions, as well as retrieving cross-references. - Enhanced common utilities with functions for reading memory regions, iterating Java items, and managing function metadata. - Added suppress_output context manager to hide process output during Ghidra startup. - Updated existing functions to improve error handling and output formatting. --- .github/instructions/ghidra.instructions.md | 1 + .github/skills/pyghidra-ghidra-ops/SKILL.md | 68 +- .gitignore | 23 + .../00/~00000006.db/{db.34.gbf => db.42.gbf} | Bin 15171584 -> 15187968 bytes .../00/~00000006.db/{db.33.gbf => db.43.gbf} | Bin 15171584 -> 15187968 bytes .../00/~00000005.db/{db.2.gbf => db.7.gbf} | Bin 81920 -> 81920 bytes .../00/~00000005.db/{db.3.gbf => db.8.gbf} | Bin 81920 -> 81920 bytes crusader_decompilation_notes.md | 58 +- .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 284 bytes .../__pycache__/__main__.cpython-314.pyc | Bin 0 -> 295 bytes .../__pycache__/cli.cpython-311.pyc | Bin 14293 -> 50426 bytes .../__pycache__/cli.cpython-314.pyc | Bin 0 -> 33856 bytes .../__pycache__/common.cpython-311.pyc | Bin 10362 -> 31448 bytes .../__pycache__/common.cpython-314.pyc | Bin 0 -> 21954 bytes tools/pyghidra_crusader/cli.py | 588 +++++++++++++++++- tools/pyghidra_crusader/common.py | 372 ++++++++++- 16 files changed, 1073 insertions(+), 37 deletions(-) rename Crusader.rep/idata/00/~00000006.db/{db.34.gbf => db.42.gbf} (99%) rename Crusader.rep/idata/00/~00000006.db/{db.33.gbf => db.43.gbf} (99%) rename Crusader.rep/user/00/~00000005.db/{db.2.gbf => db.7.gbf} (99%) rename Crusader.rep/user/00/~00000005.db/{db.3.gbf => db.8.gbf} (98%) create mode 100644 tools/pyghidra_crusader/__pycache__/__init__.cpython-314.pyc create mode 100644 tools/pyghidra_crusader/__pycache__/__main__.cpython-314.pyc create mode 100644 tools/pyghidra_crusader/__pycache__/cli.cpython-314.pyc create mode 100644 tools/pyghidra_crusader/__pycache__/common.cpython-314.pyc diff --git a/.github/instructions/ghidra.instructions.md b/.github/instructions/ghidra.instructions.md index 157b7f6..1b1d5db 100644 --- a/.github/instructions/ghidra.instructions.md +++ b/.github/instructions/ghidra.instructions.md @@ -39,6 +39,7 @@ applyTo: "**" - Invoke the toolkit with `\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader ...` from the repo root. - Keep PyGhidra batches small too: prefer one focused repair plan or 1-5 direct edits at a time. - Write operations require the Ghidra project to open successfully. If `Crusader.lock` is present because the GUI owns the project, close Ghidra first or operate on a project copy. +- If the workflow needs the user to change Ghidra state, use the ask-questions tool with a yes/no confirmation prompt instead of plain text. Ask the user to close Ghidra before PyGhidra write commands, and ask the user to open the Ghidra project before MCP server commands. The prompt should briefly describe exactly what to do and instruct the user to answer `Yes` only after the action is complete. # Current Verified Raw-Import Ports diff --git a/.github/skills/pyghidra-ghidra-ops/SKILL.md b/.github/skills/pyghidra-ghidra-ops/SKILL.md index ef676b8..f4732da 100644 --- a/.github/skills/pyghidra-ghidra-ops/SKILL.md +++ b/.github/skills/pyghidra-ghidra-ops/SKILL.md @@ -1,12 +1,14 @@ # PyGhidra Ghidra Ops -Use this skill when Ghidra MCP is missing a needed write operation and you need native CPython access to the Ghidra API for the local Crusader project. +Use this skill when Ghidra MCP is missing a needed operation and you need native CPython access to the Ghidra API for the local Crusader project. ## Use Cases - Create or delete functions in `CRUSADER-RAW.EXE`. - Apply small batched repairs driven by verified addresses. - Add comments or rename functions by address from a repeatable JSON plan. +- Decompile or disassemble functions without switching back to the MCP server. +- Query function metadata, search by name, and inspect xrefs from the same local CLI. - Inspect project root files to confirm the program name/path before running edits. ## Workspace Defaults @@ -56,6 +58,63 @@ Rename a function by entry address: .\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader rename-function --entry 0006:02cc --name entity_class_get_flag20 ``` +MCP-style read/query commands are also available from the same CLI: + +```powershell +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader get-function-by-address --address 000a:48ff +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader get-function-containing --address 000a:4901 +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader decompile-function-by-address --address 000a:48ff +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader disassemble-function --address 000a:48ff +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader read-region --start 000a:48ff --end 000a:4912 +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader search-functions-by-name --query rng_ +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-strings --limit 20 +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-imports --limit 20 +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-exports --limit 20 +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-namespaces --limit 20 +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-segments --limit 20 +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-data-items --limit 20 +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-classes --limit 20 +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader get-xrefs-to --address 000a:48ff +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader get-function-xrefs --name rng_next_modulo +``` + +All commands also support structured output for scripting: + +```powershell +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader --format json get-function-by-address --address 000a:48ff +``` + +For ad hoc investigation, prefer `run-script` over multiline `python -c` or pasted PowerShell here-strings. It avoids leaving the shared shell stuck in an unfinished string/block state: + +```powershell +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader run-script --script .\pyghidra_plans\inspect_rng.py --read-only +``` + +Script globals available inside `run-script`: + +```python +config +project +program +helpers["get_function"] +helpers["get_function_containing"] +helpers["decompile_function"] +helpers["disassemble_function"] +helpers["get_xrefs_to"] +helpers["get_xrefs_from"] +helpers["read_region_bytes"] +helpers["rename_function"] +helpers["set_comment"] +``` + +Write-side MCP-style aliases are available too: + +```powershell +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader rename-function-by-address --entry 000a:48ff --name rng_next_modulo +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader set-decompiler-comment --address 000a:48ff --text "Returns RNG output modulo the requested bound." +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader set-disassembly-comment --address 000a:48ff --text "Modulo wrapper around rng_advance_state" +``` + Apply a small JSON plan: ```json @@ -109,4 +168,9 @@ Dry-run a plan before touching the project: - Address strings accept raw `SSSS:OOOO` form or plain integers such as `0x75a90`. - The CLI tries a few root folder path variants when opening the program so it can tolerate minor project path differences. -- Plan files support `remove_functions`, `rename_functions`, `create_functions`, `comments`, and `assert_functions`. \ No newline at end of file +- Plan files support `remove_functions`, `rename_functions`, `create_functions`, `comments`, and `assert_functions`. +- `set-decompiler-comment` maps to a pre-comment and `set-disassembly-comment` maps to an EOL comment. +- Read/query commands open the program read-only; create/rename/comment/plan commands still require the project to be writable. +- `run-script --read-only` is the safest way to do one-off inspection without getting the shared PowerShell session stuck in a multiline Python string. +- `read-region` now reads bytes one address at a time instead of relying on a bulk `getBytes` path that produced misleading all-zero results in this project under PyGhidra. +- PyGhidra startup now suppresses the noisy local GhidraMCP `Module.manifest` warnings during normal CLI operation. \ No newline at end of file diff --git a/.gitignore b/.gitignore index 95db82e..e3938a4 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,26 @@ Thumbs.db # Local Python environments .venv-pyghidra311/ + +# Python caches, bytecode, and tooling state +__pycache__/ +*.py[cod] +*$py.class +.python-version +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.pyre/ +.hypothesis/ +.tox/ +.nox/ +.coverage +.coverage.* +htmlcov/ +build/ +dist/ +*.egg-info/ + +# Local scratch and probe files +.tmp_*.txt +.tmp_*.py diff --git a/Crusader.rep/idata/00/~00000006.db/db.34.gbf b/Crusader.rep/idata/00/~00000006.db/db.42.gbf similarity index 99% rename from Crusader.rep/idata/00/~00000006.db/db.34.gbf rename to Crusader.rep/idata/00/~00000006.db/db.42.gbf index 5a1037b3c2e89b8582d48bd98df441cd7a8d1055..8983d8b074b3fcce1f8c07e3e30112af186e5d42 100644 GIT binary patch delta 14088 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 delta 250 zcmZo@U~On%70@>@)G^W2(R!2cJdr_+U?W zKS!W$f*KV{^NLc7^K;5lQ{vMSi{eW%O7pVgQ! raw `0007:5a90` - `seg043:017a` -> raw `0007:5b7a` - `seg043:021c` -> raw `0007:5c1c` -- The first recovered standalone function spans `0x0090..0x0179`, which means the current raw label at `0007:5b6f` falls inside the tail of that routine and overlaps the true return at raw `0007:5b79`. -- Practical consequence: the missing raw `0007:5a00` seg043 function boundary should not start at segment offset `0x0000`, and the current `0007:5b6f` function object should be treated as a mis-split internal block until Ghidra-side function creation/repair is available. +- The first recovered standalone function spans `0x0090..0x0179`, which means raw `0007:5b6f` falls inside the tail of that routine and overlaps the true return at raw `0007:5b79`. +- Repair status: applied in `CRUSADER-RAW.EXE` via the local PyGhidra toolkit. The bad function object at `0007:5b6f` was removed, and three conservative replacement functions were created: + - `0007:5a90` = `seg043_func_0090` with body `0007:5a90..0007:5b79` + - `0007:5b7a` = `entity_set_at_target_update_facing` with body `0007:5b7a..0007:5c1b` + - `0007:5c1c` = `seg043_func_021c` with body `0007:5c1c..0007:5c80` +- Follow-up re-decompilation now supports one real behavioral rename: `0007:5b7a` sets entity `+0x3a` to 1, calls `entity_set_facing_direction`, clears class-detail bit `0x10` at `0x7e1e[type*0x79+0x59]`, then continues into downstream dispatch, so the repaired middle function has been renamed `entity_set_at_target_update_facing`. +- `0007:5a90` now has a stronger structural read from standalone disassembly: it allocates an object when the incoming far pointer is null (literal `0x98`), runs a far setup helper using DS:`0x4b48..0x4b4e` and the second incoming far pointer, writes `0x4c13` at the object base, calls `entity_set_at_target_update_facing` with the third incoming far pointer, then adjusts the nested object at `+0x38` using extents read from the object at `+0x34` before returning the object pointer. +- `0007:5c1c` also has a stronger structural read: it optionally calls a virtual method through `[object->vtable + 0x4c]` when `object+0x44/+0x46` is non-null, passes a local stack word through `entity_class_get_flag20`, then dispatches one or two downstream far helpers using `object+0x48`, gated by a local status byte at `[bp-0xe]`. +- `0007:5a90` and `0007:5c1c` remain intentionally positional because their current decompiles still collapse into unresolved thunk dispatches and do not yet support safe behavioral names. ### Entity Class Flag Helper @@ -1306,7 +1330,7 @@ Named via systematic analysis of 11,692 NE relocation fixup entries. These are t | Rank | Address | Name | Calls | Description | |------|---------|------|-------|-------------| -| 1 | `000a:44fd` | *(no function in Ghidra)* | 331 | Analysis gap at seg091:00fd. In comutils.c segment near joystick code. Needs manual function creation. | +| 1 | `000a:44fd` | `seg091_func_00fd` | 331 | Recovered boundary. Shares init flag `0x44a4` with `runtime_init_or_abort`; thunk-heavy non-returning wrapper. | | 2 | `0003:ac7e` | `mem_alloc` | 272 | Allocation wrapper → seg082:0000 (`0009:a200`) | | 3 | `0008:dbec` | `entity_word_list_destroy` | 238 | Already named. Frees entity word-list buffer. | | 4 | `0003:a751` | `mem_free` | 207 | Free wrapper → seg082:007a (`0009:a27a` = `mem_free_checked`) | @@ -1372,7 +1396,7 @@ Named via systematic analysis of 11,692 NE relocation fixup entries. These are t |------|---------|------|-------|-------------| | 41 | `000a:7b58` | `nop_return_zero_b` | 56 | Returns 0 (default vtable slot) | | 42 | `000b:3ab2` | `sprite_node_dispatch_event` | 56 | Large event dispatch: checks event type (2/4/8/0x100), updates global focus ptr at [0x4fd0:4fd2], dispatches via vtable methods [+0x14/+0x18/+0x20/+0x24] by event code. Switch table for 16 event types. | -| 43 | `000a:48ff` | *(no function in Ghidra)* | 55 | Analysis gap in comutils.c segment | +| 43 | `000a:48ff` | `rng_next_modulo` | 55 | Advances seg091 RNG state and returns the result modulo the requested bound; returns 0 when bound is 0. | | 44 | `000b:3362` | `sprite_tree_unwind_check` | 55 | Validates SS == param_2 (stack segment guard), then decrements global counter at [0x4fd6] | | 45 | `000b:40ee` | `sprite_node_update_and_dispatch` | 55 | If `sprite_node_is_dirty` returns false: marks dirty, calcs accumulated bounds via `sprite_tree_get_accumulated_bounds` (3ed8), then dispatches via thunk | | 46 | `000a:7b5f` | `vtable_stub_trampoline` | 55 | Calls through fixup thunk (forwarder to another function) | @@ -1397,7 +1421,7 @@ Named via systematic analysis of 11,692 NE relocation fixup entries. These are t - The earlier standalone seg001 port hypothesis in this subrange was wrong. - Relocation data places raw `0007:5a00` at `seg043:0000`, and the already-named helper at `0007:5b6f` sits at `seg043:016f`. - Because of that segment placement, standalone seg001 names such as `debris_spawn` (`0x7490`) and `entity_die` (`0x75ff`) should NOT be ported into this raw range. -- `0007:5b6f` currently remains `entity_set_at_target_update_facing` from direct raw analysis; its behavioral name is no longer in conflict with the standalone seg001 `entity_die` note. +- `0007:5b6f` no longer exists as a function after the PyGhidra repair pass. Its old raw-analysis behavior now lines up with the repaired function `0007:5b7a = entity_set_at_target_update_facing`, so `0007:5b6f` should be treated only as an internal control-flow location inside that function. - Additional resolved call targets inside the missing seg043 block were annotated in Ghidra from relocation data: - `0007:5a8a` -> `entity_set_event_type_checked` - `0007:5a98` -> `FUN_0008_cc01` (timer-related flag/event helper; tests `+0x16 & 0x2`, sets `+0x16 |= 0x800`, copies event field `+0x06` to `+0x22`, checks `0x1000`, then conditionally dispatches) @@ -1406,19 +1430,21 @@ Named via systematic analysis of 11,692 NE relocation fixup entries. These are t - `0007:5bb8` -> `entity_is_type_match` - `0007:5c49` -> `entity_class_get_flag20` - `0007:5c8b` -> `mem_alloc_far` -- Current boundary caveat: - - Ghidra likely split the real seg043 routine incorrectly. `0007:5b6f` has no inbound xrefs, while relocation-resolved calls exist on both sides of it inside the same segment window. Treat the current `0007:5b6f` label as a behavioral anchor for one internal block, not yet as a proven standalone function boundary. - - Standalone seg043 disassembly now strengthens that conclusion: real prologues are at raw `0007:5a90`, `0007:5b7a`, and `0007:5c1c`, so the current `0007:5b6f` boundary demonstrably overlaps an earlier function. +- Current boundary state: + - The seg043 split has now been repaired in Ghidra. Verified temporary functions exist at raw `0007:5a90`, `0007:5b7a`, and `0007:5c1c`. + - The repaired middle function at `0007:5b7a` has now been promoted from a positional label to `entity_set_at_target_update_facing` based on direct decompile/disassembly behavior. + - The remaining repaired functions at `0007:5a90` and `0007:5c1c` should keep their positional names until a later pass resolves the thunk-heavy bodies more clearly. + - The next pass on this region should continue re-decompiling `seg043_func_0090` and `seg043_func_021c`, resolve the still-unknown far thunks they call, and replace the positional names only when their behavior is directly supported. | Address | NE Segment | Callers | Notes | |---------|-----------|---------|-------| -| `000a:44fd` | seg091:00fd | 331 | #1 most-called target! In comutils.c segment. | +| `000a:44fd` | seg091:00fd | 331 | Recovered as `seg091_func_00fd`; thunk-heavy init wrapper sharing flag `0x44a4`. | | `000b:2e00` | seg109:0000 | 74 | Start of segment 109. | | `0007:5a00` | seg043:0000 | 64 | Start of segment 43. Earlier seg001 `debris_spawn` port was rejected; still needs manual function creation and direct analysis. | -| `000a:48ff` | seg091:04ff | 55 | In comutils.c segment near joystick code. | +| `000a:48ff` | seg091:04ff | 55 | Recovered as `rng_next_modulo`; bounded wrapper around seg091 RNG state advance. | | `0003:a880` | seg005:0880 | 49 | In CRT segment near `far_memcpy`. | | `0003:ad75` | seg005:0d75 | 43 | In CRT segment near `mem_alloc`. | -| `000a:454d` | seg091:014d | 32 | In comutils.c segment. | +| `000a:454d` | seg091:014d | 32 | Recovered as `seg091_func_014d`; init/context helper using the `0x45a6` cookie/context global. | ### Tier 4: Ranks 61-80 (29-42 callers) @@ -1438,7 +1464,7 @@ Named via systematic analysis of 11,692 NE relocation fixup entries. These are t | 72 | `0009:c433` | `event_queue_align_index` | 34 | Returns `param_1 & 0xFFF8` — aligns ring index to 8-byte event slot boundary | | 73 | `0009:2156` | `dos_file_get_size` | 33 | Saves file position, does INT 21h AH=42h AL=02 (seek to end), restores position. Returns file size in DX:AX | | 74 | `000a:2c41` | `list_iterate_next` | 33 | Linked list iterator: if *out==0 returns first from obj+2; else follows next at ptr+2/+4. Returns bool (has more) | -| 75 | `000a:454d` | *(no function in Ghidra)* | 32 | Analysis gap in comutils.c segment | +| 75 | `000a:454d` | `seg091_func_014d` | 32 | Recovered boundary. Shares flag `0x44a4`; checks optional long argument against the `0x45a6` cookie/context global. | | 76 | `000b:2446` | `sprite_clear_redraw_flag` | 31 | Clears flag at obj+0x17e, then dispatches via thunk | | 77 | `0005:1238` | `entity_get_class_word` | 30 | Looks up table at [0x7e01] indexed by *param_1 * 2, returns word. Sister of `entity_get_type_word` (which uses [0x7df9]) | | 78 | `000b:1446` | `display_null_check_dispatch` | 30 | Null-checks far ptr params, dispatches to different thunks based on result | @@ -1559,13 +1585,13 @@ Compares two 5-byte `map_position` structs: `{ x:word, y:word, layer:byte }`. Re | `0x7df1` | word[] | 2 | Entity base type word | | `0x7e1e` | struct[] | 0x79 | Entity class detail records (121 bytes per class) | -### Analysis Gaps (No Function in Ghidra) +### Recent Manual Boundary Repairs -These high-traffic addresses need manual function creation in Ghidra (Script Manager or UI): +Recent high-traffic addresses recovered with manual function creation in Ghidra/PyGhidra: | Address | NE Segment | Callers | Notes | |---------|-----------|---------|-------| -| `000a:44fd` | seg091:00fd | 331 | #1 most-called target! In comutils.c segment. | +| `000a:48ff` | seg091:04ff | 55 | Recovered as `rng_next_modulo`; manual boundary repair narrowed to `000a:48ff-000a:4912`. | | `000b:2e00` | seg109:0000 | 74 | Start of segment 109. | | `0007:5a00` | seg043:0000 | 64 | Start of segment 43. Earlier seg001 `debris_spawn` port was rejected; still needs manual function creation and direct analysis. | | `0009:a200` | seg082:0000 | - | Target of `mem_alloc`. Start of segment 82. | diff --git a/tools/pyghidra_crusader/__pycache__/__init__.cpython-314.pyc b/tools/pyghidra_crusader/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..161be0656c85b0dffd6a464d6687d972ea98edd0 GIT binary patch literal 284 zcmdPq<5TjJML|M|IXUt1D;Yk6+;Yp-+bSjUMhB(F`Mo$J0#u7$(hFGQ`CW{CLh7>*q z1{sD}W<`crMnwh_s0I^82qS|rh*_CIljS9dugQ3eB{wlM?-oaVJcy2u_tRv##hjdz zS;P#K1_|dS=BCES-{K0cEG|jSb*;!OS;_DjWYjHNZ>yN}jLejx#2DwI(&EIF)S`Ho z)a3l!f|!#0{G8&Lf=Y;Vd@@uz24;(1LFFwDo80`A(wtPgB5t5*AlDSD0ErLGjEszT n8ALy@F|hD-R9#_~yvZdpF=TSsWiFWuEHWRN8Cax>IDrZQ0v}4Y literal 0 HcmV?d00001 diff --git a/tools/pyghidra_crusader/__pycache__/cli.cpython-311.pyc b/tools/pyghidra_crusader/__pycache__/cli.cpython-311.pyc index d54a6c5f726d27da0af959e32a37fe656c64853c..a363335a5f01547c60adc20ac892ce8cb8fdff14 100644 GIT binary patch literal 50426 zcmeIb33OXmdL{@E4+KB}1W1DW25}WBky@;Mvnf%#L@lIciIz-Le2_$$i~IoE5{imt zr;@NLPD*injyV-q7f#|h^puieCib+Fadk;9&$Mc$GV>nh)gcbfVYjAdl9QV0(@nMQ zqz-4g&wT%VI~G|iS0z2C>k+)~?)$&o|Ni&BKk@sEZ1fENi_zNOvf2I*=1>hzdD}mG zrod)<$Hv=u`M8D72}nI zmE%=|RpZrz)#EjTHRH8|wc~Yzb>sDe_2UhL4dac2jpO0L@c631RpU*AP2bNjZR72O?WoH!-u-oMa5aCN_u<{a_wgtA;Mbjlo%~6@6h3SCQ+x>Twft$m z3hyqypRdKcn;+oo@Lq>^J>Ki_Zoqp3-i>%~6dnexO@!o~^2E2FUy%Fy{ct3^r(|B*f`x(49 z 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 V+R$&h!wJ^ literal 0 HcmV?d00001 diff --git a/tools/pyghidra_crusader/__pycache__/common.cpython-311.pyc b/tools/pyghidra_crusader/__pycache__/common.cpython-311.pyc index acf58974d5a375e4cf29ecfaa35d4e8a5762d355..610f1966b85bb4128732d7917ec36ad51e9f179a 100644 GIT binary patch literal 31448 zcmeHw32+=&dfps82hf9=0cLPtgNrx_0=xi{AP5O00UiQCE&-AfNC`Vc4=Iqq0NXPV zN$g;SmE{|Y%7UfkmXG& zs!Ao_|9Y+-47kv)T}i6a)AP^kclSH~_h0YLn*{~=23&vl#J`$4bJk$^b9zW#R?T7j z@T(?+;T3~m5RB7?8RNLoNN&@#X~sNmW^VH|H)9#MFgG`Cow1GEnA z&twSL0{LGz8VujUSG(5vq$~6H>v;H9%HyUZ=@hKxNvC1FfaPbu=}da9yOOTVU%@8W zzmEEU3t#Qh*I?WoC>Spk^2UpV{Be)Kk9(1?MR0t(Kp)R!IAsu=&l?0+R&K~NUDxqq z;nRX&D0<#9UJ@`1p63nvcNs4gt_fw&bK_;gq)?9EazPL(@LM5NK5rPW6as<|PgTNo zp$@;*!VRGjzct9~X`%UfE-SZM_)iI~Tl{^(O`#3x>V#WDJAUg?+8+Ei2-Cto{5A^v zQCbtqK7glP!i>;`-)1~_6T(T)lAZhHFmfgR5QmjPNMp?-4}d zBz`-DkT8JXP9ZFu!tY*TUU)`0i{U$`v{ zYQcGN_GVx*d}20ued>lt^IPSof}yZ~dU`^b5>9=bYrH!(UU6T{&u0%vA}9dmcZ0?Lhm7vInwfTfJjgR1 zOG6UZuH7=%9xu!GC~c!W+O%8d+TvApOXno6PP^r#L-e6=tv=McAM-TyeG%WtY%nmH zLdyuD!GSSq{7{7h$RDF~+OTL0C&n#24H3hfHO!GqyYvWi(G)SRXT(m%GGw8|P;WSI zyRa^U(STem>F~5GV*0j8Um{y_ur%orw9ydc8w~plA?~*6j`<10Z7g3;AYaNEv%)+g z(!6gqveDLRRqe@+Wb;F$6-TvUiiqKWpq7T_=jJfPLo~i8>34QMJU1U!Ei6W>ndKrj zQX&W9%}zfP5NI}qs3m+ppIAphrGa2*o+e3h%p~#`TM=4=Z|K_qONKb-Svp6b_dKO* zyj0fnC(S*w=cwX2x-=B$Ju$vA%2&#KwZd0#8cde1jetZ18@V+$i z;>dbrc|>*`R2&B-$H7f=GL%L6fFl2WDn&YNmg}RBKFac8j}LfMGDNfNho${K^E*DS zztQ$ivk`C-o~g?QR|Acx!)S}g(EhI&!WknjoLZm_hMOrj#+~Wg=C32gxA4`jF2kZB za~$4Gg$SH}MCiwBMm$SaJX4+_Y)|Ho@>3&jo#w%oOTP>o^=aun*|o#8soJC5R2?FQ z^^94O%rSfJ;C<0jY0?HY`B`$aWalOrE*W0IeAy;fxgQ+{jp^}e0|U1Zx~fO$ckBE zDtP04no1&#y;ff3X6FJy)ifJYZ9?Fg;QaKoY8K|_RCDNVNVSH-0Y5d&ZcGLJ(-VobIuE4Qp*L|CRfRwczM_+M~mWUksK`>j_t$Z zj>6>u`n<;?om>Bu>}XaT&61;8OP1|(dUZl}G%1cI$1oo=g7 zX3AVSee@CX+pI(@v8H0+18TItCy@5r*2U2F!=i@T(M@l(4!X?m5B%66tibFzC_MRiMH})1osNGm5E z!;i{QbE%F{;KocK7@lBH{OtAXp@8-fB4P?eLV`*tn)8dHz=W=?Pa{STzM<;?AQ1S{ z7+)Rbt7X1c;cJ&p#%=k_!qR7#J{xardGqX7&fW{%@0T09mBwz_UbpO6y|v-?tPZW$ z%kCz{-E^-=cJKa9=>E~SO>*Z6rSpXBIw9FlZ1B$I@am^t=w0cRxKc(kPNr65l$+|( zFW_OPU7EWav%1|BF|B8og#5KDoLW`4t`YhbW7O~s<47x~7TySi`?X;@=$`~na#t;Z z*U7+j|NL|^WOU9y8Bp`nQAKLtRNJ_u2K9kNsN!?VdQ63-*?aePC> zbfI^=l`-$0sCSR-?Nq#-G4Ej1J1BckE8f#f!*RYW#y3RyhCgZSz8{erPb!TkWqv^6 z2PAGlYxB&3PwP6HMm{+7lvBiz-2x&+IYiRZumt~UOH@BZ872mo^ZKkf;}55*K8m1I z_=borSTfKMxUCxinU$e&+CX#qPvO`~%0<8~=x3dHK9kf#c-Gt%9|c~>ECKmymwteV zZHSk1fav3CWXsO-yx&F(Ys7HU@T7GSSVv2{Vq?p#c|^56?GKI6#N~p3G&bPj4~IqZ zBC04}0%$dCmZ{MQ19K!o zQ5_fNgW;)}z6=HI)TXgt2 zizK`O91k=>6JpDy9|dB0#Jo;pb;~7KA|{k%ech~|Iv_Vu$K)m0a+C0ptW^3uurk<3 z7Azgpvy-s@wgFb&Mx5?SLyFN^4-)#|Nj7kzU+1G;UsIS$=q$5lY zl31$)ccwyNP=fwB)JYJZ!V9b1&Dp6SR;nv_5U-L?9#w})cZiG(`=_QuSu2z`fkYDZ zp8zE|D~i{Vcnsgr>j1!l?h?gaf6uD49F>}mYq#t^zI5h+eRREBY3P>hqqyI?uJoPP zoPZm4e)+@;_7(f4#q6>!o!Yb;?1eE~Y1CFK+sYMNIZcZExZU-V^F?Re@tJ#hlH)Vl z{ejt(m;d($0Pw_l`9HQA9A#_V8+l*KTW^-j+LW?3ncuDOByQP?W>ZVsU!d;UXf{2b z^*s*31E>*_hc6n9hCC8kN>zuHxnFwoHvd6bp>?blR8qE1_e6N_~B_d2wC1R5}}KQ zrxMaK$jhEFip3Z_2;F+2Gl5RisuQDOrHVjtdNVQ*wocEf4@|M2gOD9rsCNds#mL*&c?kBBx`D6VA&2a z)LM_kH{p&uN@9-csH1xA=@j?$QJgQ9xMFR2$rA&9VPZBoeODwwsBR&lVctM(^Otzb zTqK45?Xip)7R~r_kldh6lNTmTiagM#Kr0_vv|w^>vvfo(NJ~A$F32Ht({0fVIbu1c zTuK6AFks3#7OfHMBKHLN_(kjItSkGq-}Ll~n3w9qB>7ETU~4i;!4gD^VQRYL*&N}% zYl@q;s2?`sI@3r=#rKfE1r^|Hmog zwjqUjxRG4zBaz)$`P|Jus7+>4Yh3e!xw*&Jn#WKB8=Kk)Pa+^jtrJ4jFb=J0+LA}? z64pS5dqQwC+Md)P;s`zELkSd4tXwo@G}J_00!EB3O=O!wPpljg;nO#C8lyEK+;CQ` zb<56r#aX{}3N#i#Ve!)0O_SSFwNbhMez$U9K&~8ADhD?Wmb^L!mLJ_E zo@m9Md$-=|R*sHH=bw-(#+8ckRU2rxRm(>ko+==x0whX;c(rdeKmAFdr-C{-9`HrW zk8Bjx#NFY0-OBDh$sMNq{3Yf5WWxQls5!7ydYo7)Jxd*ViB z5?>Q{l}N7oxV!X~f-e-T-H_e86!$L4y({kUyfpmc@C)Zw&MmXgrp@4}MS_o(5A8e| zOGJ3}AX@a5k`o5QuUGXuOz-5I;C#oC-(PQer^M7>ZF#56LO#_N@~Phg_wO0@p4e+v zc_9#*1k+8#st&8r65BvnYIR6H@}w@-s72;Vg=c z&xj>Hi*&2_hNb|tQT(8|DptHFT8yD67xyT|Jxe3;f}*8Cj8n^@xV>Q6LLZFmW$OdG zD`u~W+H2N6ckdb5-mTa{$tCWOmTkGisGCW!{blPB!|#;!7n*eoq!}Uj8b72ZjFe8# zQv<@i_0prRo$!t)vlLID(@i6~;tBf$1sFfuj{^tlTxEbPdJ2E=Id9%3YV0 zuFEJ}<{wuOi+h|EujYf75$`f#*OsZt`g$4}|7=1e9k)m-1hjBkDWYFW(M3!+VpfZ& z8Rmm*W({kG0j#Bxe*Ix4&-sQ)n`AFgBV=@)>OlI5>xm4+Uq97i|%4(XQh! zvq(sgzaxuzk!>Uz;SAd;$V_sRm3IA1(mx571+=){4}_&Gg=YN z^kBxoRJqX9LIBD%m=~$G$$3!(`csV)A>tbtImv)VQx-CQNDSP-+CIU?vPhHUO?*Rf z08EquZ>*puT2Lbw_>=melqbNlF>ZIf#J|Y@%851W8_q8|*Kf;Zol04!?A@z)_ufA! zdk@L>!;1Z|WIr4)Du1Q@3+*p=e6eHMwrtzr-K#8ni7U}i9<4gbi>4o&wj@E{>9ozg z=tK03oYQIhnLJiPb|Q^~LuoPCY;l#C0&rAG|*% z^F0dRBXK=j-;6^#_GYwxZvSQwAY~yPEL2}Z63`N~P(i{Ho(pKOO4bkwuX{{dH4v$>+2wJuBQ5 zM^Oc~*bh$y6G|fO(SIlLhyn9WiUhMZ;jV6x^Q79Cf+jSy@m<9I*Z7780TPXzHqKc( zxxtsOHO4C1qZRFPMTb(+A@iLI-}$|ox4GD(7ov||kRKgW9vzeST~zj6l=zDhcX8`j zav*OumNa3zUZtXcYo-h}5)ZQ-2cmCtl0@jqsCAl?TP}SneVHV^$Xueb^-{F3{R5O? zVN0ZyEs+i^4YrYm{J{hnhBbgA!TEwCe>6D(;AhVtAR-hlTmEr$8XRWd9$%|Pq<#~h zBaj*rEb4g(!^3RqK$6A=LO$zlMu(_&<_5aYlB6ZX;i&-1@SUgLIvwl15beDn z_l_yOWAdJh%ASkq@Qsp|du4LTex+ppra77Dy&~UwS*-3*wC<2xcSNZ>A{X^2MLiIw zCvpX|UsSbe%XqA7&3H6hELJuG-xpA4)vBqfRm=1gm`WYV7A-5T2$vhPM65d*$GTB#&24X} zuaAD@XQj6$-y!q&q3JWpFKce{>!ImA>5}lqL~CrhK0$hZOJaC@g7k!^wp^beeTKX= zoz2723+C6dh*K!?s?%w+$?%Bj7OiG5*??5nv}#=GjBvmtoMsEkD9u(o^u1uEanhoNG`cd4 zXov?NC8JW+3fBOXaF0*|5*v&nUnU<{3#LM6GK#WJg)UHGwyDr%((|c#Q=tK^K-G-8 zLC8tujQBeQewP3#;8Yvb{F6^Z{%;H2nu5X)wY|pbWJ73zB-QBA0>=C&%u_<*A0wQ; zpp)2|mQ9PsoohcopQ@zrRA_(encrhvQgT!9(Z&5E7QLk z{Ay4x?NUm+HY%#u0&mQGY39}0*Jib~bfdOmeIV9&DB5^PZakti9+7K%l-iz+vZ}R< zZ(RM-)mNW-?J2peT`6ndD9=n>-l3FtY*f~+Ti$ei#U=apDZYJj<$k4d|7Kwx#GwX& z56nDi69F~}{J6~EaA}qiYhdYp67OTSV;`6ejyecSmqxT@J9F_Qc^5;F573UE96{;` zAaias2YU+SohEwqPvHH&y9ITc6R6XdT*&fFUFhfQ7`@A*?8Kh&RE zc*~_PGh$nU$|&f37@sL{uVwMvKqI8=OI!~{BgU2AVo1cIA(1f+i*VuvBo==NpxS4G z;1+{Zld1*m>wEw^GRX312(mqrtdL!Z>VyYOl%|3K;Uuw;geMrksv!^}Qp8~b3|$a@ z8PbpvnL7<9^4E9-O&j|oL~gZbAqLe(Go1?kca+y35m+Z61B8hCPvC_FTcJq>iKk82 zun>3pR}}Z}06^|Ki(^h-)ags>Ka!|n!(F>>i}|{vzV7>B+1IQ1dS&-9#eHnk;ItGm z0F#t$kc8Vy3Wt|nUvvSHXuAIQDqHV0{zm6FJLR^cO50JnvRA3>C7d9<5&Tk6uIW%} zI^>E@rJ^%l<%?DAk5=uMs}3qv2jg|S)~naNYu@jjc+2veXMg+bW}b!SIKT&H8_jWm zO#(l5=bqy*ui18vThR_|j^{HXh}i>RuN!lx`iyR0d3kV`>raWl7Ij2v% zWwx(lw%T?=#zf3%D}-ANXNkrYAJWp=u;tR1x@eEsKktQvC#SSXA2*2YBal8>a?ezh zw*!(Tl84grcCuqoq?ztP+#Tc!(jg0XLb3jCN0D2fiDLy8W?ftwd zy+=`#TT$nK!>Hzu-krHNJ3R&-9=zob;Hg?+;0f`NP6t7q0BX+o;Vw96u@H>u4Ci2G z0v~hQhqzm!TmBTW#aRNBb5aXoLGI$+4Ee^#_TFj#%r(K^w@||td$t8G5Xw|FXyHz* zCkctc`oO)Gubz_{kK7mE>Uz862gjs_GfM`x&Or`gz7@FJTA)@YEJR=&LK@Nz?DGmt zcZAqGi~k9AVyG?A24oVY;X6WG34Eu8g%}^6ZY$MtVyJ&~l#x>Bu94bSlZUXDBMl2P z$xM@1s(mVSYI^pXe;T5=SuvcH*`lD6jeOzfGm4d`~5jEwlh%+sc=SP84N36ivKFPRC0^gM-f@5M`{%xU zPChuG92{8hSZiEse7F1G_x-3(e(Z|!*cC92ge?I+Fk1*)0+0?n0UQR4;}?dW8uQX;1P8l_}hM}uy? z^tdGhb21F3#n%fO^cYhA1nS67)D5suM*4KA`qI|Z0U0iR9GLMKS9V7rqDZl-koPls z%erzXV#!TMMY5#cdRxRu7S@TFGguho%C!jgzkVaJnN1@-tYFq>Z^I;y2n#02)C3*Z z=rNNl_*?Qw%@5E9lqw)(&<5W`j2vV^SqQT8$){6Fun>gUY^$gEArcf326+<>q7OVT z-ForX3o|P-#4)6318>9EM(%@VKdLkymA$=+w|CQEC#nE|OpA)vT4EJ#(TX;?qFt$I zk5%lCR_vE64k{G~C7x~?``geC+Av?eBint7-6z?7DU)U$;hwdqGxVv`#-+yOSMVyN z6o!=@-Yj|PsPioS$Ch!{2@`^CD0uw{f{=sJ3@``V&Ri+W1{JH;-bh;1nXA*2<^pVm zU?`9ED@{Nm_cR``5(rt}p@BQlnrPknV??c|ZY9fS>Q?mRMy^gYS(->=(8TP+GPfzN zd!Z9$wqhRImbi9Ub~GxEM#<3#OR*&PBz_Osf1D2qbYf?!_%Glw$yjzc=(6IA*=qdo zBRJUhR1=*|ut;YR7zH!4#k67uWpD;L^m?|eaPu72alQ5!S~YzhT#h(6+W<6xJ(8D| zHXpQKX055_?9%skZk`E7CALU@#F{n_`#QSnThv|bN((Ho3g;)p)}w(iqcvvd#mNAp zHSB6t&ZRk_I}@n^#XlqPF#%G*XsQzDBt#qGKvJ}-Ixdc4cQm$GpBWiA3F}ZMYj}a8 zvjIhY#`;Oikup;Y82yplW1tqLMW`u1cT$v^^bvm(DMLH}4a&r&jZK$Uqtx&ejTnn< zG#w-Tn6$2Zkzo+)1LkxE*mwjP;3s_>*I%U1GtNBM53$l~^528$GX z?FV@lhZU=(!-~~1AyU=hmz2i(pWq#`Esodma$B#g(ZeuBb}Stiu|=b;$4vUCl)8io zp%P<31n2rpYAk4@902%beyS+)`0y3u$`y*RwbQm|boo^B{w-^d@2btOq}kDNOtysN6zvaMqX93cyf* zJ_y})*w<>!n0UX8c;c%7kke_lLn4ur&{3o(8n9bpnlj8ysD(|C#Pk3&0jf3K{G=x8 zqvj`LNSYqmBcJ?XGKFQt}=NvK4 z8|A#KkFPB#wcRpzNMVx=3dN;!te@Vmq3XM6rwBHfPK0ng0#qcrK1<>4|A>0&`z(dG zF=~KM&H7Q!Y%Hk9w_Mq3CT4+e$&Gi3OGDmS=cr_v`ocORH%8XiDAa^_JK&SSv~i2J zFl~@eT-lT!#@UW!Sj2HJxiWLYwraFvTJDFO>^tFlL4y*8-E!&Q&&8k6Ax(Wh-Q#W6XVGm3UnLYh{bSm>~=r6Zk0bfAe;cOq;)JT=`BM&V)s zyv2Dmfm)b~oLcOPQeGz#wGdPnNF%lbY8VGU&^am`?>$rC1dp#ZNX5ZK4ffb0XGF+DJ124wTAK^HY@(nDJy{QB0C->AI$; zeg(28+vE_4AU5^8Bu;n7xdW2^e&DWMb;<5J#a*{_Ce9Vaxbi4hzSb;r^$J%narM}V zZK;YER>TV1qlNAF9>4#rbb1`OeEO<#`l?*`q*C}K_)Ax@wgp3WwJEMPkb!v}@v8bv zQJhruxODjO4o}#fl0@e_$K5B248K=Y za-zkYdl@dml9GX#pds3!0FP-jGZ&TZHlM)?Vh2QF_NHY0L}&WY1B;f-y#<)4J3vSJ ze3-GBWhWv$7^Im=iI7`_r$-z+K|$iHJ3}la$`pStf>V&n(SO>NTR5k$1CXU*y&#z) z^H(2l-I*DdbVsuEi4b1qeI6`5HV)?HK4?szE85kn>zl0n$h=Pz2n35M*PjqKAv{2f zxsx@_{Ee7yrj{3d|1Rc7@g2++zzSW^b{lkIX3^C4k+Ao^AMzq{g9|5MLA}Y63t)s%LNnl{n7~aYN#-Q_v`Z zVF1-0p4HSDDvy|hkWCO*qe2&Y0=t>mF7FG0>yTFjCj+5DY#3p7xnr{leu$B;;tFN= z9A!re7hrVSf3NZb=Y}RuoEsh<7#U*%#3WP0ur#Y@jMBj7Aqe#;J1HwsO_d`*HD62> zty-bYMdl!X*l!9I!wOIx)#R`E_$O1DEiKfzs9cZL0guY+h27N!+qH%FJVGdZj zYyEbtxi8w>_tu@*Bj=)zoRc5QQ;e7{O%~fTjuvD{2qvI@_OUt zjj{6XXnD6>epo3#ynGf&d(&VoKtRKK|7+E;@&nQG19EwnQr@*{jn}uv>U*R0y>fk@ zQr{P=ABxrwy?aTnA5-ebR$XyVz2xbLd&{J%C)OW{HS|OqdgO*)rJ)xdxc{IATU+G* z%S!)c%|rG+v3hd#C zTeg9l!~UAQ#<;`1dTy>gY`iCqsbIbL+Ej>+~4#a#(&UISC0fXo^w7D(7eybWk|MGQx>}>A8y3NrOq(M9Le-bYV}}5y~3h&~p=j+Px%P-sdqj5kDDIvOXVuyhvFZcS>H~6hmr~s&JG&KUHyy5VCg$yk zdOI|eSlPW#aqrtGXR*7Z)!lOSVWm1@+yDh6lL};t20pGJJb5M8ka|)lDyLRwV`>*F zyJMq*Y(w^JVV4EsO)Hd;HXQ@at)rx^SGGu6KT050AHW)H%MgOw5<+m>3K<>b9Ctcn zf~+-DpFf!VjMY7R<;oI|4@RsIYi!~jT5YN)bIc@merOU6l9V$tKQ*L6W3!rWiH(aN zQ3eT8{utaDQghgHCsv_}#3_^5k)^E!2}zr3)2RE9rf)w?gN#IrS=2n+R&ocqg(koo z^&{42DdtxZ6D)jLZLI7-wCsRf)}@qzpfBDihJO8Cm0aAd6nA5@R$91T+$c45$C?JC zO@nWLPHwuSG+mO5FDu2Dfr?4S4gf{bL#5}x^6dIuxpbdWx=*(6SM2*G`~DqC(f|KQ z&1A}d>X9n;qUX~iQ*5BeYc%eD!e|sp>Z3YCObVP5tUgM~nXSimV~?y)CbY3fcv(D& z6fffY(~rK>(WcYy*2qndD@~8f#ZM^3pgsPvN8kGr7C4p1UOPH@g=7P|@t6|0_CavE zbIh4E&xn{s@6K#inbE_JFzHV;-}<*C`pJzk^($@2xKgUep`UzxrfouEDKK~5PG}RR z?EDmzooTyVD-PKLZeFymJh>fDnkmHE*(M=9_Sczl*>@t$(x;E$9NpKpsL;Y`787Ex ztQ46xqsEmVk-`;6fUzA(31csF+Sp4Rz%*?w&={Q$Xv#7z?0jRWji=EpyNHkx z6#{H-r+6Pm%7}kL0c_z(>^}M(^7vf>gEXOuMME`~05!-YLZuN?crv6mnD;v=%FL2)$@w*lq>;cLPBC%=F8 zyJvrM=(mTYnn!W}MD6b8HzpN>>P7u3p&IyCdr6~EEw)!7$YZ&P~BMlSiwoEGy_$@eu-6=Y1ULbZ8oQZ z#9=fWtNTMx#?;IljH^Q8Ma2Crd_z?LKpCYqvC@4}(2J!9lu}5Ty=j6pUe*#Tdo)`1 zs9bhjDLcMta9L^@SaxibHA^jrV=cqcmf?4&<(4Z-%N4oos#10pl8QXU0$4tcQz+I- zqOMxWRg0r2*7m*Zpz|@x;@+y5w<+pvlD*A}w|RLWUQ!b)X^ob&$|bv%lHFu)&WsJ- ztF2$}m23AZwR_{c_e#5aUmsmBes%n{@wM@{8{<9wQqS<~pSyQMuI+|;OTs;R891R3 zI12zcp#Vo^001WxIDr$u>m7O_vJzRFkGdKqS0mni)tUUjti+8|JE`(D28ZXNX5k&5 zgbvyE42rDjm^LTf0eaA<)lVBBhd%^z(68@FV zjP6LN*t9*-B(79#^TAue+1o*ES7R3EKOj%;7Q`s64)H(ZL6d|s4!1f3y$q0rDBJCJ z28hBBmXk?KnPBC-I7QpnG-J#R;^aBXj$xj^AXiSzBmNhvfIF%5-im&-YO=aJ=d^+* zG~>&Mpq;=S4edM$Z1W>RI|CZp*#@J!|=E`O?$R zz;DWU!t@88x1Wj)_@e`Ud0KFk}xT$2y)7(1_HenP&iH(zta9zK zgdKWC)6a^+&}ZxqG7Wv@Ujk*pko?y z3q=S$Zm|Q~F0THJ@@%VNpxgB6MGzjQ&=>vXN^V|E;Ien6=(xhH5(I0O#gkblGQ#P) zIBXIvl0Iz|Mi8{o#5N+9&;-Cg`mp1yCdfr+T1`xd?KD01P$);cU}4*K9nGoc!?WKoYoGXfP)lsf`?dH8Und?%RHB5%@cN%cwSzC_+>rU>d z!X1^kqZ^QoAB}lhqMnxf&61}@Tl0T-;%#2;9a4IS-n}mK7Zv^@PIut!%iiVBt$j}B zIux!$;yNI-khpTuhnk3c=#3l+E${@XxpeZ#)bNWUUWVlf%-|wi`ZP;DQ9CBtC z+$jnlZpw{aV0A4dNQhXl&J|_S&H@)PlP!RrHihY!VJ9>(1P+;0@D|fj;uUhQl3OT! zGH1JhQZw|4$=YY{H_yFtZOM^VY0&rEtbbW=tew%PYZ-R7M2?oRZ<8b1H!U*EK5kdb zAOG0FWP~DNmsX2v#`(^wWe&D(0h}0wzs!OW!5j>&X(N)#!B{N3P%)3c2bv>F7j|xm zapR+L19OXd*uBwY@Xf;lgHD4^QrH=0rB0FdAc zx-f8lpnt6Y_)sc9b(}vpI+jeuj8Zj0=6Q4v+n<#*0l^t*zX(5#WuU2kLbG*9?z&=1 zG8+HlXHqpP$ealwO@8?Z)g+JJYl7a3ow$*c4d77*4i;ku9O4pE!Svn`^1wm%tvU-g z7`)+(3wIfWi1EU5rLZ~fDkXPGjZ(4?as&MT!6t7lhFvzm((tC!VE3*alWZ-xnGNj8 z`_oeSN!+q?KyeO8_JJ){7uni3cWKPs6m>VP2W5Ac;_iYWoy{%T%HqVJ?1_5z$evEc z(;4#|i+YZ|6_h>0if5Q@l8|_uPwdr>@msg0{{Aox_TASh5c@ z8@;k9SGHEY9#P5<-oGJpeG1nnC2xj)bW0s!5Vq9qg=2fMBn+m}qG{2YHj)$nDu8`; z{%2?p<1^IrWSp!s0(Vd-o#V_7O8q(HQ%_w~fq2wKJna%&EtR=)g(DQH-5R1ii#W)a z46?*}a%=yap^*LF&Uhu-mmT7&9R$n5$x;(>uTT($BWFTl z897gDzNr{uh{EZZVx+s9jGHWv#Wsm-_d}xWFC^ozlfg1_W+#Uw>iKOgKQxFze2d(i zrb4Fo(q#JV4Xxtek_Y9SGhEw0%EGa?J5>vfh-RtSZjAp<>1peXNG_C>j>%euy>GRL`?jRcj%JT3D@*(LW4cpQmH5K#|a3#{t;dUq6zplh#Ze#R*)S z*!C*Y%8B!U@gH~;&Pv-yMIIzDg_<&@J^hOt#iGr+AvF&O^W4ORbLYlz-be76DG>*= znjukPe-$TIQpzd<)dXq?uo2lxu3ZF96F5&`lmO#wu9NE)0g=ER0?!gyCh*GyUMBEs z1l}a@Edt*q@O=V*Odw9+ZwP!y;Ku}vcncAKmrt&iy)d#e5_8l?9XRBj_K+!#_IsaF9Gy#f z@sf&FAGsM9^QCqE5VX- zEyTnxm9I8R#!}f>su)W*O;%$)R*Eg)1M-7h*jSGvKul$+s6{rT7pDnq1zv;6y=ih9 z@z+fNl*s*jjpk8rF_vMMkPf&_#z7;DsIy_dk)r8=-D=%s)hpd*JLi`P*_DRO{5tFeM=nhA1!+5Z<@;t{o?K?3_-U!_K{}71~0ZHqGHU4M}JkqtXUsS{akK365f~^Snt( zNDo28CFu1eg!q9XP%jmsf)N7L{s4gxf2ssU5G?{E#Gn4Fgg~fBg_&Kap@P-!%26xNXpdFU zJ=71pl6KG<;C{N7)&j4hoiqr1lWL<~v~kgX5z<`UNVpmBZrc1GyqfmV7VxW~VX6SH zrM*5-^4n?N zC_g1tERZET|9o>fzb=>VSRi0-8(|_CDTH~DNrp2sH}Oo8vILU>V7NbqBdaHvu2Mee z7<9D)QUhOhKYz=yBS@s}YfkSO&#RuL&X+wa&TXsCZOhJWYlWq!qAUP*xm}(j(s~QF zU%2H3V&$_LQdVcl46w{pQfFI1qu80kCyNK9o$%9~`?&ZB@u8^L3`d4(6D*Meg9cwn z>H@TlAMy?5JY|29(q8yA?2}qD4I#SWhxPEPicV<=#0PV+iZ2Oi<#qmAvY&VRn<~R- zfjY%zS?@=nhtK=#q!AFqxwrl8g!J?8ss^OP07P^3o6d_e>eylaS@q^n7)4CWd_W9o zY)Z?jio*&wiV#*jsgCJvFaNu`ob>SWng;0zXdcgX)hvqgUUWXnzpiVS#z53^w!r&@ zJjXu_Mw?>*WO2g+EC|#no=Rr*IMXyeKAxOXvup~`dEQcgleF_c>J>Nc#}$#8F;hI$ zP%AM&PVi{Mo-Kwwok*#M>uHV7#m$4fckc_zFM|HOz}0S@RUd(svlF0Er2OWf{Y&dkhI2hAztJC*UjzIN zf&bb+SztKEv{Xt>>pYoiBqRKE>XNu1^UUrF{%Oq3FOT~8-3E_*+_B&|Rl;pr6LIjw zLYb8-@MR;do#4z=8geLtM->oO}feHCz*_K5SVUZjjeg_Uh(N8ZKCWpS1|ZX{}zt zg};vEG?KFz#+_eiT%XIRS;LV}ZWLg}ajZD&1iL`~!3()bFbW}fJnzZg0sRtu*=2!$ zlR4J0=B@nR+w`rs>B8i-%1=To%>%2=11sLaRqx=bp*4qh+2Q}u<6o`{Ukd?&cg54Y z>gio}_VOFk(|gQ|LJWI0F{>&f!)7qc&*Gp81oWmDhC zm-UFp5Vfq~jGIFw9_PWCM+R}4vz%5Dq)LrVCWVc7f6btD3R1j6e)?Meoz#t-xO~}k&-P5fk;Tgi3D&9 z@FdZWO*^rVmvbU|?8eL_9@7)I;Wh4*nYvSTYPWZ`+qvm;j}9Qg$+vfBJ@e`+o1+_rCA@`(cNx(7_?>AODY`OFKF4e^5pbRxQDQ z^oJ&no8zu<5`Tg_&d2$BN}EoYj+^6VmNuUdj$7gumKIJ}kK5ulmbRR*$L*JG4cz5| zJ-jyBNU1)2_PHmM94X6|>8UHGwkkc%ad9Ub%|4P-rq_Z$BgvP!l+m^}r32JxcH z&bV8$$BU(cc!}hQdoEiVI7yV8doyz^<&ZBd=OmZWR~v$hd!>_75o+A!+?0EYkC#ft z$dynoUM6{v_Of)jREl&NOIJwcOK+7O#7krCX)FNbh6m?b3dvdsuphbXw{~ zu8(r)-zFVE?jWmcmkuF)n58@Lo_^$xWZLaSegOHSnfxo#AaX;j?JnsU(#KhPw{!wu z(kWS*xYjd2qn&BxRWT3_N8*8aC=!k#>zas!pwauwM+fu9fKDJRet9vhPGalBeTKqLncakEW2A z<1TY?UgF}WE1tM{3wK$FTO|InRWfZw?PXis&T?j!D~LNJ0d*FZ6XQ;nv$9-a+{JP> z$^o8>qyn^bu$%}8cVc}C)xr@u@>*~rJ`f3C3tgA3Xse2$a4a5}n(|MEWL1o64Oqq; zi_5B0F9`>xgDiJl4ov%5PP`VG!sq*=Sim7wxEhH}sYP-y7LUk5f2Q9%+;D5LYWK4q ze!uGQ`==w5GgCpNoqqr8Gl3}@3%Me{|5`|n#iv5yU^s#T&(Xo5{`03tbGJk;q&cH$ z&KBo39g)tS92^*>-IHC^)Tw}I@jS(=h8V+p>*09G{Y3Em)Kk7FrYd?86+I7HdK6FZ-08IFNr}}-u{tHz-WO}rcK2P^Tdt+PdDjE` zE2x}1n_X)i5RA3>*D3og?5f;!iQdA(nCXgJue;)I%juod#++?KJEVsu zE~gEDh118u3i2x7(`qKLkV8>CqEn%`{0cg@3UUV?sz7V1I%C1>)4_0@t*|Oau3d`- zwM>k53;>gXn38Bfjs^W|He?^Qz<+EM(Hxf+%K;Ecu~rf5=Z>arj`_*o{Y-kxwm&=h z?UT!~m40PQchcT4?|kg`ESz3yRNT$WB@f*@z8hQVeb=Py9C%nXkgyLt7G3l4g;(G1 zO9Fb;{eG^tLPBohk27(mSMhkI$V+ixnxcDJeER*wVf7S!K&nMI1~BgZ~J73b}!M zxjpSfOM@Qpga8mN#@Hh}5j0|fwuk{&1rl@IB^~I11G395_n{I1V3 z!>+Dtj8qxxbqrN(CPZ}SID+t7>);y|EC`63h+e*rL?g(}+G$9}5!Z>z4l@ zJdDb{_>Ub!^fOvjn`?e%A(XQD?%RCd8DBoXGL&lWz2DrMc6shjzcqa~@_UhVN%=Rn ze|7uU+8#LTKCruf;x3&R(yo%PI6g9S#bv*Yk!bqGp8iJu2hE7qFfzo(GT7LH#ETdi zqF<6lvhFkk1KTtjW`$rtd@$|3Y7X9>PzwfcPXwbRILHLas#BT?$3xS>L0OK-o1&@1 z=n`u{L}<#vw>_n3sdkkfD;L@0-9A>h0miBUv@(o^an=XyeJ+;ER699|+ihwSv#p~h ziGN{R6Kb+ejTA;P@3!O;E&X71n4)H2LBP@aphKMjp}^6AP;)*uN)pCQc!y=TSsxb+ zn=xm}Izrshx-e$6)%(!q&00F9A`>@fD`vuht5ZQ=JmQN)gJEAr;_$W1T3dXfu&+hs zTV^ZuQtE;x!5B!YFAztogDt(jiK$2|=sU*5J6}Zh-HOOJeUY#)V2m~qiQe%6Yxr)- z0B?GetVx!$rd}T)$%Q#Hc_PCE1#gF9@tA52L@}pH`5dZM;k8I84C+qnF27181vDKd zIH|VDU_20-ifL5c-~+uGOqH3xKgdAv)98E{|FOG>fZyF^DR*Pi-MDN`ZR<^L>rJ%u z-5X3aA9?6LGI!#Mqj9M_)zqDA>P|H7U3uf)wba3p)4rqm7UDZSOPG|ThLZpAUcklp0zV*WRMRhzyB$RXt9<( zvJ*ig77Tvi=g6+b+xbqE4sA?v#glR?3Po4-3j~BL6ys#kJUKm77GmhKrgm8LG33CY`k$HQj)y5;Yw%h4>KHYp&%8zcWz<)UZ$k z@)1qIA8r+yK%vRg9!(hSxEhEBJ2C(#U%@NEb^?kseKfkBg?BF+$Pz^am3R{aZ|cTi ze63jHq=9s$fE++0wu7%B0RkaZFAk+@I+8UV4}_h6E$mtqY?l4$qMB6Ewq((^$IhY! zs|M$avnlN?NjdA2&U%(;OgbAsvI%a-Dkl^;e(|w`bC!P0@s|CMT>yT?wR7cM!oKhC zR?R5-RjeGZ`{sTqn%*lH`#a6=H9Gq{%tpkKDUnFi4-oIcXveP# zrho`O4JQBcroem&opOYsG7^8vmOUg4m912=-19JP%#oIzC`Jblk%tGx$7ze2#vAtI z7+%L$)NrSOXCd6`#}e)JFlN%B6I?@`#v3pZ*CdlDj7_D7%3o56u^QKk2p(0(L?ApF zf+8&#Q|;HFK)M>3xVd)E%l$?he~zjsCWkNp`(`4tz+xq}3$=46f8uSLw?jsKBsQm= zWhrNE(pkHB6f-s=Pj3)% zVk7dPGh!pg1ShxKs?YBRKi<@70{aORI%W!P(^!NcLu8mS>D$(YEZL_X`C90VM?F}d zCiV|BNh~4@K$Dmm(4=+L8Wn0$AGIE`p898Cidia)9(~-XDG2VRh8s0&GaAkNzEOh_ zsYj2EXB6Ue!_Z@ zf6N4^B~JdQuBt^K)yBDF;LQ;gm(HDBHMuP{kE?gDbf-E8lbwT)s)y!Jt(t_whICa! zs;WI%)xLZ))jOQ*9Zrl~RH{C;VEe#b`qakN9OGA=oYV7rhaQ*IrAxLicc*q7Ozt?C z*nTK+=xkzSEHyHb9GOU*olHz#Pu!3b*JBBH{3El;<67lRg)U%(qOydm?!LY5iCA)9 ztVkr&p(@xLbGjE;wgR^*@v;G$!7jxcPPzOw{*7t`Fq6&o# zf4Az!BsS{)`)60F``^a~u@+x+5A5N;Rxx1VzFTu-yXif97n1KCXh-P}w~GTN%MZIu zM-EtixYvsEA0DuvR#O$(dVw*59$5OyQU#K!M^N)f3~@OU0skwC?e)dCI0BwZYK9o?c#4_V!ZhPbL> z7Ss+p_z*MtWKfLWo}S}PI=_O$%M*Vynz!A>oOWvtlzC`)6!NH>=|xB7KsW~F3M4kw z1%5jx4}SYF-Y3Tpsjhgop+7XKLiC%S4#n5;++t2E`40Mh7XPsUL8Kfc3*yiG zGRDyA!i|kI@NVP*>(Ow*SI2HNOp}c-PAbHN6rT1lH!Gg zaN(Et#S09azFpBL{ARleZfgro*#Qw%zLV9gZ@Q^(qgT$RjEm>thEn)Q`!oL<%+X9~-AOxj8P9r_Hm zMHy|K6aD?yG6Tr-sAgZ%? zrSYsx7M4vH{VvT`S4#7IfGv7}U+JVp5B!rYy8I=~OD)Y=d02a)Uv6J z>!A7Pu6B9uYWvA(p6T@!v}9kKZ{ZIl#RK1e?fr7)pkEPxD>=(B#5nR_{?G^bjiW%Al!a!7ylTI z*iMZh0im$an<}hJ7S<_+zPX{t&a#DDigU-@;A4k(VO(*v%nhVP*WJFi`W7vUShrOC zP;8t#`q<%FIQsTZ)CrC^&wkHkklpFy*Wk82Npa80s3PJM zdY(UBFOSxaV(d*1%eQG3I;+U@44ZW>4G>c6%)T6rSP&4i^&?~Bi`Gwq+@*sfhy#5m z6jDSSpjnDX;&-CKbFhVmucOp%tVzylmFR{3Sm=5<04sA){tiZAb0v<4Rt$#ktx0j~ zQhXUpwL2l~&d!#GqVUp7m7&B;kuzHqpNUQdNt4s&dnt9!KgX*K%3y-+h0K&={E}aL zX6MBwg~;HszKqO^R#7L%xn2-Z>QLWxL2s!;W)}Ty>t zC5(8V357FOO)VR^oyh|B|CAcR5oq$x8dl3=>>pM6!HUBV>(9}atrXdcG8;&q3yN5a zgDh4qZb?TMbez-ABg*WK zLE>AHvN;H3kTCn&eef8et(HGekpV@l=?Kgsadwh%;27*U7=SRcui9A}q-32|F=H4p zU@A5@YVQ;cwuhOtzDd|ZR z_oAY&I9*)5;9MM7vMbTs&$LLN|yXc;~>co5%%$NNpJP@A-X_IfZ-QW-u2C@HV@SO>wV z&;bSH72;6<{#wWdIUPjxCsdYD|3+=&$2J1&d>RM+jilSwJk?IMK&NnZA4JMY96wke zt>Bvs1t#%Lr{GZmUduQd`V3KO@I$$_$T5pP&oT49?;ARF7&)Mhgrg}_mE!phuh~|t z*pJ99*yn~ZGNa@gtdq7}HBSY@I=A$1sGc@O{NV>d%E8&0yb zpmR-3JtsF(kBS($v56aoNE8F>#2hQ-AE26TH=QydQyA87zIF5MX;Rd=DJq&f_SoC> zo#BVWJ&mW@_ z%@nwB`+?n;agtc{b?Gi#5g@^9_NL7g8b8Jd z8yMO`K#piR#va;C=Ux>px-I@_(R+(B(SyRduca$6zCP8*ID=A@89*TdPAz z5QG3Mo`Z;t^B_p@l9!obJa>oR8eZJ@$hn1#FbBv8lXr)S!)KHa>V1s)Iv>DmYITDlI0$B z4=>fNa}j!p%yDmE%jV3KvtBG-)>-h4OHjoj%G z(e~hR&&+b#>;NuCFlP5f6zN{|$9b{BB;TpJ4ScDDv19o4?1-lKmfgAjo zkdL(*ZyIqJ?==!SCIcfy_+P_1@_$DCY-u>+(^-1>Tg`+gNyKQi%I~9ALml%-sgAM0 z)Qskk2YyM@^~?ViHIOq7Q&Wa99#D$mn?FS=Mo6BWb&Ze0-m^<5`upTG>Yq@MZgNyWbgy%j9`6H0@OU0X3=+5q zwrDMg=okH>0=CcG!0BX%X)+Cxh?izURs?0fQkA2#HARg5LId0xOxLwo7D0XCW&C0FH z?HgTagaks~N9%$5| zo{}AKZtTc*kqK?8?o8cGJT%o2$IImpQ7lhWU9r*Fp!Ow3(LiPvlWGwdSmtVb1~*IS zuHHH7Oty@pnfV7NjR6D{55Rje6pO-#6Fw7%>2-A6WtAeD6q|xt04;;IJak7T2IqS) z4&+YhpVr=^I*iFo#t4$v@deqrX2}JeE_TBH6%vFpx;_*-|GT?haW~AJNDGB2p)x5{ zF19=n8dpsYOHI1CDplN;EN)xAkUah=<@hC~`0~6pT~w<1xTT7=-!HR-$k80lZdAS-kKqThUEFZbT|KEyxVTQX3v)&Y*d z3zTE?%VF|o&~j7e{QshnA*6ZvV)BwIUcSQ~8}Ja1FpV4>1S2rGh+Kvp!&1;sQPd29 zPRJ6g{I(J3GB#kMCBs6a^B8V!kRNJ<@Mp<7M&UGXfOGVoqJMp#A1gp+p}CfpEzz*b~4+TUXBZCAM5E7c?9qOdL)Yiq~aSFfu?RMHV;JoPZB1+2VvQS~eM|S#p8;vYSg%@LCXT(nK&e1P?Wp3|ga+jBv>0H~Gsn z=I3cRy1oP~PPf%maq#SE|G?QZX9kByHHXoRB*!o_o31T0dD3Hsq4r`|kTX+N1u9b= zvhjM=ihGoJfm)jnF-@3bZfsdsHZLdxZg>Z$aYw-DBlBQu7&bb<_#2?@ zC$7?z%a?TdG`}{d3pU2`txLC3EeDe=2k+fZ9XgvlbXGa^YU13u(sChHesNwTm2mYs zmGHgY_im;RO38zga&RY~if$dr-lYB5QMO)kwiZSd*sUsraCHsJqB;o!p20%=wZqsL|i@@ zNk^SV4|9&Gp4*54)_X85nxG2gM5>zN5nLUmb6m)7slZht6rK#;hF9n@Ok8!*QAb1l z5dR+(YQ_aOYK-$vm_Rc#5Ic63?z@C1Pq2aT%L&O`fNb_m)~ zEcX+;>+7z?JE{7;$@;xY{eH#W1C7U5!iyJEwVlb@PNlX>aX}U0`nq}HM9SNd^mZuT zR}}ZIw7vXKJk+W?S=+7D?o%rFuWq0oF_aJ6*lS;`9wAE${T>Ld-#d0q3h&UDLsQYe zLRr$&YewKI!dair2dbOHYAvmVu9|d`iW1O9nhfu?gs~CAD441Ew*)*78^P3 zL59jB3+4c}LC^KEp?ol8y)4CCV|zKwY%kV~?FCQy-(j|`Hr100xfvHTP2pqEK?AU3 zqY*}s7<9`2o(9MW4Q%VbhXTzo1Lu?s?ek}TGX(ylj0WjCMAfFr5i$Au)Jvod&d3dG zys0re|80zhYw{7f6s;I25Y2=JzM?)=(V491R4TgQoAS7{e#xekcEhhUYq2Tal4$Nu zH4i15hu;01(meL4bbP+xh1Qv`e}3tXQoiefeRp0HO%YdifnRm~12y=}N~xb6*@bV~ zl>2X^<#`VNVSps~k7@9)Jrd_5{~!JV9SAQ4|FWCb{|K6EMx$qxBD>>eFfNy%qw0z= z^}Y_(GNJ5^A)07N2BL}P$d%~9w)fYD>Ep@f!89RIM)iG2 z5Yo)|6DR9`*+SHO>)GW@?I&`a*QX*^15+_Z+f;#3xXtvly;vAS`DW#-h%c#H@~+Zj zWt8Qf7kmvQPYR4R{v*7OEb)sbru=?r*~{e+lk>MRf_q%-=SUjS5*E{HJmebc<;v`7W+PPrLkHJc zPmAQ4us_p|CL?b{PqvL$F#tc`n6)iE3;%2FcOfKw}KUfspSS8(kM0p)Xw*pd}FUU0j1pl911|0J&Vq zcl&myC2AS(V*VnUDZ|j)NbsFqXUgC<($&Db#n95^7)fm4cnBM;3t7D~(;E*m?E$xu z4mh9oC1(`Upn$)E4x%8R9wzrDr)0;^&t$)|;5n0xJ>Ll9=W=yH7?0#R1neEgYjrT4 zv;4nMqMC7$UbRG_PKJkX5I_3`c|8haF5Smh1?XervsE+r9iwP8LO+2-ie?hjeFT6# zzTw$M=$7dS`evZ`pnLbaB__x6=lG@%k{aAdH5 zwExIyqe69#oIN+H_mUscV5<}nxn!=3y5v`IpqrgIcwKzROSFrMYO(kyvkM7V!=^*t0c}2{hT6`zcy7MKZiB3S9HI zQ$7--8PG$!_knF2 zvsrbmOs!l=R324ag9-cKT4hHL&*?7LhFA(K?k*^>ZSMQFinOOHcG&3F1O_qZnBrnv|FEEkYke92 zF&A6wv>?8D`U|HQ${%IWuQsNMLHf@o(FR`fWCd^CLqA-1ZH7FL@q1=8UtECJepYZQ zbX69pKs0_;5O)$RRAQrF#-n<|Z>lL0lYImtBs|cse^EVDZ1}Z^0)X#XzjXHOsA>y_ z-w4UL&SnOy#ePXlu0e0PmLi&qTu%|~P@Hyye;=h73rJGF#s|Vzjmx=1O5O|XJ{!B^ z#4ggvU!k%;rs!J~eTSkyr|2&z`T<4nQS_G-k!_qANo6v5FeRGCVUkS4m_SA?87#}R z$c)Ocjn*u#_CD=b{|?G0P>Vk=YlFqi}S06fe&J}!6R+Sd4Z=U?Z$v20;Fr4tTBwCLv!UE5WaiJ#*79 zOg(ep=ZOB+Zsk4CO7Qb7&#LhA63^;zA0M|xP416Pxa>`x-K&&Z?OY11B z)jhM*1uTjKh;iTKZ&BP!`Oar{ateRufJ5;oUbtdD^TBcLnH%m(&wS)tLCw9Ncwwb| kwiVWrXB|**eNqCE>61D{&+5QPJ}Utg{&+XfSFxr3e>q`&cK`qY literal 0 HcmV?d00001 diff --git a/tools/pyghidra_crusader/cli.py b/tools/pyghidra_crusader/cli.py index 41f62fd..ed938fa 100644 --- a/tools/pyghidra_crusader/cli.py +++ b/tools/pyghidra_crusader/cli.py @@ -12,13 +12,30 @@ from .common import ( DEFAULT_FOLDER_PATH, ProjectConfig, create_function, + decompile_function, + disassemble_function, + format_function_summary, get_function, + get_function_containing, + get_functions_by_exact_name, + get_xrefs_from, + get_xrefs_to, + list_classes, + list_data_items, + list_exports, list_root_files, + list_imports, + list_namespaces, + list_segments, + list_strings, open_program, open_project, + read_region_bytes, remove_function, rename_function, + run_script_file, save_program, + search_functions_by_name, set_comment, transaction, ) @@ -58,6 +75,12 @@ def build_parser() -> argparse.ArgumentParser: action="store_true", help="Restore project tool state while opening the project.", ) + parser.add_argument( + "--format", + choices=["text", "json"], + default="text", + help="Output format.", + ) subparsers = parser.add_subparsers(dest="command", required=True) @@ -66,6 +89,13 @@ def build_parser() -> argparse.ArgumentParser: help="List root-level files in the Ghidra project.", ) + dump_parser = subparsers.add_parser( + "dump-region", + help="Dump instructions and resolved call targets for an address range.", + ) + dump_parser.add_argument("--start", required=True, help="Start address.") + dump_parser.add_argument("--end", required=True, help="Inclusive end address.") + create_parser = subparsers.add_parser( "create-function", help="Create a function at an address with an optional explicit body range.", @@ -92,6 +122,15 @@ def build_parser() -> argparse.ArgumentParser: rename_parser.add_argument("--entry", required=True, help="Function entry address.") rename_parser.add_argument("--name", required=True, help="New function name.") + rename_by_address_parser = subparsers.add_parser( + "rename-function-by-address", + help="Rename an existing function by entry address (MCP-style alias).", + ) + rename_by_address_parser.add_argument( + "--entry", required=True, help="Function entry address." + ) + rename_by_address_parser.add_argument("--name", required=True, help="New function name.") + comment_parser = subparsers.add_parser( "set-comment", help="Set a code-unit comment by address.", @@ -105,6 +144,161 @@ def build_parser() -> argparse.ArgumentParser: help="Comment type.", ) + decompiler_comment_parser = subparsers.add_parser( + "set-decompiler-comment", + help="Set a decompiler-visible pre-comment by address.", + ) + decompiler_comment_parser.add_argument("--address", required=True, help="Comment target address.") + decompiler_comment_parser.add_argument("--text", required=True, help="Comment text.") + + disassembly_comment_parser = subparsers.add_parser( + "set-disassembly-comment", + help="Set a disassembly EOL comment by address.", + ) + disassembly_comment_parser.add_argument("--address", required=True, help="Comment target address.") + disassembly_comment_parser.add_argument("--text", required=True, help="Comment text.") + + get_function_parser = subparsers.add_parser( + "get-function-by-address", + help="Show function metadata for an exact entry address.", + ) + get_function_parser.add_argument("--address", required=True, help="Function entry address.") + + get_function_containing_parser = subparsers.add_parser( + "get-function-containing", + help="Show function metadata for the function containing an address.", + ) + get_function_containing_parser.add_argument( + "--address", required=True, help="Address inside the desired function body." + ) + + list_functions_parser = subparsers.add_parser( + "list-functions", + help="List all defined functions.", + ) + list_functions_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") + list_functions_parser.add_argument("--limit", type=int, default=100, help="Maximum functions to print.") + + list_segments_parser = subparsers.add_parser( + "list-segments", + help="List memory segments or blocks.", + ) + list_segments_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") + list_segments_parser.add_argument("--limit", type=int, default=100, help="Maximum segments to print.") + + list_data_items_parser = subparsers.add_parser( + "list-data-items", + help="List defined data items.", + ) + list_data_items_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") + list_data_items_parser.add_argument("--limit", type=int, default=100, help="Maximum data items to print.") + + list_classes_parser = subparsers.add_parser( + "list-classes", + help="List class namespaces.", + ) + list_classes_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") + list_classes_parser.add_argument("--limit", type=int, default=100, help="Maximum classes to print.") + + list_strings_parser = subparsers.add_parser( + "list-strings", + help="List defined strings in the program.", + ) + list_strings_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") + list_strings_parser.add_argument("--limit", type=int, default=2000, help="Maximum strings to print.") + list_strings_parser.add_argument("--filter", help="Optional substring filter.") + + list_imports_parser = subparsers.add_parser( + "list-imports", + help="List imported external symbols.", + ) + list_imports_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") + list_imports_parser.add_argument("--limit", type=int, default=100, help="Maximum imports to print.") + + list_exports_parser = subparsers.add_parser( + "list-exports", + help="List exported entry points and symbols.", + ) + list_exports_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") + list_exports_parser.add_argument("--limit", type=int, default=100, help="Maximum exports to print.") + + list_namespaces_parser = subparsers.add_parser( + "list-namespaces", + help="List non-global namespaces, classes, and libraries.", + ) + list_namespaces_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") + list_namespaces_parser.add_argument("--limit", type=int, default=100, help="Maximum namespaces to print.") + + search_functions_parser = subparsers.add_parser( + "search-functions-by-name", + help="List functions whose names contain a substring.", + ) + search_functions_parser.add_argument("--query", required=True, help="Substring to match.") + search_functions_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") + search_functions_parser.add_argument("--limit", type=int, default=100, help="Maximum functions to print.") + + decompile_name_parser = subparsers.add_parser( + "decompile-function", + help="Decompile an exact-named function.", + ) + decompile_name_parser.add_argument("--name", required=True, help="Exact function name.") + decompile_name_parser.add_argument("--timeout", type=int, default=30, help="Decompile timeout in seconds.") + + decompile_address_parser = subparsers.add_parser( + "decompile-function-by-address", + help="Decompile a function by entry address.", + ) + decompile_address_parser.add_argument("--address", required=True, help="Function entry address.") + decompile_address_parser.add_argument("--timeout", type=int, default=30, help="Decompile timeout in seconds.") + + disassemble_parser = subparsers.add_parser( + "disassemble-function", + help="Disassemble a function body by entry address.", + ) + disassemble_parser.add_argument("--address", required=True, help="Function entry address.") + + read_region_parser = subparsers.add_parser( + "read-region", + help="Dump raw bytes for an inclusive address range.", + ) + read_region_parser.add_argument("--start", required=True, help="Start address.") + read_region_parser.add_argument("--end", required=True, help="Inclusive end address.") + + run_script_parser = subparsers.add_parser( + "run-script", + help="Execute a Python file with project/program context to avoid interactive shell quoting issues.", + ) + run_script_parser.add_argument("--script", required=True, help="Path to the Python script file.") + run_script_parser.add_argument( + "--read-only", + action="store_true", + help="Open the program read-only for script execution.", + ) + + xrefs_to_parser = subparsers.add_parser( + "get-xrefs-to", + help="List references to an address.", + ) + xrefs_to_parser.add_argument("--address", required=True, help="Target address.") + xrefs_to_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") + xrefs_to_parser.add_argument("--limit", type=int, default=100, help="Maximum references to print.") + + xrefs_from_parser = subparsers.add_parser( + "get-xrefs-from", + help="List references from an address.", + ) + xrefs_from_parser.add_argument("--address", required=True, help="Source address.") + xrefs_from_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") + xrefs_from_parser.add_argument("--limit", type=int, default=100, help="Maximum references to print.") + + function_xrefs_parser = subparsers.add_parser( + "get-function-xrefs", + help="List references to a function entry by exact function name.", + ) + function_xrefs_parser.add_argument("--name", required=True, help="Exact function name.") + function_xrefs_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") + function_xrefs_parser.add_argument("--limit", type=int, default=100, help="Maximum references to print.") + plan_parser = subparsers.add_parser( "apply-plan", help="Apply a JSON edit plan containing function and comment operations.", @@ -128,13 +322,85 @@ def build_config(args: argparse.Namespace) -> ProjectConfig: folder_path=args.folder_path, restore_project=args.restore_project, ) + + +def _emit(args: argparse.Namespace, payload, text: str | None = None) -> int: + if args.format == "json": + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + if text is not None: + print(text) + return 0 + if isinstance(payload, list): + for item in payload: + print(item) + return 0 + if isinstance(payload, dict): + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + print(payload) + return 0 + + +def _function_to_dict(function) -> dict[str, str]: + summary_text = format_function_summary(function) + lines = summary_text.splitlines() + body_line = lines[3].split(": ", 1)[1] + body_start, body_end = body_line.split(" - ", 1) + return { + "name": function.getName(), + "signature": lines[1].split(": ", 1)[1], + "entry": str(function.getEntryPoint()), + "body_start": body_start, + "body_end": body_end, + } + + +def _function_line(function) -> str: + return f"{function.getName()} @ {function.getEntryPoint()}" + + +def _text_or_empty(lines: list[str], empty_message: str) -> str: + return "\n".join(lines) if lines else empty_message + + def command_project_files(config: ProjectConfig, _args: argparse.Namespace) -> int: project = open_project(config) try: - for name in list_root_files(project): - print(name) + names = list_root_files(project) finally: project.close() + return _emit(_args, names, "\n".join(names)) + + +def command_dump_region(config: ProjectConfig, args: argparse.Namespace) -> int: + from .common import to_address + + with open_program(config, read_only=True) as (_project, program): + listing = program.getListing() + memory = program.getMemory() + start = to_address(program, args.start) + end = to_address(program, args.end) + size = end.subtract(start) + 1 + buf = bytearray(size) + memory.getBytes(start, buf) + print(f"REGION {args.start}..{args.end} BYTES {bytes(buf[:32]).hex()}") + + instruction = listing.getInstructionAt(start) + while instruction is not None and instruction.getAddress().compareTo(end) <= 0: + line = f"{instruction.getAddress()}: {instruction.toString()}" + if instruction.getFlowType().isCall(): + references = instruction.getReferencesFrom() + if references: + target = references[0].getToAddress() + function = program.getFunctionManager().getFunctionAt(target) + if function is not None: + line += f" -> {function.getName()} @ {target}" + else: + line += f" -> {target}" + print(line) + instruction = instruction.getNext() + return 0 @@ -145,8 +411,11 @@ def command_create_function(config: ProjectConfig, args: argparse.Namespace) -> if args.plate_comment: set_comment(program, args.entry, args.plate_comment, "plate") save_program(project, program) - print(f"created {function.getName()} at {args.entry}") - return 0 + return _emit( + args, + {"status": "ok", "entry": args.entry, "name": function.getName(), "action": "create-function"}, + f"created {function.getName()} at {args.entry}", + ) def command_delete_function(config: ProjectConfig, args: argparse.Namespace) -> int: @@ -156,8 +425,11 @@ def command_delete_function(config: ProjectConfig, args: argparse.Namespace) -> if not removed: raise RuntimeError(f"no function removed at {args.entry}") save_program(project, program) - print(f"deleted function at {args.entry}") - return 0 + return _emit( + args, + {"status": "ok", "entry": args.entry, "action": "delete-function"}, + f"deleted function at {args.entry}", + ) def command_rename_function(config: ProjectConfig, args: argparse.Namespace) -> int: @@ -165,17 +437,276 @@ def command_rename_function(config: ProjectConfig, args: argparse.Namespace) -> with transaction(program, f"Rename function {args.entry}"): function = rename_function(program, args.entry, args.name) save_program(project, program) - print(f"renamed {args.entry} to {function.getName()}") - return 0 + return _emit( + args, + {"status": "ok", "entry": args.entry, "name": function.getName(), "action": "rename-function"}, + f"renamed {args.entry} to {function.getName()}", + ) + + +def _set_comment_with_type(config: ProjectConfig, args: argparse.Namespace, address: str, text: str, comment_type: str) -> int: + with open_program(config, read_only=False) as (project, program): + with transaction(program, f"Set comment {address}"): + set_comment(program, address, text, comment_type) + save_program(project, program) + return _emit( + args, + {"status": "ok", "address": address, "type": comment_type, "text": text, "action": "set-comment"}, + f"set {comment_type} comment at {address}", + ) def command_set_comment(config: ProjectConfig, args: argparse.Namespace) -> int: - with open_program(config, read_only=False) as (project, program): - with transaction(program, f"Set comment {args.address}"): - set_comment(program, args.address, args.text, args.type) - save_program(project, program) - print(f"set {args.type} comment at {args.address}") - return 0 + return _set_comment_with_type(config, args, args.address, args.text, args.type) + + +def command_set_decompiler_comment(config: ProjectConfig, args: argparse.Namespace) -> int: + return _set_comment_with_type(config, args, args.address, args.text, "pre") + + +def command_set_disassembly_comment(config: ProjectConfig, args: argparse.Namespace) -> int: + return _set_comment_with_type(config, args, args.address, args.text, "eol") + + +def _require_function_by_address(program, address_text: str): + function = get_function(program, address_text) + if function is None: + raise RuntimeError(f"no function found at {address_text}") + return function + + +def _require_single_function_by_name(program, name: str): + matches = get_functions_by_exact_name(program, name) + if not matches: + raise RuntimeError(f"no function found with exact name '{name}'") + if len(matches) > 1: + raise RuntimeError( + f"multiple functions match exact name '{name}'; use search-functions-by-name or an address-specific command" + ) + return matches[0] + + +def _print_function_lines(functions) -> None: + for function in functions: + print(f"{function.getName()} @ {function.getEntryPoint()}") + + +def _print_reference_lines(references: list[dict[str, str | int]]) -> None: + for reference in references: + print( + f"{reference['from']} -> {reference['to']} [{reference['type']}] operand={reference['operand_index']}" + ) + + +def command_get_function_by_address(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + function = _require_function_by_address(program, args.address) + payload = _function_to_dict(function) + text = format_function_summary(function) + return _emit(args, payload, text) + + +def command_get_function_containing(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + function = get_function_containing(program, args.address) + if function is None: + raise RuntimeError(f"no containing function found at {args.address}") + payload = _function_to_dict(function) + text = format_function_summary(function) + return _emit(args, payload, text) + + +def command_list_functions(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + functions = search_functions_by_name(program, "", offset=args.offset, limit=args.limit) + payload = [{"name": function.getName(), "entry": str(function.getEntryPoint())} for function in functions] + text = _text_or_empty([_function_line(function) for function in functions], "no functions found") + return _emit(args, payload, text) + + +def command_search_functions_by_name(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + functions = search_functions_by_name(program, args.query, offset=args.offset, limit=args.limit) + payload = [{"name": function.getName(), "entry": str(function.getEntryPoint())} for function in functions] + text = _text_or_empty([_function_line(function) for function in functions], "no matching functions found") + return _emit(args, payload, text) + + +def command_list_strings(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + strings = list_strings(program, offset=args.offset, limit=args.limit, filter_text=args.filter) + text = _text_or_empty([f"{entry['address']}: {entry['text']}" for entry in strings], "no strings found") + return _emit(args, strings, text) + + +def command_list_segments(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + segments = list_segments(program, offset=args.offset, limit=args.limit) + text = _text_or_empty( + [ + f"{entry['name']} {entry['start']} - {entry['end']} len={entry['length']}" + f" r={entry['read']} w={entry['write']} x={entry['execute']} init={entry['initialized']}" + for entry in segments + ], + "no segments found", + ) + return _emit(args, segments, text) + + +def command_list_data_items(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + items = list_data_items(program, offset=args.offset, limit=args.limit) + text = _text_or_empty( + [ + f"{entry['address']} {entry['mnemonic']} len={entry['length']}" + + (f" value={entry['value']}" if entry['value'] is not None else "") + for entry in items + ], + "no data items found", + ) + return _emit(args, items, text) + + +def command_list_classes(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + classes = list_classes(program, offset=args.offset, limit=args.limit) + text = _text_or_empty( + [ + f"{entry['name']}" + (f" parent={entry['parent']}" if entry['parent'] else "") + for entry in classes + ], + "no classes found", + ) + return _emit(args, classes, text) + + +def command_list_imports(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + imports = list_imports(program, offset=args.offset, limit=args.limit) + text = _text_or_empty([ + f"{entry['library']}!{entry['label'] or ''} @ {entry['address'] or ''}" + for entry in imports + ], "no imports found") + return _emit(args, imports, text) + + +def command_list_exports(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + exports = list_exports(program, offset=args.offset, limit=args.limit) + text = _text_or_empty([ + f"{entry['name'] or ''} @ {entry['address']} [{entry['kind']}]" + for entry in exports + ], "no exports found") + return _emit(args, exports, text) + + +def command_list_namespaces(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + namespaces = list_namespaces(program, offset=args.offset, limit=args.limit) + text = _text_or_empty([ + f"{entry['name']} [{entry['type']}]" + (f" parent={entry['parent']}" if entry['parent'] else "") + for entry in namespaces + ], "no namespaces found") + return _emit(args, namespaces, text) + + +def command_decompile_function_by_address(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + function = _require_function_by_address(program, args.address) + output = decompile_function(program, function, args.timeout) + return _emit(args, {"address": args.address, "decompiled": output}, output) + + +def command_decompile_function(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + function = _require_single_function_by_name(program, args.name) + output = decompile_function(program, function, args.timeout) + return _emit(args, {"name": args.name, "decompiled": output}, output) + + +def command_disassemble_function(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + function = _require_function_by_address(program, args.address) + lines = disassemble_function(program, function) + if not lines: + code_unit = program.getListing().getCodeUnitAt(function.getEntryPoint()) + lines = [ + f"no instructions found in body {function.getBody().getMinAddress()} - {function.getBody().getMaxAddress()}; entry code unit = {code_unit}" + ] + return _emit(args, {"address": args.address, "lines": lines}, "\n".join(lines)) + + +def command_read_region(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + data = read_region_bytes(program, args.start, args.end) + text = f"REGION {args.start}..{args.end} BYTES {data.hex()}" + return _emit(args, {"start": args.start, "end": args.end, "bytes": data.hex()}, text) + + +def command_get_xrefs_to(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + references = get_xrefs_to(program, args.address, offset=args.offset, limit=args.limit) + text = _text_or_empty([ + f"{reference['from']} -> {reference['to']} [{reference['type']}] operand={reference['operand_index']}" + for reference in references + ], "no xrefs found") + return _emit(args, references, text) + + +def command_get_xrefs_from(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + references = get_xrefs_from(program, args.address, offset=args.offset, limit=args.limit) + text = _text_or_empty([ + f"{reference['from']} -> {reference['to']} [{reference['type']}] operand={reference['operand_index']}" + for reference in references + ], "no xrefs found") + return _emit(args, references, text) + + +def command_get_function_xrefs(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + function = _require_single_function_by_name(program, args.name) + references = get_xrefs_to( + program, + str(function.getEntryPoint()), + offset=args.offset, + limit=args.limit, + ) + text = _text_or_empty([ + f"{reference['from']} -> {reference['to']} [{reference['type']}] operand={reference['operand_index']}" + for reference in references + ], "no xrefs found") + return _emit(args, references, text) + + +def command_run_script(config: ProjectConfig, args: argparse.Namespace) -> int: + script_path = Path(args.script).resolve() + if not script_path.is_file(): + raise RuntimeError(f"script file not found: {script_path}") + + with open_program(config, read_only=args.read_only) as (project, program): + script_globals = { + "config": config, + "project": project, + "program": program, + "helpers": { + "create_function": create_function, + "decompile_function": decompile_function, + "disassemble_function": disassemble_function, + "format_function_summary": format_function_summary, + "get_function": get_function, + "get_function_containing": get_function_containing, + "get_xrefs_from": get_xrefs_from, + "get_xrefs_to": get_xrefs_to, + "read_region_bytes": read_region_bytes, + "rename_function": rename_function, + "set_comment": set_comment, + }, + } + run_script_file(script_path, script_globals) + if not args.read_only: + save_program(project, program) + return _emit(args, {"status": "ok", "script": str(script_path)}, f"ran script {script_path}") def _load_plan(plan_path: str) -> dict: @@ -190,6 +721,9 @@ def _print_plan(plan: dict) -> None: def command_apply_plan(config: ProjectConfig, args: argparse.Namespace) -> int: plan = _load_plan(args.plan) if args.dry_run: + if args.format == "json": + _print_plan(plan) + return 0 _print_plan(plan) return 0 @@ -234,8 +768,7 @@ def command_apply_plan(config: ProjectConfig, args: argparse.Namespace) -> int: save_program(project, program) - print(f"applied plan {args.plan}") - return 0 + return _emit(args, {"status": "ok", "plan": args.plan}, f"applied plan {args.plan}") def main(argv: list[str] | None = None) -> int: @@ -244,11 +777,34 @@ def main(argv: list[str] | None = None) -> int: config = build_config(args) command_map = { + "dump-region": command_dump_region, "project-files": command_project_files, "create-function": command_create_function, "delete-function": command_delete_function, "rename-function": command_rename_function, + "rename-function-by-address": command_rename_function, "set-comment": command_set_comment, + "set-decompiler-comment": command_set_decompiler_comment, + "set-disassembly-comment": command_set_disassembly_comment, + "get-function-by-address": command_get_function_by_address, + "get-function-containing": command_get_function_containing, + "list-functions": command_list_functions, + "list-segments": command_list_segments, + "list-data-items": command_list_data_items, + "list-classes": command_list_classes, + "list-strings": command_list_strings, + "list-imports": command_list_imports, + "list-exports": command_list_exports, + "list-namespaces": command_list_namespaces, + "search-functions-by-name": command_search_functions_by_name, + "decompile-function": command_decompile_function, + "decompile-function-by-address": command_decompile_function_by_address, + "disassemble-function": command_disassemble_function, + "read-region": command_read_region, + "get-xrefs-to": command_get_xrefs_to, + "get-xrefs-from": command_get_xrefs_from, + "get-function-xrefs": command_get_function_xrefs, + "run-script": command_run_script, "apply-plan": command_apply_plan, } return command_map[args.command](config, args) diff --git a/tools/pyghidra_crusader/common.py b/tools/pyghidra_crusader/common.py index 48cd972..54279b3 100644 --- a/tools/pyghidra_crusader/common.py +++ b/tools/pyghidra_crusader/common.py @@ -4,6 +4,7 @@ from contextlib import contextmanager from dataclasses import dataclass from pathlib import Path import os +import sys REPO_ROOT = Path(__file__).resolve().parents[2] @@ -31,10 +32,29 @@ def ensure_pyghidra_started(install_dir: Path | None = None): resolved_dir = Path(install_dir or DEFAULT_INSTALL_DIR) if not pyghidra.started(): - pyghidra.start(install_dir=resolved_dir) + with suppress_process_output(): + pyghidra.start(install_dir=resolved_dir) return pyghidra +@contextmanager +def suppress_process_output(): + with open(os.devnull, "w", encoding="utf-8") as devnull: + original_stdout = os.dup(1) + original_stderr = os.dup(2) + try: + sys.stdout.flush() + sys.stderr.flush() + os.dup2(devnull.fileno(), 1) + os.dup2(devnull.fileno(), 2) + yield + finally: + os.dup2(original_stdout, 1) + os.dup2(original_stderr, 2) + os.close(original_stdout) + os.close(original_stderr) + + def parse_address_text(address_text: str) -> int: text = address_text.strip() if ":" in text: @@ -48,6 +68,19 @@ def to_address(program, address_text: str): return address_space.getAddress(parse_address_text(address_text)) +def format_address(address) -> str: + return str(address) + + +def iter_java_items(items): + if hasattr(items, "hasNext") and hasattr(items, "next"): + while items.hasNext(): + yield items.next() + return + for item in items: + yield item + + def format_project_error(config: ProjectConfig, exc: Exception) -> RuntimeError: lock_path = config.project_dir / f"{config.project_name}.lock" details = [ @@ -127,6 +160,141 @@ def get_function(program, entry_text: str): return program.getFunctionManager().getFunctionAt(to_address(program, entry_text)) +def get_function_containing(program, address_text: str): + return program.getFunctionManager().getFunctionContaining(to_address(program, address_text)) + + +def read_region_bytes(program, start_text: str, end_text: str) -> bytes: + memory = program.getMemory() + start = to_address(program, start_text) + end = to_address(program, end_text) + size = end.subtract(start) + 1 + if size < 0: + raise ValueError(f"invalid address range: {start_text}..{end_text}") + + data = bytearray() + current = start + for _ in range(size): + data.append(int(memory.getByte(current)) & 0xFF) + current = current.next() + return bytes(data) + + +def iter_functions(program): + return program.getFunctionManager().getFunctions(True) + + +def function_signature(function) -> str: + return function.getPrototypeString(True, True) + + +def function_body_range(function) -> tuple[str, str]: + body = function.getBody() + return format_address(body.getMinAddress()), format_address(body.getMaxAddress()) + + +def format_function_summary(function) -> str: + body_start, body_end = function_body_range(function) + return ( + f"Function: {function.getName()} at {format_address(function.getEntryPoint())}\n" + f"Signature: {function_signature(function)}\n" + f"Entry: {format_address(function.getEntryPoint())}\n" + f"Body: {body_start} - {body_end}" + ) + + +def list_segments(program, offset: int = 0, limit: int = 100): + memory = program.getMemory() + matches = [] + skipped = 0 + for block in memory.getBlocks(): + if skipped < offset: + skipped += 1 + continue + matches.append( + { + "name": block.getName(), + "start": format_address(block.getStart()), + "end": format_address(block.getEnd()), + "length": int(block.getSize()), + "initialized": bool(block.isInitialized()), + "read": bool(block.isRead()), + "write": bool(block.isWrite()), + "execute": bool(block.isExecute()), + } + ) + if len(matches) >= limit: + break + return matches + + +def list_data_items(program, offset: int = 0, limit: int = 100): + listing = program.getListing() + matches = [] + skipped = 0 + for data in iter_java_items(listing.getDefinedData(True)): + if skipped < offset: + skipped += 1 + continue + value = data.getValue() + matches.append( + { + "address": format_address(data.getAddress()), + "length": int(data.getLength()), + "mnemonic": data.getMnemonicString(), + "value": None if value is None else str(value), + } + ) + if len(matches) >= limit: + break + return matches + + +def list_classes(program, offset: int = 0, limit: int = 100): + from ghidra.program.model.symbol import SymbolType + + symbol_table = program.getSymbolTable() + matches = [] + skipped = 0 + for symbol in iter_java_items(symbol_table.getDefinedSymbols()): + if symbol.getSymbolType() != SymbolType.CLASS: + continue + namespace = symbol.getObject() + parent = namespace.getParentNamespace() if namespace is not None else None + matches.append( + { + "name": symbol.getName(), + "parent": None if parent is None or parent.isGlobal() else parent.getName(), + } + ) + matches.sort(key=lambda entry: (entry["parent"] or "", entry["name"])) + return matches[offset: offset + limit] + + +def search_functions_by_name(program, query: str, offset: int = 0, limit: int = 100): + lowered = query.lower() + matches = [] + skipped = 0 + for function in iter_java_items(iter_functions(program)): + if lowered not in function.getName().lower(): + continue + if skipped < offset: + skipped += 1 + continue + matches.append(function) + if len(matches) >= limit: + break + return matches + + +def get_functions_by_exact_name(program, name: str): + matches = [] + for function in iter_java_items(iter_functions(program)): + if function.getName() == name: + matches.append(function) + return matches + + def create_function(program, entry_text: str, name: str, body_start: str | None, body_end: str | None): from ghidra.program.model.address import AddressSet from ghidra.program.model.symbol import SourceType @@ -157,6 +325,199 @@ def rename_function(program, entry_text: str, new_name: str): return function +def decompile_function(program, function, timeout_seconds: int = 30) -> str: + from ghidra.app.decompiler import DecompInterface + from ghidra.util.task import ConsoleTaskMonitor + + interface = DecompInterface() + interface.openProgram(program) + try: + result = interface.decompileFunction(function, timeout_seconds, ConsoleTaskMonitor()) + if not result.decompileCompleted(): + error_message = result.getErrorMessage() or "decompilation did not complete" + raise RuntimeError(error_message) + decompiled = result.getDecompiledFunction() + if decompiled is None: + raise RuntimeError("decompiler returned no function text") + return decompiled.getC() + finally: + interface.dispose() + + +def disassemble_function(program, function) -> list[str]: + from ghidra.program.model.listing import CodeUnit + + listing = program.getListing() + lines = [] + for instruction in iter_java_items(listing.getInstructions(function.getBody(), True)): + line = f"{format_address(instruction.getAddress())}: {instruction.toString()}" + if instruction.getFlowType().isCall(): + references = instruction.getReferencesFrom() + if references: + target = references[0].getToAddress() + target_function = program.getFunctionManager().getFunctionAt(target) + if target_function is not None: + line += f" -> {target_function.getName()} @ {format_address(target)}" + else: + line += f" -> {format_address(target)}" + comment = instruction.getComment(CodeUnit.EOL_COMMENT) + if comment: + line += f" ; {comment}" + lines.append(line) + return lines + + +def _reference_dict(reference) -> dict[str, str | int]: + return { + "from": format_address(reference.getFromAddress()), + "to": format_address(reference.getToAddress()), + "type": str(reference.getReferenceType()), + "operand_index": int(reference.getOperandIndex()), + } + + +def get_xrefs_to(program, address_text: str, offset: int = 0, limit: int = 100) -> list[dict[str, str | int]]: + reference_manager = program.getReferenceManager() + target_address = to_address(program, address_text) + results = [] + skipped = 0 + for reference in iter_java_items(reference_manager.getReferencesTo(target_address)): + if skipped < offset: + skipped += 1 + continue + results.append(_reference_dict(reference)) + if len(results) >= limit: + break + return results + + +def get_xrefs_from(program, address_text: str, offset: int = 0, limit: int = 100) -> list[dict[str, str | int]]: + reference_manager = program.getReferenceManager() + source_address = to_address(program, address_text) + results = [] + skipped = 0 + for reference in iter_java_items(reference_manager.getReferencesFrom(source_address)): + if skipped < offset: + skipped += 1 + continue + results.append(_reference_dict(reference)) + if len(results) >= limit: + break + return results + + +def list_strings(program, offset: int = 0, limit: int = 2000, filter_text: str | None = None): + listing = program.getListing() + matches = [] + skipped = 0 + lowered_filter = filter_text.lower() if filter_text else None + for data in iter_java_items(listing.getDefinedData(True)): + if not data.hasStringValue(): + continue + text = str(data.getValue()) + if lowered_filter and lowered_filter not in text.lower(): + continue + if skipped < offset: + skipped += 1 + continue + matches.append( + { + "address": format_address(data.getAddress()), + "length": int(data.getLength()), + "text": text, + } + ) + if len(matches) >= limit: + break + return matches + + +def list_imports(program, offset: int = 0, limit: int = 100): + external_manager = program.getExternalManager() + matches = [] + skipped = 0 + for library_name in external_manager.getExternalLibraryNames(): + for location in iter_java_items(external_manager.getExternalLocations(library_name)): + if skipped < offset: + skipped += 1 + continue + label = location.getLabel() + address = location.getAddress() + matches.append( + { + "library": str(library_name), + "label": str(label) if label is not None else None, + "address": format_address(address) if address is not None else None, + } + ) + if len(matches) >= limit: + return matches + return matches + + +def list_exports(program, offset: int = 0, limit: int = 100): + symbol_table = program.getSymbolTable() + function_manager = program.getFunctionManager() + matches = [] + skipped = 0 + for address in iter_java_items(symbol_table.getExternalEntryPointIterator()): + if skipped < offset: + skipped += 1 + continue + function = function_manager.getFunctionAt(address) + primary_symbol = symbol_table.getPrimarySymbol(address) + matches.append( + { + "address": format_address(address), + "name": function.getName() if function is not None else (primary_symbol.getName() if primary_symbol is not None else None), + "kind": "function" if function is not None else (str(primary_symbol.getSymbolType()) if primary_symbol is not None else "unknown"), + } + ) + if len(matches) >= limit: + break + return matches + + +def list_namespaces(program, offset: int = 0, limit: int = 100): + from ghidra.program.model.symbol import SymbolType + + symbol_table = program.getSymbolTable() + matches = [] + skipped = 0 + for symbol in iter_java_items(symbol_table.getDefinedSymbols()): + symbol_type = symbol.getSymbolType() + if symbol_type not in (SymbolType.NAMESPACE, SymbolType.CLASS, SymbolType.LIBRARY): + continue + namespace = symbol.getObject() + parent = namespace.getParentNamespace() if namespace is not None else None + if parent is not None and parent.isGlobal(): + parent_name = None + else: + parent_name = parent.getName() if parent is not None else None + if skipped < offset: + skipped += 1 + continue + matches.append( + { + "name": symbol.getName(), + "type": str(symbol_type), + "parent": parent_name, + } + ) + if len(matches) >= limit: + break + return matches + + +def run_script_file(script_path: Path, globals_dict: dict): + script_globals = dict(globals_dict) + script_globals.setdefault("__name__", "__main__") + script_globals.setdefault("__file__", str(script_path)) + code = compile(script_path.read_text(encoding="utf-8"), str(script_path), "exec") + exec(code, script_globals, script_globals) + return script_globals + + def set_comment(program, address_text: str, comment: str, comment_type: str): from ghidra.program.model.listing import CodeUnit @@ -171,9 +532,14 @@ def set_comment(program, address_text: str, comment: str, comment_type: str): raise ValueError(f"unsupported comment type: {comment_type}") listing = program.getListing() - code_unit = listing.getCodeUnitAt(to_address(program, address_text)) + target_address = to_address(program, address_text) + code_unit = listing.getCodeUnitAt(target_address) if code_unit is None: - raise ValueError(f"no code unit found at {address_text}") + function = program.getFunctionManager().getFunctionAt(target_address) + if function is not None: + function.setComment(comment) + return + raise ValueError(f"no code unit or function found at {address_text}") code_unit.setComment(comment_types[comment_type], comment)