Add various scripts and JSON plans for Ghidra project
- Introduced `seg043_boundary_repair.json` to manage function boundaries in segment 043. - Created `read_file.py` for reading and printing file content size. - Added `resolve_bb4f.py` to resolve specific function call targets. - Implemented `resolve_top_targets.py` to find resolved NE targets for top-called wrapper functions. - Added `script_contents.txt` to summarize NE relocation far calls. - Updated `tier4_ghidra.txt`, `tier4_ghidra_check.txt`, `tier4_output.txt`, and `tier4_result.txt` with function call statistics. - Created `tier5_errors.txt` for error logging and `tier5_output.txt` for additional function call statistics. - Established `tools` directory with helper scripts for the Ghidra project, including CLI and common functionalities. - Implemented command-line interface in `cli.py` for various project operations. - Added `common.py` for shared functions and configurations across tools. - Introduced `validate_fixups.py` to validate NE relocation fixups against known addresses.
This commit is contained in:
parent
6b9eb205d4
commit
24d4416003
36 changed files with 145712 additions and 14 deletions
1
tools/__init__.py
Normal file
1
tools/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Workspace helper packages."""
|
||||
BIN
tools/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
tools/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
5
tools/pyghidra_crusader/__init__.py
Normal file
5
tools/pyghidra_crusader/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
"""PyGhidra helpers for the Crusader Ghidra project."""
|
||||
|
||||
from .cli import main
|
||||
|
||||
__all__ = ["main"]
|
||||
5
tools/pyghidra_crusader/__main__.py
Normal file
5
tools/pyghidra_crusader/__main__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .cli import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
BIN
tools/pyghidra_crusader/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
tools/pyghidra_crusader/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
tools/pyghidra_crusader/__pycache__/__main__.cpython-311.pyc
Normal file
BIN
tools/pyghidra_crusader/__pycache__/__main__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
tools/pyghidra_crusader/__pycache__/cli.cpython-311.pyc
Normal file
BIN
tools/pyghidra_crusader/__pycache__/cli.cpython-311.pyc
Normal file
Binary file not shown.
BIN
tools/pyghidra_crusader/__pycache__/common.cpython-311.pyc
Normal file
BIN
tools/pyghidra_crusader/__pycache__/common.cpython-311.pyc
Normal file
Binary file not shown.
258
tools/pyghidra_crusader/cli.py
Normal file
258
tools/pyghidra_crusader/cli.py
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from .common import (
|
||||
DEFAULT_INSTALL_DIR,
|
||||
DEFAULT_PROJECT_DIR,
|
||||
DEFAULT_PROJECT_NAME,
|
||||
DEFAULT_PROGRAM_NAME,
|
||||
DEFAULT_FOLDER_PATH,
|
||||
ProjectConfig,
|
||||
create_function,
|
||||
get_function,
|
||||
list_root_files,
|
||||
open_program,
|
||||
open_project,
|
||||
remove_function,
|
||||
rename_function,
|
||||
save_program,
|
||||
set_comment,
|
||||
transaction,
|
||||
)
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="PyGhidra helpers for the Crusader project."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--install-dir",
|
||||
default=str(DEFAULT_INSTALL_DIR),
|
||||
help="Ghidra install directory.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--project-dir",
|
||||
default=str(DEFAULT_PROJECT_DIR),
|
||||
help="Directory containing the Ghidra project.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--project-name",
|
||||
default=DEFAULT_PROJECT_NAME,
|
||||
help="Ghidra project name.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--program-name",
|
||||
default=DEFAULT_PROGRAM_NAME,
|
||||
help="Program name inside the project.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--folder-path",
|
||||
default=DEFAULT_FOLDER_PATH,
|
||||
help="Project folder path containing the program.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--restore-project",
|
||||
action="store_true",
|
||||
help="Restore project tool state while opening the project.",
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
subparsers.add_parser(
|
||||
"project-files",
|
||||
help="List root-level files in the Ghidra project.",
|
||||
)
|
||||
|
||||
create_parser = subparsers.add_parser(
|
||||
"create-function",
|
||||
help="Create a function at an address with an optional explicit body range.",
|
||||
)
|
||||
create_parser.add_argument("--entry", required=True, help="Function entry address.")
|
||||
create_parser.add_argument("--name", required=True, help="New function name.")
|
||||
create_parser.add_argument("--body-start", help="Function body start address.")
|
||||
create_parser.add_argument("--body-end", help="Function body end address.")
|
||||
create_parser.add_argument(
|
||||
"--plate-comment",
|
||||
help="Optional plate comment to set at the entry address after creation.",
|
||||
)
|
||||
|
||||
delete_parser = subparsers.add_parser(
|
||||
"delete-function",
|
||||
help="Delete a function at an address.",
|
||||
)
|
||||
delete_parser.add_argument("--entry", required=True, help="Function entry address.")
|
||||
|
||||
rename_parser = subparsers.add_parser(
|
||||
"rename-function",
|
||||
help="Rename an existing function by entry address.",
|
||||
)
|
||||
rename_parser.add_argument("--entry", required=True, help="Function entry address.")
|
||||
rename_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.",
|
||||
)
|
||||
comment_parser.add_argument("--address", required=True, help="Comment target address.")
|
||||
comment_parser.add_argument("--text", required=True, help="Comment text.")
|
||||
comment_parser.add_argument(
|
||||
"--type",
|
||||
choices=["pre", "plate", "eol", "repeatable", "post"],
|
||||
default="plate",
|
||||
help="Comment type.",
|
||||
)
|
||||
|
||||
plan_parser = subparsers.add_parser(
|
||||
"apply-plan",
|
||||
help="Apply a JSON edit plan containing function and comment operations.",
|
||||
)
|
||||
plan_parser.add_argument("--plan", required=True, help="Path to the JSON plan file.")
|
||||
plan_parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Validate and print the plan without modifying the project.",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def build_config(args: argparse.Namespace) -> ProjectConfig:
|
||||
return ProjectConfig(
|
||||
install_dir=Path(args.install_dir),
|
||||
project_dir=Path(args.project_dir),
|
||||
project_name=args.project_name,
|
||||
program_name=args.program_name,
|
||||
folder_path=args.folder_path,
|
||||
restore_project=args.restore_project,
|
||||
)
|
||||
def command_project_files(config: ProjectConfig, _args: argparse.Namespace) -> int:
|
||||
project = open_project(config)
|
||||
try:
|
||||
for name in list_root_files(project):
|
||||
print(name)
|
||||
finally:
|
||||
project.close()
|
||||
return 0
|
||||
|
||||
|
||||
def command_create_function(config: ProjectConfig, args: argparse.Namespace) -> int:
|
||||
with open_program(config, read_only=False) as (project, program):
|
||||
with transaction(program, f"Create function {args.entry}"):
|
||||
function = create_function(program, args.entry, args.name, args.body_start, args.body_end)
|
||||
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
|
||||
|
||||
|
||||
def command_delete_function(config: ProjectConfig, args: argparse.Namespace) -> int:
|
||||
with open_program(config, read_only=False) as (project, program):
|
||||
with transaction(program, f"Delete function {args.entry}"):
|
||||
removed = remove_function(program, args.entry)
|
||||
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
|
||||
|
||||
|
||||
def command_rename_function(config: ProjectConfig, args: argparse.Namespace) -> int:
|
||||
with open_program(config, read_only=False) as (project, program):
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _load_plan(plan_path: str) -> dict:
|
||||
with open(plan_path, "r", encoding="utf-8") as handle:
|
||||
return json.load(handle)
|
||||
|
||||
|
||||
def _print_plan(plan: dict) -> None:
|
||||
print(json.dumps(plan, indent=2, sort_keys=True))
|
||||
|
||||
|
||||
def command_apply_plan(config: ProjectConfig, args: argparse.Namespace) -> int:
|
||||
plan = _load_plan(args.plan)
|
||||
if args.dry_run:
|
||||
_print_plan(plan)
|
||||
return 0
|
||||
|
||||
transaction_name = plan.get("transaction", f"Apply plan {args.plan}")
|
||||
with open_program(config, read_only=False) as (project, program):
|
||||
with transaction(program, transaction_name):
|
||||
for entry in plan.get("remove_functions", []):
|
||||
removed = remove_function(program, entry)
|
||||
if not removed:
|
||||
raise RuntimeError(f"no function removed at {entry}")
|
||||
|
||||
for entry in plan.get("rename_functions", []):
|
||||
rename_function(program, entry["entry"], entry["name"])
|
||||
|
||||
for entry in plan.get("create_functions", []):
|
||||
create_function(
|
||||
program,
|
||||
entry["entry"],
|
||||
entry["name"],
|
||||
entry.get("body_start"),
|
||||
entry.get("body_end"),
|
||||
)
|
||||
if entry.get("comment"):
|
||||
set_comment(
|
||||
program,
|
||||
entry["entry"],
|
||||
entry["comment"],
|
||||
entry.get("comment_type", "plate"),
|
||||
)
|
||||
|
||||
for entry in plan.get("comments", []):
|
||||
set_comment(
|
||||
program,
|
||||
entry["address"],
|
||||
entry["text"],
|
||||
entry.get("type", "plate"),
|
||||
)
|
||||
|
||||
for entry in plan.get("assert_functions", []):
|
||||
if get_function(program, entry) is None:
|
||||
raise RuntimeError(f"expected function missing at {entry}")
|
||||
|
||||
save_program(project, program)
|
||||
|
||||
print(f"applied plan {args.plan}")
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
config = build_config(args)
|
||||
|
||||
command_map = {
|
||||
"project-files": command_project_files,
|
||||
"create-function": command_create_function,
|
||||
"delete-function": command_delete_function,
|
||||
"rename-function": command_rename_function,
|
||||
"set-comment": command_set_comment,
|
||||
"apply-plan": command_apply_plan,
|
||||
}
|
||||
return command_map[args.command](config, args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
181
tools/pyghidra_crusader/common.py
Normal file
181
tools/pyghidra_crusader/common.py
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
|
||||
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")
|
||||
)
|
||||
DEFAULT_PROJECT_DIR = REPO_ROOT
|
||||
DEFAULT_PROJECT_NAME = "Crusader"
|
||||
DEFAULT_PROGRAM_NAME = "CRUSADER-RAW.EXE"
|
||||
DEFAULT_FOLDER_PATH = "/"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProjectConfig:
|
||||
install_dir: Path = DEFAULT_INSTALL_DIR
|
||||
project_dir: Path = DEFAULT_PROJECT_DIR
|
||||
project_name: str = DEFAULT_PROJECT_NAME
|
||||
program_name: str = DEFAULT_PROGRAM_NAME
|
||||
folder_path: str = DEFAULT_FOLDER_PATH
|
||||
restore_project: bool = False
|
||||
|
||||
|
||||
def ensure_pyghidra_started(install_dir: Path | None = None):
|
||||
import pyghidra
|
||||
|
||||
resolved_dir = Path(install_dir or DEFAULT_INSTALL_DIR)
|
||||
if not pyghidra.started():
|
||||
pyghidra.start(install_dir=resolved_dir)
|
||||
return pyghidra
|
||||
|
||||
|
||||
def parse_address_text(address_text: str) -> int:
|
||||
text = address_text.strip()
|
||||
if ":" in text:
|
||||
segment_text, offset_text = text.split(":", 1)
|
||||
return (int(segment_text, 16) << 16) + int(offset_text, 16)
|
||||
return int(text, 0)
|
||||
|
||||
|
||||
def to_address(program, address_text: str):
|
||||
address_space = program.getAddressFactory().getDefaultAddressSpace()
|
||||
return address_space.getAddress(parse_address_text(address_text))
|
||||
|
||||
|
||||
def format_project_error(config: ProjectConfig, exc: Exception) -> RuntimeError:
|
||||
lock_path = config.project_dir / f"{config.project_name}.lock"
|
||||
details = [
|
||||
f"unable to open project '{config.project_name}' in '{config.project_dir}'",
|
||||
str(exc),
|
||||
]
|
||||
if lock_path.exists():
|
||||
details.append(
|
||||
f"project lock present at '{lock_path}'; close Ghidra or work on a project copy for write operations"
|
||||
)
|
||||
return RuntimeError("; ".join(details))
|
||||
|
||||
|
||||
def open_project(config: ProjectConfig):
|
||||
ensure_pyghidra_started(config.install_dir)
|
||||
|
||||
from ghidra.base.project import GhidraProject
|
||||
|
||||
try:
|
||||
return GhidraProject.openProject(
|
||||
str(config.project_dir),
|
||||
config.project_name,
|
||||
config.restore_project,
|
||||
)
|
||||
except Exception as exc: # pragma: no cover - depends on local Ghidra state
|
||||
raise format_project_error(config, exc) from exc
|
||||
|
||||
|
||||
def _candidate_folder_paths(folder_path: str) -> list[str]:
|
||||
candidates = [folder_path]
|
||||
for fallback in ("/", "\\", ""):
|
||||
if fallback not in candidates:
|
||||
candidates.append(fallback)
|
||||
return candidates
|
||||
|
||||
|
||||
@contextmanager
|
||||
def open_program(config: ProjectConfig, read_only: bool):
|
||||
project = open_project(config)
|
||||
program = None
|
||||
last_error = None
|
||||
try:
|
||||
for folder_path in _candidate_folder_paths(config.folder_path):
|
||||
try:
|
||||
program = project.openProgram(folder_path, config.program_name, read_only)
|
||||
break
|
||||
except Exception as exc: # pragma: no cover - depends on local Ghidra state
|
||||
last_error = exc
|
||||
if program is None:
|
||||
raise RuntimeError(
|
||||
f"unable to open program '{config.program_name}' from project '{config.project_name}': {last_error}"
|
||||
)
|
||||
yield project, program
|
||||
finally:
|
||||
if project is not None:
|
||||
if program is not None:
|
||||
project.close(program)
|
||||
project.close()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def transaction(program, description: str):
|
||||
transaction_id = program.startTransaction(description)
|
||||
commit = False
|
||||
try:
|
||||
yield
|
||||
commit = True
|
||||
finally:
|
||||
program.endTransaction(transaction_id, commit)
|
||||
|
||||
|
||||
def list_root_files(project) -> list[str]:
|
||||
return [domain_file.getName() for domain_file in project.getRootFolder().getFiles()]
|
||||
|
||||
|
||||
def get_function(program, entry_text: str):
|
||||
return program.getFunctionManager().getFunctionAt(to_address(program, entry_text))
|
||||
|
||||
|
||||
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
|
||||
|
||||
entry_address = to_address(program, entry_text)
|
||||
body_start_address = to_address(program, body_start or entry_text)
|
||||
body_end_address = to_address(program, body_end or entry_text)
|
||||
body = AddressSet(body_start_address, body_end_address)
|
||||
return program.getFunctionManager().createFunction(
|
||||
name,
|
||||
entry_address,
|
||||
body,
|
||||
SourceType.USER_DEFINED,
|
||||
)
|
||||
|
||||
|
||||
def remove_function(program, entry_text: str) -> bool:
|
||||
return bool(program.getFunctionManager().removeFunction(to_address(program, entry_text)))
|
||||
|
||||
|
||||
def rename_function(program, entry_text: str, new_name: str):
|
||||
from ghidra.program.model.symbol import SourceType
|
||||
|
||||
function = get_function(program, entry_text)
|
||||
if function is None:
|
||||
raise ValueError(f"no function found at {entry_text}")
|
||||
function.setName(new_name, SourceType.USER_DEFINED)
|
||||
return function
|
||||
|
||||
|
||||
def set_comment(program, address_text: str, comment: str, comment_type: str):
|
||||
from ghidra.program.model.listing import CodeUnit
|
||||
|
||||
comment_types = {
|
||||
"pre": CodeUnit.PRE_COMMENT,
|
||||
"plate": CodeUnit.PLATE_COMMENT,
|
||||
"eol": CodeUnit.EOL_COMMENT,
|
||||
"repeatable": CodeUnit.REPEATABLE_COMMENT,
|
||||
"post": CodeUnit.POST_COMMENT,
|
||||
}
|
||||
if comment_type not in comment_types:
|
||||
raise ValueError(f"unsupported comment type: {comment_type}")
|
||||
|
||||
listing = program.getListing()
|
||||
code_unit = listing.getCodeUnitAt(to_address(program, address_text))
|
||||
if code_unit is None:
|
||||
raise ValueError(f"no code unit found at {address_text}")
|
||||
code_unit.setComment(comment_types[comment_type], comment)
|
||||
|
||||
|
||||
def save_program(project, program):
|
||||
project.save(program)
|
||||
Loading…
Add table
Add a link
Reference in a new issue