mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-05-09 20:18:01 +08:00
Python3 support
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 -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 -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>
|
||||
@@ -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
|
||||
@@ -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> Move backward in command stack</LI>
|
||||
<LI><B>(down):</B> Move forward in command stack</LI>
|
||||
<LI><B>TAB:</B> Show code completion window</LI>
|
||||
</UL>
|
||||
|
||||
<P>
|
||||
With the code completion window open:
|
||||
<UL>
|
||||
<LI><B>TAB:</B> Insert currently-selected code completion (if no completion selected, select the first available)</LI>
|
||||
<LI><B>ENTER:</B> Insert selected completion (if any) and close the completion window</LI>
|
||||
<LI><B>(up):</B> Select previous code completion</LI>
|
||||
<LI><B>(down):</B> Select next code completion</LI>
|
||||
<LI><B>ESC:</B> 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> CTRL+C / CTRL+V</LI>
|
||||
<LI><B>Linux:</B> CTRL+C / CTRL+V</LI>
|
||||
<LI><B>OS X:</B> 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> </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();
|
||||
}
|
||||
}
|
||||
+76
@@ -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);
|
||||
}
|
||||
}
|
||||
+19
@@ -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();
|
||||
}
|
||||
+88
@@ -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();
|
||||
}
|
||||
}
|
||||
+83
@@ -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);
|
||||
}
|
||||
}
|
||||
+26
@@ -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);
|
||||
}
|
||||
}
|
||||
+26
@@ -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);
|
||||
}
|
||||
}
|
||||
+26
@@ -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;
|
||||
}
|
||||
+48
@@ -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
Reference in New Issue
Block a user