diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ed8c002..d5c57ea 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,9 +8,38 @@ on: jobs: + code_style: + runs-on: ubuntu-latest + steps: + - name: Checkout Ghidrathon + uses: actions/checkout@v4 + - name: Configure Java + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" + - name: Configure Python + uses: actions/setup-python@v2 + with: + python-version: "3.12" + - name: Lint with isort + run: | + pip install isort + isort --profile black --length-sort --line-width 120 -c . + - name: Lint with black + run: | + pip install black + black -l 120 --check . + - name: Lint with google-java-format + run: | + mkdir ../tmp + wget https://github.com/google/google-java-format/releases/download/v1.19.2/google-java-format-1.19.2-all-deps.jar -O ../tmp/google-java-format.jar + find . -name "*.java" -type f -print | xargs java -jar ../tmp/google-java-format.jar --dry-run --set-exit-if-changed + tests: name: Tests in ${{ matrix.python-version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} + needs: code_style strategy: fail-fast: false matrix: @@ -36,10 +65,10 @@ jobs: python-version: ${{ matrix.python-version }} - name: Configure temp folder run: mkdir ../tmp - - name: Install Python Jep + - name: Install Python requirements run: | - pip install numpy - pip install jep==4.2.0 + pip install -r util/requirements.txt + pip install -r data/python/tests/requirements.txt python -c "import importlib.util;import pathlib;print(pathlib.Path(importlib.util.find_spec('jep').origin).parent)" - name: Download dependencies Linux/macOS if : ${{ matrix.os != 'windows-latest' }} @@ -73,7 +102,6 @@ jobs: run: python util/ghidrathon_configure.py ../tmp/ghidra/ghidra_PUBLIC -d - name: Run tests run: | - pip install packaging ../tmp/ghidra/ghidra_PUBLIC/support/analyzeHeadless ${{ github.workspace }}/../tmp/ghidra test -Import ${{ github.workspace }}/../tmp/ghidra/ghidra_PUBLIC/GPL/DemanglerGnu/os/linux_x86_64/demangler_gnu_v2_24 -PostScript ${{ github.workspace }}/data/python/tests/hello.py -PostScript ${{ github.workspace }}/data/python/tests/runall.py > ../tmp/log.txt - name: Check tests run: | diff --git a/README.md b/README.md index f6879db..8b56f11 100644 --- a/README.md +++ b/README.md @@ -3,27 +3,67 @@ [![License](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE.txt) [![CI](https://github.com/mandiant/ghidrathon/actions/workflows/tests.yml/badge.svg)](https://github.com/mandiant/ghidrathon/actions/workflows/tests.yml) -Ghidrathon is a Ghidra extension that adds Python 3 scripting capabilities to Ghidra. Why? Ghidra natively supports scripting in Java and Jython. Unfortunately, many open-source analysis tools, like [capa](https://github.com/mandiant/capa), [Unicorn Engine](https://github.com/unicorn-engine/unicorn), [angr](https://github.com/angr/angr), etc., are written in Python 3 making it difficult, and in some cases, impossible to use these tools in Ghidra. More so the security community has released several great plugins for other SRE frameworks like IDA Pro and Binary Ninja, but again, because many of these plugins use Python 3 it is difficult to port them to Ghidra. Ghidrathon helps you use existing and develop new Python 3 tooling in Ghidra and script Ghidra using modern Python in a way that tightly integrates with Ghidra's UI. +Ghidrathon is a Ghidra extension that adds Python 3 scripting capabilities to Ghidra. Why? Ghidra natively supports scripting in Java and Jython. Unfortunately, many open-source analysis tools, like [capa](https://github.com/mandiant/capa), [Unicorn Engine](https://github.com/unicorn-engine/unicorn), [angr](https://github.com/angr/angr), etc., are written in Python 3 making it difficult, and in some cases, impossible to use these tools in Ghidra. More so the security community has released several great plugins for other SRE frameworks like IDA Pro and Binary Ninja, but again, because many of these plugins use Python 3 it is difficult to port them to Ghidra. Ghidrathon helps you use existing and develop new Python 3 tooling in Ghidra and script Ghidra using modern Python in a way that tightly integrates with Ghidra's UI. It replaces the existing Python 2.7 extension implemented via Jython. This includes the interactive interpreter window, integration with the Ghidra Script Manager, and script execution in Ghidra headless mode. + +![example](./data/ghidrathon_interp.png) + +Please see our Ghidra Python 3 script example [here](./ghidra_scripts/ghidrathon_example.py) for a closer look at writing Python 3 scripts for Ghidra. Check out: - The overview in our first [Ghidrathon blog post](https://www.mandiant.com/resources/blog/ghidrathon-snaking-ghidra-python-3-scripting) -Ghidrathon replaces the existing Python 2.7 extension implemented via Jython. This includes the interactive interpreter window, integration with the Ghidra Script Manager, and script execution in Ghidra headless mode. +## Installing Ghidrathon + +### Requirements +Tool | Version |Source | +|---|---|---| +| Ghidrathon | `>= 4.0.0` | https://github.com/mandiant/Ghidrathon/releases | +| Python | `>= 3.8.0` | https://www.python.org/downloads | +| Jep | `== 4.2.0` | https://pypi.org/project/jep | +| Ghidra | `>= 10.3.2` | https://github.com/NationalSecurityAgency/ghidra/releases | +| Java | `>= 17.0.0` | https://adoptium.net/temurin/releases | + +Use the following steps to install Ghidrathon to your Ghidra environment: + +1. Download and unzip the latest Ghidrathon [release](https://github.com/mandiant/Ghidrathon/releases) +2. Execute the following commands using the Python interpreter that you'd like to use with Ghidrathon: +``` +$ python -m pip install -r requirements.txt +$ python ghidrathon_configure.py +``` +3. Install the Ghidrathon extension (`.zip`) into Ghidra: + * Using Ghidra's UI: + * Navigate to `File > Install Extensions...` + * Click the green `+` button + * Navigate to the Ghidrathon extension (`.zip`) + * Click `Ok` + * Using a limited environment: + * Extract the Ghidrathon extension (`.zip`) to `\Ghidra\Extensions` + +### Switching Python Interpreters + +You can switch Ghidrathon to use a different Python interpreter by running `ghidrathon_configure.py` using the new Python interpreter. + +### Python Virtual Environments + +Ghidrathon supports Python virtual environments. **To use a Python virtual environment, complete step `1` from within your virtual environment.** Do the same when running `ghidrathon_configure.py` to switch the Ghidrathon to use a different interpreter. -## Python 3 Interpreter Window +## Using Ghidrathon + +### Python 3 Interpreter Window The interpreter window provides interactive access to your Python 3 interpreter. Click "Window" and select "Ghidrathon" to open the interpreter window. ![example](./data/ghidrathon_interp.png) -## Ghidra Script Manager Integration +### Ghidra Script Manager Integration Ghidrathon integrates directly with the Ghidra Script Manager enabling you to create, edit, and execute Python 3 scripts within Ghidra. Click "Create New Script" and select "Python 3" to create a new Python 3 script. Click "Run Script" or "Run Editors's Script" to execute your Python 3 script and check the Ghidra Console window for script output. ![example](./data/ghidrathon_script.png) -## Ghidra Headless Mode +### Ghidra Headless Mode Ghidrathon helps you execute Python 3 scripts in Ghidra headless mode. Execute the `analyzeHeadless` script located in your Ghidra installation folder, specify your Python 3 script, and check the console window for script output. @@ -39,17 +79,6 @@ Function _start @ 0x101060: 1 blocks, 13 instructions Function deregister_tm_clones @ 0x101090: 4 blocks, 9 instructions Function register_tm_clones @ 0x1010c0: 4 blocks, 14 instructions Function __do_global_dtors_aux @ 0x101100: 5 blocks, 14 instructions -Function frame_dummy @ 0x101140: 1 blocks, 2 instructions -Function main @ 0x101149: 1 blocks, 9 instructions -Function __libc_csu_init @ 0x101170: 4 blocks, 34 instructions -Function __libc_csu_fini @ 0x1011e0: 1 blocks, 2 instructions -Function _fini @ 0x1011e8: 1 blocks, 4 instructions -Function _ITM_deregisterTMCloneTable @ 0x105000: 0 blocks, 0 instructions -Function printf @ 0x105008: 0 blocks, 0 instructions -Function __libc_start_main @ 0x105010: 0 blocks, 0 instructions -Function __gmon_start__ @ 0x105018: 0 blocks, 0 instructions -Function _ITM_registerTMCloneTable @ 0x105020: 0 blocks, 0 instructions -Function __cxa_finalize @ 0x105028: 0 blocks, 0 instructions [...] INFO REPORT: Post-analysis succeeded for file: /example.o (HeadlessAnalyzer) INFO REPORT: Save succeeded for processed file: /example.o (HeadlessAnalyzer) @@ -75,42 +104,6 @@ Ghidrathon links your local Python installation to Ghidra using the open-source For more information on how Jep works to embed Python in Java see their documentation [here](https://github.com/ninia/jep/wiki/How-Jep-Works). -## Installing Ghidrathon - -### Requirements -Tool | Version |Source | -|---|---|---| -| Ghidrathon | `>= 4.0.0` | https://github.com/mandiant/Ghidrathon/releases | -| Python | `>= 3.8.0` | https://www.python.org/downloads | -| Jep | `== 4.2.0` | https://pypi.org/project/jep | -| Ghidra | `>= 10.3.2` | https://github.com/NationalSecurityAgency/ghidra/releases | -| Java | `>= 17.0.0` | https://adoptium.net/temurin/releases | - -Use the following steps to install Ghidrathon to your Ghidra environment: - -1. Download and unzip the latest Ghidrathon [release](https://github.com/mandiant/Ghidrathon/releases) -2. Execute the following commands using the Python interpreter that you'd like to use with Ghidrathon: -``` -$ python -m pip install jep==4.2.0 -$ python ghidrathon_configure.py -``` -3. Install the Ghidrathon extension (`.zip`) into Ghidra: - * Using Ghidra's UI: - * Navigate to `File > Install Extensions...` - * Click the green `+` button - * Navigate to the Ghidrathon extension (`.zip`) - * Click `Ok` - * Using a limited environment: - * Extract the Ghidrathon extension (`.zip`) to `\Ghidra\Extensions` - -### Switching Python Interpreters - -You can switch Ghidrathon to use a different Python interpreter by running `ghidrathon_configure.py` using the new Python interpreter. - -### Python Virtual Environments - -Ghidrathon supports Python virtual environments. **To use a Python virtual environment, complete step `1` from within your virtual environment.** Do the same when running `ghidrathon_configure.py` to switch the Ghidrathon to use a different interpreter. - ## Considerations Ghidrathon uses the open-source library [Jep](https://github.com/ninia/jep) which uses the Java Native Interface (JNI) to embed Python in the JVM. The Ghidra developers advise against JNI in Ghidra for reasons discussed [here](https://github.com/NationalSecurityAgency/ghidra/issues/175). diff --git a/data/python/jepwelcome.py b/data/python/jepwelcome.py index 5013a17..11e0ad5 100644 --- a/data/python/jepwelcome.py +++ b/data/python/jepwelcome.py @@ -14,7 +14,6 @@ import sys - message = r""" _____ _ _ _ _ _ / ____| | (_) | | | | | | diff --git a/data/python/jepwrappers.py b/data/python/jepwrappers.py index 9018254..2959337 100644 --- a/data/python/jepwrappers.py +++ b/data/python/jepwrappers.py @@ -5,10 +5,10 @@ # Unless required by applicable law or agreed to in writing, software distributed under the License # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. -import sys -import abc import io import os +import abc +import sys import java.lang @@ -107,20 +107,16 @@ def wrapped(*args, **kwargs): class GhidrathonTextIOWrapperBase(abc.ABC): @abc.abstractproperty - def __stream__(self): - ... + def __stream__(self): ... @abc.abstractproperty - def name(self): - ... + def name(self): ... @abc.abstractproperty - def closed(self): - ... + def closed(self): ... @abc.abstractmethod - def fileno(self): - ... + def fileno(self): ... @property def line_buffering(self): diff --git a/data/python/tests/hello.py b/data/python/tests/hello.py index d8fa3c9..6c207b5 100644 --- a/data/python/tests/hello.py +++ b/data/python/tests/hello.py @@ -10,6 +10,7 @@ # See the License for the specific language governing permissions and limitations under the License. import pathlib + import jep path = pathlib.Path("hello.txt") diff --git a/data/python/tests/requirements.txt b/data/python/tests/requirements.txt new file mode 100644 index 0000000..0a1814e --- /dev/null +++ b/data/python/tests/requirements.txt @@ -0,0 +1,2 @@ +numpy +packaging \ No newline at end of file diff --git a/data/python/tests/runall.py b/data/python/tests/runall.py index 4ef29d3..ba9d05e 100644 --- a/data/python/tests/runall.py +++ b/data/python/tests/runall.py @@ -14,9 +14,9 @@ Note: you must run this harness from the Ghidra script manager or headless mode """ -import unittest -import pathlib import sys +import pathlib +import unittest def main(): diff --git a/data/python/tests/test_jepbridge.py b/data/python/tests/test_jepbridge.py index e86151c..c660f58 100644 --- a/data/python/tests/test_jepbridge.py +++ b/data/python/tests/test_jepbridge.py @@ -29,8 +29,8 @@ def assertIsNotJavaObject(self, o): def test_type_instance(self): # see Jep: https://github.com/ninia/jep/blob/15e36a7ba54eb7d8f7ffd85f16675fa4fd54eb1d/src/test/python/test_import.py#L54-L65 - from java.lang import Object from java.io import Serializable + from java.lang import Object from java.util import Date from ghidra.program.database import ProgramDB diff --git a/doc/building.md b/doc/building.md index e63c27c..93fff01 100644 --- a/doc/building.md +++ b/doc/building.md @@ -1,4 +1,4 @@ -# Building Ghidrathon +# Building ## Requirements diff --git a/doc/contributing.md b/doc/contributing.md new file mode 100644 index 0000000..db2629a --- /dev/null +++ b/doc/contributing.md @@ -0,0 +1,18 @@ +# Contributing + +## Linting + +### Requirements + +Tool | Source | +|---| ---| +| isort | https://pypi.org/project/isort | +| black | https://pypi.org/project/black | +| google-java-format | https://github.com/google/google-java-format/releases/download/v1.19.2/google-java-format-1.19.2-all-deps.jar | + +Use the following commands to identify format errors: +``` +$ isort --profile black --length-sort --line-width 120 -c /local/path/to/src +$ black -l 120 -c /local/path/to/src +$ find /local/path/to/src -name "*.java" -type f -print | xargs java -jar google-java-format-1.19.2-all-deps.jar --dry-run --set-exit-if-changed +``` diff --git a/ghidra_scripts/ghidrathon_example.py b/ghidra_scripts/ghidrathon_example.py index 581d07e..ccfd5fa 100644 --- a/ghidra_scripts/ghidrathon_example.py +++ b/ghidra_scripts/ghidrathon_example.py @@ -10,8 +10,7 @@ # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. -from ghidra.program.model.block import SimpleBlockIterator -from ghidra.program.model.block import BasicBlockModel +from ghidra.program.model.block import BasicBlockModel, SimpleBlockIterator for func in currentProgram().getListing().getFunctions(True): block_count = 0 diff --git a/src/main/java/ghidrathon/GhidrathonConsoleInputThread.java b/src/main/java/ghidrathon/GhidrathonConsoleInputThread.java index b7a9f47..b3d6091 100644 --- a/src/main/java/ghidrathon/GhidrathonConsoleInputThread.java +++ b/src/main/java/ghidrathon/GhidrathonConsoleInputThread.java @@ -140,7 +140,8 @@ private boolean evalPython(String line) throws RuntimeException { GhidrathonScript interactiveScript = plugin.getInteractiveScript(); Program program = plugin.getCurrentProgram(); - try (Transaction tx = program != null ? program.openTransaction("Ghidrathon console command") : null) { + try (Transaction tx = + program != null ? program.openTransaction("Ghidrathon console command") : null) { interactiveTaskMonitor.clearCanceled(); interactiveScript.setSourceFile(new ResourceFile(new File("Ghidrathon"))); diff --git a/src/main/java/ghidrathon/interpreter/GhidrathonInterpreter.java b/src/main/java/ghidrathon/interpreter/GhidrathonInterpreter.java index 82c79ae..1f42c42 100644 --- a/src/main/java/ghidrathon/interpreter/GhidrathonInterpreter.java +++ b/src/main/java/ghidrathon/interpreter/GhidrathonInterpreter.java @@ -10,6 +10,7 @@ package ghidrathon.interpreter; +import com.google.gson.*; import generic.jar.ResourceFile; import ghidra.app.script.GhidraScript; import ghidra.app.script.GhidraScriptUtil; @@ -22,6 +23,8 @@ import java.io.*; import java.lang.reflect.*; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import jep.Jep; import jep.JepConfig; @@ -32,6 +35,11 @@ /** Utility class used to configure a Jep instance to access Ghidra */ public class GhidrathonInterpreter { + private class GhidrathonSave { + String executable; + String home; + } + private static final String GHIDRATHON_SAVE_FILENAME = "ghidrathon.save"; private static final String SUPPORTED_JEP_VERSION = "4.2.0"; @@ -50,7 +58,8 @@ public class GhidrathonInterpreter { private static File jepPythonPackageDir = null; private static File jepNativeFile = null; - private static File pythonFile = null; + private static File pythonExecutableFile = null; + private static File pythonHomeDir = null; /** * Create and configure a new GhidrathonInterpreter instance. @@ -66,23 +75,31 @@ private GhidrathonInterpreter(GhidrathonConfig config) throws JepException, IOEx // we must configure jep.MainInterpreter once before creating our first jep.SharedInterpreter if (jepMainInterpreterInitialized.get() == false) { + Msg.info(GhidrathonInterpreter.class, "Configuring jep.MainInterpreter."); + configureJepMainInterpreter(); jepMainInterpreterInitialized.set(true); } // we must set JepConfig once before creating the first jep.SharedInterpreter if (jepConfigInitialized.get() == false) { + Msg.info(GhidrathonInterpreter.class, "Configuring jep.JepConfig."); setJepConfig(); jepConfigInitialized.set(true); } + Msg.info(GhidrathonInterpreter.class, "Creating new jep.SharedInterpreter."); + // create new Jep SharedInterpreter instance jep_ = new jep.SharedInterpreter(); // we must configure Python sys module AFTER the first jep.SharedInterpreter is created if (jepPythonSysModuleInitialized.get() == false) { + Msg.info(GhidrathonInterpreter.class, "Configuring Python sys module."); + jep_.eval( - String.format("import sys;sys.executable=sys._base_executable=r\"%s\"", this.pythonFile)); + String.format( + "import sys;sys.executable=sys._base_executable=r\"%s\"", this.pythonExecutableFile)); // site module configures other necessary sys vars, e.g. sys.prefix, using sys.executable jep_.eval("import site;site.main()"); jep_.eval( @@ -191,28 +208,61 @@ private void configureJepMainInterpreter() throws JepException, FileNotFoundExce GhidrathonInterpreter.class, String.format("Using save file at %s.", ghidrathonSaveFile.getAbsolutePath())); - // read absolute path of Python interpreter from save file + GhidrathonSave ghidrathonSave = null; try (BufferedReader reader = new BufferedReader(new FileReader(ghidrathonSaveFile))) { - String pythonFilePath = reader.readLine().trim(); - if (pythonFilePath != null && !pythonFilePath.isEmpty()) { - this.pythonFile = new File(pythonFilePath); + String json = reader.readLine().trim(); + if (json != null && !json.isEmpty()) { + try { + ghidrathonSave = new Gson().fromJson(json, GhidrathonSave.class); + } catch (JsonSyntaxException e) { + throw new JepException( + String.format( + "Failed to parse JSON from %s (%s). Please configure Ghidrathon before running" + + " it.", + ghidrathonSaveFile.getAbsolutePath(), e)); + } } } catch (IOException e) { throw new JepException( String.format("Failed to read %s (%s)", ghidrathonSaveFile.getAbsolutePath(), e)); } - // validate Python file path exists and is a file - if (this.pythonFile == null || !(this.pythonFile.exists() && this.pythonFile.isFile())) { + if (ghidrathonSave.home == null || ghidrathonSave.executable == null) { throw new JepException( String.format( - "Python path %s is not valid. Please configure Ghidrathon before running it.", - this.pythonFile.getAbsolutePath())); + "%s JSON is not valid. Please configure Ghidrathon before running it.", + ghidrathonSaveFile.getAbsolutePath())); } Msg.info( GhidrathonInterpreter.class, - String.format("Using Python interpreter at %s.", this.pythonFile.getAbsolutePath())); + String.format( + "ghidrathonSave.home = \"%s\", ghidrathonSave.executable = \"%s\"", + ghidrathonSave.home, ghidrathonSave.executable)); + + // validate Python home directory exists and is a directory + this.pythonHomeDir = new File(ghidrathonSave.home); + if (!(this.pythonHomeDir.exists() && this.pythonHomeDir.isDirectory())) { + throw new JepException( + String.format( + "Python home path %s is not valid. Please configure Ghidrathon before running it.", + this.pythonHomeDir.getAbsolutePath())); + } + + // validate Python executable path exists and is a file + this.pythonExecutableFile = new File(ghidrathonSave.executable); + if (!(this.pythonExecutableFile.exists() && this.pythonExecutableFile.isFile())) { + throw new JepException( + String.format( + "Python executable path %s is not valid. Please configure Ghidrathon before running" + + " it.", + this.pythonExecutableFile.getAbsolutePath())); + } + + Msg.info( + GhidrathonInterpreter.class, + String.format( + "Using Python interpreter at %s.", this.pythonExecutableFile.getAbsolutePath())); String jepPythonPackagePath = findJepPackageDir(); if (jepPythonPackagePath.isEmpty()) { @@ -294,26 +344,48 @@ private void configureJepMainInterpreter() throws JepException, FileNotFoundExce GhidrathonInterpreter.class, String.format("Using Jep version %s.", GhidrathonInterpreter.SUPPORTED_JEP_VERSION)); + /* + * We need to ensure Jep nativate can link its dependencies, namely + * Python. This must be done before jep.MainInterpreter is initialized so we attempt + * to load Jep native here and resolve any linking issues. Linking issues are most common + * when a non-standard Python install is used. + */ try { - MainInterpreter.setJepLibraryPath(this.jepNativeFile.getAbsolutePath()); + System.load(this.jepNativeFile.getAbsolutePath()); + } catch (UnsatisfiedLinkError e) { + Msg.info( + GhidrathonInterpreter.class, + String.format("Link error encountered when loading Jep native (%s)", e)); + + // https://github.com/ninia/jep/blob/dd2bf345392b1b66fd6c9aeb12c234a557690ba1/src/main/java/jep/LibraryLocator.java#L244 + Matcher m = Pattern.compile("libpython[\\w\\.]*").matcher(e.getMessage()); + if (!(m.find() && findPythonLibrary(m.group(0)))) { + if (!findPythonLibraryWindows()) { + // failed to resolve link error + throw new JepException(String.format("Failed to load native Jep (%s).", e)); + } + } + } - PyConfig config = new PyConfig(); + /* + * This is hacky but we do not have a way to force Jep to use the Jep native + * that we have resolved without calling jep.MainInterpreter.setJepLibraryPath. + * This results in System.load() twice which, according to the Java + * documentation is ok as the second load attempt is ignored. + */ + MainInterpreter.setJepLibraryPath(this.jepNativeFile.getAbsolutePath()); - // we can't auto import the site module becuase we are running an embedded Python interpreter - config.setNoSiteFlag(1); - config.setIgnoreEnvironmentFlag(1); + // delay site module import + PyConfig config = new PyConfig(); + config.setNoSiteFlag(1); - MainInterpreter.setInitParams(config); - } catch (IllegalStateException e) { - e.printStackTrace(this.err); - throw new RuntimeException(e); - } + MainInterpreter.setInitParams(config); } private String findJepPackageDir() { String output = execCmd( - this.pythonFile.getAbsolutePath(), + this.pythonExecutableFile.getAbsolutePath(), "-c", "import importlib.util;import" + " pathlib;print(pathlib.Path(importlib.util.find_spec('jep').origin).parent)"); @@ -346,6 +418,52 @@ private String execCmd(String... commands) { return output; } + /** + * Attempt to load libpython from within PYTHONHOME + * + * @param libraryName the full file name of libpython + * @return true if libpython was found and loaded. + */ + private boolean findPythonLibrary(String libraryName) { + // https://github.com/ninia/jep/blob/dd2bf345392b1b66fd6c9aeb12c234a557690ba1/src/main/java/jep/LibraryLocator.java#L275 + if (this.pythonHomeDir != null) { + for (String libDirName : new String[] {"lib", "lib64", "Lib"}) { + File libDir = new File(this.pythonHomeDir, libDirName); + if (!libDir.isDirectory()) { + continue; + } + File libraryFile = new File(libDir, libraryName); + if (libraryFile.exists()) { + System.load(libraryFile.getAbsolutePath()); + return true; + } + } + } + return false; + } + + /** + * Attempt to load pythonXX.dll from within PYTHONHOME + * + * @return true if pythonXX.dll was found and loaded. + */ + private boolean findPythonLibraryWindows() { + // https://github.com/ninia/jep/blob/dd2bf345392b1b66fd6c9aeb12c234a557690ba1/src/main/java/jep/LibraryLocator.java#L297 + if (this.pythonHomeDir != null) { + Pattern re = Pattern.compile("^python\\d\\d+\\.dll$"); + for (File file : this.pythonHomeDir.listFiles()) { + if (!file.isFile()) { + continue; + } + if (re.matcher(file.getName()).matches() && file.exists()) { + System.load(file.getAbsolutePath()); + return true; + } + } + } + return false; + } + /** * Configure wrapper functions in Python land. * diff --git a/util/ghidrathon_configure.py b/util/ghidrathon_configure.py index 7a3e2ce..fc18a61 100644 --- a/util/ghidrathon_configure.py +++ b/util/ghidrathon_configure.py @@ -6,14 +6,17 @@ # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and limitations under the License. -import importlib.util -import argparse -import pathlib -import logging import sys - +import json +import logging +import pathlib +import argparse +import importlib.util +from typing import Dict SUPPORTED_JEP_VERSION = "4.2.0" +PYTHON_HOME_DIR_KEY = "home" +PYTHON_EXECUTABLE_FILE_KEY = "executable" logger = logging.getLogger(__name__) @@ -34,7 +37,7 @@ def main(args): jep_spec = importlib.util.find_spec("jep") if jep_spec is None: logger.error( - "Jep is not installed. Please install Jep version %s before configuring Ghidrathon.", SUPPORTED_JEP_VERSION + "Jep is not installed. Please install Jep using the requirements.txt file before configuring Ghidrathon." ) return -1 @@ -63,6 +66,8 @@ def main(args): ) return -1 + ghidrathon_save: Dict[str, str] = {} + python_path: pathlib.Path = pathlib.Path("None" if not sys.executable else sys.executable) if not all((python_path.exists(), python_path.is_file())): logger.error( @@ -71,13 +76,26 @@ def main(args): ) return -1 + ghidrathon_save[PYTHON_EXECUTABLE_FILE_KEY] = str(python_path) logger.debug('Using Python interpreter located at "%s".', python_path) + home_path: pathlib.Path = pathlib.Path("None" if not sys.base_prefix else sys.base_prefix) + if not all((home_path.exists(), home_path.is_dir())): + logger.error( + 'sys.base_prefix value "%s" is not valid. Please verify your Python environment is correct before configuring Ghidrathon.', + home_path, + ) + return -1 + + ghidrathon_save[PYTHON_HOME_DIR_KEY] = str(home_path) + logger.debug('Using Python home located at "%s".', home_path) + + json_: str = json.dumps(ghidrathon_save) save_path: pathlib.Path = install_path / "ghidrathon.save" try: - save_path.write_text(str(python_path), encoding="utf-8") + save_path.write_text(json_, encoding="utf-8") except Exception as e: - logger.error('Failed to write "%s" to "%s" (%s).', python_path, save_path, e) + logger.error('Failed to write "%s" to "%s" (%s).', json_, save_path, e) return -1 try: @@ -100,10 +118,9 @@ def main(args): ) return -1 - logger.debug('Wrote "%s" to "%s".', python_path, save_path) + logger.debug('Wrote "%s" to "%s".', json_, save_path) logger.info( - 'Ghidrathon has been configured to use the Python interpreter located at "%s". Please restart Ghidra for these changes to take effect.', - python_path, + "Ghidrathon has been configured to use this Python interpreter. Please restart Ghidra for these changes to take effect." ) return 0 diff --git a/util/requirements.txt b/util/requirements.txt new file mode 100644 index 0000000..942c0b4 --- /dev/null +++ b/util/requirements.txt @@ -0,0 +1 @@ +jep==4.2.0 \ No newline at end of file