pyhidra -> pyghidra

This commit is contained in:
clearbluejar
2025-02-08 15:48:30 -07:00
parent 61e1ffa6ed
commit c17af8acc2
13 changed files with 74 additions and 67 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
{
"name": "ghidriff",
// image from https://github.com/clearbluejar/ghidra-python
"image": "ghcr.io/clearbluejar/ghidra-python:11.2.1ghidra3.12python-bookworm",
"image": "ghcr.io/clearbluejar/ghidra-python:11.3ghidra3.12python-bookworm",
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
+2 -2
View File
@@ -24,8 +24,8 @@ fi
# install local workspace and test requirements
pip install -e ".[testing]"
# initialize pyhidra
python -m pyhidra.install_plugins
# initialize pyghidra
python -m pyghidra.install_plugins
# git clone test data if dir doesn't exist
TEST_DATA_PATH="tests/data"
@@ -22,6 +22,8 @@ jobs:
# cover the latest and all versions of all subreleases
image: [
"latest",
"11.3ghidra3.12python-bookworm",
"11.2.1ghidra3.10python-bookworm",
"11.1.2ghidra3.12python-bookworm",
"11.0.3ghidra3.11python-bookworm",
"10.4ghidra3.11python-bookworm",
@@ -57,7 +59,7 @@ jobs:
pip install "ghidriff[testing]"
pip list
# install plugins before use
python -m pyhidra.install_plugins
python -m pyghidra.install_plugins
# download data to shared test data
if [ ! -d "tests/data" ]; then git clone https://github.com/clearbluejar/ghidriff-test-data.git tests/data; fi
pytest -rA -n auto
@@ -26,6 +26,8 @@ jobs:
# cover the latest and all versions of all subreleases
image: [
"latest",
"11.3ghidra3.12python-bookworm",
"11.2.1ghidra3.10python-bookworm"
"11.1.1ghidra3.12python-bookworm",
"11.0.3ghidra3.11python-bookworm",
"10.4ghidra3.11python-bookworm",
@@ -60,7 +62,7 @@ jobs:
# install package and testing
pip install -e ".[testing]"
# install plugins before use
python -m pyhidra.install_plugins
python -m pyghidra.install_plugins
# download data to shared test data
if [ ! -d "tests/data" ]; then git clone https://github.com/clearbluejar/ghidriff-test-data.git tests/data; fi
pytest -rA -n auto
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
# install package and testing
pip install -e ".[testing]"
# install plugins before use
python -m pyhidra.install_plugins
python -m pyghidra.install_plugins
# download data to shared test data
if [ ! -d "tests/data" ]; then git clone https://github.com/clearbluejar/ghidriff-test-data.git tests/data; fi
pytest -rA -n auto
+3 -3
View File
@@ -54,12 +54,12 @@ jobs:
# install ghidriff package and testing reqs
pip install ".[testing]"
ls -R /root
#pyhidra &
#pyghidra &
#sleep 10
#killall python
# source .env/bin/activate
# #init pyhidra
python -m pyhidra.install_plugins
# #init pyghidra
python -m pyghidra.install_plugins
# pushd /tmp
# popd
# download data to shared test data
+4 -3
View File
@@ -10,6 +10,7 @@ if TYPE_CHECKING:
import ghidra
from ghidra_builtins import *
@JImplements(FunctionHasher, deferred=True)
class StructuralGraphHasher:
"""
@@ -347,7 +348,7 @@ def get_defined_data(program: "ghidra.program.model.listing.Program"):
# its a string, find which functions use it
for ref in sym.references:
# print(ref.referenceType.toString())
f = program.getFunctionManager().getFunctionContaining(ref.fromAddress)
f = program.getFunctionManager().getFunctionContaining(ref.getFromAddress())
if f is not None:
func_str_map.setdefault(f.entryPoint, []).append(str(data))
@@ -452,7 +453,7 @@ def get_func_to_switch(program: "ghidra.program.model.listing.Program"):
# if f is None:
# for ref in sym.references:
# # print(ref.referenceType.toString())
# f = program.getFunctionManager().getFunctionContaining(ref.fromAddress)
# f = program.getFunctionManager().getFunctionContaining(ref.getFromAddress())
# if f is not None:
# func_switch_map.setdefault(f.entryPoint, []).append(str(sym))
# else:
@@ -461,7 +462,7 @@ def get_func_to_switch(program: "ghidra.program.model.listing.Program"):
func_switch_map.setdefault(f.entryPoint, []).append(str(sym))
for ref in sym.references:
# print(ref.referenceType.toString())
f = program.getFunctionManager().getFunctionContaining(ref.fromAddress)
f = program.getFunctionManager().getFunctionContaining(ref.getFromAddress())
if f is not None:
func_switch_map.setdefault(f.entryPoint, []).append(str(sym))
+10 -10
View File
@@ -12,7 +12,7 @@ from typing import List, Tuple, Union, TYPE_CHECKING
from argparse import Namespace
import logging
from pyhidra.launcher import PyhidraLauncher
from pyghidra.launcher import PyGhidraLauncher
from .utils import sha1_file, get_microsoft_download_url, get_pe_extra_data
from .markdown import GhidriffMarkdown
@@ -25,10 +25,10 @@ if TYPE_CHECKING:
from ghidra_builtins import *
class HeadlessLoggingPyhidraLauncher(PyhidraLauncher):
class HeadlessLoggingPyGhidraLauncher(PyGhidraLauncher):
"""
Headless pyhidra launcher
Slightly Modified from Pyhidra to allow the Ghidra log path to be set
Headless pyghidra launcher
Slightly Modified from PyGhidra to allow the Ghidra log path to be set
"""
def __init__(self, verbose=False, log_path=None):
@@ -36,7 +36,7 @@ class HeadlessLoggingPyhidraLauncher(PyhidraLauncher):
self.log_path = log_path
def _launch(self):
from pyhidra.launcher import _silence_java_output
from pyghidra.launcher import _silence_java_output
from ghidra.framework import Application, HeadlessGhidraApplicationConfiguration
from java.io import File
with _silence_java_output(not self.verbose, not self.verbose):
@@ -89,8 +89,8 @@ class GhidraDiffEngine(GhidriffMarkdown, metaclass=ABCMeta):
else:
self.logger.warn('Engine File Log: {engine_log_path}')
# Init Pyhidra
launcher = HeadlessLoggingPyhidraLauncher(verbose=verbose, log_path=engine_log_path)
# Init PyGhidra
launcher = HeadlessLoggingPyGhidraLauncher(verbose=verbose, log_path=engine_log_path)
# JVM Settings
@@ -301,8 +301,8 @@ class GhidraDiffEngine(GhidriffMarkdown, metaclass=ABCMeta):
ref_types = set()
for ref in sym.references:
ref_types.add(ref.referenceType.toString())
f = prog.getFunctionManager().getFunctionContaining(ref.fromAddress)
ref_types.add(ref.getReferenceType().toString())
f = prog.getFunctionManager().getFunctionContaining(ref.getFromAddress())
if f:
calling.add(f.getName())
@@ -333,7 +333,7 @@ class GhidraDiffEngine(GhidriffMarkdown, metaclass=ABCMeta):
# instruction and mnemonic bulker
for code in code_units:
instructions.append(str(code))
mnemonics.append(str(code.mnemonicString))
mnemonics.append(str(code.getMnemonicString))
from ghidra.program.model.block import BasicBlockModel
+1 -1
View File
@@ -67,7 +67,7 @@ def get_microsoft_download_url(filename, timestamp, virtual_size):
return f'https://msdl.microsoft.com/download/symbols/{filename}/{timestamp}{virtual_size}/{filename}'
# utils from Pyhidra
# utils from PyGhidra
def get_private_class(path: str):
from java.lang import ClassLoader
+12 -11
View File
@@ -1,7 +1,7 @@
from pathlib import Path
import json
import pytest
from pyhidra import HeadlessPyhidraLauncher
from pyghidra import HeadlessPyGhidraLauncher
from ghidriff import get_parser, VersionTrackingDiff, GhidraDiffEngine
@@ -18,8 +18,8 @@ def test_diff_afd_cve_2023_21768_gzf(shared_datadir: Path):
"""
# check ghidra version and bail if old
if HeadlessPyhidraLauncher().app_info.version < '11.0':
if HeadlessPyGhidraLauncher().app_info.version < '11.0':
# gzf files were made with 11.0
print('Skip testing gzf on < 11.0')
return
@@ -29,10 +29,10 @@ def test_diff_afd_cve_2023_21768_gzf(shared_datadir: Path):
output_path.mkdir(exist_ok=True, parents=True)
symbols_path = shared_datadir / SYMBOLS_DIR
bins_path = shared_datadir / BINS_DIR
bins_path = shared_datadir / BINS_DIR
bins_path = shared_datadir / BINS_DIR
ghidra_project_path = output_path / 'ghidra_projects'
ghidra_project_path.mkdir(exist_ok=True,parents=True)
ghidra_project_path.mkdir(exist_ok=True, parents=True)
# setup bins
old_bin_path = bins_path / 'afd.sys.x64.10.0.22621.1028.gzf'
new_bin_path = bins_path / 'afd.sys.x64.10.0.22621.1415.gzf'
@@ -44,7 +44,8 @@ def test_diff_afd_cve_2023_21768_gzf(shared_datadir: Path):
GhidraDiffEngine.add_ghidra_args_to_parser(parser)
args = parser.parse_args(['-s', str(symbols_path), str(old_bin_path.absolute()), str(new_bin_path.absolute()), '-p', str(ghidra_project_path.absolute())])
args = parser.parse_args(['-s', str(symbols_path), str(old_bin_path.absolute()),
str(new_bin_path.absolute()), '-p', str(ghidra_project_path.absolute())])
engine_log_path = output_path / parser.get_default('log_path')
@@ -110,7 +111,7 @@ def test_diff_afd_cve_2023_21768_gzf_with_one_nongzf(shared_datadir: Path):
"""
# check ghidra version and bail if old
if HeadlessPyhidraLauncher().app_info.version < '11.0':
if HeadlessPyGhidraLauncher().app_info.version < '11.0':
# gzf files were made with 11.0
print('Skip testing gzf on < 11.0')
return
@@ -121,8 +122,7 @@ def test_diff_afd_cve_2023_21768_gzf_with_one_nongzf(shared_datadir: Path):
symbols_path = shared_datadir / SYMBOLS_DIR
bins_path = shared_datadir / BINS_DIR
ghidra_project_path = output_path / 'ghidra_projects'
ghidra_project_path.mkdir(exist_ok=True,parents=True)
ghidra_project_path.mkdir(exist_ok=True, parents=True)
# setup bins
@@ -136,7 +136,8 @@ def test_diff_afd_cve_2023_21768_gzf_with_one_nongzf(shared_datadir: Path):
GhidraDiffEngine.add_ghidra_args_to_parser(parser)
args = parser.parse_args(['-s', str(symbols_path), str(old_bin_path.absolute()), str(new_bin_path.absolute()), '-p', str(ghidra_project_path.absolute())])
args = parser.parse_args(['-s', str(symbols_path), str(old_bin_path.absolute()),
str(new_bin_path.absolute()), '-p', str(ghidra_project_path.absolute())])
engine_log_path = output_path / parser.get_default('log_path')
+17 -16
View File
@@ -1,7 +1,7 @@
from pathlib import Path
import json
import pytest
from pyhidra import HeadlessPyhidraLauncher
from pyghidra import HeadlessPyGhidraLauncher
from ghidriff import get_parser, get_engine_classes, VersionTrackingDiff, GhidraDiffEngine
@@ -17,7 +17,7 @@ def test_gzf_import_program(shared_datadir: Path):
Tests that gzf files contain expected programs
"""
if HeadlessPyhidraLauncher().app_info.version < '11.0':
if HeadlessPyGhidraLauncher().app_info.version < '11.0':
# gzf files were made with 11.0
print('Skip testing gzf on < 11.0')
return
@@ -28,15 +28,16 @@ def test_gzf_import_program(shared_datadir: Path):
symbols_path = shared_datadir / SYMBOLS_DIR
bins_path = shared_datadir / BINS_DIR
ghidra_project_path = output_path / 'ghidra_projects'
ghidra_project_path.mkdir(exist_ok=True,parents=True)
ghidra_project_path.mkdir(exist_ok=True, parents=True)
# bins
bins_to_import = [
# bin path , expected program
['afd.sys.x64.10.0.22621.1028', 'afd.sys.x64.10.0.22621.1028-00a2b7'], #if a gzf file is used first, this becomes really unstable...
# if a gzf file is used first, this becomes really unstable...
['afd.sys.x64.10.0.22621.1028', 'afd.sys.x64.10.0.22621.1028-00a2b7'],
['afd.sys.x64.10.0.22621.1415', 'afd.sys.x64.10.0.22621.1415-095200'],
['afd.sys.x64.10.0.22621.1028.gzf', 'afd.sys.x64.10.0.22621.1028.gzf-338a92'],
['afd.sys.x64.10.0.22621.1415.gzf', 'afd.sys.x64.10.0.22621.1415.gzf-fc498a'],
['afd.sys.x64.10.0.22621.1028.gzf', 'afd.sys.x64.10.0.22621.1028.gzf-338a92'],
['afd.sys.x64.10.0.22621.1415.gzf', 'afd.sys.x64.10.0.22621.1415.gzf-fc498a'],
['ntoskrnl.exe.x64.10.0.22621.2792.10-1-5.gzf', 'ntoskrnl.exe.x64.10.0.22621.2792.10-1-5.gzf-acb020'],
['ntoskrnl.exe.x64.10.0.22621.2861.10-1-5.gzf', 'ntoskrnl.exe.x64.10.0.22621.2861.10-1-5.gzf-0e4e43'],
]
@@ -46,13 +47,14 @@ def test_gzf_import_program(shared_datadir: Path):
GhidraDiffEngine.add_ghidra_args_to_parser(parser)
engine_log_path = output_path / parser.get_default('log_path')
binary_paths = [path for path in [bins_path / name[0] for name in bins_to_import ]]
args = parser.parse_args(['-s', str(symbols_path),'test', 'test2', '-p', str(ghidra_project_path.absolute())]) # these args will not be tested
expected_names = [name for name in [name[1] for name in bins_to_import ]]
binary_paths = [path for path in [bins_path / name[0] for name in bins_to_import]]
args = parser.parse_args(['-s', str(symbols_path), 'test', 'test2', '-p',
str(ghidra_project_path.absolute())]) # these args will not be tested
expected_names = [name for name in [name[1] for name in bins_to_import]]
binary_paths = [Path(path) for path in binary_paths]
if any([not path.exists() for path in binary_paths]):
@@ -62,7 +64,7 @@ def test_gzf_import_program(shared_datadir: Path):
import uuid
# ensure fresh test each time
project_name = f'import-test-{uuid.uuid4()}'
#project_name = f'import-test'
# project_name = f'import-test'
DiffEngine: GhidraDiffEngine = VersionTrackingDiff
@@ -89,8 +91,7 @@ def test_gzf_import_program(shared_datadir: Path):
# print(data)
# assert expected_names[i] == data[0]
for i,import_path in enumerate(binary_paths):
for i, import_path in enumerate(binary_paths):
imports_result = d.setup_project([binary_paths[i]], args.project_location, project_name, args.symbols_path)
#d.project.wait()
# d.project.wait()
assert expected_names[i] == imports_result[0][0]
+16 -16
View File
@@ -4,9 +4,9 @@ from pytest import MonkeyPatch
import pytest
def test_pyhidra_start():
import pyhidra
pyhidra.start(verbose=True)
def test_pyghidra_start():
import pyghidra
pyghidra.start(verbose=True)
def test_ghidra_install_dir():
@@ -19,31 +19,31 @@ def test_ghidra_install_dir():
# def setup_bogus_env(mon):
# @patch('pyhidra.GHIDRA_INSTALL_DIR', "/someboguspath"):
# @patch('pyghidra.GHIDRA_INSTALL_DIR', "/someboguspath"):
# def test_bogus_ghidra_install_dira():
# with pytest.raises(FileNotFoundError):
# import pyhidra as err_pyhidra
# err_pyhidra.start(verbose=True)
# import pyghidra as err_pyghidra
# err_pyghidra.start(verbose=True)
# def test_ghidra_install_dir():
# # import sys
# # sys.modules.pop('pyhidra')
# # sys.modules.pop('pyghidra')
# with MonkeyPatch.context() as mp:
# mp.delenv("GHIDRA_INSTALL_DIR")
# import pyhidra
# import pyghidra
# # print(os.getenv("GHIDRA_INSTALL_DIR"))
# with pytest.raises(SystemExit) as pytest_wrapped_e:
# launcher = pyhidra.start(verbose=True)
# launcher = pyghidra.start(verbose=True)
# assert pytest_wrapped_e.type == SystemExit
# #assert pytest_wrapped_e.value.code == 42
# import os
# print(os.getenv("GHIDRA_INSTALL_DIR"))
# # from importlib import reload
# # reload(pyhidra)
# # reload(pyghidra)
# import sys
# del sys.modules['pyhidra']
# del sys.modules['pyghidra']
# @pytest.mark.forked
@@ -58,15 +58,15 @@ def test_ghidra_install_dir():
# launcher = None
# # with pytest.raises(SystemExit):
# import pyhidra
# mp.setattr(pyhidra.constants, 'GHIDRA_INSTALL_DIR', '/someboguspath')
# print(pyhidra.constants)
# import pyghidra
# mp.setattr(pyghidra.constants, 'GHIDRA_INSTALL_DIR', '/someboguspath')
# print(pyghidra.constants)
# print(os.getenv("GHIDRA_INSTALL_DIR"))
# launcher = pyhidra.start(verbose=True)
# launcher = pyghidra.start(verbose=True)
# assert launcher == None
# launcher = pyhidra.start(verbose=True)
# launcher = pyghidra.start(verbose=True)
# print(launcher.check_ghidra_version())
# det test_file_not_exist():
+1 -1
View File
@@ -15,6 +15,6 @@ sidebar_position: 1
## Ghidriff - Ghidra Binary Diffing Engine
`ghidriff` provides a command-line binary diffing capability with a fresh take on diffing workflow and results.
It leverages the power of Ghidra's ProgramAPI and [FlatProgramAPI](https://ghidra.re/ghidra_docs/api/ghidra/program/flatapi/FlatProgramAPI.html) to find the *added*, *deleted*, and *modified* functions of two arbitrary binaries. It is written in Python3 using `pyhidra` to orchestrate Ghidra and `jpype` as the Python to Java interface to Ghidra.
It leverages the power of Ghidra's ProgramAPI and [FlatProgramAPI](https://ghidra.re/ghidra_docs/api/ghidra/program/flatapi/FlatProgramAPI.html) to find the *added*, *deleted*, and *modified* functions of two arbitrary binaries. It is written in Python3 using `pyghidra` to orchestrate Ghidra and `jpype` as the Python to Java interface to Ghidra.
Its primary use case is patch diffing. Its ability to perform a patch diff with a single command makes it ideal for automated analysis. The diffing results are stored in JSON and rendered in markdown (optionally side-by-side HTML). The markdown output promotes "social" diffing, as results are easy to publish in a gist or include in your next writeup or blog post.