Python3 support

This commit is contained in:
DC3-TSD
2024-09-09 09:56:46 -04:00
parent d7c1f65f43
commit 92d0f1dacf
101 changed files with 11413 additions and 13 deletions
+5
View File
@@ -86,3 +86,8 @@ Release
*.log
core.*
!core.png
!core.py
# python files
*.egg-info
__pycache__
@@ -65,6 +65,7 @@ public class VSCodeProjectScript extends GhidraScript {
writeSettings(installDir, projectDir, classpathSourceMap);
writeLaunch(installDir, projectDir, classpathSourceMap);
writeSampleScriptJava(projectDir);
writeSampleScriptPyhidra(projectDir);
writeSampleModule(installDir, projectDir);
println("Successfully created VSCode project directory at: " + projectDir);
@@ -226,6 +227,25 @@ public class VSCodeProjectScript extends GhidraScript {
}
FileUtils.writeStringToFile(scriptFile, sampleScript, StandardCharsets.UTF_8);
}
private void writeSampleScriptPyhidra(File projectDir) throws IOException {
File scriptsDir = new File(projectDir, "ghidra_scripts");
File scriptFile = new File(scriptsDir, "sample_script.py");
String sampleScript = """
# Sample Pyhidra GhidraScript
# @category Examples
# @runtime Pyhidra
from java.util import LinkedList
java_list = LinkedList([1,2,3])
block = currentProgram.memory.getBlock('.text')
""";
if (!FileUtilities.mkdirs(scriptFile.getParentFile())) {
throw new IOException("Failed to create: " + scriptFile.getParentFile());
}
FileUtils.writeStringToFile(scriptFile, sampleScript, StandardCharsets.UTF_8);
}
/**
* Write a sample Java-based Ghidra module into the VSCode project directory
+1
View File
@@ -0,0 +1 @@
/.pytest_cache/
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.jdt.launching.remoteJavaApplication">
<booleanAttribute key="org.eclipse.debug.core.ATTR_FORCE_SYSTEM_CONSOLE_ENCODING" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/Features Pyhidra"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="4"/>
</listAttribute>
<booleanAttribute key="org.eclipse.jdt.launching.ALLOW_TERMINATE" value="true"/>
<mapAttribute key="org.eclipse.jdt.launching.CONNECT_MAP">
<mapEntry key="hostname" value="localhost"/>
<mapEntry key="port" value="18001"/>
</mapAttribute>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="Features Pyhidra"/>
<stringAttribute key="org.eclipse.jdt.launching.VM_CONNECTOR_ID" value="org.eclipse.jdt.launching.socketAttachConnector"/>
</launchConfiguration>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.debug.core.groups.GroupLaunchConfigurationType">
<stringAttribute key="org.eclipse.debug.core.launchGroup.0.action" value="OUTPUT_REGEXP"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.0.actionParam" value="Listening for transport dt_socket at address: \d+"/>
<booleanAttribute key="org.eclipse.debug.core.launchGroup.0.adoptIfRunning" value="false"/>
<booleanAttribute key="org.eclipse.debug.core.launchGroup.0.enabled" value="true"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.0.mode" value="debug"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.0.name" value="_Pyhidra GUI Debug"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.1.action" value="NONE"/>
<booleanAttribute key="org.eclipse.debug.core.launchGroup.1.adoptIfRunning" value="true"/>
<booleanAttribute key="org.eclipse.debug.core.launchGroup.1.enabled" value="true"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.1.mode" value="debug"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.1.name" value="Ghidra Attach"/>
<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
</listAttribute>
</launchConfiguration>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.python.pydev.debug.regularLaunchConfigurationType">
<booleanAttribute key="org.eclipse.debug.core.ATTR_FORCE_SYSTEM_CONSOLE_ENCODING" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/Features Pyhidra/src/main/py/src/pyhidra"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="2"/>
</listAttribute>
<mapAttribute key="org.eclipse.debug.core.environmentVariables">
<mapEntry key="GHIDRA_INSTALL_DIR" value="${project_loc:/___root}"/>
<mapEntry key="JAVA_HOME_OVERRIDE" value="${ee_home:JavaSE-21}"/>
</mapAttribute>
<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
</listAttribute>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_OTHER_WORKING_DIRECTORY" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="-v&#13;&#10;-g"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.python.pydev.debug.ATTR_INTERPRETER" value="__default"/>
<stringAttribute key="org.python.pydev.debug.ATTR_PROJECT" value="Features Pyhidra"/>
<stringAttribute key="process_factory_id" value="org.python.pydev.debug.processfactory.PyProcessFactory"/>
</launchConfiguration>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.debug.core.groups.GroupLaunchConfigurationType">
<stringAttribute key="org.eclipse.debug.core.launchGroup.0.action" value="OUTPUT_REGEXP"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.0.actionParam" value="Listening for transport dt_socket at address: \d+"/>
<booleanAttribute key="org.eclipse.debug.core.launchGroup.0.adoptIfRunning" value="false"/>
<booleanAttribute key="org.eclipse.debug.core.launchGroup.0.enabled" value="true"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.0.mode" value="debug"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.0.name" value="_Pyhidra Interpreter Debug"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.1.action" value="NONE"/>
<booleanAttribute key="org.eclipse.debug.core.launchGroup.1.adoptIfRunning" value="true"/>
<booleanAttribute key="org.eclipse.debug.core.launchGroup.1.enabled" value="true"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.1.mode" value="debug"/>
<stringAttribute key="org.eclipse.debug.core.launchGroup.1.name" value="Ghidra Attach"/>
<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
</listAttribute>
</launchConfiguration>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.python.pydev.debug.regularLaunchConfigurationType">
<booleanAttribute key="org.eclipse.debug.core.ATTR_FORCE_SYSTEM_CONSOLE_ENCODING" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/Features Pyhidra/src/main/py/src/pyhidra"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="2"/>
</listAttribute>
<mapAttribute key="org.eclipse.debug.core.environmentVariables">
<mapEntry key="GHIDRA_INSTALL_DIR" value="${project_loc:/___root}"/>
<mapEntry key="JAVA_HOME_OVERRIDE" value="${ee_home:JavaSE-21}"/>
</mapAttribute>
<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
</listAttribute>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_OTHER_WORKING_DIRECTORY" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="-v"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.python.pydev.debug.ATTR_INTERPRETER" value="__default"/>
<stringAttribute key="org.python.pydev.debug.ATTR_PROJECT" value="Features Pyhidra"/>
<stringAttribute key="process_factory_id" value="org.python.pydev.debug.processfactory.PyProcessFactory"/>
</launchConfiguration>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.python.pydev.debug.regularLaunchConfigurationType">
<booleanAttribute key="org.eclipse.debug.core.ATTR_FORCE_SYSTEM_CONSOLE_ENCODING" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/Features Pyhidra/src/main/py/src/pyhidra"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="2"/>
</listAttribute>
<mapAttribute key="org.eclipse.debug.core.environmentVariables">
<mapEntry key="GHIDRA_INSTALL_DIR" value="${project_loc:/___root}"/>
<mapEntry key="JAVA_HOME_OVERRIDE" value="${ee_home:JavaSE-21}"/>
<mapEntry key="PYHIDRA_DEBUG" value="1"/>
</mapAttribute>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_OTHER_WORKING_DIRECTORY" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="-v&#13;&#10;-g"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.python.pydev.debug.ATTR_INTERPRETER" value="__default"/>
<stringAttribute key="org.python.pydev.debug.ATTR_PROJECT" value="Features Pyhidra"/>
<stringAttribute key="process_factory_id" value="org.python.pydev.debug.processfactory.PyProcessFactory"/>
</launchConfiguration>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.python.pydev.debug.regularLaunchConfigurationType">
<booleanAttribute key="org.eclipse.debug.core.ATTR_FORCE_SYSTEM_CONSOLE_ENCODING" value="false"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
<listEntry value="/Features Pyhidra/src/main/py/src/pyhidra"/>
</listAttribute>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
<listEntry value="2"/>
</listAttribute>
<mapAttribute key="org.eclipse.debug.core.environmentVariables">
<mapEntry key="GHIDRA_INSTALL_DIR" value="${project_loc:/___root}"/>
<mapEntry key="JAVA_HOME_OVERRIDE" value="${ee_home:JavaSE-21}"/>
<mapEntry key="PYHIDRA_DEBUG" value="1"/>
</mapAttribute>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_OTHER_WORKING_DIRECTORY" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="-v"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY" value="${workspace_loc:Features Pyhidra/src/main/py/src/pyhidra}"/>
<stringAttribute key="org.python.pydev.debug.ATTR_INTERPRETER" value="__default"/>
<stringAttribute key="org.python.pydev.debug.ATTR_PROJECT" value="Features Pyhidra"/>
<stringAttribute key="process_factory_id" value="org.python.pydev.debug.processfactory.PyProcessFactory"/>
</launchConfiguration>
+15
View File
@@ -0,0 +1,15 @@
EXCLUDE FROM GHIDRA JAR: true
MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp310-cp310-macosx_10_9_universal2.whl Apache License 2.0
MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl Apache License 2.0
MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp310-cp310-win_amd64.whl Apache License 2.0
MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp311-cp311-macosx_10_9_universal2.whl Apache License 2.0
MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl Apache License 2.0
MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp311-cp311-win_amd64.whl Apache License 2.0
MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp312-cp312-macosx_10_9_universal2.whl Apache License 2.0
MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl Apache License 2.0
MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp312-cp312-win_amd64.whl Apache License 2.0
MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl Apache License 2.0
MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0-cp39-cp39-win_amd64.whl Apache License 2.0
MODULE FILE LICENSE: pypkg/dist/JPype1-1.5.0.tar.gz Apache License 2.0
MODULE FILE LICENSE: pypkg/dist/packaging-23.2-py3-none-any.whl Apache License 2.0
MODULE FILE LICENSE: pypkg/dist/setuptools-68.0.0-py3-none-any.whl MIT
+81
View File
@@ -0,0 +1,81 @@
/* ###
* IP: GHIDRA
*
* 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.
*/
apply from: "$rootProject.projectDir/gradle/distributableGhidraModule.gradle"
apply from: "$rootProject.projectDir/gradle/javaProject.gradle"
apply from: "$rootProject.projectDir/gradle/helpProject.gradle"
apply from: "$rootProject.projectDir/gradle/jacocoProject.gradle"
apply from: "$rootProject.projectDir/gradle/javaTestProject.gradle"
apply from: "$rootProject.projectDir/gradle/javadoc.gradle"
apply from: "${rootProject.projectDir}/gradle/hasPythonPackage.gradle"
apply plugin: 'eclipse'
eclipse.project.name = 'Features Pyhidra'
dependencies {
api project(':Base')
}
// NOTE: The Python package is a "Pure Python" package. Building the wheel does not
// require any dependencies except setuptools. Installing the wheel will require
// the correct os/python version of Jpype and packaging. Installing the wheel does
// not require Ghidra.
distributePyDep("JPype1-1.5.0-cp310-cp310-macosx_10_9_universal2.whl")
distributePyDep("JPype1-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl")
distributePyDep("JPype1-1.5.0-cp310-cp310-win_amd64.whl")
distributePyDep("JPype1-1.5.0-cp311-cp311-macosx_10_9_universal2.whl")
distributePyDep("JPype1-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl")
distributePyDep("JPype1-1.5.0-cp311-cp311-win_amd64.whl")
distributePyDep("JPype1-1.5.0-cp312-cp312-macosx_10_9_universal2.whl")
distributePyDep("JPype1-1.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl")
distributePyDep("JPype1-1.5.0-cp312-cp312-win_amd64.whl")
distributePyDep("JPype1-1.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl")
distributePyDep("JPype1-1.5.0-cp39-cp39-win_amd64.whl")
distributePyDep("JPype1-1.5.0.tar.gz")
distributePyDep("packaging-23.2-py3-none-any.whl")
distributePyDep("setuptools-68.0.0-py3-none-any.whl")
// Install JPype into the development virtual environment
task installJPype(type: Exec) {
dependsOn(":createPythonVirtualEnvironment")
File depsDir = file("${DEPS_DIR}/Pyhidra")
File binRepoDir = file("${BIN_REPO}/Ghidra/Features/Pyhidra")
def dir = depsDir.exists() ? depsDir : binRepoDir
commandLine "$PYTHON3_VENV", "-m", "pip", "install", "--no-index", "-f", "$dir", "JPype1"
}
// Install Pyhidra in editable mode to the development virtual environment
task installEditablePyhidra(type: Exec) {
dependsOn("installJPype")
commandLine "$PYTHON3_VENV", "-m", "pip", "install", "-e", "src/main/py"
}
rootProject.prepDev.dependsOn installEditablePyhidra
// Add pyhidraLauncher.py to the release
rootProject.assembleDistribution {
dependsOn(buildPyPackage)
def p = this.project
def zipPath = getZipPath(p)
from (this.project.projectDir.toString()) {
include "pyhidraLauncher.py"
into { zipPath }
}
}
@@ -0,0 +1,7 @@
##VERSION: 2.0
##MODULE IP: Apache License 2.0
Module.manifest||GHIDRA||||END|
data/python.theme.properties||GHIDRA||||END|
src/main/help/help/TOC_Source.xml||GHIDRA||||END|
src/main/help/help/topics/Pyhidra/interpreter.html||GHIDRA||||END|
src/main/resources/images/python.png||GHIDRA||||END|
@@ -0,0 +1,21 @@
[Defaults]
color.fg.plugin.python.syntax.class = color.palette.blue
color.fg.plugin.python.syntax.code = color.palette.darkgreen
color.fg.plugin.python.syntax.function = color.palette.green
color.fg.plugin.python.syntax.instance = color.palette.purple
color.fg.plugin.python.syntax.map = color.palette.steelblue
color.fg.plugin.python.syntax.method = color.palette.teal
color.fg.plugin.python.syntax.null = color.palette.red
color.fg.plugin.python.syntax.number = color.palette.darkgray
color.fg.plugin.python.syntax.package = color.palette.darkred
color.fg.plugin.python.syntax.sequence = color.palette.saddlebrown
color.fg.plugin.python.syntax.special = color.palette.darkgreen
icon.plugin.python = python.png
[Dark Defaults]
@@ -0,0 +1,77 @@
# Examples of Pyhidra-specific functionality
# @category: Examples.Python
# @runtime Pyhidra
# we can import java libraries just as if they were python libraries
from java.util import LinkedList
# and then use them like they are natural classes
java_list = LinkedList([1,2,3])
print(f"linked list object class: {java_list.__class__}")
# importing and using Ghidra modules is the same
from ghidra.program.flatapi import FlatProgramAPI
print(f"max references to a flat program api: {FlatProgramAPI.MAX_REFERENCES_TO}")
# we can also do normal python-ish things on our Java objects, like:
# indexing
print(f"first element of the list: {java_list[0]}")
# slicing
print(f"first two elements of the list: {java_list[0:2]}")
# list comprehension
java_list_double = [i * 2 for i in java_list]
print(f"list comprehension result: {java_list_double}")
# automatic calls to getters
print(f"current program name: {currentProgram.name}") # calls currentProgram.getName()
# here's an example of how this stuff might come in handy with Ghidra:
print('current program memory blocks:\n')
for block in currentProgram.memory.blocks:
print(block.name)
# many Ghidra functions need a Java-native array to pass or receive values
# JPype provides objects of JByte, JChar, etc. to meet this need
# this example demonstrates how you would create an array of bytes to get
# the first 10 bytes of memory from the .text section
# we need this import to get at the helper classes
import jpype
# get the block we need
block = currentProgram.memory.getBlock('.text')
if block:
# the verbose way of getting the array
byte_array_maker = jpype.JArray(jpype.JByte)
byte_array = byte_array_maker(10)
# we also could have taken a shortcut with just:
# byte_array = jpype.JByte[10]
# let's have a look at our new object
print(f"array class: {byte_array.__class__}")
# will be <java class 'byte[]'>
print(f"array length: {len(byte_array)}")
# we can now use this array wherever a Java method requires a byte[] type
# the signature of getBytes is getBytes(Address addr, byte[] b)
block.getBytes(block.start, byte_array)
# after the call, we can get the bytes out as desired
# we just put them in a list comprehension here
print(f"first 10 bytes of .text: {['%#x' % ((b+256)%256) for b in byte_array]}")
# if the data isn't being changed, a bytes-like objct may be used
data = b"Hello"
clearListing(block.start, block.start.add(len(data) - 1))
block.putBytes(block.start, data)
else:
print('no block named .text in this program.')
# see the user manual of JPype for more details on interoperability:
# https://jpype.readthedocs.io/en/latest/userguide.html
@@ -0,0 +1,91 @@
import argparse
import os
import sys
import subprocess
from pathlib import Path
from typing import List
from sys import stderr
def upgrade(pip_args: List[str], dist_dir: Path, current_pyhidra_version: str) -> bool:
from packaging.version import Version # if pyhidra imported, we know we have packaging
included_pyhidra: Path = next(dist_dir.glob('pyhidra-*.whl'), None)
if included_pyhidra is None:
print('Warning: included pyhidra wheel was not found', file=sys.stderr)
return
included_version: Version = Version(included_pyhidra.name.split('-')[1])
current_version: Version = Version(current_pyhidra_version)
if included_version > current_version:
choice: str = input(f'Do you wish to upgrade Pyhidra {current_version} to {included_version} (y/n)? ')
if choice.lower() in ('y', 'yes'):
pip_args.append('-U')
subprocess.check_call(pip_args)
return True
else:
print('Skipping upgrade')
return False
def install(pip_args: List[str], dist_dir: Path) -> bool:
choice: str = input('Do you wish to install Pyhidra (y/n)? ')
if choice.lower() in ('y', 'yes'):
subprocess.check_call(pip_args)
return True
elif choice.lower() in ('n', 'no'):
return False
else:
print('Please answer yes or no.')
return False
def main() -> None:
# Parse command line arguments
parser = argparse.ArgumentParser(prog=Path(__file__).name)
parser.add_argument('install_dir', metavar='<install dir>', help='Ghidra installation directory')
parser.add_argument('-c', '--console', action='store_true', help='Force console launch')
parser.add_argument('-d', '--dev', action='store_true', help='Ghidra development mode')
parser.add_argument('-H', '--headless', action='store_true', help='Ghidra headless mode')
args, remaining = parser.parse_known_args()
# Setup variables
python_cmd: str = sys.executable
install_dir: Path = Path(args.install_dir)
venv_dir: Path = install_dir / 'build' / 'venv'
pyhidra_dir: Path = install_dir / 'Ghidra' / 'Features' / 'Pyhidra'
src_dir: Path = pyhidra_dir / 'src' / 'main' / 'py'
dist_dir: Path = pyhidra_dir / 'pypkg' / 'dist'
# If headless, force console mode
if args.headless:
args.console = True
if args.dev:
# If in dev mode, launch pyhidra from the source tree using the development virtual environment
if not venv_dir.is_dir():
print('Virtual environment not found!')
print('Run "gradle prepdev" and try again.')
return
win_python_cmd = str(venv_dir / 'Scripts' / 'python.exe')
linux_python_cmd = str(venv_dir / 'bin' / 'python3')
python_cmd = win_python_cmd if os.name == 'nt' else linux_python_cmd
else:
# If in release mode, offer to install or upgrade pyhidra before launching from user-controlled environment
pip_args: List[str] = [python_cmd, '-m', 'pip', 'install', '--no-index', '-f', str(dist_dir), 'pyhidra']
try:
import pyhidra
upgrade(pip_args, dist_dir, pyhidra.__version__)
except ImportError:
if not install(pip_args, dist_dir):
return
# Launch Pyhidra
py_args: List[str] = [python_cmd, '-m', 'pyhidra.ghidra_launch', '--install-dir', str(install_dir)]
if args.headless:
py_args += ['ghidra.app.util.headless.AnalyzeHeadless']
else:
py_args += ['-g', 'ghidra.GhidraRun']
if args.console:
subprocess.call(py_args + remaining)
else:
creation_flags = getattr(subprocess, 'CREATE_NO_WINDOW', 0)
subprocess.Popen(py_args + remaining, creationflags=creation_flags, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if __name__ == "__main__":
main()
@@ -0,0 +1,9 @@
<?xml version='1.0' encoding='ISO-8859-1' ?>
<tocroot>
<tocref id="Ghidra Functionality">
<tocref id="Scripting">
<tocdef id="Pyhidra Interpreter" sortgroup="z" text="Pyhidra Interpreter" target="help/topics/Pyhidra/interpreter.html" />
</tocref>
</tocref>
</tocroot>
@@ -0,0 +1,167 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<HTML>
<HEAD>
<TITLE>Pyhidra Interpreter</TITLE>
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
</HEAD>
<BODY lang="EN-US">
<H1><A name="Pyhidra"></A>Pyhidra Interpreter</H1>
<P>
The Ghidra <I>Pyhidra Interpreter</I> provides a full general-purpose Python interactive shell
and allows you to interact with your current Ghidra session by exposing Ghidra's powerful Java
API through the magic of Jpype.
</P>
<H2>Environment</H2>
<BLOCKQUOTE>
<P>
The Ghidra <I>Pyhidra Interpreter</I> is configured to run in a similar context as a Ghidra
script. Therefore, you immediately have access to variables such as <TT>currentProgram</TT>,
<TT>currentSelection</TT>, <TT>currentAddress</TT>, etc without needing to import them.
These variables exist as Java objects behind the scenes, but Jpype allows you to interact with
them through a Python interface, which is similar to Java in some ways.
</P>
<P>
As in Java, classes outside of your current package/module need to be explicitly imported.
For example, consider the following code snippet:
</P>
<BR>
<PRE>
<FONT COLOR="GREEN"># Get a data type from the user</FONT>
tool = state.getTool()
dtm = currentProgram.getDataTypeManager()
from ghidra.app.util.datatype import DataTypeSelectionDialog
from ghidra.util.data.DataTypeParser import AllowedDataTypes
selectionDialog = DataTypeSelectionDialog(tool, dtm, -1, AllowedDataTypes.FIXED_LENGTH)
tool.showDialog(selectionDialog)
dataType = selectionDialog.getUserChosenDataType()
if dataType != None: print("Chosen data type: " + str(dataType))
</PRE>
<P>
<TT>currentProgram</TT> and <TT>state</TT> are defined within the Ghidra scripting class
hierarchy, so nothing has to be explicitly imported before they can be used. However, because
the <TT>DataTypeSelectionDialog</TT> class and <TT>AllowedDataType</TT> enum reside in
different packages, they must be explicitly imported. Failure to do so will result in a
Python <TT><FONT COLOR="RED">NameError</FONT></TT>.
</P>
</BLOCKQUOTE>
<H2><A name="Clear_Interpreter"></A>Clear <IMG border="0" src="images/erase16.png"></H2>
<BLOCKQUOTE>
<P>
This command clears the interpreter's display. Its effect is purely visual.
It does not affect the state of the interpreter in any way.
</P>
</BLOCKQUOTE>
<H2><A name="Interrupt_Interpreter"></A>Interrupt <IMG border="0" src="images/dialog-cancel.png"></H2>
<BLOCKQUOTE>
<P>
This command issues a keyboard interrupt to the interpreter, which can be used to interrupt
long running commands or loops.
</P>
</BLOCKQUOTE>
<H2><A name="Reset_Interpreter"></A>Reset <IMG border="0" src="images/reload3.png"></H2>
<BLOCKQUOTE>
<P>
This command resets the interpreter, which clears the display and resets all state.
</P>
</BLOCKQUOTE>
<H2>Keybindings</H2>
<BLOCKQUOTE>
<P>
The Ghidra <I>Pyhidra Interpreter</I> supports the following hard-coded keybindings:
<UL>
<LI><B>(up):</B>&nbsp;&nbsp;Move backward in command stack</LI>
<LI><B>(down):</B>&nbsp;&nbsp;Move forward in command stack</LI>
<LI><B>TAB:</B>&nbsp;&nbsp;Show code completion window</LI>
</UL>
<P>
With the code completion window open:
<UL>
<LI><B>TAB:</B>&nbsp;&nbsp;Insert currently-selected code completion (if no completion selected, select the first available)</LI>
<LI><B>ENTER:</B>&nbsp;&nbsp;Insert selected completion (if any) and close the completion window</LI>
<LI><B>(up):</B>&nbsp;&nbsp;Select previous code completion</LI>
<LI><B>(down):</B>&nbsp;&nbsp;Select next code completion</LI>
<LI><B>ESC:</B>&nbsp;&nbsp;Hide code completion window</LI>
</UL>
</P>
</BLOCKQUOTE>
<H2>Copy/Paste</H2>
<BLOCKQUOTE>
<P>
Copy and paste from within the Ghidra <I>Pyhidra Interpreter</I> should work as expected for
your given environment:
<UL>
<LI><B>Windows:</B>&nbsp;&nbsp;CTRL+C / CTRL+V</LI>
<LI><B>Linux:</B>&nbsp;&nbsp;CTRL+C / CTRL+V</LI>
<LI><B>OS X:</B>&nbsp;&nbsp;COMMAND+C / COMMAND+V</LI>
</UL>
</P>
</BLOCKQUOTE>
<H2>API Documentation</H2>
<BLOCKQUOTE>
<P>
The built-in <TT>help()</TT> Python function has been altered by the Ghidra <I>Pyhidra Interpreter</I>
to add support for displaying Ghidra's Javadoc (where available) for a given Ghidra class, method,
or variable. For example, to see Ghidra's Javadoc on the <TT>state</TT> variable, simply do:
<PRE>
>>> help(state)
#####################################################
class ghidra.app.script.GhidraState
extends java.lang.Object
Represents the current state of a Ghidra tool
#####################################################
PluginTool getTool()
Returns the current tool.
@return ghidra.framework.plugintool.PluginTool: the current tool
-----------------------------------------------------
Project getProject()
Returns the current project.
@return ghidra.framework.model.Project: the current project
-----------------------------------------------------
...
...
...
</PRE>
<P>
Calling help() with no arguments will show the Javadoc for the GhidraScript class.
</P>
</BLOCKQUOTE>
<H2>Additional Help</H2>
<BLOCKQUOTE>
<P>
For more information on the Jpype environment, such as how to interact with Java objects
through a Python interface, please refer to Jpype's documentation which can be found on the
Internet at <I><B>jpype.readthedocs.io</B></I>
</P>
</BLOCKQUOTE>
<P align="left" class="providedbyplugin">Provided by: <I>PyhidraPlugin</I></P>
<P>&nbsp;</P>
<BR>
<BR>
<BR>
</BODY>
</HTML>
@@ -0,0 +1,104 @@
package ghidra.pyhidra;
import java.util.function.Consumer;
import ghidra.app.CorePluginPackage;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.ProgramPlugin;
import ghidra.app.plugin.core.interpreter.*;
import ghidra.app.script.GhidraState;
import ghidra.framework.plugintool.PluginInfo;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.program.util.ProgramSelection;
import ghidra.pyhidra.interpreter.InterpreterGhidraScript;
import ghidra.pyhidra.interpreter.PyhidraInterpreter;
import ghidra.util.exception.AssertException;
/**
* This plugin provides the interactive Python interpreter.
*/
//@formatter:off
@PluginInfo(
status = PluginStatus.RELEASED,
packageName = CorePluginPackage.NAME,
category = PluginCategoryNames.COMMON,
shortDescription = "Pyhidra Interpreter",
description = "Provides an interactive Python Interpreter that is tightly integrated with a loaded Ghidra program.",
servicesRequired = { InterpreterPanelService.class }
)
//@formatter:on
public class PyhidraPlugin extends ProgramPlugin {
public static final String TITLE = "Pyhidra";
private static Consumer<PyhidraPlugin> initializer = null;
public final InterpreterGhidraScript script = new InterpreterGhidraScript();
public PyhidraInterpreter interpreter;
public PyhidraPlugin(PluginTool tool) {
super(tool);
GhidraState state = new GhidraState(tool, tool.getProject(), null, null, null, null);
// use the copy constructor so this state doesn't fire plugin events
script.set(new GhidraState(state), null, null);
}
/**
* Sets the plugin's Python side initializer.<p>
*
* This method is for <b>internal use only</b> and is only public so it can be
* called from Python.
*
* @param initializer the Python side initializer
* @throws AssertException if the code completer has already been set
*/
public static void setInitializer(Consumer<PyhidraPlugin> initializer) {
if (PyhidraPlugin.initializer != null) {
throw new AssertException("PyhidraPlugin initializer has already been set");
}
PyhidraPlugin.initializer = initializer;
}
@Override
public void init() {
interpreter = new PyhidraInterpreter(this, PyhidraPlugin.initializer != null);
if (initializer != null) {
initializer.accept(this);
}
}
@Override
public void dispose() {
interpreter.dispose();
super.dispose();
}
@Override
protected void programActivated(Program program) {
script.setCurrentProgram(program);
}
@Override
protected void programDeactivated(Program program) {
if (script.getCurrentProgram() == program) {
script.setCurrentProgram(null);
}
}
@Override
protected void locationChanged(ProgramLocation location) {
script.setCurrentLocation(location);
}
@Override
protected void selectionChanged(ProgramSelection selection) {
script.setCurrentSelection(selection);
}
@Override
protected void highlightChanged(ProgramSelection highlight) {
script.setCurrentHighlight(highlight);
}
}
@@ -0,0 +1,133 @@
package ghidra.pyhidra;
import java.io.*;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.function.Consumer;
import generic.jar.ResourceFile;
import ghidra.app.script.*;
import ghidra.app.util.headless.HeadlessScript;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.program.util.ProgramSelection;
import ghidra.pyhidra.PythonFieldExposer.ExposedFields;
import ghidra.util.exception.AssertException;
import ghidra.util.SystemUtilities;
import ghidra.util.task.TaskMonitor;
/**
* {@link GhidraScript} provider for native python3 scripts
*/
public final class PyhidraScriptProvider extends AbstractPythonScriptProvider {
private static Consumer<GhidraScript> scriptRunner = null;
/**
* Sets the Python side script runner.
*
* This method is for <b>internal use only</b> and is only public so it can be
* called from Python.
*
* @param scriptRunner the Python side script runner
* @throws AssertException if the script runner has already been set
*/
public static void setScriptRunner(Consumer<GhidraScript> scriptRunner) {
if (PyhidraScriptProvider.scriptRunner != null) {
throw new AssertException("scriptRunner has already been set");
}
PyhidraScriptProvider.scriptRunner = scriptRunner;
}
@Override
public String getDescription() {
return PyhidraPlugin.TITLE;
}
@Override
public String getRuntimeEnvironmentName() {
return PyhidraPlugin.TITLE;
}
@Override
public GhidraScript getScriptInstance(ResourceFile sourceFile, PrintWriter writer)
throws GhidraScriptLoadException {
if (scriptRunner == null) {
String msg = "Ghidra was not started with pyhidra. Python is not available";
throw new GhidraScriptLoadException(msg);
}
GhidraScript script = SystemUtilities.isInHeadlessMode() ? new PyhidraHeadlessScript()
: new PyhidraGhidraScript();
script.setSourceFile(sourceFile);
return script;
}
@ExposedFields(
exposer = PyhidraGhidraScript.ExposedField.class,
names = {
"currentAddress", "currentLocation", "currentSelection",
"currentHighlight", "currentProgram", "monitor",
"potentialPropertiesFileLocs", "propertiesFileParams",
"sourceFile", "state", "writer"
},
types = {
Address.class, ProgramLocation.class, ProgramSelection.class,
ProgramSelection.class, Program.class, TaskMonitor.class,
List.class, GhidraScriptProperties.class,
ResourceFile.class, GhidraState.class, PrintWriter.class
}
)
final static class PyhidraGhidraScript extends GhidraScript
implements PythonFieldExposer {
@Override
public void run() {
scriptRunner.accept(this);
}
/**
* Helper inner class that can create a {@link MethodHandles.Lookup}
* that can access the protected fields of the {@link GhidraScript}
*/
private static class ExposedField extends PythonFieldExposer.ExposedField {
public ExposedField(String name, Class<?> type) {
super(MethodHandles.lookup().in(PyhidraGhidraScript.class), name, type);
}
}
}
@ExposedFields(
exposer = PyhidraHeadlessScript.ExposedField.class,
names = {
"currentAddress", "currentLocation", "currentSelection",
"currentHighlight", "currentProgram", "monitor",
"potentialPropertiesFileLocs", "propertiesFileParams",
"sourceFile", "state", "writer"
},
types = {
Address.class, ProgramLocation.class, ProgramSelection.class,
ProgramSelection.class, Program.class, TaskMonitor.class,
List.class, GhidraScriptProperties.class,
ResourceFile.class, GhidraState.class, PrintWriter.class
}
)
final static class PyhidraHeadlessScript extends HeadlessScript
implements PythonFieldExposer {
@Override
public void run() {
scriptRunner.accept(this);
}
/**
* Helper inner class that can create a {@link MethodHandles.Lookup}
* that can access the protected fields of the {@link GhidraScript}
*/
private static class ExposedField extends PythonFieldExposer.ExposedField {
public ExposedField(String name, Class<?> type) {
super(MethodHandles.lookup().in(PyhidraHeadlessScript.class), name, type);
}
}
}
}
@@ -0,0 +1,144 @@
package ghidra.pyhidra;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.invoke.ConstantBootstraps;
import java.lang.invoke.VarHandle;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Constructor;
import java.util.Map;
import ghidra.util.Msg;
import ghidra.util.exception.AssertException;
/**
* A marker interface to apply Jpype class customizations to a class.
*
* The Jpype class customizations will create Python properties which can access protected fields.
*
* This interface is for <b>internal use only</b> and is only public so it can be
* visible to Python to apply the Jpype class customizations.
*/
public sealed interface PythonFieldExposer permits PyhidraScriptProvider.PyhidraGhidraScript,
PyhidraScriptProvider.PyhidraHeadlessScript {
/**
* Gets a mapping of all the explicitly exposed fields of a class.
*
* This method is for <b>internal use only</b> and is only public so it can be
* called from Python.
*
* @param cls the PythonFieldExposer class
* @return a map of the exposed fields
*/
public static Map<String, ExposedField> getProperties(
Class<? extends PythonFieldExposer> cls) {
try {
return doGetProperties(cls);
}
catch (Throwable t) {
Msg.error(PythonFieldExposer.class,
"Failed to expose fields for " + cls.getSimpleName(), t);
return Map.of();
}
}
@SuppressWarnings("unchecked")
private static Map<String, ExposedField> doGetProperties(
Class<? extends PythonFieldExposer> cls)
throws Throwable {
ExposedFields fields = cls.getAnnotation(ExposedFields.class);
String[] names = fields.names();
Class<?>[] types = fields.types();
if (names.length != types.length) {
throw new AssertException("Improperly applied ExposedFields on " + cls.getSimpleName());
}
Constructor<? extends ExposedField> c =
fields.exposer().getConstructor(String.class, Class.class);
Map.Entry<String, ExposedField>[] properties = new Map.Entry[names.length];
for (int i = 0; i < names.length; i++) {
properties[i] = Map.entry(names[i], c.newInstance(names[i], types[i]));
}
return Map.ofEntries(properties);
}
/**
* An annotation for exposing protected fields of a class to Python
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
static @interface ExposedFields {
/**
* @return the {@link ExposedField} subclass with access to the protected fields
*/
public Class<? extends ExposedField> exposer();
/**
* @return the names of the protected fields to be exposed
*/
public String[] names();
/**
* @return the types of the protected fields to be exposed
*/
public Class<?>[] types();
}
/**
* Base class for making a protected field accessible from Python.
*
* Child classes are to be defined inside the class containing the fields to be exposed.
* The only requirement of the child class is to provide a {@link Lookup} with access
* to the protected fields, to the {@link ExposedField} constructor as shown below.
*
* {@snippet lang="java" :
* public class ExampleClass implements PythonFieldExposer {
* protected int counter = 0;
*
* private static class ExposedField extends PythonFieldExposer.ExposedField {
* public ExposedField(String name, Class<?> type) {
* super(MethodHandles.lookup().in(ExampleClass.class), name, type);
* }
* }
* }
* }
*/
static abstract class ExposedField {
private final VarHandle handle;
/**
* Constructs a new {@link ExposedField}
*
* @param lookup the {@link Lookup} with access to the protected field
* @param name the name of the protected field
* @param type the type of the protected field
*/
protected ExposedField(Lookup lookup, String name, Class<?> type) {
handle = ConstantBootstraps.fieldVarHandle(lookup, name, VarHandle.class,
lookup.lookupClass(), type);
}
/**
* Gets the field value
*
* @param self the instance containing the field
* @return the field value
*/
public final Object fget(Object self) {
return handle.get(self);
}
/**
* Sets the field value
*
* @param self the instance containing the field
* @param value the field value
*/
public final void fset(Object self, Object value) {
handle.set(self, value);
}
}
}
@@ -0,0 +1,36 @@
package ghidra.pyhidra.interpreter;
import java.awt.event.KeyEvent;
import javax.swing.ImageIcon;
import ghidra.pyhidra.PyhidraPlugin;
import docking.ActionContext;
import docking.action.KeyBindingData;
import docking.action.DockingAction;
import docking.action.ToolBarData;
import ghidra.util.HelpLocation;
import resources.ResourceManager;
import static docking.DockingUtils.CONTROL_KEY_MODIFIER_MASK;
final class CancelAction extends DockingAction {
private final PyhidraConsole console;
CancelAction(PyhidraConsole console) {
super("Cancel", PyhidraPlugin.class.getSimpleName());
this.console = console;
setDescription("Interrupt the interpreter");
ImageIcon image = ResourceManager.loadImage("images/dialog-cancel.png");
setToolBarData(new ToolBarData(image));
setEnabled(true);
KeyBindingData key = new KeyBindingData(KeyEvent.VK_I, CONTROL_KEY_MODIFIER_MASK);
setKeyBindingData(key);
setHelpLocation(new HelpLocation(PyhidraPlugin.TITLE, "Interrupt_Interpreter"));
}
@Override
public void actionPerformed(ActionContext context) {
console.interrupt();
}
}
@@ -0,0 +1,76 @@
package ghidra.pyhidra.interpreter;
import java.io.PrintWriter;
import ghidra.app.script.GhidraScript;
import ghidra.app.script.GhidraState;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.program.util.ProgramSelection;
/**
* Custom {@link GhidraScript} only for use with the pyhidra interpreter console
*/
public final class InterpreterGhidraScript extends GhidraScript {
// public default constructor for use by PyhidraPlugin
// the default constructor for FlatProgramAPI has protected visibility
public InterpreterGhidraScript() {
}
@Override
public void run() {
// we run in the interpreter console so we do nothing here
}
public Address getCurrentAddress() {
return currentAddress;
}
public ProgramLocation getCurrentLocation() {
return currentLocation;
}
public ProgramSelection getCurrentSelection() {
return currentSelection;
}
public ProgramSelection getCurrentHighlight() {
return currentHighlight;
}
public PrintWriter getWriter() {
return writer;
}
public void setCurrentProgram(Program program) {
currentProgram = program;
state.setCurrentProgram(program);
}
public void setCurrentAddress(Address address) {
currentAddress = address;
state.setCurrentAddress(address);
}
public void setCurrentLocation(ProgramLocation location) {
currentLocation = location;
currentAddress = location != null ? location.getAddress() : null;
state.setCurrentLocation(location);
}
public void setCurrentSelection(ProgramSelection selection) {
currentSelection = selection;
state.setCurrentSelection(selection);
}
public void setCurrentHighlight(ProgramSelection highlight) {
currentHighlight = highlight;
state.setCurrentHighlight(highlight);
}
public void set(GhidraState state, PrintWriter writer) {
set(state, new InterpreterTaskMonitor(writer), writer);
}
}
@@ -0,0 +1,19 @@
package ghidra.pyhidra.interpreter;
import java.io.PrintWriter;
import ghidra.util.task.TaskMonitorAdapter;
final class InterpreterTaskMonitor extends TaskMonitorAdapter {
private PrintWriter output = null;
InterpreterTaskMonitor(PrintWriter stdOut) {
output = stdOut;
}
@Override
public void setMessage(String message) {
output.println("<pyhidra-interactive>: " + message);
}
}
@@ -0,0 +1,37 @@
package ghidra.pyhidra.interpreter;
import java.util.List;
import ghidra.app.plugin.core.console.CodeCompletion;
import ghidra.app.plugin.core.interpreter.InterpreterConnection;
import ghidra.util.Disposable;
/**
* Console interface providing only the methods which need to be implemented in Python.
*
* This interface is for <b>internal use only</b> and is only public so it can be
* implemented in Python.
*/
public interface PyhidraConsole extends Disposable {
/**
* Generates code completions for the pyhidra interpreter
*
* @param cmd The command to get code completions for
* @param caretPos The position of the caret in the input string 'cmd'.
* It should satisfy the constraint {@literal "0 <= caretPos <= cmd.length()"}
* @return A {@link List} of {@link CodeCompletion code completions} for the given command
* @see InterpreterConnection InterpreterConnection.getCompletions(String, int)
*/
List<CodeCompletion> getCompletions(String cmd, int caretPos);
/**
* Restarts the pyhidra console
*/
void restart();
/**
* Interrupts the code running in the pyhidra console
*/
void interrupt();
}
@@ -0,0 +1,88 @@
package ghidra.pyhidra.interpreter;
import java.io.PrintWriter;
import java.util.List;
import javax.swing.Icon;
import ghidra.app.plugin.core.console.CodeCompletion;
import ghidra.app.plugin.core.interpreter.InterpreterConnection;
import ghidra.app.plugin.core.interpreter.InterpreterConsole;
import ghidra.app.plugin.core.interpreter.InterpreterPanelService;
import ghidra.pyhidra.PyhidraPlugin;
import ghidra.util.Disposable;
import ghidra.util.exception.AssertException;
import resources.ResourceManager;
/**
* The pyhidra interpreter connection
*/
public final class PyhidraInterpreter implements Disposable, InterpreterConnection {
private PyhidraConsole pyhidraConsole = null;
public final InterpreterConsole console;
public PyhidraInterpreter(PyhidraPlugin plugin, boolean isPythonAvailable) {
InterpreterPanelService service =
plugin.getTool().getService(InterpreterPanelService.class);
console = service.createInterpreterPanel(this, false);
if (!isPythonAvailable) {
console.addFirstActivationCallback(this::unavailableCallback);
}
}
@Override
public void dispose() {
if (pyhidraConsole != null) {
pyhidraConsole.dispose();
}
console.dispose();
}
@Override
public Icon getIcon() {
return ResourceManager.loadImage("images/python.png");
}
@Override
public String getTitle() {
return PyhidraPlugin.TITLE;
}
@Override
public List<CodeCompletion> getCompletions(String cmd) {
throw new AssertException("Unreachable, unimplemented and deprecated method");
}
@Override
public List<CodeCompletion> getCompletions(String cmd, int caretPos) {
if (pyhidraConsole == null) {
return List.of();
}
return pyhidraConsole.getCompletions(cmd, caretPos);
}
private void unavailableCallback() {
console.setInputPermitted(false);
PrintWriter out = console.getOutWriter();
out.println("Ghidra was not started with pyhidra. Python is not available.");
}
/**
* Initializes the interpreter with the provided PyhidraConsole.
*
* This method is for <b>internal use only</b> and is only public so it can be
* called from Python.
*
* @param pythonSideConsole the python side console
* @throws AssertException if the interpreter has already been initialized
*/
public void init(PyhidraConsole pythonSideConsole) {
if (pyhidraConsole != null) {
throw new AssertException("the interpreter has already been initialized");
}
pyhidraConsole = pythonSideConsole;
console.addFirstActivationCallback(pyhidraConsole::restart);
console.addAction(new CancelAction(pyhidraConsole));
console.addAction(new ResetAction(pyhidraConsole));
}
}
@@ -0,0 +1,36 @@
package ghidra.pyhidra.interpreter;
import java.awt.event.KeyEvent;
import javax.swing.ImageIcon;
import ghidra.pyhidra.PyhidraPlugin;
import ghidra.util.HelpLocation;
import docking.ActionContext;
import docking.action.DockingAction;
import docking.action.KeyBindingData;
import docking.action.ToolBarData;
import resources.ResourceManager;
import static docking.DockingUtils.CONTROL_KEY_MODIFIER_MASK;
final class ResetAction extends DockingAction {
private final PyhidraConsole console;
ResetAction(PyhidraConsole console) {
super("Reset", PyhidraPlugin.class.getSimpleName());
this.console = console;
setDescription("Reset the interpreter");
ImageIcon image = ResourceManager.loadImage("images/reload3.png");
setToolBarData(new ToolBarData(image));
setEnabled(true);
KeyBindingData key = new KeyBindingData(KeyEvent.VK_D, CONTROL_KEY_MODIFIER_MASK);
setKeyBindingData(key);
setHelpLocation(new HelpLocation(PyhidraPlugin.TITLE, "Reset_Interpreter"));
}
@Override
public void actionPerformed(ActionContext context) {
console.restart();
}
}
@@ -0,0 +1,83 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* Abstract base class for implementing a {@link JavaProperty}.
*
* This class provides the fset implementation as well as all helpers so
* that each child class only needs to define a constructor and a fget
* method returning the correct primitive type. Each child class can
* implement fget as follows:
*
* {@snippet lang="java" :
* public type fget(Object self) throws Throwable { // @highlight substring="type"
* return doGet(self);
* }
* }
*
* The pyhidra internals expects every {@link JavaProperty} to be an instance of this class.
* No checking is required or performed since the {@link JavaProperty} interface and this
* class are sealed.
*/
abstract sealed class AbstractJavaProperty<T> implements JavaProperty<T> permits
BooleanJavaProperty, ByteJavaProperty, CharacterJavaProperty,
DoubleJavaProperty, FloatJavaProperty, IntegerJavaProperty,
LongJavaProperty, ObjectJavaProperty, ShortJavaProperty {
/**
* The name of the property
*/
public final String field;
// The handles to the underlying get/set methods
private final MethodHandle getter;
private final MethodHandle setter;
protected AbstractJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
this.field = field;
this.getter = getter;
this.setter = setter;
}
/**
* Checks if this property has a getter
*
* @return true if this property has a getter
*/
public boolean hasGetter() {
return getter != null;
}
/**
* Checks if this property has a setter
*
* @return true if this property has a setter
*/
public boolean hasSetter() {
return setter != null;
}
// this is only for testing
boolean hasValidSetter() {
if (setter == null) {
return false;
}
if (getter == null) {
return true;
}
Class<?> getterType = PropertyUtils.boxPrimitive(getter.type().returnType());
// for a MethodType the parameter we want is at index 1
Class<?> setterType = PropertyUtils.boxPrimitive(setter.type().parameterType(1));
return getterType == setterType;
}
protected final T doGet(Object self) throws Throwable {
return (T) getter.invoke(self);
}
@Override
public final void fset(Object self, T value) throws Throwable {
setter.invoke(self, value);
}
}
@@ -0,0 +1,26 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* The {@link JavaProperty} for the primitive <b>boolean</b> type
*/
public final class BooleanJavaProperty extends AbstractJavaProperty<Boolean> {
BooleanJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
super(field, getter, setter);
}
/**
* The method to be used as the fget value for a Python property.
*
* This method will be called by the Python property __get__ function.
*
* @param self the object containing the property
* @return the property's value
* @throws Throwable if any exception occurs while getting the value
*/
public boolean fget(Object self) throws Throwable {
return doGet(self);
}
}
@@ -0,0 +1,26 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* The {@link JavaProperty} for the primitive <b>byte</b> type
*/
public final class ByteJavaProperty extends AbstractJavaProperty<Byte> {
ByteJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
super(field, getter, setter);
}
/**
* The method to be used as the fget value for a Python property.
*
* This method will be called by the Python property __get__ function.
*
* @param self the object containing the property
* @return the property's value
* @throws Throwable if any exception occurs while getting the value
*/
public byte fget(Object self) throws Throwable {
return doGet(self);
}
}
@@ -0,0 +1,26 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* The {@link JavaProperty} for the primitive <b>char</b> type
*/
public final class CharacterJavaProperty extends AbstractJavaProperty<Character> {
CharacterJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
super(field, getter, setter);
}
/**
* The method to be used as the fget value for a Python property.
*
* This method will be called by the Python property __get__ function.
*
* @param self the object containing the property
* @return the property's value
* @throws Throwable if any exception occurs while getting the value
*/
public char fget(Object self) throws Throwable {
return doGet(self);
}
}
@@ -0,0 +1,26 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* The {@link JavaProperty} for the primitive <b>double</b> type
*/
public final class DoubleJavaProperty extends AbstractJavaProperty<Double> {
DoubleJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
super(field, getter, setter);
}
/**
* The method to be used as the fget value for a Python property.
*
* This method will be called by the Python property __get__ function.
*
* @param self the object containing the property
* @return the property's value
* @throws Throwable if any exception occurs while getting the value
*/
public double fget(Object self) throws Throwable {
return doGet(self);
}
}
@@ -0,0 +1,26 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* The {@link JavaProperty} for the primitive <b>float</b> type
*/
public final class FloatJavaProperty extends AbstractJavaProperty<Float> {
FloatJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
super(field, getter, setter);
}
/**
* The method to be used as the fget value for a Python property.
*
* This method will be called by the Python property __get__ function.
*
* @param self the object containing the property
* @return the property's value
* @throws Throwable if any exception occurs while getting the value
*/
public float fget(Object self) throws Throwable {
return doGet(self);
}
}
@@ -0,0 +1,26 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* The {@link JavaProperty} for the primitive <b>int</b> type
*/
public final class IntegerJavaProperty extends AbstractJavaProperty<Integer> {
IntegerJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
super(field, getter, setter);
}
/**
* The method to be used as the fget value for a Python property.
*
* This method will be called by the Python property __get__ function.
*
* @param self the object containing the property
* @return the property's value
* @throws Throwable if any exception occurs while getting the value
*/
public int fget(Object self) throws Throwable {
return doGet(self);
}
}
@@ -0,0 +1,28 @@
package ghidra.pyhidra.property;
/**
* Property interface for creating a Python property for getters and setters.
*
* Each implementation is required to have a defined fget method which returns
* the corresponding primitive type. By doing so we can utilize Python duck typing,
* auto boxing/unboxing and the Jpype conversion system to automatically convert
* the primitive return types to the equivalent Python type. This removes the
* headache of having to carefully and explicitly cast things to an int to
* avoid exceptions in Python code related to type conversion or type attributes.
*
* The fget and fset methods are named to correspond with the fget and fset members
* of Python's property type.
*/
public sealed interface JavaProperty<T> permits AbstractJavaProperty {
/**
* The method to be used as the fset value for a Python property.
*
* This method will be called by the Python property __set__ function.
*
* @param self the object containing the property
* @param value the value to be set
* @throws Throwable if any exception occurs while setting the value
*/
public abstract void fset(Object self, T value) throws Throwable;
}
@@ -0,0 +1,48 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* Factory class for a {@link JavaProperty}
*/
class JavaPropertyFactory {
private JavaPropertyFactory() {
}
static JavaProperty<?> getProperty(String field, MethodHandle getter, MethodHandle setter) {
Class<?> cls =
getter != null ? getter.type().returnType() : setter.type().lastParameterType();
if (!cls.isPrimitive()) {
return new ObjectJavaProperty(field, getter, setter);
}
if (cls == Boolean.TYPE) {
return new BooleanJavaProperty(field, getter, setter);
}
if (cls == Byte.TYPE) {
return new ByteJavaProperty(field, getter, setter);
}
if (cls == Character.TYPE) {
return new CharacterJavaProperty(field, getter, setter);
}
if (cls == Double.TYPE) {
return new DoubleJavaProperty(field, getter, setter);
}
if (cls == Float.TYPE) {
return new FloatJavaProperty(field, getter, setter);
}
if (cls == Integer.TYPE) {
return new IntegerJavaProperty(field, getter, setter);
}
if (cls == Long.TYPE) {
return new LongJavaProperty(field, getter, setter);
}
if (cls == Short.TYPE) {
return new ShortJavaProperty(field, getter, setter);
}
// it's better than nothing at all
// users will just need to be extra careful about casting to whatever the new primitive
// type is when using a getter/setter
return new ObjectJavaProperty(field, getter, setter);
}
}
@@ -0,0 +1,26 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* The {@link JavaProperty} for the primitive <b>long</b> type
*/
public final class LongJavaProperty extends AbstractJavaProperty<Long> {
LongJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
super(field, getter, setter);
}
/**
* The method to be used as the fget value for a Python property.
*
* This method will be called by the Python property __get__ function.
*
* @param self the object containing the property
* @return the property's value
* @throws Throwable if any exception occurs while getting the value
*/
public long fget(Object self) throws Throwable {
return doGet(self);
}
}
@@ -0,0 +1,26 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* The {@link JavaProperty} for a reference type
*/
public final class ObjectJavaProperty extends AbstractJavaProperty<Object> {
ObjectJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
super(field, getter, setter);
}
/**
* The method to be used as the fget value for a Python property.
*
* This method will be called by the Python property __get__ function.
*
* @param self the object containing the property
* @return the property's value
* @throws Throwable if any exception occurs while getting the value
*/
public Object fget(Object self) throws Throwable {
return doGet(self);
}
}
@@ -0,0 +1,269 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import ghidra.util.Msg;
/**
* Utility class for working with classes to obtain and create Python properties.
*
* This class is for <b>internal use only</b> and is only public so it can be
* reached from Python.
*/
public class PropertyUtils {
private PropertyUtils() {
}
/**
* Gets the boxed class for a primitive type
*
* @param cls the primitive class type
* @return the boxed class for a primitive type or the original class if not a primitive type
*/
static Class<?> boxPrimitive(Class<?> cls) {
if (!cls.isPrimitive()) {
return cls;
}
// sure there are cleaner ways to do this
// you could do a switch over the first character from Class.descriptorString
// however, for a primitive class, descriptorString goes through exactly this
// just to produce the descriptor string so there is really no point
if (cls == Boolean.TYPE) {
return Boolean.class;
}
if (cls == Byte.TYPE) {
return Byte.class;
}
if (cls == Character.TYPE) {
return Character.class;
}
if (cls == Double.TYPE) {
return Double.class;
}
if (cls == Float.TYPE) {
return Float.class;
}
if (cls == Integer.TYPE) {
return Integer.class;
}
if (cls == Long.TYPE) {
return Long.class;
}
if (cls == Short.TYPE) {
return Short.class;
}
// this allows us to still give a functional property
// if a new primitive type is ever added it can still work
return cls;
}
/**
* Gets an array of {@link JavaProperty} for the provided class.
*
* This method is for <b>internal use only</b> and is only public
* so it can be called from Python.
*
* @param cls the class to get the properties for
* @return an array of properties
*/
public static JavaProperty<?>[] getProperties(Class<?> cls) {
if (cls == Object.class) {
return new JavaProperty[0];
}
try {
return doGetProperties(cls);
}
catch (Throwable t) {
Msg.error(PropertyUtils.class,
"Failed to extract properties for " + cls.getSimpleName(), t);
return new JavaProperty<?>[0];
}
}
private static JavaProperty<?>[] doGetProperties(Class<?> cls) throws Throwable {
PropertyPairFactory factory;
try {
factory = new PropertyPairFactory(cls);
}
catch (IllegalArgumentException e) {
// skip illegal lookup class
return new JavaProperty<?>[0];
}
return getMethods(cls)
.filter(PropertyUtils::methodFilter)
.map(PropertyUtils::toProperty)
.collect(Collectors.groupingBy(PartialProperty::getName))
.values()
.stream()
.map(factory::merge)
.flatMap(Optional::stream)
.toArray(JavaProperty<?>[]::new);
}
private static Stream<Method> getMethods(Class<?> cls) {
// customizations added using JClass._customize are inherited
// therfore we only care about the ones declared by this class
return Arrays.stream(cls.getDeclaredMethods())
.filter(PropertyUtils::methodFilter);
}
private static boolean methodFilter(Method m) {
/*
This is much simpler than it looks.
A method is considered a getter/setter if it meets the following:
1. Has public visibility and is not static.
2. Has a name starting with lowercase get/set/is with the character after
the prefix being uppercase.
3. A getter has 0 parameters and a non-void return type.
A setter has 1 parameter and must not return anything.
An is getter must return a boolean or Boolean.
4. The method name must be longer than the prefix.
The first few checks are done to short circuit and return false sooner rather than later.
*/
if (!isPublic(m)) {
return false;
}
int paramCount = m.getParameterCount();
if (paramCount > 1) {
return false;
}
Class<?> resultType = m.getReturnType();
String name = m.getName();
int nameLength = name.length();
if (nameLength < 3) {
return false;
}
switch (name.charAt(0)) {
case 'g':
if (paramCount == 0 && resultType != Void.TYPE) {
if (nameLength > 3 && name.startsWith("get")) {
return Character.isUpperCase(name.charAt(3));
}
}
return false;
case 'i':
if (paramCount == 0 &&
(resultType == Boolean.TYPE || resultType == Boolean.class)) {
if (nameLength > 2 && name.startsWith("is")) {
return Character.isUpperCase(name.charAt(2));
}
}
return false;
case 's':
if (paramCount == 1 && resultType == Void.TYPE) {
if (nameLength > 3 && name.startsWith("set")) {
return Character.isUpperCase(name.charAt(3));
}
}
return false;
default:
return false;
}
}
private static boolean isPublic(Method m) {
int mod = m.getModifiers();
return Modifier.isPublic(mod) && !Modifier.isStatic(mod);
}
/**
* Helper class for merging methods and removing a layer of reflection
*/
private static class PropertyPairFactory {
private final Lookup lookup;
private PropertyPairFactory(Class<?> c) {
lookup = MethodHandles.publicLookup();
}
private Optional<JavaProperty<?>> merge(List<PartialProperty> pairs) {
try {
if (pairs.size() == 1) {
PartialProperty p = pairs.get(0);
MethodHandle h = lookup.unreflect(p.m);
JavaProperty<?> res =
p.isGetter() ? JavaPropertyFactory.getProperty(p.name, h, null)
: JavaPropertyFactory.getProperty(p.name, null, h);
return Optional.of(res);
}
PartialProperty g = pairs.stream()
.filter(PartialProperty::isGetter)
.findFirst()
.orElse(null);
if (g != null) {
// go through all remaining methods and take the first matching pair
// it does not matter if one is a boxed primitive and the other is
// unboxed because the JavaProperty will use the primitive type anyway
Class<?> target = boxPrimitive(g.m.getReturnType());
PartialProperty s = pairs.stream()
.filter(PartialProperty::isSetter)
.filter(p -> boxPrimitive(p.m.getParameterTypes()[0]) == target)
.findFirst()
.orElse(null);
MethodHandle gh = lookup.unreflect(g.m);
MethodHandle sh = s != null ? lookup.unreflect(s.m) : null;
return Optional.of(JavaPropertyFactory.getProperty(g.name, gh, sh));
}
}
catch (IllegalAccessException e) {
// this is a class in java.lang.invoke or java.lang.reflect
// the JVM doesn't allow the creation of handles for these
}
return Optional.empty();
}
}
private static PartialProperty toProperty(Method m) {
// all non properties have already been filtered out
String name = m.getName();
if (name.charAt(0) == 'i') {
name = name.substring(2);
}
else {
name = name.substring(3);
}
name = Character.toLowerCase(name.charAt(0)) + name.substring(1);
return new PartialProperty(m, name);
}
/**
* Helper class for combining the methods into a property
*/
private static class PartialProperty {
private final Method m;
private final String name;
private PartialProperty(Method m, String name) {
this.m = m;
this.name = name;
}
public boolean isGetter() {
return m.getParameterCount() == 0 && m.getReturnType() != Void.TYPE;
}
public boolean isSetter() {
return m.getParameterCount() == 1 && m.getReturnType() == Void.TYPE;
}
public String getName() {
return name;
}
}
}
@@ -0,0 +1,26 @@
package ghidra.pyhidra.property;
import java.lang.invoke.MethodHandle;
/**
* The {@link JavaProperty} for the primitive <b>short</b> type
*/
public final class ShortJavaProperty extends AbstractJavaProperty<Short> {
ShortJavaProperty(String field, MethodHandle getter, MethodHandle setter) {
super(field, getter, setter);
}
/**
* The method to be used as the fget value for a Python property.
*
* This method will be called by the Python property __get__ function.
*
* @param self the object containing the property
* @return the property's value
* @throws Throwable if any exception occurs while getting the value
*/
public short fget(Object self) throws Throwable {
return doGet(self);
}
}
@@ -0,0 +1,11 @@
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.
@@ -0,0 +1,2 @@
graft tests
global-exclude *.pyc
@@ -0,0 +1,175 @@
# pyhidra
Pyhidra is a Python library that provides direct access to the Ghidra API within a native CPython interpreter using [jpype](https://jpype.readthedocs.io/en/latest). As well, Pyhidra contains some conveniences for setting up analysis on a given sample and running a Ghidra script locally. It also contains a Ghidra plugin to allow the use of CPython from the Ghidra user interface.
Pyhidra was initially developed for use with Dragodis and is designed to be installable without requiring Java or Ghidra. This allows other Python projects
have pyhidra as a dependency and provide optional Ghidra functionality without requiring all users to install Java and Ghidra. It is recommended to recommend that users set the `GHIDRA_INSTALL_DIR` environment variable to simplify locating Ghidra.
## Usage
### Raw Connection
To get a raw connection to Ghidra use the `start()` function.
This will setup a Jpype connection and initialize Ghidra in headless mode,
which will allow you to directly import `ghidra` and `java`.
*NOTE: No projects or programs get setup in this mode.*
```python
import pyhidra
pyhidra.start()
import ghidra
from ghidra.app.util.headless import HeadlessAnalyzer
from ghidra.program.flatapi import FlatProgramAPI
from ghidra.base.project import GhidraProject
from java.lang import String
# do things
```
### Customizing Java and Ghidra initialization
JVM configuration for the classpath and vmargs may be done through a `PyhidraLauncher`.
```python
from pyhidra.launcher import HeadlessPyhidraLauncher
launcher = HeadlessPyhidraLauncher()
launcher.add_classpaths("log4j-core-2.17.1.jar", "log4j-api-2.17.1.jar")
launcher.add_vmargs("-Dlog4j2.formatMsgNoLookups=true")
launcher.start()
```
### Registering an Entry Point
The `PyhidraLauncher` can also be configured through the use of a registered entry point on your own python project.
This is useful for installing your own Ghidra plugin which uses pyhidra and self-compiles.
First create an [entry_point](https://setuptools.pypa.io/en/latest/userguide/entry_point.html) for `pyhidra.setup`
pointing to a single argument function which accepts the launcher instance.
```python
# setup.py
from setuptools import setup
setup(
# ...,
entry_points={
'pyhidra.setup': [
'acme_plugin = acme.ghidra_plugin.install:setup',
]
}
)
```
Then we create the target function.
This function will be called every time a user starts a pyhidra launcher.
In the same fashion, another entry point `pyhidra.pre_launch` may be registered and will be called after Ghidra and all
plugins have been loaded.
```python
# acme/ghidra_plugin/install.py
from pathlib import Path
import pyhidra
def setup(launcher):
"""
Run by pyhidra launcher to install our plugin.
"""
launcher.add_classpaths("log4j-core-2.17.1.jar", "log4j-api-2.17.1.jar")
launcher.add_vmargs("-Dlog4j2.formatMsgNoLookups=true")
# Install our plugin.
source_path = Path(__file__).parent / "java" / "plugin" # path to uncompiled .java code
details = pyhidra.ExtensionDetails(
name="acme_plugin",
description="My Cool Plugin",
author="acme",
plugin_version="1.2",
)
launcher.install_plugin(source_path, details) # install plugin (if not already)
```
### Analyze a File
To have pyhidra setup a binary file for you, use the `open_program()` function.
This will setup a Ghidra project and import the given binary file as a program for you.
Again, this will also allow you to import `ghidra` and `java` to perform more advanced processing.
```python
import pyhidra
with pyhidra.open_program("binary_file.exe") as flat_api:
program = flat_api.getCurrentProgram()
listing = program.getListing()
print(listing.getCodeUnitAt(flat_api.toAddr(0x1234)))
# We are also free to import ghidra while in this context to do more advanced things.
from ghidra.app.decompiler.flatapi import FlatDecompilerAPI
decomp_api = FlatDecompilerAPI(flat_api)
# ...
decomp_api.dispose()
```
By default, pyhidra will run analysis for you. If you would like to do this yourself, set `analyze` to `False`.
```python
import pyhidra
with pyhidra.open_program("binary_file.exe", analyze=False) as flat_api:
from ghidra.program.util import GhidraProgramUtilities
program = flat_api.getCurrentProgram()
if GhidraProgramUtilities.shouldAskToAnalyze(program):
flat_api.analyzeAll(program)
```
The `open_program()` function can also accept optional arguments to control the project name and location that gets created.
(Helpful for opening up a sample in an already existing project.)
```python
import pyhidra
with pyhidra.open_program("binary_file.exe", project_name="EXAM_231", project_location=r"C:\exams\231") as flat_api:
...
```
### Run a Script
Pyhidra can also be used to run an existing Ghidra Python script directly in your native python interpreter
using the `run_script()` command.
However, while you can technically run an existing Ghidra script unmodified, you may
run into issues due to differences between Jython 2 and CPython 3.
Therefore, some modification to the script may be needed.
```python
import pyhidra
pyhidra.run_script(r"C:\input.exe", r"C:\some_ghidra_script.py")
```
This can also be done on the command line using `pyhidra`.
```console
> pyhidra C:\input.exe C:\some_ghidra_script.py <CLI ARGS PASSED TO SCRIPT>
```
### Handling Package Name Conflicts
There may be some Python modules and Java packages with the same import path. When this occurs the Python module takes precedence.
While jpype has its own mechanism for handling this situation, pyhidra automatically makes the Java package accessible by allowing
it to be imported with an underscore appended to the package name.
```python
import pdb # imports Python's pdb
import pdb_ # imports Ghidra's pdb
```
@@ -0,0 +1,57 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "pyhidra"
dynamic = ["version", "readme"]
description = "Native CPython for Ghidra"
license = {text = "Apache-2.0"}
requires-python = ">= 3.9"
authors = [
{ name = "DC3", email = "dc3.tsd@us.af.mil" },
]
maintainers = [
{ name = "Ghidra Development Team" },
{ name = "DC3", email = "dc3.tsd@us.af.mil" },
]
keywords = [
"ghidra",
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"Jpype1>=1.5.0",
]
[project.optional-dependencies]
testing = [
"pytest",
"pytest-datadir",
]
[project.scripts]
pyhidra = "pyhidra.__main__:main"
[project.gui-scripts]
pyhidraw = "pyhidra.gui:_gui"
[project.urls]
Repository = "https://github.com/NationalSecurityAgency/ghidra"
[tool.setuptools.dynamic]
version = {attr = "pyhidra.__version__"}
readme = {file = ["README.md"], content-type = "text/markdown"}
[tool.pytest.ini_options]
required_plugins = ["pytest-datadir"]
addopts = "-p no:faulthandler -m \"not plugin\""
markers = ["plugin"]
@@ -0,0 +1,14 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
from setuptools import setup
if __name__ == "__main__":
# This is necessary so that we can build the sdist using pip wheel.
# Unfortunately we have to have this work without having setuptools
# which pip will install in an isolated environment from the
# dependencies directory.
if "bdist_wheel" in sys.argv and "sdist" not in sys.argv:
sys.argv.append("sdist")
setup()
@@ -0,0 +1,53 @@
__version__ = "2.0.0"
# stub for documentation and typing
# this is mostly to hide the function parameter
def debug_callback(suspend=False, **kwargs):
"""
Decorator for enabling debugging of functions called from a thread started in Java.
All parameters are forwarded to `pydevd.settrace`.
It is recommended to remove this decorator from a function when it is no longer needed.
:param suspend: The suspend parameter for `pydevd.settrace` (Defaults to False)
:return: The decorated function
"""
# this is the actual implementation
def _debug_callback(fun=None, *, suspend=False, **pydevd_kwargs):
import functools
import sys
if not fun:
return functools.partial(_debug_callback, suspend=suspend, **pydevd_kwargs)
@functools.wraps(fun)
def wrapper(*args, **kwargs):
# NOTE: sys.modules is used directly to prevent errors in settrace
# the debugger is responsible for connecting so it will have already
# been imported
pydevd = sys.modules.get("pydevd")
if pydevd:
pydevd_kwargs["suspend"] = suspend
pydevd.settrace(**pydevd_kwargs)
return fun(*args, **kwargs)
return wrapper
debug_callback = _debug_callback
# Expose API
from .core import run_script, start, started, open_program
from .launcher import DeferredPyhidraLauncher, GuiPyhidraLauncher, HeadlessPyhidraLauncher
from .script import get_current_interpreter
from .version import ApplicationInfo, ExtensionDetails
__all__ = [
"debug_callback", "get_current_interpreter", "open_program", "run_script", "start",
"started", "ApplicationInfo", "DeferredPyhidraLauncher", "ExtensionDetails",
"GuiPyhidraLauncher", "HeadlessPyhidraLauncher"
]
@@ -0,0 +1,273 @@
import argparse
import code
import logging
import sys
from pathlib import Path
import pyhidra
import pyhidra.core
import pyhidra.gui
# NOTE: this must be "pyhidra" and not __name__
logger = logging.getLogger("pyhidra")
def _interpreter(interpreter_globals: dict):
from ghidra.framework import Application
version = Application.getApplicationVersion()
name = Application.getApplicationReleaseName()
banner = f"Python Interpreter for Ghidra {version} {name}\n"
banner += f"Python {sys.version} on {sys.platform}"
code.interact(banner=banner, local=interpreter_globals, exitmsg='')
# pylint: disable=too-few-public-methods
class PyhidraArgs(argparse.Namespace):
"""
Custom namespace for holding the command line arguments
"""
def __init__(self, parser: argparse.ArgumentParser, **kwargs):
super().__init__(**kwargs)
self.parser = parser
self.valid = True
self.verbose = False
self.skip_analysis = False
self.binary_path: Path = None
self.script_path: Path = None
self.project_name = None
self.project_path: Path = None
self.install_dir: Path = None
self._script_args = []
self.gui = False
self.debug = False
self._xargs = []
self._dargs = []
def func(self):
"""
Run script or enter repl
"""
if not self.valid:
self.parser.print_usage()
return
if self.debug:
logger.setLevel(logging.DEBUG)
vmargs = self.jvm_args
if self.gui:
pyhidra.gui.gui(self.install_dir, vmargs)
return
# not in gui mode so it is easier to start Ghidra now
launcher = pyhidra.HeadlessPyhidraLauncher(
verbose=self.verbose, install_dir=self.install_dir)
launcher.vm_args = vmargs + launcher.vm_args
launcher.start()
if self.script_path is not None:
try:
pyhidra.run_script(
self.binary_path,
self.script_path,
project_location=self.project_path,
project_name=self.project_name,
script_args=self._script_args,
verbose=self.verbose,
analyze=not self.skip_analysis,
install_dir=self.install_dir
)
except KeyboardInterrupt:
# gracefully finish when cancelled
pass
elif self.binary_path is not None:
args = (
self.binary_path,
self.project_path,
self.project_name,
self.verbose,
not self.skip_analysis
)
with pyhidra.core._flat_api(*args, install_dir=self.install_dir) as api:
_interpreter(api)
else:
_interpreter(globals())
@property
def script_args(self):
return self._script_args
@script_args.setter
def script_args(self, value):
if self._script_args is None:
self._script_args = value
else:
# append any remaining args to the ones which were previously consumed
self._script_args.extend(value)
@property
def jvm_args(self):
vmargs = []
for arg in self._dargs:
vmargs.append("-D" + arg)
for arg in self._xargs:
vmargs.append("-X" + arg)
return vmargs
class PathAction(argparse.Action):
"""
Custom action for handling script and binary paths as positional arguments
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.nargs = '*'
self.type = str
def __call__(self, parser, namespace: PyhidraArgs, values, option_string=None):
if not values:
return
if namespace.script_path is not None:
# Any arguments after the script path get passed to the script
namespace.script_args = values
return
value = Path(values.pop(0))
if not value.exists():
# File must exist
namespace.valid = False
if value.suffix == ".py":
namespace.script_path = value
namespace.script_args = values
return
if namespace.binary_path is None:
# Peek at the next value, if present, to check if it is a script
# The optional binary file MUST come before the script
if len(values) > 0 and not values[0].endswith(".py"):
namespace.valid = False
namespace.binary_path = value
if not values:
return
# Recurse until all values are consumed
# The remaining arguments in the ArgParser was a lie for pretty help text
# and to pick up trailing optional arguments meant for the script
self(parser, namespace, values)
def _get_parser():
parser = argparse.ArgumentParser(prog="pyhidra")
parser.add_argument(
"-v",
"--verbose",
dest="verbose",
action="store_true",
help="Enable verbose JVM output during Ghidra initialization"
)
parser.add_argument(
"-d",
"--debug",
default=False,
action="store_true",
help="Sets the log level to DEBUG"
)
parser.add_argument(
"-g",
"--gui",
action="store_true",
dest="gui",
help="Start Ghidra GUI"
)
parser.add_argument(
"--install-dir",
type=Path,
default=None,
dest="install_dir",
metavar="",
help="Path to Ghidra installation. "
"(defaults to the GHIDRA_INSTALL_DIR environment variable)"
)
parser.add_argument(
"--skip-analysis",
dest="skip_analysis",
action="store_true",
help="Switch to skip analysis after loading the binary file if provided"
)
parser.add_argument(
"binary_path",
action=PathAction,
help="Optional binary path"
)
parser.add_argument(
"script_path",
action=PathAction,
help=(
"Headless script path. The script must have a .py extension. "
"If a script is not provided, pyhidra will drop into a repl."
)
)
parser.add_argument(
"--project-name",
type=str,
dest="project_name",
metavar="name",
help="Project name to use. "
"(defaults to binary filename with \"_ghidra\" suffix if provided else None)"
)
parser.add_argument(
"--project-path",
type=Path,
dest="project_path",
metavar="path",
help="Location to store project. "
"(defaults to same directory as binary file if provided else None)"
)
parser.add_argument(
"-D",
dest="_dargs",
action="append",
metavar="",
help="Argument to be forwarded to the JVM"
)
parser.add_argument(
"-X",
dest="_xargs",
action="append",
metavar="",
help="Argument to be forwarded to the JVM"
)
parser.add_argument(
"script_args",
help="Arguments to be passed to the headless script",
nargs=argparse.REMAINDER
)
return parser
def main():
"""
pyhidra module main function
"""
handler = logging.StreamHandler()
formatter = logging.Formatter("%(filename)s:%(lineno)d %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
parser = _get_parser()
parser.parse_args(namespace=PyhidraArgs(parser)).func()
if __name__ == "__main__":
main()
@@ -0,0 +1,14 @@
from pathlib import Path
from jpype import JConversion, JClass
@JConversion("java.lang.String", instanceof=Path)
def pathToString(cls: JClass, path: Path):
return cls(path.resolve().__str__())
@JConversion("java.io.File", instanceof=Path)
def pathToFile(cls: JClass, path: Path):
return cls(path)
@@ -0,0 +1,351 @@
import contextlib
from pathlib import Path
from typing import Union, TYPE_CHECKING, Tuple, ContextManager, List, Optional
from pyhidra.converters import * # pylint: disable=wildcard-import, unused-wildcard-import
if TYPE_CHECKING:
from pyhidra.launcher import PyhidraLauncher
from ghidra.base.project import GhidraProject
from ghidra.program.flatapi import FlatProgramAPI
from ghidra.program.model.lang import CompilerSpec, Language, LanguageService
from ghidra.program.model.listing import Program
def start(verbose=False, *, install_dir: Path = None) -> "PyhidraLauncher":
"""
Starts the JVM and fully initializes Ghidra in Headless mode.
:param verbose: Enable verbose output during JVM startup (Defaults to False)
:param install_dir: The path to the Ghidra installation directory.
(Defaults to the GHIDRA_INSTALL_DIR environment variable)
:return: The PhyidraLauncher used to start the JVM
"""
from pyhidra.launcher import HeadlessPyhidraLauncher
launcher = HeadlessPyhidraLauncher(verbose=verbose, install_dir=install_dir)
launcher.start()
return launcher
def started() -> bool:
"""
Whether the PyhidraLauncher has already started.
"""
from pyhidra.launcher import PyhidraLauncher
return PyhidraLauncher.has_launched()
def _get_language(id: str) -> "Language":
from ghidra.program.util import DefaultLanguageService
from ghidra.program.model.lang import LanguageID, LanguageNotFoundException
try:
service: "LanguageService" = DefaultLanguageService.getLanguageService()
return service.getLanguage(LanguageID(id))
except LanguageNotFoundException:
# suppress the java exception
pass
raise ValueError("Invalid Language ID: "+id)
def _get_compiler_spec(lang: "Language", id: str = None) -> "CompilerSpec":
if id is None:
return lang.getDefaultCompilerSpec()
from ghidra.program.model.lang import CompilerSpecID, CompilerSpecNotFoundException
try:
return lang.getCompilerSpecByID(CompilerSpecID(id))
except CompilerSpecNotFoundException:
# suppress the java exception
pass
lang_id = lang.getLanguageID()
raise ValueError(f"Invalid CompilerSpecID: {id} for Language: {lang_id.toString()}")
def _setup_project(
binary_path: Union[str, Path],
project_location: Union[str, Path] = None,
project_name: str = None,
language: str = None,
compiler: str = None,
loader: Union[str, JClass] = None
) -> Tuple["GhidraProject", "Program"]:
from ghidra.base.project import GhidraProject
from java.lang import ClassLoader
from java.io import IOException
if binary_path is not None:
binary_path = Path(binary_path)
if project_location:
project_location = Path(project_location)
else:
project_location = binary_path.parent
if not project_name:
project_name = f"{binary_path.name}_ghidra"
project_location /= project_name
project_location.mkdir(exist_ok=True, parents=True)
if isinstance(loader, str):
from java.lang import ClassNotFoundException
try:
gcl = ClassLoader.getSystemClassLoader()
loader = JClass(loader, gcl)
except (TypeError, ClassNotFoundException) as e:
raise ValueError from e
if isinstance(loader, JClass):
from ghidra.app.util.opinion import Loader
if not Loader.class_.isAssignableFrom(loader):
raise TypeError(f"{loader} does not implement ghidra.app.util.opinion.Loader")
# Open/Create project
program: "Program" = None
try:
project = GhidraProject.openProject(project_location, project_name, True)
if binary_path is not None:
if project.getRootFolder().getFile(binary_path.name):
program = project.openProgram("/", binary_path.name, False)
except IOException:
project = GhidraProject.createProject(project_location, project_name, False)
# NOTE: GhidraProject.importProgram behaves differently when a loader is provided
# loaderClass may not be null so we must use the correct method override
if binary_path is not None and program is None:
if language is None:
if loader is None:
program = project.importProgram(binary_path)
else:
program = project.importProgram(binary_path, loader)
if program is None:
raise RuntimeError(f"Ghidra failed to import '{binary_path}'. Try providing a language manually.")
else:
lang = _get_language(language)
comp = _get_compiler_spec(lang, compiler)
if loader is None:
program = project.importProgram(binary_path, lang, comp)
else:
program = project.importProgram(binary_path, loader, lang, comp)
if program is None:
message = f"Ghidra failed to import '{binary_path}'. "
if compiler:
message += f"The provided language/compiler pair ({language} / {compiler}) may be invalid."
else:
message += f"The provided language ({language}) may be invalid."
raise ValueError(message)
project.saveAs(program, "/", program.getName(), True)
return project, program
def _setup_script(project: "GhidraProject", program: "Program"):
from pyhidra.script import PyGhidraScript
from ghidra.app.script import GhidraState
from ghidra.program.util import ProgramLocation
from ghidra.util.task import TaskMonitor
from java.io import PrintWriter
from java.lang import System
if project is not None:
project = project.getProject()
location = None
if program is not None:
# create a GhidraState and setup a HeadlessScript with it
mem = program.getMemory().getLoadedAndInitializedAddressSet()
if not mem.isEmpty():
location = ProgramLocation(program, mem.getMinAddress())
state = GhidraState(None, project, program, location, None, None)
script = PyGhidraScript()
script.set(state, TaskMonitor.DUMMY, PrintWriter(System.out))
return script
def _analyze_program(flat_api, program):
from ghidra.program.util import GhidraProgramUtilities
from ghidra.app.script import GhidraScriptUtil
if GhidraProgramUtilities.shouldAskToAnalyze(program):
GhidraScriptUtil.acquireBundleHostReference()
try:
flat_api.analyzeAll(program)
if hasattr(GhidraProgramUtilities, "markProgramAnalyzed"):
GhidraProgramUtilities.markProgramAnalyzed(program)
else:
GhidraProgramUtilities.setAnalyzedFlag(program, True)
finally:
GhidraScriptUtil.releaseBundleHostReference()
@contextlib.contextmanager
def open_program(
binary_path: Union[str, Path],
project_location: Union[str, Path] = None,
project_name: str = None,
analyze=True,
language: str = None,
compiler: str = None,
loader: Union[str, JClass] = None
) -> ContextManager["FlatProgramAPI"]: # type: ignore
"""
Opens given binary path in Ghidra and returns FlatProgramAPI object.
:param binary_path: Path to binary file, may be None.
:param project_location: Location of Ghidra project to open/create.
(Defaults to same directory as binary file)
:param project_name: Name of Ghidra project to open/create.
(Defaults to name of binary file suffixed with "_ghidra")
:param analyze: Whether to run analysis before returning.
:param language: The LanguageID to use for the program.
(Defaults to Ghidra's detected LanguageID)
:param compiler: The CompilerSpecID to use for the program. Requires a provided language.
(Defaults to the Language's default compiler)
:param loader: The `ghidra.app.util.opinion.Loader` class to use when importing the program.
This may be either a Java class or its path. (Defaults to None)
:return: A Ghidra FlatProgramAPI object.
:raises ValueError: If the provided language, compiler or loader is invalid.
:raises TypeError: If the provided loader does not implement `ghidra.app.util.opinion.Loader`.
"""
from pyhidra.launcher import PyhidraLauncher, HeadlessPyhidraLauncher
if not PyhidraLauncher.has_launched():
HeadlessPyhidraLauncher().start()
from ghidra.app.script import GhidraScriptUtil
from ghidra.program.flatapi import FlatProgramAPI
project, program = _setup_project(
binary_path,
project_location,
project_name,
language,
compiler,
loader
)
GhidraScriptUtil.acquireBundleHostReference()
try:
flat_api = FlatProgramAPI(program)
if analyze:
_analyze_program(flat_api, program)
yield flat_api
finally:
GhidraScriptUtil.releaseBundleHostReference()
project.save(program)
project.close()
@contextlib.contextmanager
def _flat_api(
binary_path: Union[str, Path] = None,
project_location: Union[str, Path] = None,
project_name: str = None,
verbose=False,
analyze=True,
language: str = None,
compiler: str = None,
loader: Union[str, JClass] = None,
*,
install_dir: Path = None
):
"""
Runs a given script on a given binary path.
:param binary_path: Path to binary file, may be None.
:param script_path: Path to script to run.
:param project_location: Location of Ghidra project to open/create.
(Defaults to same directory as binary file)
:param project_name: Name of Ghidra project to open/create.
(Defaults to name of binary file suffixed with "_ghidra")
:param script_args: Command line arguments to pass to script.
:param verbose: Enable verbose output during Ghidra initialization.
:param analyze: Whether to run analysis, if a binary_path is provided, before returning.
:param language: The LanguageID to use for the program.
(Defaults to Ghidra's detected LanguageID)
:param compiler: The CompilerSpecID to use for the program. Requires a provided language.
(Defaults to the Language's default compiler)
:param loader: The `ghidra.app.util.opinion.Loader` class to use when importing the program.
This may be either a Java class or its path. (Defaults to None)
:param install_dir: The path to the Ghidra installation directory. This parameter is only
used if Ghidra has not been started yet.
(Defaults to the GHIDRA_INSTALL_DIR environment variable)
:raises ValueError: If the provided language, compiler or loader is invalid.
:raises TypeError: If the provided loader does not implement `ghidra.app.util.opinion.Loader`.
"""
from pyhidra.launcher import PyhidraLauncher, HeadlessPyhidraLauncher
if not PyhidraLauncher.has_launched():
HeadlessPyhidraLauncher(verbose=verbose, install_dir=install_dir).start()
project, program = None, None
if binary_path or project_location:
project, program = _setup_project(
binary_path,
project_location,
project_name,
language,
compiler,
loader
)
from ghidra.app.script import GhidraScriptUtil
# always aquire a bundle reference to avoid a NPE when attempting to run any Java scripts
GhidraScriptUtil.acquireBundleHostReference()
try:
script = _setup_script(project, program)
if analyze and program is not None:
_analyze_program(script, program)
yield script
finally:
GhidraScriptUtil.releaseBundleHostReference()
if project is not None:
if program is not None:
project.save(program)
project.close()
# pylint: disable=too-many-arguments
def run_script(
binary_path: Optional[Union[str, Path]],
script_path: Union[str, Path],
project_location: Union[str, Path] = None,
project_name: str = None,
script_args: List[str] = None,
verbose=False,
analyze=True,
lang: str = None,
compiler: str = None,
loader: Union[str, JClass] = None,
*,
install_dir: Path = None
):
"""
Runs a given script on a given binary path.
:param binary_path: Path to binary file, may be None.
:param script_path: Path to script to run.
:param project_location: Location of Ghidra project to open/create.
(Defaults to same directory as binary file if None)
:param project_name: Name of Ghidra project to open/create.
(Defaults to name of binary file suffixed with "_ghidra" if None)
:param script_args: Command line arguments to pass to script.
:param verbose: Enable verbose output during Ghidra initialization.
:param analyze: Whether to run analysis, if a binary_path is provided, before running the script.
:param lang: The LanguageID to use for the program.
(Defaults to Ghidra's detected LanguageID)
:param compiler: The CompilerSpecID to use for the program. Requires a provided language.
(Defaults to the Language's default compiler)
:param loader: The `ghidra.app.util.opinion.Loader` class to use when importing the program.
This may be either a Java class or its path. (Defaults to None)
:param install_dir: The path to the Ghidra installation directory. This parameter is only
used if Ghidra has not been started yet.
(Defaults to the GHIDRA_INSTALL_DIR environment variable)
:raises ValueError: If the provided language, compiler or loader is invalid.
:raises TypeError: If the provided loader does not implement `ghidra.app.util.opinion.Loader`.
"""
script_path = str(script_path)
args = binary_path, project_location, project_name, verbose, analyze, lang, compiler, loader
with _flat_api(*args, install_dir=install_dir) as script:
script.run(script_path, script_args)
@@ -0,0 +1,103 @@
import argparse
import ctypes
from pathlib import Path
import sys
import threading
from .launcher import PyhidraLauncher, _run_mac_app
class GhidraLauncher(PyhidraLauncher):
def __init__(self, verbose=False, class_name=str, gui=False, *, install_dir: Path = None):
super().__init__(verbose=verbose, install_dir=install_dir)
self._class_name = class_name
self._gui = gui
def _launch(self):
from ghidra import Ghidra
from java.lang import Runtime, Thread
if self._gui:
if sys.platform == "win32":
appid = ctypes.c_wchar_p(self.app_info.name)
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid)
Thread(lambda: Ghidra.main([self._class_name, *self.args])).start()
is_exiting = threading.Event()
Runtime.getRuntime().addShutdownHook(Thread(is_exiting.set))
if sys.platform == "darwin":
_run_mac_app()
is_exiting.wait()
else:
Ghidra.main([self._class_name, *self.args])
class ParsedArgs(argparse.Namespace):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.gui = False
self._dargs = []
self._xargs = []
self.install_dir: Path = None
self.class_name: str = None
@property
def jvm_args(self):
vmargs = []
for arg in self._dargs:
vmargs.append("-D" + arg)
for arg in self._xargs:
vmargs.append("-X" + arg)
return vmargs
def get_parser():
parser = argparse.ArgumentParser()
parser.add_argument(
"-g",
"--gui",
action="store_true",
dest="gui",
help="Start Ghidra GUI"
)
parser.add_argument(
"-D",
dest="_dargs",
action="append",
metavar="",
help="Argument to be forwarded to the JVM"
)
parser.add_argument(
"-X",
dest="_xargs",
action="append",
metavar="",
help="Argument to be forwarded to the JVM"
)
parser.add_argument(
"--install-dir",
type=Path,
default=None,
dest="install_dir",
metavar="",
help="Path to Ghidra installation. " \
"(defaults to the GHIDRA_INSTALL_DIR environment variable)"
)
parser.add_argument(
"class_name",
metavar="class"
)
return parser
if __name__ == "__main__":
parser = get_parser()
args = ParsedArgs()
_, remaining = parser.parse_known_args(namespace=args)
launcher = GhidraLauncher(False, args.class_name, args.gui, install_dir=args.install_dir)
launcher.vm_args = args.jvm_args + launcher.vm_args
launcher.args = remaining
launcher.start()

Some files were not shown because too many files have changed in this diff Show More