From c487e4d39445d836e8626aacc65f206f127ab063 Mon Sep 17 00:00:00 2001 From: DC3-DCCI <12175126+DC3-DCCI@users.noreply.github.com> Date: Tue, 25 Jan 2022 19:30:14 -0500 Subject: [PATCH] Initial commit --- CHANGELOG.md | 13 + LICENSE | 23 ++ MANIFEST.in | 5 + NOTICE | 3 + README.md | 142 ++++++++++ images/image-20220111152440065.png | Bin 0 -> 12909 bytes images/image-20220111154029764.png | Bin 0 -> 33943 bytes images/image-20220111154120531.png | Bin 0 -> 41056 bytes noxfile.py | 36 +++ pyhidra/__init__.py | 12 + pyhidra/__main__.py | 160 +++++++++++ pyhidra/constants.py | 7 + pyhidra/converters.py | 14 + pyhidra/ghidra.py | 191 +++++++++++++ pyhidra/gui.py | 15 ++ pyhidra/java/__init__.py | 0 pyhidra/java/plugin/PyScriptProvider.java | 104 +++++++ pyhidra/java/plugin/PyhidraPlugin.java | 137 ++++++++++ pyhidra/java/plugin/PythonFieldExposer.java | 77 ++++++ pyhidra/java/plugin/__init__.py | 1 + pyhidra/java/plugin/completions.py | 104 +++++++ pyhidra/java/plugin/handler.py | 43 +++ pyhidra/java/plugin/plugin.py | 187 +++++++++++++ .../java/property/AbstractJavaProperty.java | 33 +++ .../java/property/BooleanJavaProperty.java | 14 + pyhidra/java/property/ByteJavaProperty.java | 14 + .../java/property/CharacterJavaProperty.java | 14 + pyhidra/java/property/DoubleJavaProperty.java | 14 + pyhidra/java/property/FloatJavaProperty.java | 14 + .../java/property/IntegerJavaProperty.java | 14 + pyhidra/java/property/JavaProperty.java | 6 + .../java/property/JavaPropertyFactory.java | 37 +++ pyhidra/java/property/LongJavaProperty.java | 14 + pyhidra/java/property/ObjectJavaProperty.java | 14 + pyhidra/java/property/PropertyUtils.java | 202 ++++++++++++++ pyhidra/java/property/ShortJavaProperty.java | 14 + pyhidra/javac.py | 61 +++++ pyhidra/launcher.py | 255 ++++++++++++++++++ pyhidra/properties.py | 37 +++ pyhidra/script.py | 199 ++++++++++++++ pyhidra/version.py | 92 +++++++ pyhidra/win_shortcut.py | 81 ++++++ setup.cfg | 40 +++ setup.py | 5 + tests/conftest.py | 14 + tests/example_script.py | 13 + tests/programless_script.py | 5 + tests/projectless_script.py | 5 + tests/strings.c | 76 ++++++ tests/strings.exe | Bin 0 -> 49152 bytes tests/test_core.py | 81 ++++++ 51 files changed, 2642 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 NOTICE create mode 100644 README.md create mode 100644 images/image-20220111152440065.png create mode 100644 images/image-20220111154029764.png create mode 100644 images/image-20220111154120531.png create mode 100644 noxfile.py create mode 100644 pyhidra/__init__.py create mode 100644 pyhidra/__main__.py create mode 100644 pyhidra/constants.py create mode 100644 pyhidra/converters.py create mode 100644 pyhidra/ghidra.py create mode 100644 pyhidra/gui.py create mode 100644 pyhidra/java/__init__.py create mode 100644 pyhidra/java/plugin/PyScriptProvider.java create mode 100644 pyhidra/java/plugin/PyhidraPlugin.java create mode 100644 pyhidra/java/plugin/PythonFieldExposer.java create mode 100644 pyhidra/java/plugin/__init__.py create mode 100644 pyhidra/java/plugin/completions.py create mode 100644 pyhidra/java/plugin/handler.py create mode 100644 pyhidra/java/plugin/plugin.py create mode 100644 pyhidra/java/property/AbstractJavaProperty.java create mode 100644 pyhidra/java/property/BooleanJavaProperty.java create mode 100644 pyhidra/java/property/ByteJavaProperty.java create mode 100644 pyhidra/java/property/CharacterJavaProperty.java create mode 100644 pyhidra/java/property/DoubleJavaProperty.java create mode 100644 pyhidra/java/property/FloatJavaProperty.java create mode 100644 pyhidra/java/property/IntegerJavaProperty.java create mode 100644 pyhidra/java/property/JavaProperty.java create mode 100644 pyhidra/java/property/JavaPropertyFactory.java create mode 100644 pyhidra/java/property/LongJavaProperty.java create mode 100644 pyhidra/java/property/ObjectJavaProperty.java create mode 100644 pyhidra/java/property/PropertyUtils.java create mode 100644 pyhidra/java/property/ShortJavaProperty.java create mode 100644 pyhidra/javac.py create mode 100644 pyhidra/launcher.py create mode 100644 pyhidra/properties.py create mode 100644 pyhidra/script.py create mode 100644 pyhidra/version.py create mode 100644 pyhidra/win_shortcut.py create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tests/conftest.py create mode 100644 tests/example_script.py create mode 100644 tests/programless_script.py create mode 100644 tests/projectless_script.py create mode 100644 tests/strings.c create mode 100644 tests/strings.exe create mode 100644 tests/test_core.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f7dda6d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + + +## 0.1.1 - 2022-01-27 +- Fixed issue from mishandled newline in the interpreter panel +- Fixed unstarted transaction when running code that alters a program database in the interpreter panel +- Fixed noise produced from an exception during analysis due to an analyzer using a script without acquiring a bundle host reference +- Fixed exception in open_program from attempting to use a non-public field in `FlatProgramAPI` + +## 0.1.0 - 2021-06-14 +- Initial release + +[Unreleased]: https://github.com/Defense-Cyber-Crime-Center/pyhidra/compare/0.1.0...HEAD diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..af01b9f --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +DC3 Pyhidra Open Source License + +DC3 Pyhidra software was developed by the Department of Defense Cyber Crime +Center (DC3). By delegated authority pursuant to Section 801(b) of Public Law 113-66, +DC3 grants the following license for this software: + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +to whom the Software is furnished to do so, subject to the following condition: + +The above permission notice and the below warranty notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE DEVELOPERS, OR LICENSORS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..cd836a2 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +graft pyhidra +global-exclude *.py[cod] +include README.md +include LICENSE +include NOTICE diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..d65f4b1 --- /dev/null +++ b/NOTICE @@ -0,0 +1,3 @@ +Pyhidra was developed by the Department of Defense Cyber Crime Center (DC3). +By delegated authority pursuant to Section 801(b) of Public Law 113-66, +DC3 grants the MIT license for this software. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5e04c56 --- /dev/null +++ b/README.md @@ -0,0 +1,142 @@ +# pyhidra + +Pyhidra is a Python library that provides direct access to the Ghidra API within a native CPython interpreter using [jpype](https://jpype.readthedocs.io/en/latest). As well, Pyhidra contains some conveniences for setting up analysis on a given sample and running a Ghidra script locally. It also contains a Ghidra plugin to allow the use of CPython from the +Ghidra user interface. + +Pyhidra was initially developed for use with Dragodis and is designed to be installable without requiring Java or Ghidra. Due to this restriction, the Java plugin for Pyhidra is compiled and installed automatically during first use. The Java plugin is managed by Pyhidra and will automatically be rebuilt as necessary. + + +## Install + +1. Download and install [Ghidra](https://github.com/NationalSecurityAgency/ghidra/releases) to a desired location. + +1. Set the `GHIDRA_INSTALL_DIR` environment variable to point to the directory where Ghidra is installed. + +1. Install pyhidra. + +```console +> pip install pyhidra +``` +### Enabling the Ghidra User Interface Plugin + +1. Run `pyhidraw` from a terminal of your choice. +2. Open the Code Browser Tool. +3. From the `File` toolbar menu, select `Configure...`. +4. From the menu in the image below select `configure` under `Experimental`. + ![](https://raw.githubusercontent.com/Defense-Cyber-Crime-Center/pyhidra/master/images/image-20220111154029764.png) +5. Check and enable Pyhidra as seen in the image below. + ![](https://raw.githubusercontent.com/Defense-Cyber-Crime-Center/pyhidra/master/images/image-20220111154120531.png) + +## Usage + + +### Raw Connection + +To get just a raw connection to Ghidra use the `start()` function. +This will setup a Jpype connection and initialize Ghidra in headless mode, +which will allow you to directly import `ghidra` and `java`. + +*NOTE: No projects or programs get setup in this mode.* + +```python +import pyhidra +pyhidra.start() + +import ghidra +from ghidra.app.util.headless import HeadlessAnalyzer +from ghidra.program.flatapi import FlatProgramAPI +from ghidra.base.project import GhidraProject +from java.lang import String + +# do things +``` + +### Customizing Java and Ghidra initialization + +JVM configuration for the classpath and vmargs may be done through a `PyhidraLauncher`. + +```python +from pyhidra.launcher import HeadlessPyhidraLauncher + +launcher = HeadlessPyhidraLauncher() +launcher.add_classpaths("log4j-core-2.17.1.jar", "log4j-api-2.17.1.jar") +launcher.add_vmargs("-Dlog4j2.formatMsgNoLookups=true") +launcher.start() +``` + +### Analyze a File + +To have pyhidra setup a binary file for you, use the `open_program()` function. +This will setup a Ghidra project and import the given binary file as a program for you. + +Again, this will also allow you to import `ghidra` and `java` to perform more advanced processing. + +```python +import pyhidra + +with pyhidra.open_program("binary_file.exe") as flat_api: + program = flat_api.getCurrentProgram() + listing = program.getListing() + print(listing.getCodeUnitAt(flat_api.toAddr(0x1234))) + + # We are also free to import ghidra while in this context to do more advanced things. + from ghidra.app.decompiler.flatapi import FlatDecompilerAPI + decomp_api = FlatDecompilerAPI(flat_api) + # ... + decomp_api.dispose() +``` + +By default, pyhidra will run analysis for you. If you would like to do this yourself, set `analyze` to `False`. + +```python +import pyhidra + +with pyhidra.open_program("binary_file.exe", analyze=False) as flat_api: + from ghidra.program.util import GhidraProgramUtilities + + program = flat_api.getCurrentProgram() + if GhidraProgramUtilities.shouldAskToAnalyze(program): + flat_api.analyzeAll(program) +``` + + +The `open_program()` function can also accept optional arguments to control the project name and location that gets created. +(Helpful for opening up a sample in an already existing project.) + +```python +import pyhidra + +with pyhidra.open_program("binary_file.exe", project_name="EXAM_231", project_location=r"C:\exams\231") as flat_api: + ... +``` + + +### Run a Script + +Pyhidra can also be used to run an existing Ghidra Python script directly in your native python interpreter +using the `run_script()` command. +However, while you can technically run an existing Ghidra script unmodified, you may +run into issues due to differences between Jython 2 and CPython 3. +Therefore, some modification to the script may be needed. + +```python + +import pyhidra + +pyhidra.run_script(r"C:\input.exe", r"C:\some_ghidra_script.py") +``` + +This can also be done on the command line using `pyhidra`. + +```console +> pyhidra C:\input.exe C:\some_ghidra_script.py +``` + +### Ghidra User Interface + +Ghidra **must** be started via `pyhidraw` and the plugin must be enabled for the user interface features to be present. Once these prerequisites are met the `pyhidra` menu item will be available in the `Window` toolbar menu and all Python scripts outside of the Ghidra installation will automatically be run with CPython. Any Python script found within the Ghidra installation will be run using Jython to prevent causing issues with any analyzers or Ghidra internals that use them. Below is a screenshot of the standard Python interpreter in Ghidra which is using CPython instead of Jython. It will appear when `pyhidra` is opened from the `Window` toolbar menu. + +![](https://raw.githubusercontent.com/Defense-Cyber-Crime-Center/pyhidra/master/images/image-20220111152440065.png) + + + diff --git a/images/image-20220111152440065.png b/images/image-20220111152440065.png new file mode 100644 index 0000000000000000000000000000000000000000..6194426fd1574ab8f5b68f28d539e223cb063732 GIT binary patch literal 12909 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 literal 0 HcmV?d00001 diff --git a/images/image-20220111154029764.png b/images/image-20220111154029764.png new file mode 100644 index 0000000000000000000000000000000000000000..cec131c57752f5ecf4f6e4cd1807b717339e37e4 GIT binary patch literal 33943 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 P*s7d9X=1?8ce(x-*q1x9 literal 0 HcmV?d00001 diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..dd1964c --- /dev/null +++ b/noxfile.py @@ -0,0 +1,36 @@ +""" +Runs tests and other routines. + +Usage: + 1. Install "nox" + 2. Run "nox" or "nox -s test" +""" + +import nox +from pathlib import Path + + +@nox.session(python="3.7") +def test(session): + """Run pytests""" + session.install("-e", ".[testing]") + session.run("pytest") + + +@nox.session(python="3.7") +def build(session): + """Build source and wheel distribution""" + session.run("python", "setup.py", "sdist") + session.run("python", "setup.py", "bdist_wheel") + + +@nox.session(python=False) +def release_patch(session): + """Generate release patch""" + Path("dist").mkdir(exist_ok=True) + with open("./dist/updates.patch", "w") as out: + session.run( + "git", "format-patch", "--stdout", "master", + external=True, + stdout=out + ) diff --git a/pyhidra/__init__.py b/pyhidra/__init__.py new file mode 100644 index 0000000..72fcd79 --- /dev/null +++ b/pyhidra/__init__.py @@ -0,0 +1,12 @@ + +__version__ = "0.1.1" + +# Expose API +from .ghidra import run_script, start, open_program +from .launcher import DeferredPyhidraLauncher, HeadlessPyhidraLauncher, GuiPyhidraLauncher + + +__all__ = [ + "run_script", "start", "open_program", + "DeferredPyhidraLauncher", "HeadlessPyhidraLauncher", "GuiPyhidraLauncher" +] diff --git a/pyhidra/__main__.py b/pyhidra/__main__.py new file mode 100644 index 0000000..2bf1def --- /dev/null +++ b/pyhidra/__main__.py @@ -0,0 +1,160 @@ +import argparse +import code +import sys +from pathlib import Path + +import pyhidra +import pyhidra.gui + + +def _create_shortcut(): + from pyhidra.win_shortcut import create_shortcut + create_shortcut(Path(sys.argv[-1])) + + +def _interpreter(interpreter_globals: dict): + from ghidra.framework import Application + version = Application.getApplicationVersion() + name = Application.getApplicationReleaseName() + banner = f"Python Interpreter for Ghidra {version} {name}\n" + banner += f"Python {sys.version} on {sys.platform}" + code.interact(banner=banner, local=interpreter_globals, exitmsg='') + + +# pylint: disable=too-few-public-methods +class PyhidraArgs(argparse.Namespace): + """ + Custom namespace for holding the command line arguments + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.verbose = False + self.binary_path = None + self.script_path = None + self.project_name = None + self.project_path = None + self.script_args = [] + + def func(self): + """ + Run script or enter repl + """ + if self.script_path is not None: + pyhidra.run_script( + self.binary_path, + self.script_path, + project_location=self.project_path, + project_name=self.project_name, + script_args=self.script_args, + verbose=self.verbose + ) + elif self.binary_path is not None: + from .ghidra import _flat_api + args = self.binary_path, self.project_path, self.project_name, self.verbose + with _flat_api(*args) as api: + _interpreter(api) + else: + pyhidra.HeadlessPyhidraLauncher(verbose=self.verbose).start() + _interpreter(globals()) + + +class PathAction(argparse.Action): + """ + Custom action for handling script and binary paths as positional arguments + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.nargs = '*' + self.help = "Headless script and/or binary path. "\ + "If neither are provided pyhidra will drop into a repl." + self.type = Path + + def __call__(self, parser, namespace, values, option_string=None): + count = 0 + for p in values: + if p.exists() and p.is_file(): + if p.suffix == ".py": + if namespace.script_path is not None: + # assume an additional script is meant to be a parameter to the first one + break + namespace.script_path = p + else: + if namespace.binary_path is not None: + if namespace.script_path is None: + raise ValueError("binary_path specified multiple times") + # assume it is a script parameter + break + namespace.binary_path = p + count += 1 + else: + break + if count > 1: + break + values[:] = values[count:] + +def _get_parser(): + parser = argparse.ArgumentParser(prog="pyhidra") + parser.add_argument( + "-v", + "--verbose", + dest="verbose", + action="store_true", + help="Enable verbose output during Ghidra initialization" + ) + parser.add_argument( + "-g", + "--gui", + action="store_const", + dest="func", + const=pyhidra.gui.gui, + help="Start Ghidra GUI" + ) + if sys.platform == "win32": + parser.add_argument( + "-s", + "--shortcut", + action="store_const", + dest="func", + const=_create_shortcut, + help="Creates a shortcut that can be pinned to the taskbar (Windows only)" + ) + parser.add_argument( + "script_path | binary_path", + metavar="script | binary", + action=PathAction + ) + parser.add_argument( + "script_args", + help="Arguments to be passed to the headless script", + nargs=argparse.REMAINDER + ) + parser.add_argument( + "--project-name", + type=str, + dest="project_name", + metavar="name", + help="Project name to use. " + "(defaults to binary filename with \"_ghidra\" suffix if provided else None)" + ) + parser.add_argument( + "--project-path", + type=Path, + dest="project_path", + metavar="path", + help="Location to store project. " + "(defaults to same directory as binary file if provided else None)" + ) + return parser + + +def main(): + """ + pyhidra module main function + """ + _get_parser().parse_args(namespace=PyhidraArgs()).func() + + +if __name__ == "__main__": + main() diff --git a/pyhidra/constants.py b/pyhidra/constants.py new file mode 100644 index 0000000..312c141 --- /dev/null +++ b/pyhidra/constants.py @@ -0,0 +1,7 @@ +import os +import pathlib + +GHIDRA_INSTALL_DIR = pathlib.Path(os.environ["GHIDRA_INSTALL_DIR"]) +LAUNCH_PROPERTIES = GHIDRA_INSTALL_DIR / "support" / "launch.properties" +UTILITY_JAR = GHIDRA_INSTALL_DIR / "Ghidra" / "Framework" / "Utility" / "lib" / "Utility.jar" +LAUNCHSUPPORT = GHIDRA_INSTALL_DIR / "support" / "LaunchSupport.jar" diff --git a/pyhidra/converters.py b/pyhidra/converters.py new file mode 100644 index 0000000..5315e5c --- /dev/null +++ b/pyhidra/converters.py @@ -0,0 +1,14 @@ + +from pathlib import Path + +from jpype import JConversion, JClass + + +@JConversion("java.lang.String", instanceof=Path) +def pathToString(cls: JClass, path: Path): + return cls(path.resolve().__str__()) + + +@JConversion("java.io.File", instanceof=Path) +def pathToFile(cls: JClass, path: Path): + return cls(path) diff --git a/pyhidra/ghidra.py b/pyhidra/ghidra.py new file mode 100644 index 0000000..097cae5 --- /dev/null +++ b/pyhidra/ghidra.py @@ -0,0 +1,191 @@ +import contextlib +import pathlib +from typing import Union, TYPE_CHECKING, Tuple, ContextManager, List + +from pyhidra.converters import * # pylint: disable=wildcard-import, unused-wildcard-import + + +if TYPE_CHECKING: + import ghidra + + +def start(verbose=False): + """ + Starts the JVM and loads the Ghidra libraries. + Full Ghidra initialization is deferred. + + :param verbose: Enable verbose output during JVM startup (Defaults to False) + :return: The DeferredPhyidraLauncher used to start the JVM + """ + from pyhidra.launcher import HeadlessPyhidraLauncher + launcher = HeadlessPyhidraLauncher(verbose=verbose) + launcher.start() + return launcher + + +def _setup_project( + binary_path: Union[str, pathlib.Path], + project_location: Union[str, pathlib.Path] = None, + project_name: str = None +) -> Tuple["ghidra.base.project.GhidraProject", "ghidra.program.model.listing.Program"]: + from ghidra.base.project import GhidraProject + from java.io import IOException + if binary_path is not None: + binary_path = pathlib.Path(binary_path) + if project_location: + project_location = pathlib.Path(project_location) + else: + project_location = binary_path.parent + if not project_name: + project_name = f"{binary_path.name}_ghidra" + project_location = project_location / project_name + project_location.mkdir(exist_ok=True, parents=True) + + # Open/Create project + program = None + try: + project = GhidraProject.openProject(project_location, project_name, True) + if binary_path is not None: + if project.getRootFolder().getFile(binary_path.name): + program = project.openProgram("/", binary_path.name, False) + except IOException: + project = GhidraProject.createProject(project_location, project_name, False) + + if binary_path is not None and program is None: + program = project.importProgram(binary_path) + project.saveAs(program, "/", program.getName(), True) + + return project, program + + +def _setup_script(project, program): + from pyhidra.script import PyGhidraScript + from ghidra.app.script import GhidraState + from ghidra.program.util import ProgramLocation + from ghidra.util.task import TaskMonitor + + from java.io import PrintWriter + from java.lang import System + + if project is not None: + project = project.getProject() + + location = None + if program is not None: + # create a GhidraState and setup a HeadlessScript with it + mem = program.getMemory().getLoadedAndInitializedAddressSet() + if not mem.isEmpty(): + location = ProgramLocation(program, mem.getMinAddress()) + state = GhidraState(None, project, program, location, None, None) + script = PyGhidraScript() + script.set(state, TaskMonitor.DUMMY, PrintWriter(System.out)) + return script + + +@contextlib.contextmanager +def open_program( + binary_path: Union[str, pathlib.Path], + project_location: Union[str, Path] = None, + project_name: str = None, + analyze=True +) -> ContextManager["ghidra.program.flatapi.FlatProgramAPI"]: + """ + Opens given binary path in Ghidra and returns FlatProgramAPI object. + + :param binary_path: + :param project_location: Location of Ghidra project to open/create. + (Defaults to same directory as binary file) + :param project_name: Name of Ghidra project to open/create. + (Defaults to name of binary file suffixed with "_ghidra") + :param analyze: Whether to run analysis before returning. + :return: A Ghidra FlatProgramAPI object. + """ + + from pyhidra.launcher import PyhidraLauncher, HeadlessPyhidraLauncher + + if not PyhidraLauncher.has_launched(): + HeadlessPyhidraLauncher().start() + + from ghidra.program.flatapi import FlatProgramAPI + + project, program = _setup_project(binary_path, project_location, project_name) + + try: + flat_api = FlatProgramAPI(program) + + if analyze: + from ghidra.program.util import GhidraProgramUtilities + from ghidra.app.script import GhidraScriptUtil + if GhidraProgramUtilities.shouldAskToAnalyze(program): + GhidraScriptUtil.acquireBundleHostReference() + try: + flat_api.analyzeAll(program) + finally: + GhidraScriptUtil.releaseBundleHostReference() + yield flat_api + finally: + project.save(program) + project.close() + + +@contextlib.contextmanager +def _flat_api( + binary_path: Union[str, Path], + project_location: Union[str, Path] = None, + project_name: str = None, + verbose=False +): + """ + Runs a given script on a given binary path. + + :param binary_path: Path to binary file. + :param script_path: Path to script to run. + :param project_location: Location of Ghidra project to open/create. + (Defaults to same directory as binary file) + :param project_name: Name of Ghidra project to open/create. + (Defaults to name of binary file suffixed with "_ghidra") + :param script_args: Command line arguments to pass to script. + :param verbose: Enable verbose output during Ghidra initialization. + """ + from pyhidra.launcher import PyhidraLauncher, HeadlessPyhidraLauncher + + if not PyhidraLauncher.has_launched(): + HeadlessPyhidraLauncher(verbose=verbose).start() + + project, program = None, None + if binary_path or project_location: + project, program = _setup_project(binary_path, project_location, project_name) + + try: + yield _setup_script(project, program) + finally: + if project is not None: + if program is not None: + project.save(program) + project.close() + + +# pylint: disable=too-many-arguments +def run_script( + binary_path: Union[str, Path], + script_path: Union[str, Path], + project_location: Union[str, Path] = None, + project_name: str = None, + script_args: List[str] = None, + verbose=False +): + """ + Runs a given script on a given binary path. + + :param binary_path: Path to binary file, may be None + :param script_path: Path to script to run. + :param project_location: Location of Ghidra project to open/create. + (Defaults to same directory as binary file if not None) + :param project_name: Name of Ghidra project to open/create. + (Defaults to name of binary file suffixed with "_ghidra" if not None) + :param script_args: Command line arguments to pass to script. + :param verbose: Enable verbose output during Ghidra initialization. + """ + script_path = str(script_path) + with _flat_api(binary_path, project_location, project_name, verbose) as script: + script.run(script_path, script_args) diff --git a/pyhidra/gui.py b/pyhidra/gui.py new file mode 100644 index 0000000..0b365bc --- /dev/null +++ b/pyhidra/gui.py @@ -0,0 +1,15 @@ +import os +import sys + + +def gui(): + """ + Starts the Ghidra GUI + """ + if not "GHIDRA_INSTALL_DIR" in os.environ: + import tkinter.messagebox + msg = "GHIDRA_INSTALL_DIR not set.\nPlease see the README for setup instructions" + tkinter.messagebox.showerror("Improper Setup", msg) + sys.exit() + import pyhidra + pyhidra.GuiPyhidraLauncher().start() diff --git a/pyhidra/java/__init__.py b/pyhidra/java/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyhidra/java/plugin/PyScriptProvider.java b/pyhidra/java/plugin/PyScriptProvider.java new file mode 100644 index 0000000..81f0fb4 --- /dev/null +++ b/pyhidra/java/plugin/PyScriptProvider.java @@ -0,0 +1,104 @@ +package dc3.pyhidra.plugin; + +import java.io.PrintWriter; +import java.lang.invoke.MethodHandles; +import java.util.List; +import java.util.function.Consumer; + +import dc3.pyhidra.plugin.PythonFieldExposer.ExposedFields; +import generic.jar.ResourceFile; +import ghidra.app.script.GhidraScript; +import ghidra.app.script.GhidraScriptProperties; +import ghidra.app.script.GhidraScriptUtil; +import ghidra.app.script.GhidraState; +import ghidra.app.util.headless.HeadlessScript; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; +import ghidra.program.util.ProgramSelection; +import ghidra.python.PythonScriptProvider; +import ghidra.util.SystemUtilities; +import ghidra.util.classfinder.ExtensionPointProperties; +import ghidra.util.task.TaskMonitor; + +@ExtensionPointProperties(priority = 2) +public final class PyScriptProvider extends PythonScriptProvider { + + // set via reflection + private static Consumer scriptRunner = null; + + @Override + public GhidraScript getScriptInstance(ResourceFile sourceFile, PrintWriter writer) + throws ClassNotFoundException, InstantiationException, IllegalAccessException { + // check if python is running or not and if not let jython handle it + if (scriptRunner == null || GhidraScriptUtil.isSystemScript(sourceFile)) { + return super.getScriptInstance(sourceFile, writer); + } + + GhidraScript script = SystemUtilities.isInHeadlessMode() ? new PyhidraHeadlessScript() + : new PyhidraGhidraScript(); + script.setSourceFile(sourceFile); + return script; + } + + @ExposedFields( + exposer = PyhidraGhidraScript._ExposedField.class, + names = { + "currentAddress", "currentLocation", "currentSelection", + "currentHighlight", "currentProgram", "monitor", + "potentialPropertiesFileLocs", "propertiesFileParams", + "sourceFile", "state", "writer" + }, + types = { + Address.class, ProgramLocation.class, ProgramSelection.class, + ProgramSelection.class, Program.class, TaskMonitor.class, + List.class, GhidraScriptProperties.class, + ResourceFile.class, GhidraState.class, PrintWriter.class + } + ) + public static class PyhidraGhidraScript extends GhidraScript implements PythonFieldExposer { + + @Override + public void run() { + scriptRunner.accept(this); + } + + private static class _ExposedField extends ExposedField { + + public _ExposedField(String name, Class type) { + super(MethodHandles.lookup().in(PyhidraGhidraScript.class), name, type); + } + } + } + + @ExposedFields( + exposer = PyhidraHeadlessScript._ExposedField.class, + names = { + "currentAddress", "currentLocation", "currentSelection", + "currentHighlight", "currentProgram", "monitor", + "potentialPropertiesFileLocs", "propertiesFileParams", + "sourceFile", "state", "writer" + }, + types = { + Address.class, ProgramLocation.class, ProgramSelection.class, + ProgramSelection.class, Program.class, TaskMonitor.class, + List.class, GhidraScriptProperties.class, + ResourceFile.class, GhidraState.class, PrintWriter.class + } + ) + public static class PyhidraHeadlessScript extends HeadlessScript + implements PythonFieldExposer { + + @Override + public void run() { + scriptRunner.accept(this); + } + + private static class _ExposedField extends ExposedField { + + public _ExposedField(String name, Class type) { + super(MethodHandles.lookup().in(PyhidraHeadlessScript.class), name, type); + } + } + } +} diff --git a/pyhidra/java/plugin/PyhidraPlugin.java b/pyhidra/java/plugin/PyhidraPlugin.java new file mode 100644 index 0000000..dcfc54c --- /dev/null +++ b/pyhidra/java/plugin/PyhidraPlugin.java @@ -0,0 +1,137 @@ +package dc3.pyhidra.plugin; + +import java.io.PrintWriter; +import java.util.function.Consumer; + +import ghidra.MiscellaneousPluginPackage; +import ghidra.app.plugin.core.interpreter.InterpreterPanelService; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.app.plugin.ProgramPlugin; +import ghidra.app.script.GhidraScript; +import ghidra.app.script.GhidraState; +import ghidra.framework.plugintool.PluginInfo; +import ghidra.framework.plugintool.PluginTool; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; +import ghidra.program.util.ProgramLocation; +import ghidra.program.util.ProgramSelection; + +@PluginInfo( + status = PluginStatus.UNSTABLE, + packageName = MiscellaneousPluginPackage.NAME, + category = PluginCategoryNames.INTERPRETERS, + shortDescription = "pyhidra plugin", + description = "Native Python access in Ghidra. This plugin has no effect if Ghidra was not started via pyhidraw.", + servicesRequired = { InterpreterPanelService.class } +) +public final class PyhidraPlugin extends ProgramPlugin { + + // set via reflection + private static Consumer initializer = (a) -> { + }; + private Runnable finalizer = () -> { + }; + + public final InterpreterGhidraScript script = new InterpreterGhidraScript(); + + public PyhidraPlugin(PluginTool tool) { + super(tool, true, true, true); + GhidraState state = new GhidraState(tool, tool.getProject(), null, null, null, null); + // use the copy constructor so this state doesn't fire plugin events + script.set(new GhidraState(state), null, null); + } + + @Override + public void init() { + initializer.accept(this); + } + + @Override + public void dispose() { + finalizer.run(); + } + + @Override + protected void programActivated(Program program) { + script.setCurrentProgram(program); + } + + @Override + protected void programDeactivated(Program program) { + if (script.getCurrentProgram() == program) { + script.setCurrentProgram(null); + } + } + + @Override + protected void locationChanged(ProgramLocation location) { + script.setCurrentLocation(location); + } + + @Override + protected void selectionChanged(ProgramSelection selection) { + script.setCurrentSelection(selection); + } + + @Override + protected void highlightChanged(ProgramSelection highlight) { + script.setCurrentHighlight(highlight); + } + + public static class InterpreterGhidraScript extends GhidraScript { + + private InterpreterGhidraScript() { + } + + @Override + public void run() { + } + + public Address getCurrentAddress() { + return currentAddress; + } + + public ProgramLocation getCurrentLocation() { + return currentLocation; + } + + public ProgramSelection getCurrentSelection() { + return currentSelection; + } + + public ProgramSelection getCurrentHighlight() { + return currentHighlight; + } + + public PrintWriter getWriter() { + return writer; + } + + public void setCurrentProgram(Program program) { + currentProgram = program; + state.setCurrentProgram(program); + } + + public void setCurrentAddress(Address address) { + currentAddress = address; + state.setCurrentAddress(address); + } + + public void setCurrentLocation(ProgramLocation location) { + currentLocation = location; + currentAddress = location != null ? location.getAddress() : null; + state.setCurrentLocation(location); + } + + public void setCurrentSelection(ProgramSelection selection) { + currentSelection = selection; + state.setCurrentSelection(selection); + } + + public void setCurrentHighlight(ProgramSelection highlight) { + currentHighlight = highlight; + state.setCurrentHighlight(highlight); + } + } +} diff --git a/pyhidra/java/plugin/PythonFieldExposer.java b/pyhidra/java/plugin/PythonFieldExposer.java new file mode 100644 index 0000000..0e7996a --- /dev/null +++ b/pyhidra/java/plugin/PythonFieldExposer.java @@ -0,0 +1,77 @@ +package dc3.pyhidra.plugin; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.invoke.ConstantBootstraps; +import java.lang.invoke.VarHandle; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.reflect.Constructor; +import java.util.Collections; +import java.util.Map; + +import ghidra.util.Msg; +import ghidra.util.exception.AssertException; + +public interface PythonFieldExposer { + // marker interface + + public static Map getProperties( + Class cls) { + try { + return doGetProperties(cls); + } + catch (Throwable t) { + Msg.error(PythonFieldExposer.class, + "Failed to expose fields for " + cls.getSimpleName(), t); + return Collections.emptyMap(); + } + } + + @SuppressWarnings("unchecked") + public static Map doGetProperties( + Class cls) + throws Throwable { + ExposedFields fields = cls.getAnnotation(ExposedFields.class); + String[] names = fields.names(); + Class[] types = fields.types(); + if (names.length != types.length) { + throw new AssertException("Improperly applied ExposedFields on " + cls.getSimpleName()); + } + Constructor c = + fields.exposer().getConstructor(String.class, Class.class); + Map.Entry[] properties = new Map.Entry[names.length]; + for (int i = 0; i < names.length; i++) { + properties[i] = Map.entry(names[i], c.newInstance(names[i], types[i])); + } + return Map.ofEntries(properties); + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + public static @interface ExposedFields { + public Class exposer(); + + public String[] names(); + + public Class[] types(); + } + + public static abstract class ExposedField { + private final VarHandle handle; + + protected ExposedField(Lookup lookup, String name, Class type) { + handle = ConstantBootstraps.fieldVarHandle(lookup, name, VarHandle.class, + lookup.lookupClass(), type); + } + + public Object fget(Object self) { + return handle.get(self); + } + + public void fset(Object self, Object value) { + handle.set(self, value); + } + } +} diff --git a/pyhidra/java/plugin/__init__.py b/pyhidra/java/plugin/__init__.py new file mode 100644 index 0000000..1de1a58 --- /dev/null +++ b/pyhidra/java/plugin/__init__.py @@ -0,0 +1 @@ +from .handler import install diff --git a/pyhidra/java/plugin/completions.py b/pyhidra/java/plugin/completions.py new file mode 100644 index 0000000..ee8f7ab --- /dev/null +++ b/pyhidra/java/plugin/completions.py @@ -0,0 +1,104 @@ +import builtins +from keyword import iskeyword +from typing import Mapping, Sequence +from rlcompleter import Completer +from types import CodeType, FunctionType, MappingProxyType, MethodType, ModuleType + +from docking.widgets.label import GLabel +from ghidra.app.plugin.core.console import CodeCompletion +from java.awt import Color +from java.util import Arrays, Collections +from jpype import JPackage +from jpype.types import JDouble, JFloat, JInt, JLong, JShort + + +NoneType = type(None) + +CLASS_COLOR = Color(0, 0, 255) +CODE_COLOR = Color(0, 64, 0) +FUNCTION_COLOR = Color(0, 128, 0) +INSTANCE_COLOR = Color(128, 0, 128) +MAP_COLOR = Color(64, 96, 128) +METHOD_COLOR = Color(0, 128, 128) +NULL_COLOR = Color(255, 0, 0) +NUMBER_COLOR = Color(64, 64, 64) +PACKAGE_COLOR = Color(128, 0, 0) +SEQUENCE_COLOR = Color(128, 96, 64) + +_TYPE_COLORS = { + type: CLASS_COLOR, + CodeType: CODE_COLOR, + FunctionType: FUNCTION_COLOR, + dict: MAP_COLOR, + MappingProxyType: MAP_COLOR, + MethodType: METHOD_COLOR, + NoneType: NULL_COLOR, + int: NUMBER_COLOR, + float: NUMBER_COLOR, + complex: NUMBER_COLOR, + JShort: NUMBER_COLOR, + JInt: NUMBER_COLOR, + JLong: NUMBER_COLOR, + JFloat: NUMBER_COLOR, + JDouble: NUMBER_COLOR, + ModuleType: PACKAGE_COLOR, + JPackage: PACKAGE_COLOR +} + + +class PythonCodeCompleter(Completer): + """ + Code Completer for Ghidra's Python interpreter window + """ + + _BUILTIN_ATTRIBUTE = object() + __slots__ = ('cmd',) + + def __init__(self, py_console): + super().__init__(py_console.locals.get_static_view()) + self.cmd: str + + def _get_label(self, i: int) -> GLabel: + match = self.matches[i].rstrip("()") + label = GLabel(match) + attr = self.namespace.get(match, PythonCodeCompleter._BUILTIN_ATTRIBUTE) + if attr is PythonCodeCompleter._BUILTIN_ATTRIBUTE: + if iskeyword(match.rstrip()): + return label + attr = builtins.__dict__.get(match, PythonCodeCompleter._BUILTIN_ATTRIBUTE) + if attr is not PythonCodeCompleter._BUILTIN_ATTRIBUTE and not match.startswith("__"): + attr = builtins.__dict__[match] + else: + return label + color = _TYPE_COLORS.get(type(attr), PythonCodeCompleter._BUILTIN_ATTRIBUTE) + if color is PythonCodeCompleter._BUILTIN_ATTRIBUTE: + t = type(attr) + if isinstance(t, Sequence): + color = SEQUENCE_COLOR + elif isinstance(t, Mapping): + color = MAP_COLOR + else: + color = INSTANCE_COLOR + label.setForeground(color) + return label + + def _supplier(self, i: int) -> CodeCompletion: + insertion = self.matches[i][len(self.cmd):] + return CodeCompletion(self.cmd, insertion, self._get_label(i)) + + def get_completions(self, cmd: str): + """ + Gets all the possible CodeCompletion(s) for the provided cmd + + :param cmd: The code to complete + :return: A Java List of all possible CodeCompletion(s) + """ + try: + self.cmd = cmd + if self.complete(cmd, 0) is None: + return Collections.emptyList() + res = CodeCompletion[len(self.matches)] + Arrays.setAll(res, self._supplier) + return Arrays.asList(res) + except: # pylint: disable=bare-except + return Collections.emptyList() diff --git a/pyhidra/java/plugin/handler.py b/pyhidra/java/plugin/handler.py new file mode 100644 index 0000000..64bd86b --- /dev/null +++ b/pyhidra/java/plugin/handler.py @@ -0,0 +1,43 @@ +from pathlib import Path + +from java.lang import ClassLoader +from utility.application import ApplicationLayout + +from pyhidra.java.plugin.plugin import PyPhidraPlugin +from pyhidra.javac import java_compile +from pyhidra.version import CURRENT_APPLICATION, ExtensionDetails + + + +PACKAGE = "dc3.pyhidra.plugin" +PLUGIN_NAME = "pyhidra" + + +def _get_extension_details(layout: ApplicationLayout): + return ExtensionDetails( + PLUGIN_NAME, + "Native Python Plugin", + "Department of Defense Cyber Crime Center (DC3)", + "", + layout.getApplicationProperties().getApplicationVersion() + ) + + +def install(): + """ + Install the plugin in Ghidra + """ + path = CURRENT_APPLICATION.extension_path / "pyhidra" + ext = path / "extension.properties" + manifest = path / "Module.manifest" + if not manifest.exists(): + jar_path = path / "lib" / (PLUGIN_NAME + ".jar") + java_compile(Path(__file__).parent.parent, jar_path) + ClassLoader.getSystemClassLoader().addPath(jar_path.absolute()) + + if not manifest.exists(): + ext.write_text(str(ExtensionDetails())) + # required empty file, might be usable for version control in the future + manifest.touch() + + PyPhidraPlugin.register() diff --git a/pyhidra/java/plugin/plugin.py b/pyhidra/java/plugin/plugin.py new file mode 100644 index 0000000..206e752 --- /dev/null +++ b/pyhidra/java/plugin/plugin.py @@ -0,0 +1,187 @@ +import contextlib +import itertools +import rlcompleter +import sys +import threading +from code import InteractiveConsole + +from ghidra.app.plugin.core.console import CodeCompletion +from ghidra.app.plugin.core.interpreter import InterpreterConnection, InterpreterPanelService +from ghidra.util.task import DummyCancellableTaskMonitor +from java.io import BufferedReader, InputStreamReader, PushbackReader +from java.lang import ClassLoader, Runnable, String +from java.util.function import Consumer +from jpype import JClass, JImplements, JOverride +from resources import ResourceManager +from utility.function import Callback + +from pyhidra.java.plugin.completions import PythonCodeCompleter +from pyhidra.script import PyGhidraScript + + +def _set_field(cls, fname, value, obj=None): + cls = cls.class_ + field = cls.getDeclaredField(fname) + field.setAccessible(True) + field.set(obj, value) + field.setAccessible(False) + + +def _run_script(script): + PyGhidraScript(script).run() + + +class PyConsole(InteractiveConsole): + """ + Pyhidra Interactive Console + """ + + def __init__(self, py_plugin) -> None: + super().__init__(locals=PyGhidraScript(py_plugin.script)) + self._plugin = py_plugin + console = py_plugin.service.createInterpreterPanel(py_plugin, False) + self._console = console + self._reader = PushbackReader(InputStreamReader(console.getStdin())) + self._line_reader = BufferedReader(self._reader) + self._out = console.getOutWriter() + self._err = console.getErrWriter() + self._writer = self._out + self._thread = None + state = self.locals._script.getState() + self.locals._script.set(state, DummyCancellableTaskMonitor(), console.getOutWriter()) + console.addFirstActivationCallback(Callback @ self.interact) + + def interact(self, banner=None, exitmsg=None): + from ghidra.framework import Application + version = Application.getApplicationVersion() + name = Application.getApplicationReleaseName() + banner = f"Python Interpreter for Ghidra {version} {name}\n" + banner += f"Python {sys.version} on {sys.platform}" + target = super().interact + targs = {'banner': banner} + self._thread = threading.Thread(target=target, name="Interpreter", kwargs=targs) + self._thread.start() + + def raw_input(self, prompt=''): + self._console.setPrompt(prompt) + c = self._reader.read() + if c == -1: + raise EOFError + if c == ord('\n'): + return '\n' + self._reader.unread(c) + return self._line_reader.readLine() + + def write(self, data: str): + self._writer.write(String @ data) + self._writer.flush() + + def dispose(self): + """ + Release the console resources + """ + self._console.dispose() + if self._thread is not None and self._thread.is_alive(): + self._thread.join() + + def showsyntaxerror(self, filename=None): + self._writer = self._err + super().showsyntaxerror(filename=filename) + self._writer = self._out + + def showtraceback(self) -> None: + self._writer = self._err + super().showtraceback() + self._writer = self._out + + @contextlib.contextmanager + def _run_context(self): + transaction = -1 + success = False + program = self._plugin.program + if program is not None: + transaction = program.startTransaction("Python command") + try: + with contextlib.redirect_stdout(self._out), contextlib.redirect_stderr(self._err): + yield + success = True + finally: + if transaction != -1: + program = self._plugin.program + if program is not None: + program.endTransaction(transaction, success) + + def runcode(self, code): + with self._run_context(): + super().runcode(code) + self._out.flush() + self._err.flush() + + +@JImplements(InterpreterConnection) +class PyPhidraPlugin: + """ + The Python side PyhidraPlugin + """ + + # pylint: disable=missing-function-docstring, invalid-name + + def __init__(self, plugin): + if hasattr(self, '_plugin'): + # this gets entered twice for some reason + return + self._plugin = plugin + gcl = ClassLoader.getSystemClassLoader() + plugin_cls = JClass("dc3.pyhidra.plugin.PyhidraPlugin", loader=gcl) + _set_field(plugin_cls, "finalizer", Runnable @ self.dispose, plugin) + self.console = PyConsole(self) + self.completer = PythonCodeCompleter(self.console) + + @staticmethod + def register(): + gcl = ClassLoader.getSystemClassLoader() + plugin = JClass("dc3.pyhidra.plugin.PyhidraPlugin", loader=gcl) + provider = JClass("dc3.pyhidra.plugin.PyScriptProvider", loader=gcl) + _set_field(plugin, "initializer", Consumer @ PyPhidraPlugin) + _set_field(provider, "scriptRunner", Consumer @ _run_script) + + def _set_plugin(self, plugin): + self._plugin = plugin + + def dispose(self): + """ + Release the plugin resources + """ + self.console.dispose() + + def _gen_completions(self, cmd: str): + completer = rlcompleter.Completer(namespace=self) + for state in itertools.count(): + completion = completer.complete(cmd, state) + if completion is None: + break + yield CodeCompletion(cmd, completion, None) + + @property + def program(self): + return self._plugin.getCurrentProgram() + + @property + def script(self): + return self._plugin.script + + @property + def service(self): + return self._plugin.getTool().getService(InterpreterPanelService.class_) + + @JOverride + def getCompletions(self, cmd: str): + return self.completer.get_completions(cmd) + + @JOverride + def getIcon(self): + return ResourceManager.loadImage("images/python.png") + + @JOverride + def getTitle(self): + return "Pyhidra" diff --git a/pyhidra/java/property/AbstractJavaProperty.java b/pyhidra/java/property/AbstractJavaProperty.java new file mode 100644 index 0000000..6b52355 --- /dev/null +++ b/pyhidra/java/property/AbstractJavaProperty.java @@ -0,0 +1,33 @@ +package dc3.pyhidra.property; + +import java.lang.invoke.MethodHandle; + +abstract class AbstractJavaProperty implements JavaProperty { + + public final String field; + private final MethodHandle getter; + private final MethodHandle setter; + + protected AbstractJavaProperty(String field, MethodHandle getter, MethodHandle setter) { + this.field = field; + this.getter = getter; + this.setter = setter; + } + + public boolean hasGetter() { + return getter != null; + } + + public boolean hasSetter() { + return setter != null; + } + + protected final T doGet(Object self) throws Throwable { + return (T) getter.invoke(self); + } + + @Override + public final void fset(Object self, T value) throws Throwable { + setter.invoke(self, value); + } +} diff --git a/pyhidra/java/property/BooleanJavaProperty.java b/pyhidra/java/property/BooleanJavaProperty.java new file mode 100644 index 0000000..3375e24 --- /dev/null +++ b/pyhidra/java/property/BooleanJavaProperty.java @@ -0,0 +1,14 @@ +package dc3.pyhidra.property; + +import java.lang.invoke.MethodHandle; + +public final class BooleanJavaProperty extends AbstractJavaProperty { + + BooleanJavaProperty(String field, MethodHandle getter, MethodHandle setter) { + super(field, getter, setter); + } + + public boolean fget(Object self) throws Throwable { + return doGet(self); + } +} diff --git a/pyhidra/java/property/ByteJavaProperty.java b/pyhidra/java/property/ByteJavaProperty.java new file mode 100644 index 0000000..afb1f1e --- /dev/null +++ b/pyhidra/java/property/ByteJavaProperty.java @@ -0,0 +1,14 @@ +package dc3.pyhidra.property; + +import java.lang.invoke.MethodHandle; + +public final class ByteJavaProperty extends AbstractJavaProperty { + + ByteJavaProperty(String field, MethodHandle getter, MethodHandle setter) { + super(field, getter, setter); + } + + public byte fget(Object self) throws Throwable { + return doGet(self); + } +} diff --git a/pyhidra/java/property/CharacterJavaProperty.java b/pyhidra/java/property/CharacterJavaProperty.java new file mode 100644 index 0000000..c0561e0 --- /dev/null +++ b/pyhidra/java/property/CharacterJavaProperty.java @@ -0,0 +1,14 @@ +package dc3.pyhidra.property; + +import java.lang.invoke.MethodHandle; + +public final class CharacterJavaProperty extends AbstractJavaProperty { + + CharacterJavaProperty(String field, MethodHandle getter, MethodHandle setter) { + super(field, getter, setter); + } + + public char fget(Object self) throws Throwable { + return doGet(self); + } +} diff --git a/pyhidra/java/property/DoubleJavaProperty.java b/pyhidra/java/property/DoubleJavaProperty.java new file mode 100644 index 0000000..95c3b58 --- /dev/null +++ b/pyhidra/java/property/DoubleJavaProperty.java @@ -0,0 +1,14 @@ +package dc3.pyhidra.property; + +import java.lang.invoke.MethodHandle; + +public final class DoubleJavaProperty extends AbstractJavaProperty { + + DoubleJavaProperty(String field, MethodHandle getter, MethodHandle setter) { + super(field, getter, setter); + } + + public double fget(Object self) throws Throwable { + return doGet(self); + } +} diff --git a/pyhidra/java/property/FloatJavaProperty.java b/pyhidra/java/property/FloatJavaProperty.java new file mode 100644 index 0000000..dc9499b --- /dev/null +++ b/pyhidra/java/property/FloatJavaProperty.java @@ -0,0 +1,14 @@ +package dc3.pyhidra.property; + +import java.lang.invoke.MethodHandle; + +public final class FloatJavaProperty extends AbstractJavaProperty { + + FloatJavaProperty(String field, MethodHandle getter, MethodHandle setter) { + super(field, getter, setter); + } + + public float fget(Object self) throws Throwable { + return doGet(self); + } +} diff --git a/pyhidra/java/property/IntegerJavaProperty.java b/pyhidra/java/property/IntegerJavaProperty.java new file mode 100644 index 0000000..30949a3 --- /dev/null +++ b/pyhidra/java/property/IntegerJavaProperty.java @@ -0,0 +1,14 @@ +package dc3.pyhidra.property; + +import java.lang.invoke.MethodHandle; + +public final class IntegerJavaProperty extends AbstractJavaProperty { + + IntegerJavaProperty(String field, MethodHandle getter, MethodHandle setter) { + super(field, getter, setter); + } + + public int fget(Object self) throws Throwable { + return doGet(self); + } +} diff --git a/pyhidra/java/property/JavaProperty.java b/pyhidra/java/property/JavaProperty.java new file mode 100644 index 0000000..4f88c33 --- /dev/null +++ b/pyhidra/java/property/JavaProperty.java @@ -0,0 +1,6 @@ +package dc3.pyhidra.property; + +public interface JavaProperty { + + public abstract void fset(Object self, T value) throws Throwable; +} diff --git a/pyhidra/java/property/JavaPropertyFactory.java b/pyhidra/java/property/JavaPropertyFactory.java new file mode 100644 index 0000000..4b9bf1d --- /dev/null +++ b/pyhidra/java/property/JavaPropertyFactory.java @@ -0,0 +1,37 @@ +package dc3.pyhidra.property; + +import java.lang.invoke.MethodHandle; +import java.util.IdentityHashMap; +import java.util.Map; + +class JavaPropertyFactory { + + private static final IdentityHashMap, JavaPropertyBuilder> PROPERTIES = + new IdentityHashMap<>( + Map.of( + Boolean.TYPE, BooleanJavaProperty::new, + Byte.TYPE, ByteJavaProperty::new, + Character.TYPE, CharacterJavaProperty::new, + Double.TYPE, DoubleJavaProperty::new, + Float.TYPE, FloatJavaProperty::new, + Integer.TYPE, IntegerJavaProperty::new, + Long.TYPE, LongJavaProperty::new, + Short.TYPE, ShortJavaProperty::new)); + + private JavaPropertyFactory() { + } + + static JavaProperty get(String field, MethodHandle getter, MethodHandle setter) { + Class cls = + getter != null ? getter.type().returnType() : setter.type().lastParameterType(); + return cls.isPrimitive() ? PROPERTIES.get(cls).build(field, getter, setter) + : new ObjectJavaProperty(field, getter, setter); + } + + @FunctionalInterface + private static interface JavaPropertyBuilder { + + AbstractJavaProperty build(String field, MethodHandle getter, MethodHandle setter); + } + +} diff --git a/pyhidra/java/property/LongJavaProperty.java b/pyhidra/java/property/LongJavaProperty.java new file mode 100644 index 0000000..25b218c --- /dev/null +++ b/pyhidra/java/property/LongJavaProperty.java @@ -0,0 +1,14 @@ +package dc3.pyhidra.property; + +import java.lang.invoke.MethodHandle; + +public final class LongJavaProperty extends AbstractJavaProperty { + + LongJavaProperty(String field, MethodHandle getter, MethodHandle setter) { + super(field, getter, setter); + } + + public long fget(Object self) throws Throwable { + return doGet(self); + } +} diff --git a/pyhidra/java/property/ObjectJavaProperty.java b/pyhidra/java/property/ObjectJavaProperty.java new file mode 100644 index 0000000..c332d74 --- /dev/null +++ b/pyhidra/java/property/ObjectJavaProperty.java @@ -0,0 +1,14 @@ +package dc3.pyhidra.property; + +import java.lang.invoke.MethodHandle; + +public class ObjectJavaProperty extends AbstractJavaProperty { + + protected ObjectJavaProperty(String field, MethodHandle getter, MethodHandle setter) { + super(field, getter, setter); + } + + public final Object fget(Object self) throws Throwable { + return doGet(self); + } +} diff --git a/pyhidra/java/property/PropertyUtils.java b/pyhidra/java/property/PropertyUtils.java new file mode 100644 index 0000000..a13801d --- /dev/null +++ b/pyhidra/java/property/PropertyUtils.java @@ -0,0 +1,202 @@ +package dc3.pyhidra.property; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodHandles.Lookup; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import ghidra.util.Msg; + +public class PropertyUtils { + + private PropertyUtils() { + } + + public static JavaProperty[] getProperties(Class cls) { + try { + return doGetProperties(cls); + } + catch (Throwable t) { + Msg.error(PropertyUtils.class, + "Failed to extract properties for " + cls.getSimpleName(), t); + return new JavaProperty[0]; + } + } + + private static JavaProperty[] doGetProperties(Class cls) throws Throwable { + PropertyPairFactory factory; + try { + factory = new PropertyPairFactory(cls); + } + catch (IllegalArgumentException e) { + // skip illegal lookup class + return new JavaProperty[0]; + } + return getMethods(cls) + .filter(PropertyUtils::methodFilter) + .map(PropertyUtils::toProperty) + .flatMap(Optional::stream) + .collect( + Collectors.groupingBy( + PartialProperty::getName)) + .values() + .stream() + .map(factory::merge) + .flatMap(Optional::stream) + .toArray(JavaProperty[]::new); + } + + private static Stream getMethods(Class cls) { + if (isPublic(cls)) { + return Arrays.stream(cls.getMethods()); + } + Class base = cls; + while (!isPublic(base)) { + base = base.getSuperclass(); + } + Stream head = Arrays.stream(base.getMethods()) + .filter(PropertyUtils::methodFilter); + Stream tail = Stream.concat( + Arrays.stream(base.getInterfaces()), + Arrays.stream(cls.getInterfaces())) + .sorted(Comparator.comparing(Class::getSimpleName)) + .distinct() + .map(Class::getDeclaredMethods) + .flatMap(Arrays::stream) + .filter(PropertyUtils::methodFilter); + return Stream.concat(head, tail) + .sorted(Comparator.comparing(Method::toGenericString)) + .distinct(); + } + + private static boolean methodFilter(Method m) { + return isPublic(m) && (m.getName().charAt(0) & 'a') == 'a' && m.getParameterCount() < 2; + } + + private static boolean isPublic(Class cls) { + return Modifier.isPublic(cls.getModifiers()); + } + + private static boolean isPublic(Method m) { + int mod = m.getModifiers(); + return Modifier.isPublic(mod) && !Modifier.isStatic(mod); + } + + private static class PropertyPairFactory { + private final Lookup lookup; + + private PropertyPairFactory(Class c) { + lookup = MethodHandles.publicLookup(); + } + + private Optional> merge(List pairs) { + try { + if (pairs.size() == 1) { + PartialProperty p = pairs.get(0); + MethodHandle h = lookup.unreflect(p.m); + JavaProperty res = p.isGetter() ? JavaPropertyFactory.get(p.name, h, null) + : JavaPropertyFactory.get(p.name, null, h); + return Optional.of(res); + } + PartialProperty g = pairs.stream() + .filter(PartialProperty::isGetter) + .findFirst() + .orElse(null); + if (g != null) { + Class target = g.m.getReturnType(); + PartialProperty s = pairs.stream() + .filter(PartialProperty::isSetter) + .filter(p -> p.m.getParameterTypes()[0] == target) + .findFirst() + .orElse(null); + MethodHandle gh = lookup.unreflect(g.m); + MethodHandle sh = s != null ? lookup.unreflect(s.m) : null; + return Optional.of(JavaPropertyFactory.get(g.name, gh, sh)); + } + } + catch (IllegalAccessException e) { + // this is a class in java.lang.invoke or java.lang.reflect + // the JVM doesn't allow the creation of handles for these + } + // multiple setters. ie not a property + return Optional.empty(); + } + } + + private static Optional toProperty(Method m) { + String name = m.getName(); + int n = m.getParameterCount(); + try { + switch ((name.charAt(0) ^ 'a') >> 2) { + case 1: // g + if (!name.startsWith("get")) { + return Optional.empty(); + } + if (n != 0 || m.getReturnType() == Void.TYPE) { + return Optional.empty(); + } + name = name.substring(3); + break; + case 2: // i + if (!name.startsWith("is")) { + return Optional.empty(); + } + if (n != 0 || m.getReturnType() != Boolean.TYPE) { + return Optional.empty(); + } + name = name.substring(2); + break; + case 4: // s + if (!name.startsWith("set")) { + return Optional.empty(); + } + if (n != 1 || m.getReturnType() != Void.TYPE) { + return Optional.empty(); + } + name = name.substring(3); + break; + default: + return Optional.empty(); + } + if (Character.isLowerCase(name.charAt(0))) { + return Optional.empty(); + } + char c = Character.toLowerCase(name.charAt(0)); + name = c + name.substring(1); + return Optional.of(new PartialProperty(m, name)); + } + catch (IndexOutOfBoundsException e) { + // probability is extremely small + return Optional.empty(); + } + } + + private static class PartialProperty { + private final Method m; + private final String name; + + private PartialProperty(Method m, String name) { + this.m = m; + this.name = name; + } + + public boolean isGetter() { + return m.getParameterCount() == 0 && m.getReturnType() != Void.TYPE; + } + + public boolean isSetter() { + return m.getParameterCount() == 1 && m.getReturnType() == Void.TYPE; + } + + public String getName() { + return name; + } + } +} diff --git a/pyhidra/java/property/ShortJavaProperty.java b/pyhidra/java/property/ShortJavaProperty.java new file mode 100644 index 0000000..7b8efb6 --- /dev/null +++ b/pyhidra/java/property/ShortJavaProperty.java @@ -0,0 +1,14 @@ +package dc3.pyhidra.property; + +import java.lang.invoke.MethodHandle; + +public final class ShortJavaProperty extends AbstractJavaProperty { + + ShortJavaProperty(String field, MethodHandle getter, MethodHandle setter) { + super(field, getter, setter); + } + + public short fget(Object self) throws Throwable { + return doGet(self); + } +} diff --git a/pyhidra/javac.py b/pyhidra/javac.py new file mode 100644 index 0000000..b0849bc --- /dev/null +++ b/pyhidra/javac.py @@ -0,0 +1,61 @@ +import shutil +import tempfile +from pathlib import Path + + +COMPILER_OPTIONS = ["-target", "11"] + + +def _to_jar_(jar_path: Path, root: Path): + from java.io import ByteArrayOutputStream + from java.util.jar import JarEntry, JarOutputStream + + out = ByteArrayOutputStream() + with JarOutputStream(out) as jar: + for p in root.glob("**/*.class"): + p = p.resolve() + jar.putNextEntry(JarEntry(str(p.relative_to(root).as_posix()))) + jar.write(p.read_bytes()) + jar.closeEntry() + jar_path.write_bytes(out.toByteArray()) + + +def java_compile(src_path: Path, jar_path: Path): + """ + Compiles the provided Java source + + :param src_path: The path to the java file or the root directory of the java source files + :param jar_path: The path to write the output jar to + """ + + from java.lang import System + from java.nio.file import Path as JPath + from javax.tools import StandardLocation, ToolProvider + + with tempfile.TemporaryDirectory() as out: + outdir = Path(out).resolve() + compiler = ToolProvider.getSystemJavaCompiler() + fman = compiler.getStandardFileManager(None, None, None) + cp = [JPath @ (Path(p)) for p in System.getProperty("java.class.path").split(';')] + fman.setLocationFromPaths(StandardLocation.CLASS_PATH, cp) + if src_path.is_dir(): + fman.setLocationFromPaths(StandardLocation.SOURCE_PATH, [JPath @ (src_path.resolve())]) + fman.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, [JPath @ (outdir)]) + sources = None + if src_path.is_file(): + sources = fman.getJavaFileObjectsFromPaths([JPath @ (src_path)]) + else: + glob = src_path.glob("**/*.java") + sources = fman.getJavaFileObjectsFromPaths([JPath @ (p) for p in glob]) + + task = compiler.getTask(None, fman, None, COMPILER_OPTIONS, None, sources) + + if not task.call(): + # errors printed to stderr + return + + if jar_path.suffix == '.jar': + jar_path.parent.mkdir(exist_ok=True, parents=True) + _to_jar_(jar_path, outdir) + else: + shutil.copytree(outdir, jar_path, dirs_exist_ok=True) diff --git a/pyhidra/launcher.py b/pyhidra/launcher.py new file mode 100644 index 0000000..fbbe6b5 --- /dev/null +++ b/pyhidra/launcher.py @@ -0,0 +1,255 @@ +import contextlib +import logging +import platform +import re +import shutil +import subprocess +import sys +import textwrap +from pathlib import Path +from typing import NoReturn + +import jpype +from jpype import imports + +from . import __version__ +from .constants import LAUNCH_PROPERTIES, LAUNCHSUPPORT, GHIDRA_INSTALL_DIR, UTILITY_JAR +from .version import CURRENT_APPLICATION, CURRENT_GHIDRA_VERSION, MINIMUM_GHIDRA_VERSION, \ + ExtensionDetails + + +_GET_JAVA_HOME = f'java -cp "{LAUNCHSUPPORT}" LaunchSupport "{GHIDRA_INSTALL_DIR}" -jdk_home -save' + + +def _jvm_args(): + suffix = "_" + platform.system().upper() + option_pattern: re.Pattern = re.compile(fr"VMARGS(?:{suffix})?=(.+)") + properties = [] + with open(LAUNCH_PROPERTIES, "r", encoding='utf-8') as fd: + # this file is small so just read it at once + for line in fd.readlines(): + match = option_pattern.match(line) + if match: + properties.append(match.group(1)) + + # even though ignoreUnrecognized is True when starting the VM this is still needed + properties.insert(0, "-XX:+IgnoreUnrecognizedVMOptions") + + return properties + + +@contextlib.contextmanager +def _silence_java_output(stdout=True, stderr=True): + from java.io import OutputStream, PrintStream + from java.lang import System + out = System.out + err = System.err + null = PrintStream(OutputStream.nullOutputStream()) + + # The user's Java SecurityManager might not allow this + with contextlib.suppress(jpype.JException): + if stdout: + System.setOut(null) + if stderr: + System.setErr(null) + + try: + yield + finally: + with contextlib.suppress(jpype.JException): + System.setOut(out) + System.setErr(err) + + +class PyhidraLauncher: + """ + Base pyhidra launcher + """ + + def __init__(self, verbose): + self.verbose = verbose + self.java_home = None + self.class_path = [str(UTILITY_JAR)] + self.vm_args = _jvm_args() + self.layout = None + self.args = [] + + def add_classpaths(self, *args): + """ + Add additional entries to the classpath when starting the JVM + """ + self.class_path += args + + def add_vmargs(self, *args): + """ + Add additional vmargs for launching the JVM + """ + self.vm_args += args + + @classmethod + def _report_fatal_error(cls, title: str, msg: str) -> NoReturn: + sys.exit(f"{title}: {msg}") + + @classmethod + def _update(cls): + ext = CURRENT_APPLICATION.extension_path / "pyhidra" / "extension.properties" + if ext.exists(): + details = ExtensionDetails(ext) + if details.pyhidra < __version__: + # delete the existing extension so it will be up-to-date + try: + shutil.rmtree(ext.parent) + except: # pylint: disable=bare-except + title = "Plugin Update Failed" + msg = f"Could not delete existing plugin at\n{ext.parent}" + logging.exception(msg) + cls._report_fatal_error(title, msg) + + @classmethod + def check_ghidra_version(cls): + """ + Checks if the currently installed Ghidra version is supported. + The launcher will report the problem and terminate if it is not supported. + """ + if CURRENT_GHIDRA_VERSION < MINIMUM_GHIDRA_VERSION: + cls._report_fatal_error( + "Unsupported Version", + textwrap.dedent(f"""\ + Ghidra version {CURRENT_GHIDRA_VERSION} is not supported + The minimum required version is {MINIMUM_GHIDRA_VERSION} + """).rstrip() + ) + + def start(self): + """ + Starts Jpype connection to Ghidra (if not already started). + """ + if not jpype.isJVMStarted(): + + self.check_ghidra_version() + + if self.java_home is None: + java_home = subprocess.check_output(_GET_JAVA_HOME, encoding="utf-8") + self.java_home = Path(java_home.rstrip()) + + jpype.startJVM( + str(self.java_home / "bin" / "server" / "jvm.dll"), + *self.vm_args, + ignoreUnrecognized=True, + convertStrings=True, + classpath=self.class_path + ) + imports.registerDomain("ghidra") + + from ghidra import GhidraLauncher + + self._update() + + self.layout = GhidraLauncher.initializeGhidraEnvironment() + + from pyhidra.java.plugin import install + + install() + + # import properties to register the property customizer + from . import properties as _ + + self._launch() + + def _launch(self): + pass + + @staticmethod + def has_launched() -> bool: + """ + Checks if jpype has started and if Ghidra has been fully initialized. + """ + if not jpype.isJVMStarted(): + return False + + from ghidra.framework import Application + return Application.isInitialized() + + +class DeferredPyhidraLauncher(PyhidraLauncher): + """ + PyhidraLauncher which allows full Ghidra initialization to be deferred. + initialize_ghidra must be called before all Ghidra classes are fully available. + """ + + def __init__(self, verbose=False): + super().__init__(verbose) + + def initialize_ghidra(self, headless=True): + """ + Finished Ghidra initialization + + :param headless: whether or not to initialize Ghidra in headless mode. + (Defaults to True) + """ + from ghidra import GhidraRun + from ghidra.framework import Application, HeadlessGhidraApplicationConfiguration + with _silence_java_output(not self.verbose, not self.verbose): + if headless: + config = HeadlessGhidraApplicationConfiguration() + Application.initializeApplication(self.layout, config) + else: + GhidraRun().launch(self.layout, self.args) + + +class HeadlessPyhidraLauncher(PyhidraLauncher): + """ + Headless pyhidra launcher + """ + + def __init__(self, verbose=False): + super().__init__(verbose) + + def _launch(self): + from ghidra.framework import Application, HeadlessGhidraApplicationConfiguration + with _silence_java_output(not self.verbose, not self.verbose): + config = HeadlessGhidraApplicationConfiguration() + Application.initializeApplication(self.layout, config) + + +def _popup_error(header: str, msg: str) -> NoReturn: + import tkinter.messagebox + tkinter.messagebox.showerror(header, msg) + sys.exit(msg) + + +class GuiPyhidraLauncher(PyhidraLauncher): + """ + GUI pyhidra launcher + """ + + def __init__(self, verbose=False): + super().__init__(verbose) + + @classmethod + def _report_fatal_error(cls, title: str, msg: str) -> NoReturn: + _popup_error(title, msg) + + @staticmethod + def _get_thread(name: str): + from java.lang import Thread + for t in Thread.getAllStackTraces().keySet(): + if t.getName() == name: + return t + return None + + def _launch(self): + import ctypes + from ghidra import GhidraRun + + if sys.platform == "win32": + appid = ctypes.c_wchar_p(CURRENT_APPLICATION.name) + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid) + GhidraRun().launch(self.layout, self.args) + t = GuiPyhidraLauncher._get_thread("main") + if t is not None: + try: + t.join() + except RuntimeError as e: + if "java.lang.InterruptedException" not in e.args: + raise diff --git a/pyhidra/properties.py b/pyhidra/properties.py new file mode 100644 index 0000000..720c895 --- /dev/null +++ b/pyhidra/properties.py @@ -0,0 +1,37 @@ +import keyword +import logging + +import jpype + + +# pylint: disable=no-member, too-few-public-methods +@jpype.JImplementationFor("java.lang.Object") +class _JavaObject: + + def __jclass_init__(self: jpype.JClass): + try: + if isinstance(self, jpype.JException): + # don't process any exceptions + return + exposer = jpype.JClass("dc3.pyhidra.plugin.PythonFieldExposer") + if exposer.class_.isAssignableFrom(self.class_): + return + utils = jpype.JClass("dc3.pyhidra.property.PropertyUtils") + for prop in utils.getProperties(self.class_): + field = prop.field + if keyword.iskeyword(field): + field += '_' + if field == "class_": + continue + fget = prop.fget if prop.hasGetter() else None + fset = prop.fset if prop.hasSetter() else None + self._customize(field, property(fget, fset)) + + # allowing any exception to escape here may cause the jvm to terminate + # pylint: disable=bare-except + except: + logger = logging.getLogger(__name__) + logger.error("Failed to add property customizations for %s", self, exc_info=1) + + def __repr__(self): + return str(self) diff --git a/pyhidra/script.py b/pyhidra/script.py new file mode 100644 index 0000000..76cf563 --- /dev/null +++ b/pyhidra/script.py @@ -0,0 +1,199 @@ +import importlib +import importlib.machinery +import importlib.util +import inspect +import logging +import sys +import traceback +from collections.abc import ItemsView, KeysView +from jpype import JClass, JImplementationFor + + +_NO_ATTRIBUTE = object() + + +class _StaticMap(dict): + + __slots__ = ('script',) + + def __init__(self, script: "PyGhidraScript"): + super().__init__() + self.script = script + + def __getitem__(self, key): + res = self.get(key, _NO_ATTRIBUTE) + if res is not _NO_ATTRIBUTE: + return res + raise KeyError(key) + + def get(self, key, default=None): + res = self.script.get_static(key) + return res if res is not _NO_ATTRIBUTE else default + + def __iter__(self): + yield from self.script + + def keys(self): + return KeysView(self) + + def items(self): + return ItemsView(self) + + +class _JavaProperty(property): + + def __init__(self, field): + super().__init__() + self._field = field + + def __get__(self, obj, cls): + return self._field.fget(obj) + + def __set__(self, obj, value): + self._field.fset(obj, value) + + +#pylint: disable=too-few-public-methods +@JImplementationFor("dc3.pyhidra.plugin.PythonFieldExposer") +class _PythonFieldExposer: + + #pylint: disable=no-member + def __jclass_init__(self): + exposer = JClass("dc3.pyhidra.plugin.PythonFieldExposer") + if self.class_ == exposer: + return + try: + for k, v in exposer.getProperties(self.class_).items(): + self._customize(k, _JavaProperty(v)) + # allowing any exception to escape here may cause the jvm to terminate + # pylint: disable=bare-except + except: + logger = logging.getLogger(__name__) + logger.error("Failed to add property customizations for %s", self, exc_info=1) + + +class _GhidraScriptModule: + + def __init__(self, spec: importlib.machinery.ModuleSpec): + super().__setattr__("__dict__", spec.loader_state["script"]) + + def __setattr__(self, attr, value): + if hasattr(self, attr): + raise AttributeError(f"readonly attribute {attr}") + super().__setattr__(attr, value) + + +class _GhidraScriptLoader(importlib.machinery.SourceFileLoader): + + def __init__(self, script: "PyGhidraScript", spec: importlib.machinery.ModuleSpec): + super().__init__(spec.name, spec.origin) + spec.loader_state = {"script": script} + + def create_module(self, spec: importlib.machinery.ModuleSpec): + return _GhidraScriptModule(spec) + + +# pylint: disable=missing-function-docstring +class PyGhidraScript(dict): + """ + Python GhidraScript Wrapper + """ + + def __init__(self, jobj=None): + super().__init__() + if jobj is None: + jobj = JClass("dc3.pyhidra.plugin.PyScriptProvider").PyhidraHeadlessScript() + self._script = jobj + + # ensure the builtin set takes priority over GhidraScript.set + super().__setitem__("set", set) + + # ensure that GhidraScript.print is used for print + # so the output goes to the expected console + super().__setitem__("print", self._print_wrapper()) + + super().__setitem__("__this__", self._script) + + def _print_wrapper(self): + def _print(*objects, sep=' ', end='\n', file=None, flush=False): + if file is None: + file = self._script.writer + print(*objects, sep=sep, end=end, file=file, flush=flush) + _print.__doc__ = print.__doc__ + return _print + + def __missing__(self, k): + attr = getattr(self._script, k, _NO_ATTRIBUTE) + if attr is not _NO_ATTRIBUTE: + return attr + raise KeyError(k) + + def __setitem__(self, k, v): + attr = inspect.getattr_static(self._script, k, _NO_ATTRIBUTE) + if attr is not _NO_ATTRIBUTE and isinstance(attr, property): + setattr(self._script, k, v) + else: + super().__setitem__(k, v) + + def __iter__(self): + yield from super().__iter__() + yield from dir(self._script) + + def get_static(self, key): + res = self.get(key, _NO_ATTRIBUTE) + if res is not _NO_ATTRIBUTE: + return res + return inspect.getattr_static(self._script, key, _NO_ATTRIBUTE) + + def get_static_view(self): + return _StaticMap(self) + + def set(self, state, monitor, writer): + """ + see GhidraScript.set + """ + self._script.set(state, monitor, writer) + + def run(self, script_path: str = None, script_args: list = None): + """ + Run this GhidraScript + + :param script_path: The path of the python script + :param script_args: The arguments for the python script + """ + sf = self._script.getSourceFile() + if sf is None and script_path is None: + return + if script_path is None: + script_path = sf.getAbsolutePath() + script_args = self._script.getScriptArgs() + + if script_args is None: + script_args = [] + + orig_argv = sys.argv + try: + # Temporarily set command line arguments. + sys.argv = [script_path] + list(script_args) + + spec = importlib.util.spec_from_file_location('__main__', script_path) + spec.loader = _GhidraScriptLoader(self, spec) + m = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(m) + # pylint: disable=bare-except + except: + # filter the traceback so that it stops at the script + exc_type, exc_value, exc_tb = sys.exc_info() + i = 0 + tb = traceback.extract_tb(exc_tb) + for fs in tb: + if fs.filename == script_path: + break + i += 1 + ss = traceback.StackSummary.from_list(tb[i:]) + e = traceback.TracebackException(exc_type, exc_value, exc_tb) + e.stack = ss + self._script.printerr(''.join(e.format())) + finally: + sys.argv = orig_argv diff --git a/pyhidra/version.py b/pyhidra/version.py new file mode 100644 index 0000000..b29fa5b --- /dev/null +++ b/pyhidra/version.py @@ -0,0 +1,92 @@ +import functools +import re +from itertools import starmap +from pathlib import Path +from typing import NamedTuple, Union + +from pyhidra import __version__ +from pyhidra.constants import GHIDRA_INSTALL_DIR + +_APPLICATION_PATTERN = re.compile(r"^application\.(\S+?)=(.+)$") +_APPLICATION_PATH = GHIDRA_INSTALL_DIR / "Ghidra" / "application.properties" + + +# this is not a NamedTuple as the fields may change +class ApplicationInfo: + """ + Ghidra Application Properties + """ + revision_ghidra_src: str = None + build_date: str = None + build_date_short: str = None + name: str + version: str + release_name: str + layout_version: str = None + gradle_min: str = None + java_min: str = None + java_max: str = None + java_compiler: str = None + + def __init__(self): + for line in _APPLICATION_PATH.read_text(encoding="utf8").splitlines(): + match = _APPLICATION_PATTERN.match(line) + if not match: + break + attr = match.group(1).replace('.', '_').replace('-', '_') + value = match.group(2) + super().__setattr__(attr, value) + + def __setattr__(self, *attr): + raise AttributeError(f"cannot assign to field '{attr[0]}'") + + def __delattr__(self, attr): + raise AttributeError(f"cannot delete field '{attr}'") + + @property + def extension_path(self) -> Path: + """ + Path to the user's Ghidra extensions folder + """ + root = Path.home() / f".{self.name.lower()}" + return root / f"{root.name}_{self.version}_{self.release_name}" / "Extensions" + + +CURRENT_APPLICATION = ApplicationInfo() +CURRENT_GHIDRA_VERSION = CURRENT_APPLICATION.version +MINIMUM_GHIDRA_VERSION = "10.1.1" + + +def _properties_wrapper(cls): + @functools.wraps(cls) + def wrapper(ext: Union[Path, dict] = None): + if isinstance(ext, dict): + return cls(**ext) + def cast(key, value): + # __annotations__ is created for NamedTuple since its first implementation + return cls.__annotations__[key](value) + + if ext is None: + return cls() + lines = ext.read_text().splitlines() + args = tuple(starmap(cast, map(lambda l: l.split('='), lines))) + return cls(*args) + return wrapper + + +@_properties_wrapper +class ExtensionDetails(NamedTuple): + """ + Python side ExtensionDetails + """ + + name: str = "pyhidra" + description: str = "Native Python Plugin" + author: str = "Department of Defense Cyber Crime Center (DC3)" + createdOn: str = "" + version: str = CURRENT_GHIDRA_VERSION + pyhidra: str = __version__ + + def __repr__(self): + cls = self.__class__ + return '\n'.join(starmap(lambda i, k: f"{k}={self[i]}", enumerate(cls.__annotations__))) diff --git a/pyhidra/win_shortcut.py b/pyhidra/win_shortcut.py new file mode 100644 index 0000000..62612df --- /dev/null +++ b/pyhidra/win_shortcut.py @@ -0,0 +1,81 @@ +import struct +import sys +import sysconfig +from pathlib import Path +from pyhidra.constants import GHIDRA_INSTALL_DIR +from pyhidra.version import CURRENT_APPLICATION + + +# creating a shortcut with the winapi to have a set app id is trivial right? + + +def create_shortcut(link: Path): + if not link.is_absolute(): + link = link.absolute() + link = link.with_suffix(".lnk") + if link.exists(): + sys.exit(f"{link} already exists") + + import ctypes + import ctypes.wintypes + + class _GUID(ctypes.wintypes.DWORD * 4): + def __init__(self, guid: str) -> None: + ctypes.oledll.ole32.CLSIDFromString(guid, ctypes.byref(self)) + + class _PROPERTYKEY(ctypes.wintypes.DWORD * 5): + def __init__(self, key: str, pid: int) -> None: + ctypes.oledll.ole32.IIDFromString(key, ctypes.byref(self)) + self[-1] = pid + + _PropertyVariant = struct.Struct(f"B7xP{ctypes.sizeof(ctypes.c_void_p())}x") + _AppUserModelId = _PROPERTYKEY("{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}", 5) + _CLSID_ShellLink = _GUID("{00021401-0000-0000-C000-000000000046}") + _IID_IShellLinkW = _GUID("{000214F9-0000-0000-C000-000000000046}") + _IID_IPersistFile = _GUID("{0000010B-0000-0000-C000-000000000046}") + _IID_IPropertyStore = _GUID("{886d8eeb-8cf2-4446-8d02-cdba1dbdcf99}") + + _CLSCTX_INPROC_SERVER = 1 + _COINIT_APARTMENTTHREADED = 2 + _COINIT_DISABLE_OLE1DDE = 4 + _VT_LPWSTR = 31 + _APP_ID = CURRENT_APPLICATION.name + + WINFUNCTYPE = ctypes.WINFUNCTYPE + _CoCreateInstance = ctypes.oledll.ole32.CoCreateInstance + _QueryInterface = WINFUNCTYPE(ctypes.HRESULT, _GUID, ctypes.c_void_p)(0, "QueryInterface") + _Release = WINFUNCTYPE(ctypes.HRESULT)(2, "Release") + _Save = WINFUNCTYPE(ctypes.HRESULT, ctypes.c_wchar_p, ctypes.wintypes.BOOL)(6, "Save") + _SetPath = WINFUNCTYPE(ctypes.HRESULT, ctypes.c_wchar_p)(20, "SetPath") + _SetDescription = WINFUNCTYPE(ctypes.HRESULT, ctypes.c_wchar_p)(7, "SetDescription") + _SetIconLocation = WINFUNCTYPE(ctypes.HRESULT, ctypes.c_wchar_p)(17, "SetIconLocation") + _SetValue = WINFUNCTYPE(ctypes.HRESULT, ctypes.c_void_p, ctypes.c_void_p)(6, "SetValue") + + link = str(link) + target = Path(sysconfig.get_path("scripts")) / "pyhidraw.exe" + icon = str(GHIDRA_INSTALL_DIR / "support" / "ghidra.ico") + p_link = ctypes.c_void_p() + p_file = ctypes.c_void_p() + p_store = ctypes.c_void_p() + p_app_id = ctypes.wintypes.LPCWSTR(_APP_ID) + ctypes.oledll.ole32.CoInitializeEx(None, _COINIT_APARTMENTTHREADED | _COINIT_DISABLE_OLE1DDE) + try: + ref = ctypes.byref(p_link) + _CoCreateInstance(_CLSID_ShellLink, None, _CLSCTX_INPROC_SERVER, _IID_IShellLinkW, ref) + _SetPath(p_link, ctypes.c_wchar_p(str(target))) + _SetDescription(p_link, p_app_id) + _SetIconLocation(p_link, ctypes.c_wchar_p(icon)) + _QueryInterface(p_link, _IID_IPropertyStore, ctypes.byref(p_store)) + value = _PropertyVariant.pack(_VT_LPWSTR, ctypes.cast(p_app_id, ctypes.c_void_p).value) + value = (ctypes.c_byte * len(value))(*value) + _SetValue(p_store, ctypes.byref(_AppUserModelId), ctypes.byref(value)) + _QueryInterface(p_link, _IID_IPersistFile, ctypes.byref(p_file)) + _Save(p_file, ctypes.c_wchar_p(link), True) + finally: + if p_file: + _Release(p_file) + if p_link: + _Release(p_link) + if p_store: + _Release(p_store) + ctypes.oledll.ole32.CoUninitialize() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..547dd1c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,40 @@ +[metadata] +name = pyhidra +author = DC3 +version = attr:pyhidra.__version__ +description = Native CPython for Ghidra +long_description_content_type = text/markdown +long_description = file:README.md +license = MIT +license_files = + LICENSE + NOTICE +url = https://github.com/Defense-Cyber-Crime-Center/pyhidra +keywords = ghidra +platform = any +classifiers = + Development Status :: 3 - Alphas + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + +[options] +python_requires = >= 3.7 +packages = find: +zip_safe = False +include_package_data = True +install_requires = + Jpype1>=1.3.0 + +[options.entry_points] +console_scripts = + pyhidra = pyhidra.__main__:main +gui_scripts = + pyhidraw = pyhidra.gui:gui + +[tool:pytest] +testpaths = tests diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..26596b2 --- /dev/null +++ b/setup.py @@ -0,0 +1,5 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from setuptools import setup + +setup() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..a034f7f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,14 @@ + +import pathlib +import shutil + +import pytest + + +@pytest.fixture +def strings_exe(tmpdir): + """Creates and returns a copy of the strings.exe file in a temporary directory.""" + orig_path = pathlib.Path(__file__).parent / "strings.exe" + new_path = tmpdir / "strings.exe" + shutil.copy(orig_path, str(new_path)) + return new_path diff --git a/tests/example_script.py b/tests/example_script.py new file mode 100644 index 0000000..9f49e0a --- /dev/null +++ b/tests/example_script.py @@ -0,0 +1,13 @@ +import sys + +if __name__ == '__main__': + print(" ".join(sys.argv)) + print(currentProgram) + assert currentProgram.name == "strings.exe" + assert currentProgram.listing + assert currentProgram.changeable + assert toAddr(0).offset == 0 + assert monitor is not None + assert hasattr(__this__, "currentAddress") + assert currentSelection is None + assert currentHighlight is None diff --git a/tests/programless_script.py b/tests/programless_script.py new file mode 100644 index 0000000..791d6aa --- /dev/null +++ b/tests/programless_script.py @@ -0,0 +1,5 @@ + +if __name__ == "__main__": + assert currentProgram is None + assert state.getProject() is not None + print("programless_script executed successfully") diff --git a/tests/projectless_script.py b/tests/projectless_script.py new file mode 100644 index 0000000..04087b6 --- /dev/null +++ b/tests/projectless_script.py @@ -0,0 +1,5 @@ + +if __name__ == "__main__": + assert currentProgram is None + assert state.getProject() is None + print("projectless_script executed successfully") diff --git a/tests/strings.c b/tests/strings.c new file mode 100644 index 0000000..f8bf932 --- /dev/null +++ b/tests/strings.c @@ -0,0 +1,76 @@ +#include +#include + +char string01[] = "Idmmn!Vnsme "; +char string02[] = "Vgqv\"qvpkle\"ukvj\"ig{\"2z20"; +char string03[] = "Wkf#rvj`h#aqltm#el{#ivnsp#lufq#wkf#obyz#gld-"; +char string04[] = "Keo$mw$wpvkjc$ej`$ehwk$cmraw$wle`a*"; +char string05[] = "Dfla%gpwkv%mji`v%lk%rjji%fijqm+"; +char string06[] = "Egru&ghb&biau&cgen&ngrc&rnc&irnct("; +char string13[] = "\\cv}3g{v3pargv3qfg3w|}4g3qavrx3g{v3t\x7fr``="; +char string17[] = "C\x7frer7c\x7fr7q{xxs7zve|7~d7cry7~yt\x7frd9"; +char string1a[] = "+()./,-\"#*"; +char string23[] = "`QFBWFsQL@FPPb"; +char string27[] = "tSUdFS"; +char string40[] = "\x01\x13\x10n\x0e\x05\x14"; +char string46[] = "-\",5 , v,tr4v,trv4t,v\x7f,ttt"; +char string73[] = "@AKJDGBA@KJGDBJKAGDC"; +char string75[] = "!\x1d\x10U\x05\x14\x06\x01U\x02\x1c\x19\x19U\x19\x1a\x1a\x1eU\x17\x07\x1c\x12\x1d\x01\x10\x07U\x01\x1a\x18\x1a\x07\x07\x1a\x02["; +char string77[] = "4\x16\x05\x04W\x16\x19\x13W\x15\x02\x04\x04\x12\x04W\x04\x03\x16\x1b\x1b\x12\x13W\x1e\x19W\x04\x16\x19\x13W\x13\x05\x1e\x11\x03\x04Y"; +char string7a[] = ".\x12\x1fZ\x10\x1b\x19\x11\x1f\x0eZ\x12\x0f\x14\x1dZ\x15\x14Z\x0e\x12\x1fZ\x18\x1b\x19\x11Z\x15\x1cZ\x0e\x12\x1fZ\r\x13\x1e\x1fZ\x19\x12\x1b\x13\x08T"; +char string7f[] = "LMFOGHKNLMGFOHKFGNLKHNMLOKGNKGHFGLHKGLMHKGOFNMLHKGFNLMJNMLIJFGNMLOJIMLNGFJHNM";; + + + +void encrypt(char *s, char key) +{ + while (*s) + *s++ ^= key; +} + +void decrypt() +{ + encrypt(&string01[0], 0x01); + encrypt(&string02[0], 0x02); + encrypt(&string03[0], 0x03); + encrypt(&string04[0], 0x04); + encrypt(&string05[0], 0x05); + encrypt(&string06[0], 0x06); + encrypt(&string13[0], 0x13); + encrypt(&string17[0], 0x17); + encrypt(&string1a[0], 0x1a); + encrypt(&string23[0], 0x23); + encrypt(&string27[0], 0x27); + encrypt(&string40[0], 0x40); + encrypt(&string46[0], 0x46); + encrypt(&string73[0], 0x73); + encrypt(&string75[0], 0x75); + encrypt(&string77[0], 0x77); + encrypt(&string7a[0], 0x7a); + encrypt(&string7f[0], 0x7f); +} + +int main() +{ + decrypt(); + printf("%s\n", string01); + printf("%s\n", string02); + printf("%s\n", string03); + printf("%s\n", string04); + printf("%s\n", string05); + printf("%s\n", string06); + printf("%s\n", string13); + printf("%s\n", string17); + printf("%s\n", string1a); + printf("%s\n", string23); + printf("%s\n", string27); + printf("%s\n", string40); + printf("%s\n", string46); + printf("%s\n", string73); + printf("%s\n", string75); + printf("%s\n", string77); + printf("%s\n", string7a); + printf("%s\n", string7f); + + return 0; +} diff --git a/tests/strings.exe b/tests/strings.exe new file mode 100644 index 0000000000000000000000000000000000000000..11e60a76ca1554dc65828d1f49e00c4554828e46 GIT binary patch literal 49152 zcmeFae|%KMxj%k3yGc&MhFxR>K>`GciUt*3qQoV-KoWuy*cjQcN&s&W*VI;oa{w!W z#FMo-8CL17-r8Gfp;15d*0$cOC|1EN!E8X~XH^ucvCY+qOEr`<2@rF>?`O_#612Ua z`+eQd_x1h$@y+YCXMR01^UO0d&ph+YGn2Brwn|n>l5F@Hh9vEVOMhPR{jWdmNFF)q z_amj3)83uD+p^%@$#(|tUz@Xf&4c%?x#xR1-?``e-+xfa`KNnx)~Mglx&Qk)MT;tO zzW3nDd#}yNNY9NI9XONvw+UPCe?IYZ@$=756yf0W3*ox>U!G4AZvXQc!u{fTuWR*+lslxX^`vhP5MxSg)J(ho5=C#Vb8x$nF z7(Rp-wKa}6eBv)hl2%;1X5~G~J(Bd~1VnAeZz6tM@f-H%Mb+1uU{dKdNNC3I68wIP z->^TgnKCTq|M&iXodS#5X;v&d_R66HO15L~V!Ih~A7qQ=hI9n9W{X}d$3CiRd58W0 zUu_9W&0dK|48zd&%hgFiCk6i_9<&ChQLrf57n9|ZSP@Mt`^IoL^vF`4lgqE+xtim}FHh+M%X6mv#0%OQ#dmr~3p@fadna0SJ@ z8;>En1y@qc>+u*OT`)*7zlz5Y^@0yj%unJmM8M!`iV4MIh=xIhV%ElEh>XDv6mw5J zhA0`_NHL4zF+|K@EyWbZV~C!?dWy-5#}G+_4HPpj9z#?OZlRb|GsfLq)lzK_9(f5- zt4B+cyV>1SpvsP?nmw)R1nxpqR2l>86F0IXoE$1VXB&j;*ZflFL+QP?K zB`FZl)qdU!sv?d*OEIi3Tx|FG4OQl!#N&1FdA+!UA2!34d@OjTwm&Oc+>cH*I2H|w zA^i%}-NJtbD6Kh%s>l^pwCvd0>Xo!Mi~B%9a_v#CXomJK;^3n z8nP&PoVHtmJ93>96*q8r9ZyB{vW}QdySGCEdQV-63PR1QT%FCokA?(z&*hRNBAmQR zMCg;zn0xyrK5Y`E)X3H2c{NtWfRQO}_exDge*t12Vwq^FM?1I+J^>~3B7)-V(s)D~ zcT)sv{es+3vy$rLIpp2(B62ppjE~RA-wDUaf0oLM_R}=9>&5L_O^2kW`1n2OR^EV5 z>{?cQn#n9pFFWNc=dB=FAkf0@<*fMBF!*@{e;+>aj$*~_{LxX=M{i#tNuAH4^L9VI z&nva;@S++1YS%KfJ$1K~gc?#jtu?8vwTV)yZ9c9LtpZK6{^%tFO{}*0^?XX>cVCo{ zTz%=1Wi9SwuTiVai|*a?VAwlC6`%xs~YQ(lRk z*FK*V`8V6CNPNb{ghpmZ2P?<(|lzNrj@qthyC;W3yr8 zMbJ=D&=o)(quR_Xqq;=iGFdz4La8;Q<9P&{fk~tly$9%hH$GK)fW3CwKF5@7=K^bWFS{T8%6z zR{}#js-kk0aejUdW3FzM-S`p+uF8_MDVHo@5D;${2=BgXuD_n?~i>5ZmKj zous<7($z_*#1czqGD`N^D6w@})jpflGu!S}QnZJGd#*9xwqXPt)sSRZ9IrIT?2f(7 z@*efONAZ)mspc8Tvh;>*SN zi1;>(ZyUb+0gRbYkDAY;_QYc1ZuZe1FGCELliEEm0#H)YS?C3Lv=ytr#tPHfmM%#> zC3dg6%+FthfFJmMFp9y2$vj@`8$jQ*X`fDFU6Fz0$fqe*`!1UGu_Sh|XLjyp^+yCZ zj^JE?YoCN!8fj0l+QMv+;S<0>54Vf&8~8Rn^%cJL z#~k&OfX`fQ4*^K}w*7D%TSg$Rao@C@SxeTBa_rsb*jrn=130!j+&wXyyT{$^*jpb7 zBC0wy(q{|kp(l~e6LI|TH(2J3+>k35&|#t+$h#GezDtm9mmujbLCjrUBA$rZxenP- zkH&M9+ca)fZq&G}TwYqLWFq7URU%mn6TIAc`=H;}dF!Cx(&>i3PQZXuNu9ZaNlBeq z@YkNdTAijvZDv2S&HbJMeGg&d2%nP^rQ3S9QB>#Wh+!e}ez!T@vG-uONIs&AaOXQn z(sxnIIwSB>u~&SvSW0z-e<_L;H0b;nQ6Tw;7br@3M3gdLSzLQQs1((ne?ZBqJ-^-& z`VQjF_DJw`-brAd2h=fh$2QL?%`o2lyiataQ}BNACTO`gh5F&{6*uxvHan54js0^}EPj z;;8=(TzwafhH!``m(TIaKA*m;4PO2WHwJsCxiLf|FQ8n;!Zh*1yJ+O`XD?u!TIZV8 z);BxqpT)|45yYc_=qy9|G*J3hI-cNQ&yM&l3FP z(o#q0pOZnneL2z4R#b&tk~#WKqInBMcZRohBT=mD`nC>u_0V>F9pO?`5DigZr66bL za|rf^h)mN#Uq|R^L=NjG9l@bwHGL?FiTR+NGt}hQh{hN-CZH4L3=T_u57q6c|941_ zo&j~lj_kL*k{+T+Cce=S5tACxxtSzP4yu0yoY2;X^CFJ=D^WJJ`RA1Q?JAV%j#uaP z`+_9DghSM1UpPc;!N>n}J#y@KVuhR(4bgO^N@AlM=k+%Tz^H(uNHe2}RcAgbVyT03 z00N1JmD)Uqlo@98X!ttbl=LvQL2>;wZ{N|rVU6g5_-hW3&i{mW2T4C>$vz&q4kM=X z5Tdod)s81Fpl(O~pYhfD?saT=L&V?bczgye-+lKh4{3ehSEg!x4?05Ik=7%6sn$dr zNJz>(@!mk&V~g2MgE;fe-C*Vzbl06X3^QAfVqe8VG&EP~DKXjf>M28n>3p6DEEta0 ztO7h!Vj`k9BGnSAB;~VIR%z3sa_4&psH1An->2Sd4kv`ASfD@*txYMqaO?KaKx;zE zrdyd9mvyaG0QB?Y*_h3OX@lJo2S-0SO+x9rXr!_ev2?Rw%I@x=K9TiM10~Jt*F#(2 ziBC#H@MO#*LsVf7KA$0q35y&R5VPgO5ET_j#sT99+e0H>+n-(cIa($uIp(BMv*IOI z82K&&FE+sgf!aMZ%kNXOF^Go&ISD9d5Xf_lIx5~LXe4Gojgg8@D>~F{3U!+v-i_E; zuEy6o>R$m{!0@iusn2O`cm)}wV%feDuov|?SR^{(en2(~09$!$=np7rdPC?vIML7% zd@&ZJ&SJoz(~!Xtx}8dn+Ik{6N|s5Po(M!9$D8x|V>X&RKZPJOub(2VKZQVdYzSz) z=l#uNH`+X{Ye)5-(2hyH9}P7wJ{h)RM0~{h)Fk&wP4u-?V;?pgF4Y7F?iGCK;-SH! zBU;py$pY!HdX|atVhE-ZYTlee$;vfM%+a6T;j5_R>{<-a$aL*ofxb-)kUsX&rWC87 zX2ZyZu|479vw5v0al}9eU1ZjGiHUj-##bskR9i4z@f%|9GVyJ-O!vlanZFA0DPFJV zklLLm_|nn7*i^x=h9C&f%;7 zMx)qn2oZ$s5etw=42NlC@O4NxGs^*Gyy>&4msD0%Pd5wU*P110dx#AKZK_mR6>8q} zPfQRlzHGdjVuXq4zUq{4aW{DE(+HIKO87>|+JeifQwv0-l4I*GSvNleZlvv@ z+)_Z@70cFy^k!h#i5@^<#FzL%fCb`$+r|X03gMa)hj^3<+HcqP5WkR=(Z(+F@cW=e z(4G@<0VX8AMr(sumFt@oRPrQ%n2?7Xk92;D_Wm77)?|pRtbh43_q%uVk!aO{maUVr zy;9Dl*ren4Tl~)BmptAp-HzYGNE7M|i1ba*U_Uh|rcVX49um{F=T2abOBK}%!l6^} zRb%hA0>z_E2ZYVA)EVyAZ(c%^o@lok|k_0XCC89OURV_RK?GbQG+1=0zjR)ddcu68^ ziy38)@8|fRke!vu<+{utg*U*u_#1Hb>DbH#Yp?K1D~)_mLt;g_PW^#gXBfY{_6mQc z=<4f>``vH(^n1CX_Oasrxbj6WhGMO2B`si53oGkdsb`QkuzdNlyV-wMm7AJmQ)6nX z#!hFM+q$)~JD)UYl`N&T*nl#po4O+Wh?Dk88Em1=df08B$yZ$kgcjPd(>23>3KwJB zYg^?AjX@S{QNxS5UeV9)hE|E|Wv8_Z1IqI7d?{?Zn=N?E6)t+rsTX%KDEs=L`F61n z_;mCEDz$`~H+nbEW5s>!1Zq_e)z)Cr<+7BEQTX#z69vCO;bwlE!o}TY3tNCVwK#SF zq^&FX&f4#_t5<1-GoiJ?q8lWazl7?v{m}X-4yf9zEq5516c0r)!_N9RMhx^L1xxJ8 zB|(!S$5|VH-;8EueR`?=n<)9dfs$9C)Wu3=wC;Z7{5^)3QBYxb)ZYP5yx6yS9g;e; z5tA5tOsbnHaK2jG;Iyi_%`^8VDKCiCae6zFQ9R|wjt zLa8Taio4kyVwG=lFosGaGZ*s#T2CW02cEhp#nu@szL2E#M1+r*#~BI#l?|IuXh(aJ z;O0M#(ZF|M{0K#7tj%1_Vr0@hPZu>)RDe&)MQf%5+h7(xdtJpyqBTQlhq8 z7Xf?+09xS;Di*6*kUU(Cmus(@~oiqWdL}=ywJ2w)HKrSx=+q6m~aHF z2fIFuOnt&T@HZAae@Ti(Qr|d2+YoQ$Po?6Ep!x&omCj`)!1vMRN zazSysGLp)yIRTX}qZ(MviP$fKUNnFA45Gnu4BbGG)EB#pD7=Wm38nlbAg{Bn1C{&`Nt*lZ7_AxR zfyxwM4-Ht*2JpRS0OPNgT9f~F%!_f7IaACt^@`vIloPy=L=D<9Sb`&=Mf;>zl;P(U zlW}$d9A~YY>E|T~XkU7)t<~1oPX7V<6%>-!2EF5x*ANmeATi3_W1{hlSp7t3huqCoy)emVT)!p2A0aa=jl2yC5LIzjx zc7n?y0;Aw#c-X=fdRZS<8GYdj?D4g8vLm#c;xm?@Vph16zDKR+JRj8Le)oW#ZCa|W zgm#J*isT8DJjeP85b_+~_$OA?&m^{KW#QvE{8!VWB`Y-)6EH{p0 z#SZ8tE1;acd-<{hEn49;Ff9qolZ=BYKg*|6t4cR%d$UM^yB)>MDpV}<{CxLBbXr(R zu{5UCH>+P%+vZjB2QxsBJ{%er(wP;w;M-;i6|p&f{zGJ7C+61PQW#LKueMb1*?5qRKmUP%A}>xCjaY|RtjZzgRJfb{dMf)XTWpV7C|^ObtfYZ+XvMNk9}R_h?Jbw7 zPIq%basNw~Ii74TK~!v(ks3Ot$c-rtNptkY_Qr{hS6Z;EIR>;bda=bwr6mEQs?6S) zViemOEG4J_)zzPbO24LGQh!x@AxBBBy^yB<0wvj6=NRJf5G`}mcLI&Zlvz27ZC09^ zQW876F!pDWa0ChBz$6XQnn2KLSiqg#2VzMjvukfzs+=Qiujq+bFoRHyVh@*CYG+=C zk=_-{!1?bFdWPD9K2#P99A%(z`>Zr&Ug)G!h+_>O zfT!@kTx~WoqcH`|GaF~54z)Cv!4JkktR+S&k`~)bI{jFLkv+9FNg|3-2509;M4~0j z?gq`QT8?B`;!?hL+Fs#4>8~u9K!XrtupaBM$w#}u?)()E6T066QxRNr2c4|y`OWBD zPm7X@w$02^tvIb_i~ETKRaAs)x_LVI28KAeULk6|b?t4etQ%^`wR2cs?pb1uEtj}{nu}6~$f`(dbijlbhAX?uBHN84%NfRaDBvh1u#VABm-*Z8A zm{|pDI*2G1T~#ni8R_+|D#%jLna*EXekqEosHn(`Rvhi~`YJ0b^x|&geF*5F%5^^e zbQ<~%#6f6sMFln-K11yWl<_|Jt=+)k{$jK}04@lg1bMRv#j%#yQmrpZv7rHv8kt^- z>hhd*)UN>zXHHboy&&}k`t;+PMLxywM7U(Cs^Abws(5xdfEL44TE%cGgN~$Dd7#e3oLJVIGwuBcw>^ zHcD*4?GEe)OYA;=?lKd3NBsdb*C>%u^<1sE!&Xq!zVXNIlk6;&kWZwdZIYpkpzT{q z?Lu2^Y)xy4tqvFM!s<-c z4Do@2naJPn5~8Vpck@eLX)k>$G4Wd0Iv6c4v@MC+JOgXaU~z!hxx0;^=<{Dd!TQ8+ zOJd#t^3k zA}cr*Nkh9WnYK4aU?)c05N0eA{~?;1*i-%As=EIlRQYOqM%K@ zRhiUelCaeA&}F=jnR+iF2U$G<@Bjg{E^8U2$QV+b6QqYALGlcum9c}UL=+o|rmsdO zEkn?)^E1q)YJ1Q}Y@ijV{@|jS^DDtLhl>%^i3q_=9Ns>IbQnhT1`4B8k3fb?ako7=d$JAG;mlxzUo~i8iRFGWfp|TcJIku`Xz2Dkvne zF}elt)pq_c0e~+-p`e#M0B$MoAnOA7jdc%jA5=qwTPf_0U|@$9v;aNeisJ7fJ^?!{ zzC?IHS4<0H`!L}FIk3nrwzcv(G z`AW@Drx{vWNL@*loS+Dh%T83H-e8-8;31cy3AJbfsP3pUsK=DlO8#pCPV`cx*-OV$ z%{X)}@;-pPQ!4qKQHb;u3S$sgogt&!6gx~RxD2~bk76D1%?7&_(dpgGw@DLZH$MUyGxksHN=h)9vw zoXSn{Ip?T<2Te35u#Xs+qn=C~c^xK}kx$wAXl&|D0D&pDiZInGYT{>cE@;l{CNUEK zj4+h=qB4F0osF)&H@JT$nqn4MhypNFn-Yw&G%>o%i9+V+o^kQ$PB*ilr13PmKc^rp z=!Ql&7=A33|1;uYOBCPhlXNau!&tC6NT*j|F7-qnJ21z%Nvdi=rX~q%SzMo`opzdr zhWGXfsk@@`&4r(NUk5#4zY$e3LM=*K?~$e_TOp3E@RJN_WIlruncDAG`;5$|;dghL zSdsgr2t#KJxUB?N-0d#za(8)p)zrOIBp2m|A0w@w$BZPglXZccss~AsiZ_iE9;A?g zKqvDZq9oer|F_F(`9@jslGN)2PK4C7f?~V!-^46pmIl_oofu4v#~F;GVpyX;tm@oq z8y`n}f~LHmUve3|VDsqzG{29#DL6Psp&-Kc2L_Hep>eTvdd+5)wTkx!j%WNrbK z(B)(JO;i=Sh8V4nfBrdC@+krSJH)nBJA=)HO%(#}lX_JL*zLbiK@febt;_YAc26PM zE7nx9-pFT3!Z6X=T9Bm_F{#IE*A$13_f7{qtz=fCxI;5s8>b2|W=k=R^;V!-5o63q z;a?MrHtid;Q|85fo?u*9RxngL_&9-;h2NA!tr24Zy9ZTfC3eEA^vi4k(>iw0B zS)&v{YhTH3$pfd^G_BI-=W7UcA9u|JarWCiQJ8|2*z21gc4+Ni>m_#V?-$y0w6~4r z$f&KdK^9!bS`yY_R1Fq(tQh$~CJ?*M&o^R{lZ=u~8|6u4mFGpZE$YFpHaVcXw!!>7*_@~bgji)Qg(jzvBqp1njH$lZ7hd1)D&l!@rkuqbIRYjn+2VEJ{YDalDcrf#Mkkj{&V!t{b5BZsREdxm;+~ z(+E7OmT+Z;3y9-+OXVvS50RS4^Ec^CR-&3{tgK=s)H4ZsANDry$5#WGht zkstaZp;~id90mdy5#bpE;ev^ZjYVVMug>Ck6CbAitLosVI$$***+C*SsJuFz2Z&mj zJOvZgX@VG){HJ3~La1Xa`2gkR&kyFpjEjxXZ;4?E{xc;ddLuC%(t&IXik%v#J*026 z%pW$Jra|{J+Q%=WL=wfSM{yfQUck8V?^xsx0Nrau#Z)-y_x6fim@T12<-wRfihS{M&~zx+&2nfC zS((>~9M?RH`J-H>ahoz$;~9#*RI%caXSWU9uVwco;%mdOZP`7F{50lRSCDj4ZX;Zh zU5WGGk<23^t3DDLMMdUznbZvOsSBP-D(&l)@>SiE+Ane%oH1*wB+d35UbBy^2k4OdD3&)U#O=YUG-I1Tg^9v^=sa294q?JPZ?!T( z+x&o}jAiSCVA(QM5F;>nA;I$#0B(WoOuWOCo!19-wZoGy?3{1J9^z6+5QAd0U@HnX zRV#5o5!gXM`n(_$(8vbR;hP`~4k`9HQ#(+>Q#4;qW&;@tD!PhrSr@4jbJuKB#q;9t|p&T2r`8J)tv5`M?4QOI;J07V-AcOWKQ%zfCC(7d21 zH=@KO4bRPnz_KSDgZ9`Q3~~Jto4(dwu$Vg@>q8Z_HD99m!|;XGuNp_zA8^#~L%{J$ z2`0;d&gbDDR?IpS!^%2P%y$n=$NCxm_blopwIgp|I@$Yf~-CP8@a#McH; zRIMy~=d0(~TpPaxIZ&itA;+vbOcG3%Wpb)Vh*QOO7Ua_yafFtjT#{oG7Bpm7j7i`M zmeM46{bQt-wHw?EpIugv;pm7bLFOu!nHTm!gWLO z-l6p+*EmdZS#YL z{IFM2g6y#u5r>FnP+Q-{9@}k(#v+6T{lr-erdKEz|LjH*9@t|qUj*>#5J0T}FkYeZ z_%DXypM;k(Hk%oXP_majwjZHuXo&jwS^%)eXe0b)Ek1csEUF^vNIZ0hD6APFJ`Y~( zZo_;3fDlV9oJx%tTktetr9$LfH3K~4CY}SZKp?!y7cG{d9+2ub_erE>7{yZ2)Db`d z%8d)8_bXW5j|(aDpi56gcTltzb>dcs8ujz{CMugI_7I4+#>Km(bV8%DiRe9p+jhpc7tSi zLX9TN|7&Q$5Go3b7s$>x0e!chP51OFQ!DkEJVK)qE8ciNr*j90ivehk288AZOJ0EH zK8xbwSu?G*L1Hs^euTC5kC290m$}xKtcIMn32SXOZ>8wf55yPTf5p6@1@}JWAeG*7 zVh%Lp!zB+ep=ILtqf*TBi{@Jdpmzzrq;{B-q#PSByPv;;{CY7AvFw4JkE&yN-G?;q zNDvj9HXO5Fhp7d7&B;HJO9~LLZrjVQmjmC%R#Lq^gfIOK{~D&PsbHw zSOm9p*wM&&^PbLAbgmFf-W|kN1x9B%3hs_oRFw1Fd`X(J50j2Id6%Qs#xl7WDXm(MvE4q#dV<>p?*c8j}(43cbLO0zc&U3Hd&8oU50y~7O}xl z*HWi&HoG4=Z>aB3a(@NB0INedlA$EU-VM(-sFu7)OL+FcpzrHJUw_;uHvS}5(bxd% zWHnCDS=Hsau+E9bfomk`SrBGu{+VGb&bsO_+>FiZ#`BU7u#%5ih66wx{7vVFvD1Ye z2y2buer^^?X%X}|1gwH3E=TAYl(C8xyI!L+nN?WQZ^VJ812v7PtnhQvpF{NEXHiB} zx*8yy!^?yuhl5)o$%&&oJH z2h{G~v#bkHxB`b1XYXg$qiaTb0JmOVUE0DtB#gf^*X2G5pA4V9_aoTke^S?7>RGnW3HyoulV0pOF)Ktx7_B&~zu(>5h+NpGjj20NeYIw!R@hE}N^<9HtbUd3 zoOa%}=K9_bwNI^zJ@04(E?Zc%PK(;Z+9K_;HA0RP&Oz-@)<|cPl5+OfC^+4O`-0$5 z!SWV-ZKxbpv8bM?;sSND7T@73FAtt7AGD!0W&Zs-@~qS^M@NuV?Q~e~0D78x99wP2 z{sZ@J?#!K`ogcuV^@NS`b**n`ttNLz&Q{03u>_OdadqSU>b!Sd%_SYo6+Pe+{Hsf!iRnrSU~q`Kt$l z4On#GI%NNQOOMsf|s;UX8M_(Y0~ z6c6*}M);)OBU-y9z7h|&qZVe=xRp&8<1OxEh3NEV(ezr2^Sr!Ztn7wDSWmX?H+|a`qn0~ zs5J^rg#+tkzgY8xS?rkbJSSqkA~qK*p-@m>AGweO+q|dwM1d4ptrFjIB7{}4)#m5P zB7rPdLFND{dj-qK(pqhU?%{DL-N;@EXAk{ zuA@Cr{pJKYAzc8Z<4WxlWE6H`#O4J`CTpoq3gktU%kY!&TbUP8i}+>KXXriH{NnOB zWVd_3H1-mTJLnTzVXR-uj*HhKaBkBovy|9ftVQDvWtPTMl)O@io3)1%8?S@`XbNJm zD#d=gm(bfmpV*ka)aD)ZiKXwQR_~xsw77%Vl#jpl35k{M0e3TY{jgd*fmj#`tEsTC ztoj3N*rD&nu4D%^zkq?{rd^PgbTkf1{|h#CJX-_{$3N)vRv;cbs6{LFvLn%XE4QE+ zin-Rbbi|7iEJGMOW-bAttp^LrPSuQKWv9sKv2v+iGzwNFr(!4hHH1z|qMI}0rA#u` zudFZ@w+CY5Kv@_p)6ouWjA1CxQWZ>77LpFETpC^RCPL5HCs z(Dcs5!6PDw10eYobna5*GzTM{mKmA5AzxrT#;zmIAkp21cK3n8v4S~>XGrelbv5l2 zhV~8cbhJ+IgCsZ%#_qz4kom(vcB?a2v(=dt1Uj`0~DUpgL^7!T3wk_ zl5(V2!4AR;(hl4&Dq;x>OqEvX%)%hwuY?ON9@SVOBLIkMJieI zec3p&A$?BC&2KB#Me11`k2o8$`0L15eOW-4*_qg`_!=k}K1ud6B>RFYmnhE(Nmjii z4gpzu5Rr$egfYt!h$Ian@*ZWiHB5}*eJQ8sxbPm;@Qwp(RUM7#4YGF*aG3~PV&evb z@**YKD08V_Qs$(FETG9K4^9e;?bW`9r76ae&JF0ehKbxbIE4G7=Hg!_n}m9M2)_>r zY6!o7LJ&L(gqofp)Ho38sIZSMxw&PCIOF)whgA?6P8xzqQ9q9t<$y!TYIau4R1>{EX91z4cAjTT$$Xb5w{;dj^7j_ekzp34f8CG zB}pi^VP2BfYSp5m^mhhJ$DOW0SOI0xT9dSB65;UtAkw@cqiw)W1cDUGpJ}$$R z;o9KBKz4ZFm^8{kc~kM5iQfV$JJB~KR#0GAUx+YTJFFkjHo%y17xkUVhcY^__uRqp z`R)OiwJkOZ_x|#HcI-`I^s^>SYj(k&H#Te~b%Ld^*-o0Cm24pIVY}AaANdL|<=lV3 zmITGl7E2LksE%#@$oYC1?y^Ibe;f%F;q2?#Xnk=v^Y`nE``F@cXdrs_SyUx*-gcXI z{?>KY>Raz(x7$3o%jz7mqN>%-=d2ya4jzw%7bMwwk568@yJ#RyodN_VkBeo8Z)+b+ zd1g21qmc;fd3kcchUa)#h0N}@;S9uD)~(-VqniTw-XY@v1!$4lY*?*TecGQbdR4b0 z^aRSu!@bd}zQ_ez^{u*UzkQkK<8?{wV=Jzwh+N3-c^@(Fdp?4(MVXyFWb-^EtDe0A z=LiEO^C zFO)%KoODA(uIZLk+4GU3z7~~$%ZBYoSpd*7DI<}8A-*!co`tfyq7me3D02)%8sH%! zUS3<{kZ^W)P>}U_4EUst6MjM6qTNMlS+T1GkP~Db1{JU&{CjHH@h`&l0`D{^&ieHf zy=XdYH@ghn(>aUu9j75AkeEbbG0W(~fMWR#5UGNaSCS&1I;^Gk*GauoQ4U4BNEcb+ zWvBUM5yYv!nW#P6KwWBS#Kx^>6h=TF} z!C}@*%pB%~BZfK4r175D7G5ywba}Ycg&STUK_Q2E1XY6}Kt0I>yUwP4Pj-pf(bPpP zf-Fq&dLgw>-00cS4IdQuG>veP2ddv0cxCLn%`O=0QZ3kvnnqOnK?~RsK~oUhpe0i- zr-HtawxQR;Kap002`ujS zoL@he9n3o%>9k|VJXzHqRE-{5H7Y*D}VS=W5e z8CuuaB)!aua{$NVi-A~A3)oySJkZsgbv)sL53DD0&ITUXr=7cXT}t(>0eIPRn`gPK z&Ncblxtz6^(roMtFR%y(x0M*&6~r#a#U^PHFu0+_XSPxUhw->YGD_3I>=xPBW?6q2 zx>RT0Axl|5-7|7L{v4ng?5oS{5yQ49yx@A=&=gE|IjiapXJ62(oM4cYzsjixWc_v< zag^I-d_4hKt!!M73NA95xJar^Gji5u*Vc?CE^>Gne;5}o+FHa8$8toPGrZ^<%%ff) zjp)QYcZw@p{o$e;`gK`fVCe^cTTBs~G8rt%noO)li;UGi?ZN_$(OEHHJD20HtQnylGLC<#mGv313=BK%pyBR0 z{sk@58G`S92)Urb#ynFN_e~iX@qwDIwYc;u*>hqI_nc5Svk6r2KY;lVFdwRS$Dk?3 zt9wH^yO8txc+Rwo(mq_%g&diDI}Cu>3Dgt6L7J9?<%8c0Lx=#o-KksHT&FhEQ2tY& zAdsBvWKOUg?L3}>Vq3((V;i>?O(`Rs596%^Z2g%z9!rZ@L82`qjpBaxqk0e@2T!rj ziQ9a8<}cV<OEd zYt(D2?aK<1bAi2*iJdkdhnpiptp$WSA3*{9)!#$sH=VyIoaHb$j=_TXuQ*H(@MjR0 zuph;a9fT<*!W=?07zzZF!|SfCZeKzNW1eFB_-3ys!T}pJ4&c7;q3Mp_ zK@dHogQC5v)!kDfthzy%_4g;t%x^c_0T~&qEhHwQ`%NI`&))+^P`FbgRGO{B2)`U9 zv#JiyC(0-!S61+LX#62|c|K9^@n0VkqoK@m4hrKzE0U9<+YV8sxM9QAZM zVK!S_h$PEoJ2037eQclw*)s&3f@4uTRoudp&}K4L4-$1=M1wJZK>Lw&=>GNB$kq7{ zO;x<1)1$&3@+VML2RdIxRJ_rS(2o%i+G{aL8WBQ;zorV&WLQv=y$O_@gx$|kXM8-) zryJNmgn1C!3XNMb+3V)}x&OR??tp-IC7at{3O#xfYxTjRs)P67@=FC@jPg(%az6qe z4$a`i9?mi56p4*gvvqtU010r?hI(*X;X5OIlkUrqD#sxOd4eRT}y5!}E@1MWbo z4-8!%_%}qsbRf2z^3q`p|HuSVf@WXE9uk;*8vz^y_~X-LhM#wnS{nqNgx*;9uAg^M zm_LrP60?CjcB9MrBZ%tUh~a|#WBB(F8bW3QLb%BQv+QF-Dn&^1Fz^SVv-zz^7gNME z-{R3%#6%g<5}SkPRyE}&KmQp{6#VQ2Up|GdJh^es;^U892BOldhc(G43Cm+iHu@yp z$7e&Kjb{R~VQMVIQ88Z|Xhcp#OQ`@VNjM&MhPu`{)6YKikg00k3^e?&p@2ZNwN1(yq~ubka-@oyW>*y!SP(|xVE-> zCGn~)6q!&S>Y*919oKiw6yNFi7N|~fZMqYOxUkc{lHUMiz?ZV%4A&uU-zCUIn}WRn z1uum%FN(Xg;rK1u6!XDpYqW7mZqmsk7xZ0IQGCO;EAg$pfLFfsy&jm&qj5K!h7fCrq2RpfdYMfwn9 zWVRulZ4=ZI1>h7YGz&Qkik*(f=78>cs0hKv5Va2vk%sOA^JpBwM62D+wdd056@d^{ z#!{?NZ3Eu6%WU+`Xjt6cQ1&;v+FYGPFTTZYckGSeQO)?V$IUEEb>_WoEyI%vUQe6j zvFC^@C51y?#DjBua2Zj+uJy%V|D0YQKc{U@=Yy%LER?3T-X!&bX-HBT(gPd#EUjW6NO#j1ZM?h;oH- z&nz6$&q`7!J6?6W53JG#K z6vtD!3QwQ;bdk<~d=aom;6W`jvB6SQAnJ2GdJZd1=aoojBO6DXST@}0n88NsMUI)B zBaz@fKtljS8RPR!j4o!*LeUWzaiN2tPJDEgVRYpn3vRcIk;KA82<%EUtbMmq`@BdI4cbT`;cqpB=4t6U!&pY$Gs|oX z8e!Q*kk&(@DR^Ix+F)(9i1}iE(E@9$*@B&5du!fw_xSz6#TihOgu7yxui-G2n->x9 z;`&&`=MRz*ethDd@OF}bArq#o<#c+ui`bPhJuzT#^e>31Jx8+;V}Yj3#pP>zC{9uz z(Dn!z<(lf`Wd&k}sxH3$ceFN=r32pyuRc9?Ykc;;fM6hYeSBUBe(#0df*66dGcOg| zxLm$M9KN`T@84d zLuw(~o9I=21ywn?$eMy{J$i8t%Rhn2aSf*`mtLT8L1M&c2;1HQER4<79P^4i6tQU7fPj`VJh)Rn}L?ftx z@%jyQKz9MoO6qSxft7gg#x(e(x9Lz9t@D3I%`Bg2Qc+<72wRZ%SsQdBp0~|gI=#hq zs#>|Kpeox@|01$@%Ca3#{4aP6H5>M|!*h0SrS!Kq6`SavA@rdnON*mFhNltw-8k;A zj;0X$Fa?TT>f={`s|9$o32(LmaJG0oN1$(7s>J*7vWf~|q}nPfVkssf1PIFJn2ZxD zaoBUyRuOsm@8E%UI?x#qU4uidu7WDr>>U>_(p=P4G_aSCr_o+~8p4#J|}EB19s;8GWqm(f@}Q+L~YG<{J?A9?{s4d~dsm{c#ZOB19-VGzYlic^Ca z=+&*H5iPHd01{_PXsx}VIE$>Q#)_!c@V)KO_Xrlu2e7vqe9;O;36q3^pJ7E&r@}hH zP;&(~G=j{5Nn87=KtkIOb?Ucmi_6Q0Ci;$UOg(e9p95iUrdJBE&Gw7Ajw^f}Kp1R8 z*7FH4He=W{r>CYwWU?qk%-_Q_Xfc1IaK-%P@QC@Fg%Oe}=5N-}{LP{H>lE|XJ~V%` z67%=h)Wx&v3=HYb>t_s~yLJqeZ_eH6fB9A)L&>DvAGY1u&@c@45!Hx!pV>}>qK^!B{5&BK)AE`^>DTQ zUSpk?Um>E3_(b8B@-f0)z()$#$5VuRCm+~Dm@eTx!d=S06z&TCnQ&L~kA)lL?+f<< zen_~hd8=?0zE`*#_$$KQ$bSo$PK26?b!ut$)D};ADc}s=&S@ISN?@sdG0I#-xTEfuC>=Y%RrFfFXB{%Q^2S5Vi zzYLu|4E-o#zJ7yZU=x(zC1U(MMls(gWD_p#c#^y0c@Bs?I8IIOi^sfb#<)Dm{qdOR z%$O|vKcgdNO+PkcvOUSRcubucljBL2<1r74n02|He7lH&28?2W4cTeqsL>xcJ;guq zq4O@-rX;1~ISy@~#qn!fE}k%e()oaac-)q0Oc6<~7DtmUckP$B=d(Wt_iu(QzF~}S zBLn!}ec1L%>MWLYvqilYnXb6`9*cT;dWSM0!kstIPEyAqa@5WDSXCzuH}EgO(mPbB zxNuEt>-grQ?f5r}Exr3N_9Z2&F$uV!n9qR#GM(z(mqhW;u3|`_@(&@lmCAn$^88Da9KY%3q2)s+T>LL9Bp!Cu(CABbjTBk9Trcx#sANlDNu(YIM?QU z4X%b0A9|Y;_hUK9y+1<^9EZ`1!g%`46z;S6B8ttC_>3VOTG{)ZM?j-*NcWcm0@D46 zK|oWw|M4$rkh@}S|Fv`vC07R^QNTesrc7%VaEDm{&gR&@*xf$92&5Ul2LRI>T3h+A z2Bt3;?kq*g{pr?Zw4^E)z%p zya@``08SNf4i6${X!GrIbaob<*2hL6U<=Q7Aqi15f?#H#-Gc{GEKqVL@!vj+tY(RI zr>A1{Hpw4ijmfalH&deTg#Huw=J(OJup8f1TQYNyuxd+&OE?p{L}c=5;atvzb4>@F zXP!vT0thR{GeyF+H96tpmRnOcg+uNAplR|FfYP)3J1DWqj!-mN!Z(fH+E2EL)VLm0 z7mx7opf`nd@e05B_I{HOK)6bh1>4pDw2b0$@u2G^FT5@cM6HkjV6p?4ZrGUyf>icJ zukath?GhLu*k~6AwJ1*MJ5yIVzG{xi@w;(bS2;E#X7fBJeRAkCojzXr_~^5OKC8oT z)`HCQ)gyG+|DSYe1;sk4*kbvoxVe8m5C7NCK@K$Ev&f|KsP?DLFvRj4R4&Oo2;-Nv zVD=iGc*lj5#of3{RBSDF=Iz5YgciC6Z}N7Z{Wqk=uI>E<*J|~eE{dj$TNK@eaR94X zYg^utJF0PaW|Jt34us<9;3P_Z71OF{J1W5m>?+69NZh7AHYIKoF7Apb>4n&y;U+G) z6V#Y=Yvdbt;3X)+96Q=+T-w7^oc)lENkDmK32V!jq#Vh&?U}psjw+MNx8Z{PFDbQ( z>x)n0v|C&@Leyg9>ASnPxF1BbDI@t(AdFE=7UN{Cij#9r!bB5UmI4#v-LM^BEUpJV z`xrVHGJLg5lB!4|0lB|*pXmm1)vC1L-Nq|mM2q`OPUf3>2lM6L!I2LVz5K1~OsE05 zoQr+9#Pb3wiT{6t_}wgD7+1MgHX$$F&6-Mgv*=cqq1>!(%$(mNx3ZZ1 zC4E6IOk?cY#+Ps}Ypi)MtLP<}&lkDjp%b{+tAzGS^Isr97DHP=^OY=v>Zp6zMmXR( z`EonE!(LD>tI2|TEfwXjPsj2W8xi-Q5Ex;|`dpj-UHjs4-;zL8;BMT&GL9s=$%RuX zlF(1{pFBY#q!Win#4mB}5|@kBX}n|G#YZa-APhEHThm9E0E@fnjS3t{p1>kiP~*Zg zb2YA{jR)|}-HiX@aJr&#B1U?6lSO;&1)y%(G&Wp(8vhv#&+qy8P2fm?fs`9_vLCe} z9SOK+I7e$I#+BSSX2MJvlnis7^&PqiWv`*G}Ix6q4*B(>Mi5uRMf6ZB7W zp^Uh?_QG`RiHlw7C^9yqF?H4kC1chS5hYX9WTc^P+#}KYoa$(Rt0M)JZPpUiHjk?K zcR~kz6z3Voda-GLKGVedF7;o%ht98;AJyR;@Ef ziyZKjxz>&Y(lTBiUUQ{t^Bi0^89|$Pa2~TNYY|>C%@H=)R#-LByo=q97ib;7t8!UC zgsz2}&PbHpyc1Vbc3RXztvyLetHb^XMkwpk(8|05wSV0eeC{)N-=#Rq5|u7P68@`V zS-%l;AO`0vOR!h5wqhEhV?od)eI~^#*50cAi#iYngR!gx%=C+rFym;>T5&bY8v#9GBOD?*b{gdJMF+m3+&Hpw-|9ht6 ztwac8R3G;5XR7gkr|4hOWJ;13|1=3c^S{yjo&WcO+ee@L!Z6-}+HNZG*;D^Yue0EQ z7SrsL#OdKCL=HbaOhFj4j7|?L5~ukyaiZISV>I&^`(tbm&0}n31Y6s$eVIlNrC9Ou zNkU+_9K(SQb)8W0#7~I3u=L@&Z7Y8U<+tE49=0iX;>O1R<7Jd)u1qrKs}2KO93s;q z;>5BSO9}rBg<)xUFa^JbSnwY7=isx97Cc%57a?kMC89PjN7RF>y!d?IhtIkd_-tB@ zg!>S;`9Z{OUXQrVn^%WlsYRKI)v<}8&S48=wqDZ>G)V0T)2^1EvNkMP?t^&;wND-7 zT)c9!qcD294fbB_koM_jy^MR#D5LYv(3$>~m_+D3TJXv^EL_&ZoDLGc^?2SE4wryGwTeRN&3kHMH!VsD!94C_4f~(or-(wH{73gG_CF5W{DkfP z@cR@e!dNeJ!+)T3xJ$_8h;zX+6TiVb693hG3Mw7@qCwcWZT>gzQwSXX;eCpD8Ly+L zo%n6X?`izD;@5y*Eq)vDgAINMZ1Dew_bI3{W>BL~;m7gorw|? zUoL*r@SB0(Ec}Y_tHcj|^MBwz1y#n3)%b3}uK~YjC??T2|Ks}<1NCm4R%EIE?u<+4;IUm2OC`_+t2&4GosUvZZK3V z#p&jb)>P8^`4XgldqW#&#g3N#DTKX}zx*WlEpc~o!5BL)XRwqOhc^+A#+v6<8xhcQ zBz_Ov_~8l8cIZarH+HlY0KkT{U=YsSkVbwi7LotXc~uj&hqff~^D3BI$^31w6f#V~ zf64wBMg8ORn}w)Y>_-JzOj&^l4c*R8O_{h;RxTx_UF_cs1OMQ<7ZzvuKNYy4fPb7_ zVoNB5uqPT47hu*>kncOx655X+ElkA4w*=2Sn4_SWB3&*qYPRfSCx*s4Gq7K}EijL)G85t5GC zXzQ)SjssjaSG8B3EVmIOj+#HC)Uwo$!$UBMD9eng&y$w(m3T@9H-R{wM!7>mCbWZ0 zpdTGDeL<3xG;J6i@kS?^#A9Q_MkC%1$1{wg8aWvnxt8jijwg|at4x?pw_Of4{TXod z=<_Q&Xt&QceC6K$9IA!3RJG94-G(hlea+dJ6{E3+y`0N>1FIW-e)?iLDsXmH{bkA# zBDDUrZU?wAKU$M!C_zMS+QJxVa9b;dSB@j$F^yr@=NcSXcYH3~q2o7GrC+7e>1O{- z_&e6wAk%^tQleWzJei)XkBLsL&pKi{@r+U|w9ki>*wm^Hp;bZg5j6+SZl|MS@G-j6 z2S)8_ducR-3Vp^NF|lpu+}rR*vLS8Pc+y_1V<(O)@nIgu+nF==RaGyaKchDtY8kY* z(Ff$)PT_;?)T7%HwC@4z#ukkjX)^6Y&J2HEXXw~{Qc{s*ERy@%>pwYRwAcUbM4Y|; zQ=FxV_Jg3kdLVlz282GiDctNsAHlhiz5#!>NAgP^TJM}4Z39em+aH%HEh zR|LGs$(nIHNjNzzbPGkE8ES&1ZJJUR%P}dVsFb_F*fxfaf@gI%4HI{O5Z$XTRwxHCZx1lSviq6k$Ph26nYx;TIhhGJD=9AGE9m@guAjfD^}zV;~5e9 zhXZX4YRaRiDeM;9XnhsO9B*tngblWR)?Sau+9uzNMB1$(j2*vi8>;pbY2lidP>ei> z&OD?xOKUF0ikAgP=DlJ$MExC4_tcyZhaSbAI-FUNKjZX@`KvxZNlQQW_PiC&2dO9%FqN{8DG zQ)o4>BUFR}z)dI4IqHat4#KW%TktCO?+&GehS-S%4r~XZAr9o4jSG(9VRI;5PH#CR zC(~@v$)(66&BQ50p-rR<)SfZMf(4_c7S%y$aWO?43#B{&78L4QG2&fde$M^#Hs{oObgAG_}pw{!5`o;&9r{|u8tr*A^ow*$WF z0FtO~6_igYx$bO2DYQC<`U}0a0-Ots?YS}6(0JU4m%Kx5t%^!vyJ5}=880d9Y%vg+C#lD7u$jt$fz^Rg(Cp(i=Ijr|9@+0U#>T#%{R&*W;M$#i z4EH@a!|3&_z1hd4%h0&apBH0Kzty&LKD}k!<#`2KE;@cFsplTjFv*wU+Hm@9oT+3# z{kC?jlPfJbPCel1?YM*ULDEOjo^yWwuJ93Kt~BWJ=*%U1+hLragtZfG-2{6p6KTz3 zpR|ANUTLUp>=@dY}U3@@Oc&f15!rO&Q8b$$(wDBGX48*3@6 z-lkK^|_{vkz!xW|uSo zUPs~8173$t+`jO7RpuPLzz)%K{#Uimk#&CbR#ew_$vpQIW3K`41D^x^o@Q(ekOR~J z60iZd3D^v51)c+51>OP90m;uWmI34e6@UP=0zU@s1)c(S18)HI8r7eG*k_RzkPeuD zQs6qk4Xg%k0Uick1YQFU10BGpK-T^)VdOXiq z3Xld&1ag2mfE#E6x|);8CiYRGiJhu2u@&o0tTf%!$ujp&6WcT(avdh5Gl9v>i{F)s zSGpj3y>Q}&w+9VpNi41h>k*q|uvjedWF}`a8*!#{(`<7RjBILVX0Nq)}jcepso&2zb2IBl`r5R`mOjgeS;3fK|;q@$4qEDx-S0r&@_4$Jm=k+Y+5j$l^hK6k-YMS`Dc^!$;&{O? z*SWc1z~MxG!S3}V*{A~3m)F%@%BwT)yjpqX6>51)ZV2rcoP5Tmmj|UovSm^lxrmA6 zizMLE=o6kGn7rf>ln zt*yZ$O0vTnsUWIJB>|EPeWIJPzuqtK=$#YLaA0u~WwBGPtEW@ANWT36UZ8#u^rIYH ze1pfSSqeLC_Fg&qD^oNX$3^p?$`s+lDW`YAFvMekX{Efo3s6(v? zEi^`tNKC|_PHfp61%|<^EjxOuPBs*7XtES@yUyJXw=#Kv0v%JQ`zJd0O*;3@I`=I) z_pLg&1>?1gw^14JBH}G(7FNzm@xO#+v5A;v^57~_WXa>BdGo!&e39oFY&$PiIlQvs zF3`EnUNlY$91|O7i0_eLyezS2uilsUN$T6LfAWBVKS)U(GdmHZ?C@w!CGs!~a zZcUDl$4tF$-8vL}W@f9cl|I#&e#JCfbt~p!Ox8Br28hk2w{B=fD2D0U?RFcD2~B}O zV;~SUPb0(H99@m6#NTL_ltGHYzcR>+WsB<3uZtp+ni=tdR^kXsX3Pl7t1c~7Y@&Tw<}-C{e`va{#Unvm{pxWJh)zx3D+Rmt79U+*qBTE zg~MN%`>*QrWj%mWO2#joB3(t?N%W}x6--zDOUrG9D~Uzis*BEp!1L&;)oa$STmPdC z8*ga4vHizCx#{LxZoTcNxBu*pJMa3ppa1*a_uRW_^L_U}@Zdua|HmW0*z%wM^~Sw(|iz>Z=T9xlRsBjLnk0N*uHB3_@|41ER+Z+v*UV zZrJ(!B2k6{fM~NEg5pFv6hA|}6emMm6b~RETrm%wWm6?i-$Z>wKWsCHSi9TJ6Q2*<$Bz(+g`bSRr}79;>Syg&g1_`r^oRk}S>1e04 zKu_H2c~4QL;Bp?8hnUmqR5D}A~*ny!zZ!QAbOrgshR^heXXh7UG= zF}(f8J&cx!^N7K8ezq{>=>B+Z5)AjK)n8e$02JCinsb)Rtq3_i$ zx>(=q>Z5h_>tpbAoa^`b9D=LFBk*SS7iJ;-M6L+sE!1;%JWoSCjb-q-65i3kU*NyP zP!ZNsb!yLdD6z^=P7C43b#~BDDM0z@f}YmWaj;}`McqGm< z7xCI9ml_FU{Uh-by%d@ys1RymNcm!kXu|9gX}g1Fc9?Q{A%Qu%2~o}Z##eh>e6HIe zR`{)?SDrWf~wl2m+4B=A?+b|r=DpyT`Ah9k7A3cZIBX_cNGavktb~* zV?mNhC;iL1`mazFr$sL83etzh{2mVd>)1kQVRNBhMM1C+4OOa^SV?ieLQ|aVp}5&H zFE-nFQCYKq5?v_(+(q3=d9Ls{7i%T5QL9QPA&A~;e2a#@Ps0u`>}B)xmHI&ZLN^2F0Ql1Rrtfrkd4=Kp@r3|Er?n;wJ3!1MEiipKt3hP zl8D;KSS_^yO1)FTc}U5D-Ln+Zm9t#0KdALpHc0J(xoUpQY^yFeWwI2ICCX!36jwF- z5v8E(s~ecwW++iJE4GxET1ud|b9w1z`Cn*?Tt1&LqDlxt3r)#Z)h%flxuhYm*vpTU z7dO<8^wcdIIdSR4Owg(q*Nze!>TBGi980{CZxrucHpHt)pXh+Em#=YL#qw&sj?s02#>EYzef6H2 zhSA=|qs97q&*)lD{Sx0;W~me9F?H@mV-|TF@)&0wFN_iDMCTY$aE|fdpOlVuV`oE4 zR^76OtN?WQ4OvTS>#`b`w@j+bTH{vcsUzo9Wn36(K}QU)a?42qo(zDWb)4b=lv`&SQ$ zH5mFCsts`i2M_Mozj|0|wSm0*#}DfpXPC<}`VF6#G&r^I@XP1*>ytcm-hkwJm%}Rs zUh@VFQQW=z4;wx&wcp_W#tM>vu@Y;3SwUfOX^FKUzpSu0zo4|FxUke(QdV40T3k?= zUr#SI~!{Kz^olc??zBk{Oz@?Tz%_bAG0o1Mr0^v3kr@0NVQ-FA&H-P;e%CSh; zxxsn?M$M+(pLC67KdcuqMr(Ir8pTUt$?gMC8q~bWuP@L~1Nn~t$bT|G*JkaS1{Au+ zl%s?x*ZeU=Dg2o3l=$burnG5#p?G`%2Lym20IlhftdtLurxhTXS84WY*hF6g5PdB` z@!Skt3EZt+Z-Pzo+z(KC4*|rt9iTkD1`z#CfaobG6e^!^nU%h8diK%O7xMazHNjWq zO?k+ql-Y+5;>BV3?SM_PrNgH3I`;scgJHi4o678A*!^KIhfQ^q2Rjw^5ZHrYcieAc zgJExhO?h7ndl+mN?BTE(Y!23*`%Fr?AJ}YC-pI>@-2{IQHYuNyU@wKuV4s6^4(){= zu0Mnw2Ri{Eev%cB-RE1$@edjbPloavHZhtbY=2e%?!t8!Pj~!b|F9hn|LwJtx;1K^ zxo*w3qfEMAj8OB}y)yjUk$=iXw@pCnqTMzD?%Pua$jSfnra<3~kBK!H@5g{$v$}cp z2(5A+{G;mF^nhxwy5|@T@yxWMRk0ri|IYi4+{dZp3;wVAT=g<(_LG;$Z`Sy?fq(zz zj?IJEdz(X>>1iO^bjH78lM-TfJY%zuEIh)o>yOkQnHbHCt5pXN|KZqK#;#tWhV0m) z>b!F`&i}Kq`!!RvEMN1Q<)#~#5+fxpCyHCs)LzIbm$dc;pQ)%BZeE)Dwl z-a%O`%8kP6%bSb8m2Rav4Vr0`p?f>MX$`31PvUhC^0(Qw2T(oJS8&_%zFzpi0n`aV z_fp|x^=?{cA&?mch8e&-fbQdd20RHI0zL-lg`iBJ6z~DzFxjBZ1sZ|dfhT~2z+ZtR P@X@DMtbiBzrp@00@$+X3 literal 0 HcmV?d00001 diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..9ff2204 --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,81 @@ + +import pathlib +import textwrap +import pyhidra +from pyhidra.__main__ import _get_parser, PyhidraArgs +from pyhidra.script import PyGhidraScript + +#pylint: disable=protected-access, missing-function-docstring + +# hack fix so capsys works correctly +PyGhidraScript._print_wrapper = lambda self: print + + +def test_run_script(capsys, strings_exe): + script_path = pathlib.Path(__file__).parent / "example_script.py" + + pyhidra.run_script(strings_exe, script_path, script_args=["my", "--commands"]) + captured = capsys.readouterr() + + expected = textwrap.dedent(f"""\ + {script_path} my --commands + strings.exe - .ProgramDB + """) + + assert captured.out == expected + + +def test_open_program(strings_exe): + with pyhidra.open_program(strings_exe, analyze=False) as flat_api: + assert flat_api.currentProgram.name == "strings.exe" + assert flat_api.getCurrentProgram().listing + assert flat_api.getCurrentProgram().changeable + + +def test_no_project(capsys): + script_path = pathlib.Path(__file__).parent / "projectless_script.py" + + pyhidra.run_script(None, script_path) + captured = capsys.readouterr() + assert captured.out.rstrip() == "projectless_script executed successfully" + + +def test_no_program(capsys): + script_path = pathlib.Path(__file__).parent / "programless_script.py" + project_path = pathlib.Path(__file__).parent / "programless_ghidra" + + pyhidra.run_script(None, script_path, project_path, "programless") + captured = capsys.readouterr() + assert captured.out.rstrip() == "programless_script executed successfully" + + +def test_arg_parser(strings_exe): + script_path = pathlib.Path(__file__).parent / "example_script.py" + parser = _get_parser() + strings_exe = str(strings_exe) + args = [str(script_path), strings_exe] + args1 = PyhidraArgs() + args2 = PyhidraArgs() + parser.parse_args(args, namespace=args1) + args.reverse() + parser.parse_args(args, namespace=args2) + assert args1 == args2 + args.insert(0, "-v") + args1 = parser.parse_args(args, namespace=PyhidraArgs()) + assert args1.verbose is True + args = ["--project-name", "stub_name"] + args + args1 = parser.parse_args(args, namespace=PyhidraArgs()) + assert args1.project_name == "stub_name" + args = ["--project-path", str(script_path.parent)] + args + args1 = parser.parse_args(args, namespace=PyhidraArgs()) + assert args1.project_path == script_path.parent + + # two scripts are ok + parser.parse_args([str(script_path), str(script_path)], namespace=PyhidraArgs()) + + try: + # two binary files without a script is not + parser.parse_args([strings_exe, strings_exe], namespace=PyhidraArgs()) + assert False + except ValueError: + pass