diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe9ed3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +*target* +*.jar +*.war +*.ear +*.class + +# eclipse specific git ignore +*.pydevproject +.project +.metadata +bin/** +tmp/** +tmp/**/* +*.tmp +*.bak +*.swp +*~.nib +local.properties +.classpath +.settings/ +.loadpath + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# Ghidra extension ignore +**/test/** +**/help/** +build/** +dist/** +*.so +*.dll +*.exe +.gradle/** +/bin/ diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..20aa701 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (C) 2022 Mandiant, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + 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. diff --git a/Module.manifest b/Module.manifest new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..64a4877 --- /dev/null +++ b/README.md @@ -0,0 +1,140 @@ +# Ghidrathon + +[![License](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE.txt) + +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 aims to enable the use of existing and development of new Python 3 tooling in Ghidra and help users script Ghidra using modern Python in a way that tightly integrates with Ghidra's UI. + +Ghidrathon links your local Python installation to Ghidra using the open-source project [Jep](https://github.com/ninia/jep). Essentially your local Python interpreter is running inside Ghidra with access to all your Python packages **and** the standard Ghidra scripting API. Ghidrathon also works with Python virtual environments helping you create, isolate, and manage packages you may only want installed for use in Ghidra. Because Ghidrathon uses your local Python installation you have control over the Python version and environment running inside Ghidra. + +Ghidrathon supports much of the functionality offered by Ghidra's native Jython extension. This includes an interactive Python 3 interpreter, integration with the Ghidra Script Manager, and script execution in Ghidra headless mode. + +## 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 + +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 + +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. + +``` +$ analyzeHeadless C:\Users\wampus example -process example.o -postScript ghidrathon_example.py +... +INFO SCRIPT: C:\Users\wampus\.ghidra\.ghidra_10.0.3_PUBLIC\Extensions\Ghidrathon-master\ghidra_scripts\ghidrathon_example.py (HeadlessAnalyzer) +Function _init @ 0x101000: 3 blocks, 8 instructions +Function FUN_00101020 @ 0x101020: 1 blocks, 2 instructions +Function __cxa_finalize @ 0x101040: 1 blocks, 2 instructions +Function printf @ 0x101050: 1 blocks, 2 instructions +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) +``` + +For more information on running Ghidra in headless mode check out `/support/analyzeHeadlessREADME.html`. + +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). + +## Third-Party Python Modules + +One of our biggest motivations in developing Ghidrathon was to enable use of third-party Python 3 modules in Ghidra. You can install a module and start using it inside Ghidra just as you would a typical Python setup. This also applies to modules you have previously installed. For example, we can install and use Unicorn to emulate ARM code inside Ghidra. + +![example](./data/ghidrathon_unicorn.png) + +## OS Support + +Ghidrathon supports the following operating systems: + +* Linux +* Windows + +## Requirements + +The following tools are needed to build, install, and run Ghidrathon: + +Tool | Version |Source | +|---|---|---| +| Ghidra | `10.1.2` | https://ghidra-sre.org | +| Jep | `>= 4.0` | https://github.com/ninia/jep | +| Gradle | `>= 6.0` | https://gradle.org/releases | +| Python | `>= 3.7` | https://www.python.org/downloads | + +## Python Virtual Environments + +Ghidrathon supports Python virtual environments. To use a Python virtual environment, simply build Ghidrathon inside your virtual environment **and** execute Ghidra inside the **same** virtual environment. + +## Building Ghidrathon + +**Note:** Review [Python Virtual Environments](#python-virtual-environments) before building if you would like to use a Python virtual environment for Ghidrathon. + +**Note**: Building Ghidrathon requires building Jep. If you are running Windows, then you will need to install the Microsoft C++ Build Tools found [here](https://visualstudio.microsoft.com/visual-cpp-build-tools/). See Jep's Windows documentation [here](https://github.com/ninia/jep/wiki/Windows) for more information. + +Use the following steps to build Ghidrathon for your environment: + +* Install Ghidra using the documentation [here](https://htmlpreview.github.io/?https://github.com/NationalSecurityAgency/ghidra/blob/stable/GhidraDocs/InstallationGuide.html#InstallationNotes) +* Install Gradle from [here](https://gradle.org/releases) +* Download the Ghidrathon source +* Run the following command from the Ghidrathon source directory: + * **Note:** Ghidrathon defaults to the Python binary found in your path. You can specify a different Python binary by adding the optional argument `-PPYTHON_BIN=` to the command below + +``` +$ gradle -PGHIDRA_INSTALL_DIR= +``` + +This command installs Jep, configures Ghidrathon with the necessary Jep binaries, and builds Ghidrathon. If successful, you will find a new directory in your Ghidrathon source directory named `dist` containing your Ghidrathon extension (`.zip`). Please open a new issue if you experience any issues building Ghidrathon. + +## Installing Ghidrathon + +Use the following steps to install your Ghidrathon extension in Ghidra: + +* Start Ghidra +* Navigate to `File > Install Extensions...` +* Click the green `+` button +* Navigate to your Ghidrathon extension built earlier (`.zip`) +* Click `Ok` +* Restart Ghidra + +### Disabling Jython + +Ghidrathon disables the built-in Jython script provider to avoid conflicts when Ghidra decides which provider should handle scripts with the `.py` file extension. This means existing Jython scripts cannot be executed with Ghidrathon installed. We recommend completely disabling the Jython extension. + +Use the following steps to disable the Jython extension: + +* Navigate to `File > Configure...` +* Click `Ghidra Core` +* Un-check `PythonPlugin` + +Use the following steps to enable the Jython extension: + +* Uninstall Ghidrathon +* Enable the Jython extension using the steps outlined above +* Restart Ghidra + +## Using Ghidrathon + +See [Python 3 Interpreter Window](#python-3-interpreter-window), [Ghidra Script Manager Integration](#ghidra-script-manager-integration), and [Ghidra Headless Mode](#ghidra-headless-mode) for more information about using Ghidrathon. + +## 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). \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..618e1e7 --- /dev/null +++ b/build.gradle @@ -0,0 +1,74 @@ +// Copyright (C) 2022 Mandiant, Inc. All Rights Reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: [package root]/LICENSE.txt +// 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. + +// Builds a Ghidra Extension for a given Ghidra installation. +// +// An absolute path to the Ghidra installation directory must be supplied either by setting the +// GHIDRA_INSTALL_DIR environment variable or Gradle project property: +// +// > export GHIDRA_INSTALL_DIR= +// > gradle +// +// or +// +// > gradle -PGHIDRA_INSTALL_DIR= +// +// Gradle should be invoked from the directory of the project to build. Please see the +// application.gradle.version property in /Ghidra/application.properties +// for the correction version of Gradle to use for the Ghidra installation you specify. + +def javaHome +def pythonBin + +if (project.hasProperty("PYTHON_BIN")) { + pythonBin = project.getProperty("PYTHON_BIN") +} +else { + pythonBin = "python" +} + +// we need to install Jep; this requires C++ build tools on Windows (see README); we need to define +// the env variable "JAVA_HOME" containing absolute path to Java JDK configured for Ghidra +task installJep(type: Exec) { + environment "JAVA_HOME", System.getProperty("java.home") + commandLine pythonBin, '-m', 'pip', 'install', 'jep' +} + +// we need to copy the Jep native binaries built in installJep to our extension directory; we use a small +// utility script written in Python +task copyJepNativeBinaries(type: Exec) { + dependsOn installJep + workingDir "${projectDir}" + commandLine pythonBin, "util${File.separator}configure_jep_native_binaries.py" +} + +// make all tasks not matching copyJepNativeBinaries or installJep depend on copyJepNativeBinaries; mostly +// used to ensure our tasks run before Ghidra buildExtension task +tasks.matching { it.name != 'copyJepNativeBinaries' && it.name != 'installJep' }.all { Task task -> + task.dependsOn copyJepNativeBinaries +} + +// from here we use the standard Gradle build provided by Ghidra framework + +//----------------------START "DO NOT MODIFY" SECTION------------------------------ +def ghidraInstallDir + +if (System.env.GHIDRA_INSTALL_DIR) { + ghidraInstallDir = System.env.GHIDRA_INSTALL_DIR +} +else if (project.hasProperty("GHIDRA_INSTALL_DIR")) { + ghidraInstallDir = project.getProperty("GHIDRA_INSTALL_DIR") +} + +if (ghidraInstallDir) { + apply from: new File(ghidraInstallDir).getCanonicalPath() + "/support/buildExtension.gradle" +} +else { + throw new GradleException("GHIDRA_INSTALL_DIR is not defined!") +} +//----------------------END "DO NOT MODIFY" SECTION------------------------------- \ No newline at end of file diff --git a/data/README.txt b/data/README.txt new file mode 100644 index 0000000..1222f67 --- /dev/null +++ b/data/README.txt @@ -0,0 +1,15 @@ +The "data" directory is intended to hold data files that will be used by this module and will +not end up in the .jar file, but will be present in the zip or tar file. Typically, data +files are placed here rather than in the resources directory if the user may need to edit them. + +An optional data/languages directory can exist for the purpose of containing various Sleigh language +specification files and importer opinion files. + +The data/buildLanguage.xml is used for building the contents of the data/languages directory. + +The skel language definition has been commented-out within the skel.ldefs file so that the +skeleton language does not show-up within Ghidra. + +See the Sleigh language documentation (docs/languages/index.html) for details Sleigh language +specification syntax. + \ No newline at end of file diff --git a/data/ghidrathon_interp.png b/data/ghidrathon_interp.png new file mode 100644 index 0000000..42dab4c Binary files /dev/null and b/data/ghidrathon_interp.png differ diff --git a/data/ghidrathon_script.png b/data/ghidrathon_script.png new file mode 100644 index 0000000..b33e5d8 Binary files /dev/null and b/data/ghidrathon_script.png differ diff --git a/data/ghidrathon_unicorn.png b/data/ghidrathon_unicorn.png new file mode 100644 index 0000000..f5da022 Binary files /dev/null and b/data/ghidrathon_unicorn.png differ diff --git a/data/python/__init__.py b/data/python/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data/python/jepeval.py b/data/python/jepeval.py new file mode 100644 index 0000000..f4bc8b9 --- /dev/null +++ b/data/python/jepeval.py @@ -0,0 +1,74 @@ +# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: [package root]/LICENSE.txt +# 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. + +"""Used to eval Python statements passed from Java + +We use a function defined in Python versus Jep.eval in Java because it +gives us better control over handling multi-line statements +""" + +jepeval_lines = [] + + +def jepeval(line): + """attempt to compile and eval a given Python statement + + Args: + line (str): Python statement to compile/eval + + Returns: + bool: True if more input needed, otherwise False + """ + + def _jepeval(line): + """attempt to compile and eval a given Python statement + + Args: + line (str): Python statement to compile/eval + + Returns: + bool: True if more input needed, otherwise False + """ + global jepeval_lines + + if not line: + # statement may be empty e.g. user hit "enter" in console + if jepeval_lines: + # we have cached statements, combine and attempt to compile/eval + source = "\n".join(jepeval_lines) + jepeval_lines = None + exec(compile(source, "", "single"), globals(), globals()) + elif not jepeval_lines: + # we don't have any cached statements, attempt to compile/eval single statement + try: + exec(compile(line, "", "single"), globals(), globals()) + except (IndentationError, TabError) as err: + # assume IndetationError/TabError indicate user's attempt to define multi-line block + # e.g. for loop; cache statement to combine and compile/eval later + jepeval_lines = [line] + return True + else: + # we have cached statements, user must be defining a multi-line block e.g. for loop; cache + # statement to combine and compile/eval later + jepeval_lines.append(line) + return True + + return False + + more_input_needed = False + + try: + more_input_needed = _jepeval(line) + except Exception as err: + # Python exceptions are printed in Python instead of Java to improve error messaging + # in the Ghidra console window + import traceback + + traceback.print_exc() + + return more_input_needed diff --git a/data/python/jepinject.py b/data/python/jepinject.py new file mode 100644 index 0000000..ff81edb --- /dev/null +++ b/data/python/jepinject.py @@ -0,0 +1,30 @@ +# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: [package root]/LICENSE.txt +# 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. + +"""Inject GhidraScript methods into Python + +This lets us provide Python with GhidraScript helper methods e.g. getBytes. We store these in __buitlins__ to provide access +across Python imports similar to how this works in Jython. + +assumes __ghidra_script__ is passed from Java to Python prior to execution +""" + +for attr in dir(__ghidra_script__): + if attr.startswith("__"): + # ignore private + continue + if attr.startswith("print"): + # ignore helper functions for print e.g. print, println + continue + if attr == "java_name": + # ignore java_name added by Jep + continue + + o = getattr(__ghidra_script__, attr) + if callable(o) and attr not in dir(__builtins__): + setattr(__builtins__, attr, o) diff --git a/data/python/jeprunscript.py b/data/python/jeprunscript.py new file mode 100644 index 0000000..456270d --- /dev/null +++ b/data/python/jeprunscript.py @@ -0,0 +1,37 @@ +# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: [package root]/LICENSE.txt +# 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. + +"""Execute a Python script passed from Java + +We use a function defined in Python versus Jep.runScript in Java because it gives +us better control over handling exceptions. +""" + + +def jep_runscript(path): + """attempt to compiled and exec Python script file + + Args: + path (str): full path to Python script file + """ + with open(path, "rb") as f_in: + # attempt to read Python script + source = f_in.read() + + # set __file__ so child script can locate itself + # TODO: do we need to set others? + __file__ = path + + try: + exec(compile(source, path, "exec"), globals()) + except Exception as err: + # Python exceptions are printed in Python instead of Java to give us better error + # messages in the Ghidra console window + import traceback + + traceback.print_exc() diff --git a/data/python/jepstream.py b/data/python/jepstream.py new file mode 100644 index 0000000..e2d4ea3 --- /dev/null +++ b/data/python/jepstream.py @@ -0,0 +1,44 @@ +# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: [package root]/LICENSE.txt +# 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. + +"""Redirect sys.stdout and sys.stderr to Ghidra console window + +Python stdout and stderr print to Python; we want to see this output print to the Ghidra console window. To do this +we must override sys.stdout and sys.stderr with Java PrintWriters that are connected to the Ghidra console window. +""" +import sys +import io + + +def get_fake_io_wrapper(): + """build a TextIOWrapper referencing an empty byte array + + we set the encoding to the system default in hopes this doesn't cause issues when sending text from Python to Java + """ + return io.TextIOWrapper(io.BytesIO(b""), encoding=sys.getdefaultencoding()) + + +# sys.stdout and sys.stderr may be None (see https://docs.python.org/3/library/sys.html#sys.__stdout__); therefore +# we must set these to an object that has enough functionality to emulate basic write functionality. we create a +# TextIOWrapper referencing an empty byte array and override the write method with the write method of our Java +# PrintWriters connected to the Ghidra console window. hopefully this is good enough but we may run into issues in the +# future if Python code tries to reference unexpected methods/members e.g. "encoding" + + +if not sys.stdout: + sys.stdout = get_fake_io_wrapper() + +if not sys.stderr: + sys.stderr = get_fake_io_wrapper() + + +# assumes GhidraPluginToolConsoleOut/ErrWriter are passed from Java to Python before execution + + +sys.stdout.write = GhidraPluginToolConsoleOutWriter.write +sys.stderr.write = GhidraPluginToolConsoleErrWriter.write diff --git a/data/python/jepwelcome.py b/data/python/jepwelcome.py new file mode 100644 index 0000000..f632462 --- /dev/null +++ b/data/python/jepwelcome.py @@ -0,0 +1,35 @@ +# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: [package root]/LICENSE.txt +# 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. + +"""Print a welcome message to the console window + +We use a small Python script to print a welcome message to the Ghidra console window. This +should only be called after the Jep subinterpreter has been configured. +""" + +import sys + + +message = """ + _____ _ _ _ _ _ + / ____| | (_) | | | | | | + | | __| |__ _ __| |_ __ __ _| |_| |__ ___ _ __ + | | |_ | '_ \| |/ _` | '__/ _` | __| '_ \ / _ \| '_ \ + | |__| | | | | | (_| | | | (_| | |_| | | | (_) | | | | + \_____|_| |_|_|\__,_|_| \__,_|\__|_| |_|\___/|_| |_| + +Python %s Interpreter for Ghidra. Developed by FLARE. +""" + + +def format_version(): + """ """ + return "%d.%d.%d" % sys.version_info[:3] + + +print(message % format_version()) diff --git a/extension.properties b/extension.properties new file mode 100644 index 0000000..22dd8a2 --- /dev/null +++ b/extension.properties @@ -0,0 +1,5 @@ +name=Ghidrathon +description=The FLARE team's open-source extension to add Python 3 scripting to Ghidra. +author=Mike Hunhoff (michael.hunhoff@mandiant.com) +createdOn=03/31/2022 +version=@extversion@ diff --git a/ghidra_scripts/README.txt b/ghidra_scripts/README.txt new file mode 100644 index 0000000..9e408f4 --- /dev/null +++ b/ghidra_scripts/README.txt @@ -0,0 +1 @@ +Java source directory to hold module-specific Ghidra scripts. diff --git a/ghidra_scripts/ghidrathon_example.py b/ghidra_scripts/ghidrathon_example.py new file mode 100644 index 0000000..9ed8778 --- /dev/null +++ b/ghidra_scripts/ghidrathon_example.py @@ -0,0 +1,31 @@ +# Print function basic block and instruction counts. +# @author Mike Hunhoff (michael.hunhoff@mandiant.com) +# @category Python 3 + +# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: [package root]/LICENSE.txt +# 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. + +from ghidra.program.model.block import SimpleBlockIterator +from ghidra.program.model.block import BasicBlockModel + +for func in currentProgram.getListing().getFunctions(True): + block_count = 0 + + # find basic block count for the current function + block_itr = SimpleBlockIterator(BasicBlockModel(currentProgram), func.getBody(), monitor) + while block_itr.hasNext(): + block_count += 1 + block_itr.next() + + # find instruction count for the current function + insn_count = len(tuple(currentProgram.getListing().getInstructions(func.getBody(), True))) + + # print counts to user + print( + f"Function {func.getName()} @ {hex(func.getEntryPoint().getOffset())}: {block_count} blocks, {insn_count} instructions" + ) diff --git a/lib/README.txt b/lib/README.txt new file mode 100644 index 0000000..224d94f --- /dev/null +++ b/lib/README.txt @@ -0,0 +1,3 @@ +The "lib" directory is intended to hold Jar files which this module +is dependent upon. This directory may be eliminated from a specific +module if no other Jar files are needed. diff --git a/os/linux_x86_64/README.txt b/os/linux_x86_64/README.txt new file mode 100644 index 0000000..7dd33ce --- /dev/null +++ b/os/linux_x86_64/README.txt @@ -0,0 +1,3 @@ +The "os/linux_x86_64" directory is intended to hold Linux native binaries +which this module is dependent upon. This directory may be eliminated for a specific +module if native binaries are not provided for the corresponding platform. diff --git a/os/win_x86_64/README.txt b/os/win_x86_64/README.txt new file mode 100644 index 0000000..e035995 --- /dev/null +++ b/os/win_x86_64/README.txt @@ -0,0 +1,3 @@ +The "os/win_x86_64" directory is intended to hold MS Windows native binaries (.exe) +which this module is dependent upon. This directory may be eliminated for a specific +module if native binaries are not provided for the corresponding platform. diff --git a/src/main/java/ghidrathon/GhidrathonConsoleInputThread.java b/src/main/java/ghidrathon/GhidrathonConsoleInputThread.java new file mode 100644 index 0000000..51c5a02 --- /dev/null +++ b/src/main/java/ghidrathon/GhidrathonConsoleInputThread.java @@ -0,0 +1,169 @@ +// Copyright (C) 2022 Mandiant, Inc. All Rights Reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: [package root]/LICENSE.txt +// 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. + +package ghidrathon; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.util.concurrent.atomic.AtomicBoolean; + +import generic.jar.ResourceFile; +import ghidra.app.plugin.core.interpreter.InterpreterConsole; +import ghidra.app.script.GhidraState; +import ghidra.util.Msg; +import ghidrathon.interpreter.GhidrathonInterpreter; + +public class GhidrathonConsoleInputThread extends Thread { + + private static int generationCount = 0; + private GhidrathonPlugin plugin = null; + private InterpreterConsole console = null; + private AtomicBoolean shouldContinue = new AtomicBoolean(true); + private GhidrathonInterpreter python = null; + private PrintWriter err = null; + private PrintWriter out = null; + + GhidrathonConsoleInputThread(GhidrathonPlugin plugin) { + + super("Ghidrathon console input thread (generation " + ++generationCount + ")"); + + this.plugin = plugin; + this.console = plugin.getConsole(); + this.err = console.getErrWriter(); + this.out = console.getOutWriter(); + + } + + /** + * Console input thread. + * + * This thread passes Python statements from Java to Python to be evaluated. The interpreter is + * is configured to print stdout and stderr to the console Window. Multi-line Python blocks are + * supported but this is mostly handled in by the interpreter. + */ + @Override + public void run() { + + console.clear(); + + try { + + python = GhidrathonInterpreter.get(); + + // set Python stdout and stderr to reference our console window + python.setStreams(out, err); + python.printWelcome(); + + } catch (RuntimeException e) { + + if (python != null) { + python.close(); + } + + e.printStackTrace(err); + return; + + } + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(console.getStdin()))) { + + plugin.flushConsole(); + console.setPrompt(python.getPrimaryPrompt()); + + // begin reading and passing input from console stdin to Python to be evaluated + while (shouldContinue.get()) { + + String line; + + if (console.getStdin().available() > 0) { + line = reader.readLine(); + } else { + try { + + Thread.sleep(50); + + } catch (InterruptedException e) { + + } + + continue; + } + + + boolean moreInputWanted = evalPython(line); + + this.plugin.flushConsole(); + this.console.setPrompt(moreInputWanted ? python.getSecondaryPrompt() : python.getPrimaryPrompt()); + } + + } catch (RuntimeException | IOException e) { + + e.printStackTrace(); + Msg.error(GhidrathonConsoleInputThread.class, + "Internal error reading commands from python console. Please reset.", e); + + } finally { + + python.close(); + + } + } + + /** + * Configures Ghidra state and passes Python statement to Python. + * + * This function must be called by the same thread that created the Jep instance. + * See https://github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Features/Python/src/main/java/ghidra/python/PythonPluginExecutionThread.java#L55 + * + * @param line Python to evaluate + * @return True if more input needed, otherwise False + * @throws RuntimeException + */ + private boolean evalPython(String line) throws RuntimeException { + + boolean status; + + // set transaction for the execution + int transactionNumber = -1; + if (plugin.getCurrentProgram() != null) { + transactionNumber = plugin.getCurrentProgram().startTransaction("Ghidrathon command"); + } + + // setup Ghidra state to be passed into interpreter + plugin.getInteractiveTaskMonitor().clearCanceled(); + plugin.getInteractiveScript().setSourceFile(new ResourceFile(new File("Ghidrathon"))); + plugin.getInteractiveScript() + .set(new GhidraState(plugin.getTool(), plugin.getTool().getProject(), plugin.getCurrentProgram(), + plugin.getProgramLocation(), plugin.getProgramSelection(), plugin.getProgramHighlight()), + plugin.getInteractiveTaskMonitor(), new PrintWriter(console.getStdOut())); + + try { + + status = python.eval(line, plugin.getInteractiveScript()); + + } finally { + + if (plugin.getCurrentProgram() != null) { + plugin.getCurrentProgram().endTransaction(transactionNumber, true); + } + + } + + return status; + + } + + void dispose() { + + shouldContinue.set(false); + + } +} diff --git a/src/main/java/ghidrathon/GhidrathonPlugin.java b/src/main/java/ghidrathon/GhidrathonPlugin.java new file mode 100644 index 0000000..5ddfb4c --- /dev/null +++ b/src/main/java/ghidrathon/GhidrathonPlugin.java @@ -0,0 +1,178 @@ +// Copyright (C) 2022 Mandiant, Inc. All Rights Reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: [package root]/LICENSE.txt +// 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. + +package ghidrathon; + +import java.util.List; +import java.io.PrintWriter; +import java.io.OutputStream; + +import javax.swing.*; + +import ghidra.util.task.TaskMonitor; +import ghidra.framework.plugintool.*; +import ghidra.util.task.TaskLauncher; +import ghidra.app.plugin.ProgramPlugin; +import ghidra.app.ExamplesPluginPackage; +import ghidra.util.task.TaskMonitorAdapter; +import ghidra.framework.options.ToolOptions; +import ghidra.app.plugin.PluginCategoryNames; +import ghidra.framework.plugintool.util.PluginStatus; +import ghidra.app.plugin.core.console.CodeCompletion; +import ghidra.framework.options.OptionsChangeListener; +import ghidra.app.plugin.core.interpreter.InterpreterConsole; +import ghidra.app.plugin.core.interpreter.InterpreterConnection; +import ghidra.app.plugin.core.interpreter.InterpreterPanelService; + +//@formatter:off +@PluginInfo( + status = PluginStatus.STABLE, + packageName = ExamplesPluginPackage.NAME, + category = PluginCategoryNames.INTERPRETERS, + shortDescription = "Python 3 Interpreter", + description = "The FLARE team's open-source Python 3 interpreter console that is tightly integrated with a loaded Ghidra program.", + servicesRequired = { InterpreterPanelService.class }, + isSlowInstallation = true +) +//@formatter:on + +public class GhidrathonPlugin extends ProgramPlugin implements InterpreterConnection, OptionsChangeListener { + + private InterpreterConsole console; + private GhidrathonConsoleInputThread inputThread; + private TaskMonitor interactiveTaskMonitor; + private GhidrathonScript interactiveScript; + + public GhidrathonPlugin(PluginTool tool) { + + super(tool, true, true, true); + + } + + InterpreterConsole getConsole() { + + return console; + + } + + public TaskMonitor getInteractiveTaskMonitor() { + + return interactiveTaskMonitor; + + } + + GhidrathonScript getInteractiveScript() { + + return interactiveScript; + + } + + @Override + protected void init() { + + super.init(); + + console = getTool().getService(InterpreterPanelService.class).createInterpreterPanel(this, false); + console.addFirstActivationCallback(() -> resetInterpreter()); + + } + + @Override + public void optionsChanged(ToolOptions options, String optionName, Object oldValue, Object newValue) { + // TODO Auto-generated method stub + } + + @Override + public String getTitle() { + + return "Ghidrathon"; + + } + + @Override + public ImageIcon getIcon() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String toString() { + + return getPluginDescription().getName(); + + } + + public void flushConsole() { + + this.getConsole().getOutWriter().flush(); + this.getConsole().getErrWriter().flush(); + + } + + @Override + public List getCompletions(String cmd) { + // TODO Auto-generated method stub + return null; + } + + private void resetInterpreter() { + + TaskLauncher.launchModal("Resetting Ghidrathon...", () -> { + resetInterpreterInBackground(); + }); + + } + + @Override + protected void dispose() { + + // Terminate the input thread + if (inputThread != null) { + inputThread.dispose(); + inputThread = null; + } + + // Dispose of the console + if (console != null) { + console.dispose(); + console = null; + } + + super.dispose(); + + } + + private void resetInterpreterInBackground() { + + interactiveScript = new GhidrathonScript(); + interactiveTaskMonitor = new PythonInteractiveTaskMonitor(console.getStdOut()); + + inputThread = new GhidrathonConsoleInputThread(this); + inputThread.start(); + + } + + class PythonInteractiveTaskMonitor extends TaskMonitorAdapter { + + private PrintWriter output = null; + + public PythonInteractiveTaskMonitor(PrintWriter stdOut) { + output = stdOut; + } + + public PythonInteractiveTaskMonitor(OutputStream stdout) { + this(new PrintWriter(stdout)); + } + + @Override + public void setMessage(String message) { + output.println(": " + message); + } + + } +} diff --git a/src/main/java/ghidrathon/GhidrathonScript.java b/src/main/java/ghidrathon/GhidrathonScript.java new file mode 100644 index 0000000..bf1e031 --- /dev/null +++ b/src/main/java/ghidrathon/GhidrathonScript.java @@ -0,0 +1,166 @@ +// Copyright (C) 2022 Mandiant, Inc. All Rights Reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: [package root]/LICENSE.txt +// 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. + +package ghidrathon; + +import java.io.PrintWriter; +import java.io.FileNotFoundException; + +import generic.jar.ResourceFile; + +import ghidra.app.script.GhidraState; +import ghidra.app.script.GhidraScript; +import ghidra.app.script.GhidraScriptUtil; +import ghidra.app.services.ConsoleService; +import ghidra.framework.plugintool.PluginTool; +import ghidra.app.script.GhidraScriptProvider; + +import ghidrathon.interpreter.GhidrathonInterpreter; + +public class GhidrathonScript extends GhidraScript { + + @Override + protected void run() { + + GhidrathonInterpreter python = null; + + final PrintWriter out = getStdOut(); + final PrintWriter err = getStdErr(); + + try { + + python = GhidrathonInterpreter.get(); + + // redirect Python stdout and stderr to console window + python.setStreams(out, err); + + // run Python script from Python interpreter + python.runScript(getSourceFile(), this); + + // flush stdout and stderr to ensure all is printed to console window + err.flush(); + out.flush(); + + } catch (RuntimeException e) { + + e.printStackTrace(err); + + } finally { + + if (python != null) { + python.close(); + } + + } + + } + + /** + * Execute Python script using given script state + * See https://github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Features/Python/src/main/java/ghidra/python/PythonScript.java#L53 + * + * @param name Script name to execute + * @param scriptState Ghidra script state + */ + @Override + public void runScript(String name, GhidraState scriptState) { + + GhidrathonInterpreter python = null; + + final PrintWriter out = getStdOut(); + final PrintWriter err = getStdErr(); + + try { + + python = GhidrathonInterpreter.get(); + + // redirect Python stdout and stderr to console window + python.setStreams(out, err); + + ResourceFile source = GhidraScriptUtil.findScriptByName(name); + if (source == null) { + throw new FileNotFoundException("could not find file " + name); + } + + GhidraScriptProvider provider = GhidraScriptUtil.getProvider(source); + GhidraScript script = provider.getScriptInstance(source, writer); + + if (script == null) { + throw new RuntimeException("could not init ghidra script instance"); + } + + if (scriptState == state) { + updateStateFromVariables(); + } + + if (script instanceof GhidrathonScript) { + script.set(scriptState, monitor, writer); + + GhidrathonScript ghidrathonScript = (GhidrathonScript) script; + + // run Python script using interpreter + python.runScript(ghidrathonScript.getSourceFile(), ghidrathonScript); + } else { + script.execute(scriptState, monitor, writer); + } + + if (scriptState == state) { + loadVariablesFromState(); + } + + } catch (Exception e) { + + e.printStackTrace(err); + + } finally { + + if (python != null) { + python.close(); + } + + } + + } + + private PrintWriter getStdOut() { + + PluginTool tool = state.getTool(); + + if (tool != null) { + ConsoleService console = tool.getService(ConsoleService.class); + + if (console != null) { + return console.getStdOut(); + } + } + + return new PrintWriter(System.out, true); + } + + private PrintWriter getStdErr() { + + PluginTool tool = state.getTool(); + + if (tool != null) { + ConsoleService console = tool.getService(ConsoleService.class); + + if (console != null) { + return console.getStdErr(); + } + } + + return new PrintWriter(System.err, true); + } + + @Override + public String getCategory() { + + return "Ghidrathon"; + + } +} diff --git a/src/main/java/ghidrathon/GhidrathonScriptProvider.java b/src/main/java/ghidrathon/GhidrathonScriptProvider.java new file mode 100644 index 0000000..f72d4f4 --- /dev/null +++ b/src/main/java/ghidrathon/GhidrathonScriptProvider.java @@ -0,0 +1,90 @@ +// Copyright (C) 2022 Mandiant, Inc. All Rights Reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: [package root]/LICENSE.txt +// 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. + +package ghidrathon; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; + +import generic.jar.ResourceFile; + +import ghidra.app.script.GhidraScript; +import ghidra.app.script.GhidraScriptProvider; + +public class GhidrathonScriptProvider extends GhidraScriptProvider { + + @Override + public String getDescription() { + + return "Python 3"; + + } + + @Override + public String getExtension() { + + return ".py"; + + } + + @Override + public GhidraScript getScriptInstance(ResourceFile sourceFile, PrintWriter writer) + throws ClassNotFoundException, InstantiationException, IllegalAccessException { + + Class clazz = Class.forName(GhidrathonScript.class.getName()); + GhidraScript script = (GhidraScript) clazz.newInstance(); + + script.setSourceFile(sourceFile); + + return script; + + } + + @Override + public void createNewScript(ResourceFile newScript, String category) throws IOException { + + PrintWriter writer = new PrintWriter(new FileWriter(newScript.getFile(false))); + + writeHeader(writer, category); + writer.println(""); + writeBody(writer); + writer.println(""); + writer.close(); + + } + + @Override + public String getCommentCharacter() { + + return "#"; + + } + + /** + * Commandeer the .py script extension + * + * Ghidra loads script providers in order determined by Collections.sort; Ghidra then selects the first script provider that accepts the file extension of the script to be executed + * see https://github.com/NationalSecurityAgency/ghidra/blob/8b2ea61e27c07c48dc21eff9095905f739208703/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScriptUtil.java#L274-L281 + * + * Collections.sort invokes GhidraScriptProvider.compareTo so we can override compareTo and check if the script provider we are being compared to uses the .py extension; if true, we simply return + * -1 to be ordered higher in the list of script providers used by Ghidra + */ + @Override + public int compareTo(GhidraScriptProvider that) { + + if (that.getExtension().equals(".py")) { + // return -1 so our script provider is preferred + return -1; + } + + return super.compareTo(that); + + } + +} diff --git a/src/main/java/ghidrathon/interpreter/GhidrathonInterpreter.java b/src/main/java/ghidrathon/interpreter/GhidrathonInterpreter.java new file mode 100644 index 0000000..3d5b6d2 --- /dev/null +++ b/src/main/java/ghidrathon/interpreter/GhidrathonInterpreter.java @@ -0,0 +1,439 @@ +// Copyright (C) 2022 Mandiant, Inc. All Rights Reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: [package root]/LICENSE.txt +// 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. + +package ghidrathon.interpreter; + +import java.io.File; +import java.lang.reflect.*; +import java.io.PrintWriter; +import java.io.IOException; +import java.io.FileNotFoundException; + +import generic.jar.ResourceFile; + +import ghidra.framework.Application; +import ghidra.app.script.GhidraScript; +import ghidra.app.script.GhidraScriptUtil; + +import jep.Jep; +import jep.JepConfig; +import jep.JepException; +import jep.MainInterpreter; + +import ghidrathon.GhidrathonScript; + +/** + * Utility class used to configure a Jep instance to access Ghidra + */ +public class GhidrathonInterpreter { + + private Jep jep; + private JepConfig config; + + private boolean scriptMethodsInjected = false; + + private PrintWriter err = null; + private PrintWriter out = null; + + private static String extname = Application.getMyModuleRootDirectory().getName(); + + /** + * Create and configure a new GhidrathonInterpreter instance. + * + * @throws JepException + * @throws IOException + */ + private GhidrathonInterpreter() throws JepException, IOException{ + + // configure the Python includes path with the user's Ghdira script directory + String paths = ""; + for (ResourceFile resourceFile : GhidraScriptUtil.getScriptSourceDirectories()) { + paths += resourceFile.getFile(false).getAbsolutePath() + File.pathSeparator; + } + + // add data/python/ to Python includes directory + paths += Application.getModuleDataSubDirectory(extname, "python") + File.pathSeparator; + + config = new JepConfig(); + + // set the class loader with access to Ghidra scripting API + config.setClassLoader(ClassLoader.getSystemClassLoader()); + + // configure Python includes Path + config.addIncludePaths(paths); + + // we must set the native Jep library before creating a Jep instance + setJepNativeBinaryPath(); + + // create a new Jep interpreter instance + jep = new jep.SubInterpreter(config); + + // now that everything is configured, we should be able to run some utility scripts + // to help us further configure the Python environment + setJepEval(); + setJepRunScript(); + + } + + /** + * Configure native Jep library. + * + * User must build and include native Jep library in the appropriate OS folder prior to + * building this extension. + * Requires os/win64/libjep.dll for Windows + * Requires os/linux64/libjep.so for Linux + * + * @throws JepException + * @throws FileNotFoundException + */ + private void setJepNativeBinaryPath() throws JepException, FileNotFoundException { + + File nativeJep; + + try { + + nativeJep = Application.getOSFile(extname, "libjep.so"); + + } catch (FileNotFoundException e) { + + // whoops try Windows + nativeJep = Application.getOSFile(extname, "jep.dll"); + + } + + try { + + MainInterpreter.setJepLibraryPath(nativeJep.getAbsolutePath()); + + } catch (IllegalStateException e) { + // library path has already been set elsewhere, we expect this to happen as Jep Maininterpreter + // thread exists forever once it's created + } + + } + + + /** + * Configure "jepeval" function in Python land. + * + * We use Python to evaluate Python statements because as of Jep 4.0 interactive mode + * is no longer supported. As a side effect we also get better tracebacks. + * Requires data/python/jepeval.py. + * + * @throws JepException + * @throws FileNotFoundException + */ + private void setJepEval() throws JepException, FileNotFoundException { + + ResourceFile file = Application.getModuleDataFile(extname, "python/jepeval.py"); + + jep.runScript(file.getAbsolutePath()); + + } + + /** + * Configure "jep_runscript" function in Python land. + * + * We use Python to run Python scripts because it gives us better access to tracebacks. + * Requires data/python/jeprunscript.py. + * + * @throws JepException + * @throws FileNotFoundException + */ + private void setJepRunScript() throws JepException, FileNotFoundException { + + ResourceFile file = Application.getModuleDataFile(extname, "python/jeprunscript.py"); + + jep.runScript(file.getAbsolutePath()); + + } + + /** + * Configure GhidraState. + * + * This exposes things like currentProgram, currentAddress, etc. similar to Jython. We need to repeat this + * prior to executing new Python code in order to provide the latest state e.g. that current currentAddress. + * Requires data/python/jepinject.py. + * + * @param script GhidrathonScript instance + * @throws JepException + * @throws FileNotFoundException + */ + private void injectScriptHierarchy(GhidraScript script) throws JepException, FileNotFoundException { + if (script == null) { + return; + } + + // inject GhidraScript public/private fields e.g. currentAddress into Python + // see https://github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Features/Python/src/main/java/ghidra/python/GhidraPythonInterpreter.java#L341-L377 + for (Class scriptClass = script.getClass(); scriptClass != Object.class; scriptClass = + scriptClass.getSuperclass()) { + for (Field field : scriptClass.getDeclaredFields()) { + if (Modifier.isPublic(field.getModifiers()) || + Modifier.isProtected(field.getModifiers())) { + try { + field.setAccessible(true); + jep.set(field.getName(), field.get(script)); + } + catch (IllegalAccessException iae) { + throw new JepException("Unexpected security manager being used!"); + } + } + } + } + + if (!scriptMethodsInjected) { + // inject GhidraScript methods into Python + ResourceFile file = Application.getModuleDataFile(extname, "python/jepinject.py"); + jep.set("__ghidra_script__", script); + jep.runScript(file.getAbsolutePath()); + } + + scriptMethodsInjected = true; + } + + /** + * Create a new GhidrathonInterpreter instance. + * + * @return GhidrathonInterpreter + * @throws RuntimeException + */ + public static GhidrathonInterpreter get() throws RuntimeException { + + try { + + return new GhidrathonInterpreter(); + + } catch (Exception e) { + + e.printStackTrace(); + throw new RuntimeException(e); + + } + + } + + /** + * Close Jep instance. + * + * We must call this function when finished with a Jep instance, otherwise, issues arise if we try to create a + * new Jep instance on the same thread. This function must be called from the same thread that created the Jep instance. + */ + public void close() { + + try { + + if (jep != null) { + jep.close(); + jep = null; + } + + } catch (JepException e) { + + e.printStackTrace(); + throw new RuntimeException(e); + + } + + } + + /** + * Pass value from Java to Python + * + * @param value name as seen in Python + * @param o Java object to be passed to Python + * @return + */ + public void set(String name, Object o) { + + try { + + jep.set(name, o); + + } catch (JepException e) { + + e.printStackTrace(); + throw new RuntimeException(e); + + } + + } + + /** + * Evaluate Python statement. + * + * This function must be called from the same thread that instantiated the Jep instance. + * + * @param line Python statement + * + * @return True (need more input), False (no more input needed) + */ + public boolean eval(String line) { + + try { + + return (boolean) jep.invoke("jepeval", line); + + } catch (JepException e) { + + // Python exceptions should be handled in Python land; something bad must have happened + e.printStackTrace(); + throw new RuntimeException(e); + + } + + } + + /** + * Evaluate Python statement. + * + * This function must be called from the same thread that instantiated the Jep instance. + * + * @param line Python statement + * @param script GhidrathonScript with desired state. + * + * @return True (need more input), False (no more input needed) + * @throws FileNotFoundException + */ + public boolean eval(String line, GhidrathonScript script) { + + try { + + injectScriptHierarchy(script); + + } catch (JepException | FileNotFoundException e) { + + // we made it here; something bad went wrong, raise to caller + e.printStackTrace(); + throw new RuntimeException(e); + + } + + try { + + return (boolean) jep.invoke("jepeval", line); + + } catch (JepException e) { + + // Python exceptions should be handled in Python land; something bad must have happened + e.printStackTrace(); + throw new RuntimeException(e); + + } + + } + + /** + * Run Python script. + * + * This function must be called from the same thread that instantiated the Jep instance. + * + * @param file Python script to execute + */ + public void runScript(ResourceFile file) { + + try { + + jep.invoke("jep_runscript", file.getAbsolutePath()); + + } catch (JepException e) { + + // Python exceptions should be handled in Python land; something bad must have happened + e.printStackTrace(); + throw new RuntimeException(e); + + } + + } + + /** + * Run Python script. + * + * This function must be called from the same thread that instantiated the Jep instance. + * + * @param file Python script to execute + * @param script GhidrathonScript with desired state. + * @throws FileNotFoundException + */ + public void runScript(ResourceFile file, GhidraScript script) { + + try { + + injectScriptHierarchy(script); + jep.invoke("jep_runscript", file.getAbsolutePath()); + + } catch (JepException | FileNotFoundException e) { + + // Python exceptions should be handled in Python land; something bad must have happened + e.printStackTrace(); + throw new RuntimeException(e); + + } + + } + + /** + * Set output and error streams for Jep instance. + * + * Output and error streams from Python interpreter are redirected to the specified streams. If these are not set, this data is lost. + * + * @param out output stream + * @param err error stream + */ + public void setStreams(PrintWriter out, PrintWriter err) { + + try { + + ResourceFile file = Application.getModuleDataFile(extname, "python/jepstream.py"); + + jep.set("GhidraPluginToolConsoleOutWriter", out); + jep.set("GhidraPluginToolConsoleErrWriter", err); + + jep.runScript(file.getAbsolutePath()); + + this.out = out; + this.err = err; + + } catch (JepException | FileNotFoundException e) { + + // ensure stack trace prints to err stream for user + e.printStackTrace(); + throw new RuntimeException(e); + + } + + } + + public void printWelcome() { + + try { + + ResourceFile file = Application.getModuleDataFile(extname, "python/jepwelcome.py"); + jep.runScript(file.getAbsolutePath()); + + } catch (JepException | FileNotFoundException e) { + + e.printStackTrace(); + throw new RuntimeException(e); + + } + + } + + public String getPrimaryPrompt() { + + return ">>> "; + + } + + public String getSecondaryPrompt() { + + return "... "; + + } +} \ No newline at end of file diff --git a/src/main/resources/images/README.txt b/src/main/resources/images/README.txt new file mode 100644 index 0000000..f20ae77 --- /dev/null +++ b/src/main/resources/images/README.txt @@ -0,0 +1,2 @@ +The "src/resources/images" directory is intended to hold all image/icon files used by +this module. diff --git a/util/configure_jep_native_binaries.py b/util/configure_jep_native_binaries.py new file mode 100644 index 0000000..bacfea6 --- /dev/null +++ b/util/configure_jep_native_binaries.py @@ -0,0 +1,125 @@ +# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: [package root]/LICENSE.txt +# 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 subprocess +import argparse +import logging +import shutil +import glob +import sys + +from pathlib import Path + +JEP_PY_FOLDER_NAME = "jep" +JEP_OS_LIB_NAME_WINDOWS = "jep.dll" +JEP_OS_LIB_NAME_LINUX = "libjep.so" + +GHIDRA_JAVA_LIB_PATH = "lib" +GHIDRA_OS_LIB_PATH_WINDOWS = "os/win_x86_64" +GHIDRA_OS_LIB_PATH_LINUX = "os/linux_x86_64" + + +logger = logging.getLogger(__name__) + +handler = logging.StreamHandler() +formatter = logging.Formatter("%(asctime)s %(levelname)-8s %(message)s") + +handler.setFormatter(formatter) + +logger.addHandler(handler) +logger.setLevel(logging.INFO) + + +def find_jep_dir(): + """attempt to locate Jep Python module directory + + we use naive method of checking each path in sys.path for a folder named jep + """ + for path in sys.path: + jep_dir = Path(path) / JEP_PY_FOLDER_NAME + logger.debug("Checking if %s exists" % jep_dir) + if jep_dir.is_dir(): + return jep_dir + return Path() + + +def main(args): + """ """ + if args.debug: + logger.setLevel(logging.DEBUG) + + if sys.platform in ("darwin",): + # we haven't tested MacOS enough to know for sure if we fully support it + logger.error("MacOS is not supported!") + return -1 + + if sys.platform in ("win32", "cygwin"): + logger.debug("Detected Windows OS") + + os_lib_name = JEP_OS_LIB_NAME_WINDOWS + os_lib_path = Path(GHIDRA_OS_LIB_PATH_WINDOWS) + else: + logger.debug("Detected Linux OS") + + os_lib_name = JEP_OS_LIB_NAME_LINUX + os_lib_path = Path(GHIDRA_OS_LIB_PATH_LINUX) + + logger.info("Searching for Jep Python module directory") + + if args.path: + jep_dir = Path(args.path) + if not jep_dir.is_dir(): + logger.error("Python module directory %s does not exist!" % args.path) + return -1 + else: + jep_dir = find_jep_dir() + if not jep_dir: + logger.error("Could not find Jep Python module directory!") + return -1 + + logger.info("Found Jep Python module directory at %s" % jep_dir) + + try: + jep_java_lib_name = glob.glob(str(Path(jep_dir) / "*.jar"), recursive=False)[0] + except IndexError: + logger.error("Could not find Jep JAR file in directory %s" % jep_dir) + return -1 + + logger.info("Copying %s and %s to extension folders" % (os_lib_name, jep_java_lib_name)) + + # copy the Jep JAR file to the appropriate extension folder + logger.debug("Copying %s to %s" % (Path(jep_dir) / jep_java_lib_name, Path(GHIDRA_JAVA_LIB_PATH))) + try: + shutil.copy(Path(jep_dir) / jep_java_lib_name, Path(GHIDRA_JAVA_LIB_PATH), follow_symlinks=True) + except Exception as e: + logger.error("%s" % e) + return -1 + + # copy the Jep OS-dependent file to the appopriate extension folder + logger.debug("Copying %s to %s" % (Path(jep_dir) / os_lib_name, os_lib_path)) + try: + shutil.copy(Path(jep_dir) / os_lib_name, os_lib_path, follow_symlinks=True) + except Exception as e: + logger.error("%s" % e) + return -1 + + logger.info("Done") + + return 0 + + +if __name__ == "__main__": + """ """ + parser = argparse.ArgumentParser( + description="Locate Jep module directory and copy necessary files to Ghidrathon extension directories." + ) + + parser.add_argument("-p", "--path", type=str, help="Full path to Jep Python module directory") + parser.add_argument("-d", "--debug", action="store_true", help="Show debug messages") + + sys.exit(main(parser.parse_args()))