Skip to content

Commit

Permalink
fix/13 (#22)
Browse files Browse the repository at this point in the history
* fixes #13 with unit tests; improves Jep bridge
  • Loading branch information
mike-hunhoff authored Nov 7, 2022
1 parent 84a9077 commit fdf6c45
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 4 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ dist/**
*.exe
.gradle/**
/bin/

# Python ignore
**/__pycache__/**
13 changes: 13 additions & 0 deletions data/python/jepbuiltins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.

"""Used to configure builtins from Java"""


def jep_set_builtin(attr, o):
setattr(__builtins__, attr, o)
36 changes: 36 additions & 0 deletions data/python/jepisinstance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.

"""Proxy Python isinstance and issubclass calls
See Jep: https://github.com/ninia/jep/issues/438
Note: we should remove this code when Jep https://github.com/ninia/jep/pull/440 is merged and released
"""

import builtins

_saved_isinstance = builtins.isinstance
_saved_issubclass = builtins.issubclass


def _proxy_isinstance(obj, classinfo):
if "jep.PyJClass" in str(type(classinfo)):
classinfo = classinfo.__pytype__
return _saved_isinstance(obj, classinfo)


def _proxy_issubclass(obj, classinfo):
if "jep.PyJClass" in str(type(obj)):
obj = obj.__pytype__
if "jep.PyJClass" in str(type(classinfo)):
classinfo = classinfo.__pytype__
return _saved_issubclass(obj, classinfo)


builtins.isinstance = _proxy_isinstance
builtins.issubclass = _proxy_issubclass
4 changes: 2 additions & 2 deletions data/python/jeprunscript.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ def jep_runscript(path):

# set __file__ so child script can locate itself
# TODO: do we need to set others?
__file__ = path
additional_globals = {"__file__": path}

try:
exec(compile(source, path, "exec"), globals())
exec(compile(source, path, "exec"), {**globals(), **additional_globals})
except Exception as err:
# Python exceptions are printed in Python instead of Java to give us better error
# messages in the Ghidra console window
Expand Down
32 changes: 32 additions & 0 deletions data/python/tests/runall.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Run Ghidrathon unit tests.
# @author Mike Hunhoff ([email protected])
# @category Python 3
# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.

"""Light harness used to run Python tests
Note: you must run this harness from the Ghidra script manager or headless mode
"""

import unittest
import pathlib


def main():
loader = unittest.TestLoader()
result = unittest.TestResult()

directory = str(pathlib.Path(__file__).resolve().parent)

suite = loader.discover(directory, pattern="test_*.py")
_ = unittest.TextTestRunner(verbosity=2, failfast=True).run(suite)


if __name__ == "__main__":
main()
48 changes: 48 additions & 0 deletions data/python/tests/test_jepbridge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at: [package root]/LICENSE.txt
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.

"""Unit tests to verify Ghidra Jep bridge
Note: you must run these tests from the Ghidra script manager or headless mode
"""

import unittest


class TestJepBridge(unittest.TestCase):
def assertIsJavaObject(self, o):
from java.lang import Object

if not (o is None or isinstance(o, Object)):
raise AssertionError("Object %s is not valid" % str(o))

def test_type_instance(self):
# see Jep: https://github.com/ninia/jep/blob/15e36a7ba54eb7d8f7ffd85f16675fa4fd54eb1d/src/test/python/test_import.py#L54-L65
from java.lang import Object
from java.io import Serializable
from java.util import Date
from ghidra.program.database import ProgramDB

self.assertIsInstance(Date(), Object.__pytype__)
self.assertIsInstance(Date(), Serializable.__pytype__)
self.assertTrue(issubclass(Date.__pytype__, Object.__pytype__))
self.assertTrue(issubclass(Date.__pytype__, Serializable.__pytype__))
self.assertIsInstance(Date(), Object)
self.assertIsInstance(Date(), Serializable)
self.assertTrue(issubclass(Date, Object))
self.assertTrue(issubclass(Date, Serializable))
self.assertIsInstance(currentProgram, ProgramDB)

def test_ghidra_script_variables(self):
self.assertIsJavaObject(currentProgram)
self.assertIsJavaObject(currentLocation)
self.assertIsJavaObject(currentHighlight)
self.assertIsJavaObject(currentSelection)

def test_ghidra_script_methods(self):
self.assertIsInstance(getGhidraVersion(), str)
25 changes: 23 additions & 2 deletions src/main/java/ghidrathon/interpreter/GhidrathonInterpreter.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ private GhidrathonInterpreter() throws JepException, IOException{
// to help us further configure the Python environment
setJepEval();
setJepRunScript();
setJepIsInstanceIsSubclass();

}

Expand Down Expand Up @@ -152,7 +153,24 @@ private void setJepRunScript() throws JepException, FileNotFoundException {
jep.runScript(file.getAbsolutePath());

}

/**
* Configure isinstance and issubclass proxy functions in Python land.
*
* We use Python to run Python scripts because it gives us better access to tracebacks.
* Requires data/python/jepisinstance.py.
*
* @throws JepException
* @throws FileNotFoundException
*/
private void setJepIsInstanceIsSubclass() throws JepException, FileNotFoundException {

ResourceFile file = Application.getModuleDataFile(extname, "python/jepisinstance.py");

jep.runScript(file.getAbsolutePath());

}

/**
* Configure GhidraState.
*
Expand All @@ -169,6 +187,9 @@ private void injectScriptHierarchy(GhidraScript script) throws JepException, Fil
return;
}

ResourceFile file = Application.getModuleDataFile(extname, "python/jepbuiltins.py");
jep.runScript(file.getAbsolutePath());

// inject GhidraScript public/private fields e.g. currentAddress into Python
// see https://github.com/NationalSecurityAgency/ghidra/blob/master/Ghidra/Features/Python/src/main/java/ghidra/python/GhidraPythonInterpreter.java#L341-L377
for (Class<?> scriptClass = script.getClass(); scriptClass != Object.class; scriptClass =
Expand All @@ -178,7 +199,7 @@ private void injectScriptHierarchy(GhidraScript script) throws JepException, Fil
Modifier.isProtected(field.getModifiers())) {
try {
field.setAccessible(true);
jep.set(field.getName(), field.get(script));
jep.invoke("jep_set_builtin", field.getName(), field.get(script));
}
catch (IllegalAccessException iae) {
throw new JepException("Unexpected security manager being used!");
Expand All @@ -189,7 +210,7 @@ private void injectScriptHierarchy(GhidraScript script) throws JepException, Fil

if (!scriptMethodsInjected) {
// inject GhidraScript methods into Python
ResourceFile file = Application.getModuleDataFile(extname, "python/jepinject.py");
file = Application.getModuleDataFile(extname, "python/jepinject.py");
jep.set("__ghidra_script__", script);
jep.runScript(file.getAbsolutePath());
}
Expand Down

0 comments on commit fdf6c45

Please sign in to comment.