Add PyGhidra Crusader Toolkit and patch scripts
- Introduced README.md for the PyGhidra Crusader Toolkit, detailing setup and usage instructions. - Added bootstrap_env.ps1 script to create and refresh the Python virtual environment with necessary packages. - Implemented _tmp_patch_hidden_cheat_menu.py and _tmp_patch_hidden_cheat_menu_deferred.py scripts for patching specific memory addresses in Ghidra.
This commit is contained in:
parent
fafd849beb
commit
ad6ebd0b86
132 changed files with 41758 additions and 99 deletions
42
tools/pyghidra_crusader/README.md
Normal file
42
tools/pyghidra_crusader/README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# PyGhidra Crusader Toolkit
|
||||
|
||||
This toolkit drives the local PyGhidra fallback workflow for the Crusader Ghidra project.
|
||||
|
||||
## Defaults
|
||||
|
||||
- Ghidra install dir: `I:\Apps\ghidra_12.0.4_PUBLIC`
|
||||
- Python env: `.venv-pyghidra311`
|
||||
- Python base interpreter: `C:\Users\Maddo\.pyenv\pyenv-win\versions\3.11.6\python.exe`
|
||||
- CLI entrypoint: `.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader`
|
||||
|
||||
## Bootstrap Or Refresh
|
||||
|
||||
Run this from the repo root to create or refresh the local environment against the bundled Ghidra 12.0.4 offline packages:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\tools\pyghidra_crusader\bootstrap_env.ps1
|
||||
```
|
||||
|
||||
The script recreates or refreshes `.venv-pyghidra311`, then installs these pinned packages from the local Ghidra tree:
|
||||
|
||||
- `pyghidra==3.0.2` from `Ghidra\Features\PyGhidra\pypkg\dist`
|
||||
- `ghidra-stubs==12.0.4` from `docs\ghidra_stubs`
|
||||
|
||||
Override the defaults when needed:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\tools\pyghidra_crusader\bootstrap_env.ps1 `
|
||||
-PythonExe C:\Path\To\python.exe `
|
||||
-GhidraInstallDir I:\Apps\ghidra_12.0.4_PUBLIC `
|
||||
-VenvPath .\.venv-pyghidra311
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
After bootstrap, validate package versions and the project-open path:
|
||||
|
||||
```powershell
|
||||
.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader project-files
|
||||
```
|
||||
|
||||
If you need to target a different installed Ghidra tree temporarily, set `GHIDRA_INSTALL_DIR` or pass `--install-dir` to the CLI.
|
||||
68
tools/pyghidra_crusader/_tmp_patch_hidden_cheat_menu.py
Normal file
68
tools/pyghidra_crusader/_tmp_patch_hidden_cheat_menu.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
from ghidra.app.cmd.disassemble import DisassembleCommand
|
||||
|
||||
|
||||
space = program.getAddressFactory().getDefaultAddressSpace()
|
||||
listing = program.getListing()
|
||||
memory = program.getMemory()
|
||||
|
||||
|
||||
def to_addr(address_text: str):
|
||||
segment_text, offset_text = address_text.split(":", 1)
|
||||
offset = (int(segment_text, 16) << 16) + int(offset_text, 16)
|
||||
return space.getAddress(offset)
|
||||
|
||||
|
||||
def read_bytes(address_text: str, length: int) -> bytes:
|
||||
start = to_addr(address_text)
|
||||
return bytes((int(memory.getByte(start.add(index))) & 0xFF) for index in range(length))
|
||||
|
||||
|
||||
def write_bytes(address_text: str, expected: bytes, new_bytes: bytes) -> None:
|
||||
if len(expected) != len(new_bytes):
|
||||
raise RuntimeError(f"length mismatch at {address_text}: {len(expected)} != {len(new_bytes)}")
|
||||
|
||||
start = to_addr(address_text)
|
||||
end = start.add(len(expected) - 1)
|
||||
actual = read_bytes(address_text, len(expected))
|
||||
if actual != expected:
|
||||
raise RuntimeError(
|
||||
f"unexpected original bytes at {address_text}: got {actual.hex(' ')}, expected {expected.hex(' ')}"
|
||||
)
|
||||
|
||||
listing.clearCodeUnits(start, end, False)
|
||||
for index, value in enumerate(new_bytes):
|
||||
signed_value = value - 256 if value > 127 else value
|
||||
memory.setByte(start.add(index), signed_value)
|
||||
|
||||
if not DisassembleCommand(start, None, True).applyTo(program):
|
||||
raise RuntimeError(f"disassembly failed at {address_text}")
|
||||
|
||||
written = read_bytes(address_text, len(new_bytes))
|
||||
if written != new_bytes:
|
||||
raise RuntimeError(
|
||||
f"verification mismatch at {address_text}: got {written.hex(' ')}, expected {new_bytes.hex(' ')}"
|
||||
)
|
||||
|
||||
print(f"patched {address_text}: {actual.hex(' ')} -> {written.hex(' ')}")
|
||||
|
||||
|
||||
def print_instructions(address_text: str, count: int) -> None:
|
||||
instruction = listing.getInstructionAt(to_addr(address_text))
|
||||
print(f"instructions from {address_text}:")
|
||||
for _ in range(count):
|
||||
if instruction is None:
|
||||
break
|
||||
print(f" {instruction.getAddress()}: {instruction}")
|
||||
instruction = instruction.getNext()
|
||||
|
||||
|
||||
tx = program.startTransaction("Patch hidden cheat menu")
|
||||
commit = False
|
||||
try:
|
||||
write_bytes("1130:2b78", bytes.fromhex("9A 76 04 D8 12"), bytes.fromhex("9A 86 00 A0 13"))
|
||||
write_bytes("13a0:008d", bytes.fromhex("6A 01 FF 76 08 FF 76 06"), bytes.fromhex("6A 01 6A 00 6A 00 90 90"))
|
||||
print_instructions("1130:2b75", 4)
|
||||
print_instructions("13a0:008d", 10)
|
||||
commit = True
|
||||
finally:
|
||||
program.endTransaction(tx, commit)
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
from ghidra.app.cmd.disassemble import DisassembleCommand
|
||||
|
||||
|
||||
space = program.getAddressFactory().getDefaultAddressSpace()
|
||||
listing = program.getListing()
|
||||
memory = program.getMemory()
|
||||
|
||||
|
||||
def to_addr(address_text: str):
|
||||
segment_text, offset_text = address_text.split(":", 1)
|
||||
offset = (int(segment_text, 16) << 16) + int(offset_text, 16)
|
||||
return space.getAddress(offset)
|
||||
|
||||
|
||||
def read_bytes(address_text: str, length: int) -> bytes:
|
||||
start = to_addr(address_text)
|
||||
return bytes((int(memory.getByte(start.add(index))) & 0xFF) for index in range(length))
|
||||
|
||||
|
||||
def write_bytes(address_text: str, expected: bytes, new_bytes: bytes) -> None:
|
||||
if len(expected) != len(new_bytes):
|
||||
raise RuntimeError(f"length mismatch at {address_text}: {len(expected)} != {len(new_bytes)}")
|
||||
|
||||
start = to_addr(address_text)
|
||||
end = start.add(len(expected) - 1)
|
||||
actual = read_bytes(address_text, len(expected))
|
||||
if actual != expected:
|
||||
raise RuntimeError(
|
||||
f"unexpected original bytes at {address_text}: got {actual.hex(' ')}, expected {expected.hex(' ')}"
|
||||
)
|
||||
|
||||
listing.clearCodeUnits(start, end, False)
|
||||
for index, value in enumerate(new_bytes):
|
||||
signed_value = value - 256 if value > 127 else value
|
||||
memory.setByte(start.add(index), signed_value)
|
||||
|
||||
if not DisassembleCommand(start, None, True).applyTo(program):
|
||||
raise RuntimeError(f"disassembly failed at {address_text}")
|
||||
|
||||
written = read_bytes(address_text, len(new_bytes))
|
||||
if written != new_bytes:
|
||||
raise RuntimeError(
|
||||
f"verification mismatch at {address_text}: got {written.hex(' ')}, expected {new_bytes.hex(' ')}"
|
||||
)
|
||||
|
||||
print(f"patched {address_text}: {actual.hex(' ')} -> {written.hex(' ')}")
|
||||
|
||||
|
||||
def print_instructions(address_text: str, count: int) -> None:
|
||||
instruction = listing.getInstructionAt(to_addr(address_text))
|
||||
print(f"instructions from {address_text}:")
|
||||
for _ in range(count):
|
||||
if instruction is None:
|
||||
break
|
||||
print(f" {instruction.getAddress()}: {instruction}")
|
||||
instruction = instruction.getNext()
|
||||
|
||||
|
||||
tx = program.startTransaction("Patch hidden cheat menu via deferred 0x103 lane")
|
||||
commit = False
|
||||
try:
|
||||
write_bytes("1130:2b78", bytes.fromhex("9A 86 00 A0 13"), bytes.fromhex("9A 76 04 D8 12"))
|
||||
write_bytes("13a0:008d", bytes.fromhex("6A 01 6A 00 6A 00 90 90"), bytes.fromhex("6A 01 FF 76 08 FF 76 06"))
|
||||
write_bytes("13e8:25e0", bytes.fromhex("9A 76 04 D8 12"), bytes.fromhex("9A 0D 02 A0 13"))
|
||||
write_bytes("13a0:024a", bytes.fromhex("FF 76 08 FF 76 06"), bytes.fromhex("6A 00 6A 00 90 90"))
|
||||
print_instructions("1130:2b75", 4)
|
||||
print_instructions("13a0:008d", 8)
|
||||
print_instructions("13e8:25dd", 5)
|
||||
print_instructions("13a0:0244", 8)
|
||||
commit = True
|
||||
finally:
|
||||
program.endTransaction(tx, commit)
|
||||
39
tools/pyghidra_crusader/bootstrap_env.ps1
Normal file
39
tools/pyghidra_crusader/bootstrap_env.ps1
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
param(
|
||||
[string]$PythonExe = "C:\Users\Maddo\.pyenv\pyenv-win\versions\3.11.6\python.exe",
|
||||
[string]$GhidraInstallDir = "I:\Apps\ghidra_12.0.4_PUBLIC",
|
||||
[string]$VenvPath = (Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) ".venv-pyghidra311")
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$resolvedPython = (Resolve-Path $PythonExe).Path
|
||||
$resolvedGhidra = (Resolve-Path $GhidraInstallDir).Path
|
||||
|
||||
$pyghidraDist = Join-Path $resolvedGhidra "Ghidra\Features\PyGhidra\pypkg\dist"
|
||||
$ghidraStubsDist = Join-Path $resolvedGhidra "docs\ghidra_stubs"
|
||||
|
||||
if (-not (Test-Path $pyghidraDist)) {
|
||||
throw "Missing PyGhidra wheel directory: $pyghidraDist"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $ghidraStubsDist)) {
|
||||
throw "Missing Ghidra stubs wheel directory: $ghidraStubsDist"
|
||||
}
|
||||
|
||||
& $resolvedPython -m venv $VenvPath
|
||||
|
||||
$venvPython = Join-Path $VenvPath "Scripts\python.exe"
|
||||
if (-not (Test-Path $venvPython)) {
|
||||
throw "Virtual environment python not found at $venvPython"
|
||||
}
|
||||
|
||||
& $venvPython -m pip install --upgrade --force-reinstall --no-index --find-links $pyghidraDist --find-links $ghidraStubsDist pyghidra==3.0.2 ghidra-stubs==12.0.4
|
||||
|
||||
$versionScript = @'
|
||||
from importlib.metadata import version
|
||||
|
||||
print(f"pyghidra={version('pyghidra')}")
|
||||
print(f"ghidra-stubs={version('ghidra-stubs')}")
|
||||
'@
|
||||
|
||||
& $venvPython -c $versionScript
|
||||
|
|
@ -9,7 +9,7 @@ import sys
|
|||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
DEFAULT_INSTALL_DIR = Path(
|
||||
os.environ.get("GHIDRA_INSTALL_DIR", r"I:\Apps\ghidra_11.3.2_PUBLIC")
|
||||
os.environ.get("GHIDRA_INSTALL_DIR", r"I:\Apps\ghidra_12.0.4_PUBLIC")
|
||||
)
|
||||
DEFAULT_PROJECT_DIR = REPO_ROOT
|
||||
DEFAULT_PROJECT_NAME = "Crusader"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue