diff --git a/README.md b/README.md index d244201..809305b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Check out: - The overview in our first [Ghidrathon blog post](https://www.mandiant.com/resources/blog/ghidrathon-snaking-ghidra-python-3-scripting) -Ghidrathon replaces the existing Python 2 extension implemented via Jython. This includes the interactive interpreter window, integration with the Ghidra Script Manager, and script execution in Ghidra headless mode. +Ghidrathon replaces the existing Python 2.7 extension implemented via Jython. This includes the interactive interpreter window, integration with the Ghidra Script Manager, and script execution in Ghidra headless mode. ## Python 3 Interpreter Window @@ -28,7 +28,7 @@ Ghidrathon helps you execute Python 3 scripts in Ghidra headless mode. Execute t ``` $ analyzeHeadless C:\Users\wampus example -process example.o -postScript ghidrathon_example.py -... +[...] INFO SCRIPT: C:\Users\wampus\.ghidra\.ghidra_10.0.3_PUBLIC\Extensions\Ghidrathon-master\ghidra_scripts\ghidrathon_example.py (HeadlessAnalyzer) Function _init @ 0x101000: 3 blocks, 8 instructions Function FUN_00101020 @ 0x101020: 1 blocks, 2 instructions @@ -49,7 +49,7 @@ Function __libc_start_main @ 0x105010: 0 blocks, 0 instructions Function __gmon_start__ @ 0x105018: 0 blocks, 0 instructions Function _ITM_registerTMCloneTable @ 0x105020: 0 blocks, 0 instructions Function __cxa_finalize @ 0x105028: 0 blocks, 0 instructions -... +[...] INFO REPORT: Post-analysis succeeded for file: /example.o (HeadlessAnalyzer) INFO REPORT: Save succeeded for processed file: /example.o (HeadlessAnalyzer) ``` @@ -62,6 +62,12 @@ One of our biggest motivations in developing Ghidrathon was to enable use of thi ![example](./data/ghidrathon_unicorn.png) +## Writing Ghidra Python 3 Scripts + +Ghidrathon provides a scripting experience that closely mirrors Ghidra's Java and Jython extensions which includes making `GhidraScript` state instance variables, e.g. `currentProgram`, and `FlatProgramAPI` methods, e.g. `findBytes` +available at the Python `builtins` scope. This means _all_ Python modules that are imported by your code have access to these variables and methods. Ghidrathon diverges slightly from Ghidra's Java and Jython extensions by exposing `GhidraScript` +state variables as Python function calls versus direct accesses e.g. your Python 3 code must access `currentProgram` using the function call `currentProgram()`. This small change ensures that your Python 3 code is provided the correct `GhidraScript` state variables during execution. Please see our Ghidra Python 3 script example [here](./ghidra_scripts/ghidrathon_example.py) for a closer look at writing Python 3 scripts for Ghidra. + ## How does it work? Ghidrathon links your local Python installation to Ghidra using the open-source project [Jep](https://github.com/ninia/jep). Essentially your local Python interpreter is running inside Ghidra with access to all your Python packages **and** the standard Ghidra scripting API. Ghidrathon also works with Python virtual environments helping you create, isolate, and manage packages you may only want installed for use in Ghidra. Because Ghidrathon uses your local Python installation you have control over the Python version and environment running inside Ghidra. @@ -85,7 +91,7 @@ Tool | Version |Source | | Ghidra | `>= 10.2` | https://ghidra-sre.org | | Jep | `>= 4.1.1` | https://github.com/ninia/jep | | Gradle | `>= 7.3` | https://gradle.org/releases | -| Python | `>= 3.7` | https://www.python.org/downloads | +| Python | `>= 3.8` | https://www.python.org/downloads | Note: Ghidra >= 10.2 requires [JDK 17 64-bit](https://adoptium.net/temurin/releases/). diff --git a/data/ghidrathon_interp.png b/data/ghidrathon_interp.png index 15add98..d5bbbbf 100644 Binary files a/data/ghidrathon_interp.png and b/data/ghidrathon_interp.png differ diff --git a/data/ghidrathon_script.png b/data/ghidrathon_script.png index b33e5d8..d76f9cf 100644 Binary files a/data/ghidrathon_script.png and b/data/ghidrathon_script.png differ diff --git a/data/ghidrathon_unicorn.png b/data/ghidrathon_unicorn.png index f5da022..0aeb285 100644 Binary files a/data/ghidrathon_unicorn.png and b/data/ghidrathon_unicorn.png differ diff --git a/data/python/jepbuiltins.py b/data/python/jepbuiltins.py deleted file mode 100644 index 2864f6c..0000000 --- a/data/python/jepbuiltins.py +++ /dev/null @@ -1,13 +0,0 @@ -# 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): - __builtins__[attr] = o diff --git a/data/python/jepeval.py b/data/python/jepeval.py index 5e06069..0d5eb34 100644 --- a/data/python/jepeval.py +++ b/data/python/jepeval.py @@ -52,6 +52,14 @@ def _jepeval(line): # e.g. for loop; cache statement to combine and compile/eval later jepeval_lines = [line] return True + except SyntaxError as err: + if err.msg == "unexpected EOF while parsing": + # python3.8 does not raise IndentationError, TabError so we must check for a SyntaxError + # with a hard-coded message + jepeval_lines = [line] + return True + else: + raise err else: # we have cached statements, user must be defining a multi-line block e.g. for loop; cache # statement to combine and compile/eval later diff --git a/data/python/jepinject.py b/data/python/jepinject.py deleted file mode 100644 index 7eeb6b5..0000000 --- a/data/python/jepinject.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at: [package root]/LICENSE.txt -# Unless required by applicable law or agreed to in writing, software distributed under the License -# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and limitations under the License. - -"""Inject GhidraScript methods into Python - -This lets us provide Python with GhidraScript helper methods e.g. getBytes. We store these in __buitlins__ to provide access -across Python imports similar to how this works in Jython. - -assumes __ghidra_script__ is passed from Java to Python prior to execution -""" - -for attr in dir(__ghidra_script__): - if attr.startswith("__"): - # ignore private - continue - if attr.startswith("print"): - # ignore helper functions for print e.g. print, println - continue - if attr == "java_name": - # ignore java_name added by Jep - continue - - o = getattr(__ghidra_script__, attr) - if callable(o) and attr not in __builtins__: - __builtins__[attr] = o diff --git a/data/python/jepstream.py b/data/python/jepstream.py deleted file mode 100644 index e2d4ea3..0000000 --- a/data/python/jepstream.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (C) 2022 Mandiant, Inc. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at: [package root]/LICENSE.txt -# Unless required by applicable law or agreed to in writing, software distributed under the License -# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and limitations under the License. - -"""Redirect sys.stdout and sys.stderr to Ghidra console window - -Python stdout and stderr print to Python; we want to see this output print to the Ghidra console window. To do this -we must override sys.stdout and sys.stderr with Java PrintWriters that are connected to the Ghidra console window. -""" -import sys -import io - - -def get_fake_io_wrapper(): - """build a TextIOWrapper referencing an empty byte array - - we set the encoding to the system default in hopes this doesn't cause issues when sending text from Python to Java - """ - return io.TextIOWrapper(io.BytesIO(b""), encoding=sys.getdefaultencoding()) - - -# sys.stdout and sys.stderr may be None (see https://docs.python.org/3/library/sys.html#sys.__stdout__); therefore -# we must set these to an object that has enough functionality to emulate basic write functionality. we create a -# TextIOWrapper referencing an empty byte array and override the write method with the write method of our Java -# PrintWriters connected to the Ghidra console window. hopefully this is good enough but we may run into issues in the -# future if Python code tries to reference unexpected methods/members e.g. "encoding" - - -if not sys.stdout: - sys.stdout = get_fake_io_wrapper() - -if not sys.stderr: - sys.stderr = get_fake_io_wrapper() - - -# assumes GhidraPluginToolConsoleOut/ErrWriter are passed from Java to Python before execution - - -sys.stdout.write = GhidraPluginToolConsoleOutWriter.write -sys.stderr.write = GhidraPluginToolConsoleErrWriter.write diff --git a/data/python/jepwelcome.py b/data/python/jepwelcome.py index a53700c..7ef1918 100644 --- a/data/python/jepwelcome.py +++ b/data/python/jepwelcome.py @@ -32,8 +32,8 @@ def format_version(): return "%d.%d.%d" % sys.version_info[:3] -# Assume GhidraVersion passed from Java to Python before execution +# Assume ghidra_version passed from Java to Python before execution -print(message % (format_version(), GhidraVersion)) +print(message % (format_version(), ghidra_version)) -del GhidraVersion +del ghidra_version diff --git a/data/python/jepwrappers.py b/data/python/jepwrappers.py new file mode 100644 index 0000000..ec50ed5 --- /dev/null +++ b/data/python/jepwrappers.py @@ -0,0 +1,338 @@ +# Copyright (C) 2023 Mandiant, Inc. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: [package root]/LICENSE.txt +# Unless required by applicable law or agreed to in writing, software distributed under the License +# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and limitations under the License. +import sys +import abc +import io +import os + +import java.lang + +cache_key = "ghidrathon_cache" +flatprogramapi_wrapper_stub = """@flatprogramapi_wrapper\ndef %s(*args, **kwargs): ...""" + + +class GhidrathonCachedStream: + def __init__(self, stream, closed=False): + self.stream = stream + self.closed = closed + + +class GhidrathonCachedGhidraState: + def __init__(self): + self.script = None + self.stdout = None + self.stderr = None + + +def get_java_thread_id(): + return java.lang.Thread.currentThread().getId() + + +def get_cache(): + cache = __builtins__.get(cache_key, None) + if cache is None: + raise RuntimeError("__builtins__ key %s does not exist" % cache_key) + + return cache + + +def get_state(): + state = get_cache().get(get_java_thread_id(), None) + if state is None: + raise RuntimeError("__builtins__[%s] key %s does not exist" % (cache_key, get_java_thread_id())) + + return state + + +def get_script(): + script = get_state().script + if script is None: + raise RuntimeError("GhidraScript not set") + + return script + + +def get_script_state(): + state = get_script().getState() + if state is None: + raise RuntimeError("GhidraState not set") + + return state + + +def get_stdout(): + stdout = get_state().stdout + if stdout is None: + raise RuntimeError("stdout not set") + + return stdout + + +def get_stderr(): + stderr = get_state().stderr + if stderr is None: + raise RuntimeError("stderr not set") + + return stderr + + +def init_state(): + __builtins__[cache_key][get_java_thread_id()] = GhidrathonCachedGhidraState() + + +def set_script(script): + get_state().script = script + + +def set_streams(stdout, stderr): + get_state().stdout = GhidrathonCachedStream(stdout) + get_state().stderr = GhidrathonCachedStream(stderr) + + +def remove_state(): + del get_cache()[get_java_thread_id()] + + +def flatprogramapi_wrapper(api): + def wrapped(*args, **kwargs): + return getattr(get_script(), api.__name__)(*args, **kwargs) + + return wrapped + + +class GhidrathonTextIOWrapperBase(abc.ABC): + @abc.abstractproperty + def __stream__(self): + ... + + @abc.abstractproperty + def name(self): + ... + + @abc.abstractproperty + def closed(self): + ... + + @abc.abstractmethod + def fileno(self): + ... + + @property + def line_buffering(self): + """If line_buffering is True, flush() is implied when a call to write contains a newline character or a carriage return.""" + return True + + @property + def write_through(self): + """If write_through is True, calls to write() are guaranteed not to be buffered: any data written on the TextIOWrapper object is immediately handled to its underlying binary buffer.""" + return False + + @property + def encoding(self): + return sys.getdefaultencoding() + + @property + def errors(self): + """Pass 'strict' to raise a ValueError exception if there is an encoding error (the default of None has the same effect)""" + return "strict" + + @property + def mode(self): + return "w" + + @property + def newlines(self): + raise io.UnsupportedOperation + + @property + def buffer(self): + raise io.UnsupportedOperation + + def isatty(self): + return False + + def writable(self): + return True + + def writelines(self, lines): + if self.closed: + raise ValueError("stream closed") + + for line in lines: + self.write(line) + + def write(self, text): + if self.closed: + raise ValueError("stream closed") + + num_chars = self.__stream__.write(text) + + if self.line_buffering: + if "\r" in text or "\n" in text: + self.flush() + + return num_chars + + def flush(self): + if self.closed: + raise ValueError("stream closed") + + self.__stream__.flush() + + def close(self): + if self.closed: + return + + self.flush() + self.closed = True + + def seekable(self): + """Return True if the stream supports random access. If False, seek(), tell() and truncate() will raise OSError.""" + return False + + def seek(*args, **kwargs): + raise OSError("operation not supported") + + def tell(*args, **kwargs): + raise OSError("operation not supported") + + def truncate(*args, **kwargs): + raise OSError("operation not supported") + + def readable(self): + """Return True if the stream can be read from. If False, read() will raise OSError.""" + return False + + def read(*args, **kwargs): + raise OSError("operation not supported") + + def readline(*args, **kwargs): + raise OSError("operation not supported") + + def readlines(*args, **kwwargs): + raise OSError("operation not supported") + + def reconfigure(*args, **kwargs): + raise io.UnsupportedOperation + + def detach(*args, **kwargs): + raise io.UnsupportedOperation + + +class GhidrathonStdoutWrapper(GhidrathonTextIOWrapperBase): + @property + def name(self): + return "" + + @property + def __stream__(self): + stream = get_stdout().stream + if stream is None: + raise RuntimeError("%s not set" % self.name) + return stream + + @property + def closed(self): + return get_stdout().closed + + @closed.setter + def closed(self, v): + get_stdout().closed = v + + def fileno(self): + return 1 + + +class GhidrathonStderrWrapper(GhidrathonTextIOWrapperBase): + @property + def name(self): + return "" + + @property + def __stream__(self): + stream = get_stderr().stream + if stream is None: + raise RuntimeError("%s not set" % self.name) + return stream + + @property + def closed(self): + return get_stderr().closed + + @closed.setter + def closed(self, v): + get_stderr().closed = v + + def fileno(self): + return 2 + + +sys.stdout = GhidrathonStdoutWrapper() +sys.stderr = GhidrathonStderrWrapper() + + +def wrap_flatprogramapi_functions(): + import ghidra.app.script + + for attr in dir(ghidra.app.script.GhidraScript): + if attr.startswith("__"): + continue + + if attr in __builtins__: + continue + + if attr.startswith("print"): + continue + + attr_o = getattr(ghidra.app.script.GhidraScript, attr) + if not callable(attr_o): + continue + + # dynamically generate wrapper stub using attribute name + exec(flatprogramapi_wrapper_stub % attr, globals()) + + # add dynamically generated wrapper stub to __builtins__ + __builtins__[attr] = globals()[attr] + + +wrap_flatprogramapi_functions() + + +def wrapped_monitor(): + return get_script().getMonitor() + + +def wrapped_currentProgram(): + return get_script_state().getCurrentProgram() + + +def wrapped_currentAddress(): + return get_script_state().getCurrentAddress() + + +def wrapped_currentLocation(): + return get_script_state().getCurrentLocation() + + +def wrapped_currentSelection(): + return get_script_state().getCurrentSelection() + + +def wrapped_currentHighlight(): + return get_script_state().getCurrentHighlight() + + +__builtins__["monitor"] = wrapped_monitor +__builtins__["currentProgram"] = wrapped_currentProgram +__builtins__["currentAddress"] = wrapped_currentAddress +__builtins__["currentLocation"] = wrapped_currentLocation +__builtins__["currentSelection"] = wrapped_currentSelection +__builtins__["currentHighlight"] = wrapped_currentHighlight + +if cache_key not in __builtins__: + __builtins__[cache_key] = {} diff --git a/data/python/tests/test_jepbridge.py b/data/python/tests/test_jepbridge.py index ff31581..501533b 100644 --- a/data/python/tests/test_jepbridge.py +++ b/data/python/tests/test_jepbridge.py @@ -42,13 +42,22 @@ def test_type_instance(self): self.assertIsInstance(Date(), Serializable) self.assertTrue(issubclass(Date, Object)) self.assertTrue(issubclass(Date, Serializable)) - self.assertIsInstance(currentProgram, ProgramDB) + self.assertIsInstance(currentProgram(), ProgramDB) def test_ghidra_script_variables(self): - self.assertIsJavaObject(currentProgram) - self.assertIsJavaObject(currentLocation) - self.assertIsJavaObject(currentHighlight) - self.assertIsJavaObject(currentSelection) + self.assertIsJavaObject(monitor()) + self.assertIsJavaObject(currentAddress()) + self.assertIsJavaObject(currentProgram()) + self.assertIsJavaObject(currentLocation()) + self.assertIsJavaObject(currentHighlight()) + self.assertIsJavaObject(currentSelection()) + + self.assertIsNotJavaObject(monitor) + self.assertIsNotJavaObject(currentAddress) + self.assertIsNotJavaObject(currentProgram) + self.assertIsNotJavaObject(currentLocation) + self.assertIsNotJavaObject(currentHighlight) + self.assertIsNotJavaObject(currentSelection) def test_ghidra_script_methods(self): self.assertIsInstance(getGhidraVersion(), str) diff --git a/ghidra_scripts/ghidrathon_example.py b/ghidra_scripts/ghidrathon_example.py index 9ed8778..97b3181 100644 --- a/ghidra_scripts/ghidrathon_example.py +++ b/ghidra_scripts/ghidrathon_example.py @@ -13,17 +13,17 @@ from ghidra.program.model.block import SimpleBlockIterator from ghidra.program.model.block import BasicBlockModel -for func in currentProgram.getListing().getFunctions(True): +for func in currentProgram().getListing().getFunctions(True): block_count = 0 # find basic block count for the current function - block_itr = SimpleBlockIterator(BasicBlockModel(currentProgram), func.getBody(), monitor) + block_itr = SimpleBlockIterator(BasicBlockModel(currentProgram()), func.getBody(), monitor()) while block_itr.hasNext(): block_count += 1 block_itr.next() # find instruction count for the current function - insn_count = len(tuple(currentProgram.getListing().getInstructions(func.getBody(), True))) + insn_count = len(tuple(currentProgram().getListing().getInstructions(func.getBody(), True))) # print counts to user print( diff --git a/src/main/java/ghidrathon/GhidrathonConsoleInputThread.java b/src/main/java/ghidrathon/GhidrathonConsoleInputThread.java index d270332..b744e42 100644 --- a/src/main/java/ghidrathon/GhidrathonConsoleInputThread.java +++ b/src/main/java/ghidrathon/GhidrathonConsoleInputThread.java @@ -10,10 +10,14 @@ package ghidrathon; +import db.Transaction; import generic.jar.ResourceFile; import ghidra.app.plugin.core.interpreter.InterpreterConsole; import ghidra.app.script.GhidraState; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.Program; import ghidra.util.Msg; +import ghidra.util.task.TaskMonitor; import ghidrathon.interpreter.GhidrathonInterpreter; import java.io.BufferedReader; import java.io.File; @@ -132,37 +136,30 @@ private boolean evalPython(String line) throws RuntimeException { boolean status; - // set transaction for the execution - int transactionNumber = -1; - if (plugin.getCurrentProgram() != null) { - transactionNumber = plugin.getCurrentProgram().startTransaction("Ghidrathon command"); - } + TaskMonitor interactiveTaskMonitor = plugin.getInteractiveTaskMonitor(); + GhidrathonScript interactiveScript = plugin.getInteractiveScript(); + Program program = plugin.getCurrentProgram(); - // setup Ghidra state to be passed into interpreter - plugin.getInteractiveTaskMonitor().clearCanceled(); - plugin.getInteractiveScript().setSourceFile(new ResourceFile(new File("Ghidrathon"))); - plugin - .getInteractiveScript() - .set( - new GhidraState( - plugin.getTool(), - plugin.getTool().getProject(), - plugin.getCurrentProgram(), - plugin.getProgramLocation(), - plugin.getProgramSelection(), - plugin.getProgramHighlight()), - plugin.getInteractiveTaskMonitor(), - new PrintWriter(console.getStdOut())); + try (Transaction tx = program != null ? program.openTransaction("Ghidrathon console command") : null) { - try { + interactiveTaskMonitor.clearCanceled(); + interactiveScript.setSourceFile(new ResourceFile(new File("Ghidrathon"))); + PluginTool tool = plugin.getTool(); - status = python.eval(line, plugin.getInteractiveScript()); + interactiveScript.set( + new GhidraState( + tool, + tool.getProject(), + program, + plugin.getProgramLocation(), + plugin.getProgramSelection(), + plugin.getProgramHighlight()), + interactiveTaskMonitor, + new PrintWriter(console.getStdOut())); + status = python.eval(line, interactiveScript); } finally { - - if (plugin.getCurrentProgram() != null) { - plugin.getCurrentProgram().endTransaction(transactionNumber, true); - } + interactiveScript.end(false); } return status; diff --git a/src/main/java/ghidrathon/interpreter/GhidrathonInterpreter.java b/src/main/java/ghidrathon/interpreter/GhidrathonInterpreter.java index dafa921..0a40015 100644 --- a/src/main/java/ghidrathon/interpreter/GhidrathonInterpreter.java +++ b/src/main/java/ghidrathon/interpreter/GhidrathonInterpreter.java @@ -42,7 +42,6 @@ public class GhidrathonInterpreter { private static final GhidrathonClassEnquirer ghidrathonClassEnquirer = new GhidrathonClassEnquirer(); private static final AtomicBoolean jepConfigInitialized = new AtomicBoolean(false); - private static final AtomicBoolean ghidraScriptMethodsInitialized = new AtomicBoolean(false); private static final AtomicBoolean jepNativeBinaryInitialized = new AtomicBoolean(false); /** @@ -74,6 +73,11 @@ private GhidrathonInterpreter(GhidrathonConfig config) throws JepException, IOEx // now that everything is configured, we should be able to run some utility scripts // to help us further configure the Python environment + setJepWrappers(); + + jep_.invoke("jepwrappers.init_state"); + jep_.invoke("jepwrappers.set_streams", this.out, this.err); + setJepEval(); setJepRunScript(); } @@ -119,7 +123,7 @@ private void setJepConfig() { } /** Extends Python sys.path to include Ghidra script source directories */ - private void setSysPath() { + private void extendPythonSysPath() { String paths = ""; for (ResourceFile resourceFile : GhidraScriptUtil.getScriptSourceDirectories()) { @@ -164,12 +168,21 @@ private void setJepNativeBinaryPath() throws JepException, FileNotFoundException MainInterpreter.setJepLibraryPath(nativeJep.getAbsolutePath()); } catch (IllegalStateException e) { - // library path has already been set elsewhere, we expect this to happen as Jep - // Maininterpreter - // thread exists forever once it's created + e.printStackTrace(this.err); + throw new RuntimeException(e); } } + /** + * Configure wrapper functions in Python land. + * + * @throws JepException + */ + private void setJepWrappers() throws JepException { + + jep_.eval("import jepwrappers"); + } + /** * Configure "jepeval" function in Python land. * @@ -206,56 +219,6 @@ private void setJepRunScript() throws JepException, FileNotFoundException { jep_.runScript(file.getAbsolutePath()); } - /** - * Configure GhidraState. - * - *

This exposes things like currentProgram, currentAddress, etc. similar to Jython. We need to - * repeat this prior to executing new Python code in order to provide the latest state e.g. that - * current currentAddress. Requires data/python/jepinject.py. - * - * @param script GhidrathonScript instance - * @throws JepException - * @throws FileNotFoundException - */ - private void injectScriptHierarchy(GhidraScript script) - throws JepException, FileNotFoundException { - if (script == null) { - return; - } - - ResourceFile file = - Application.getModuleDataFile(GhidrathonUtils.THIS_EXTENSION_NAME, "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 = scriptClass.getSuperclass()) { - for (Field field : scriptClass.getDeclaredFields()) { - if (Modifier.isPublic(field.getModifiers()) || Modifier.isProtected(field.getModifiers())) { - try { - field.setAccessible(true); - jep_.invoke("jep_set_builtin", field.getName(), field.get(script)); - } catch (IllegalAccessException iae) { - throw new JepException("Unexpected security manager being used!"); - } - } - } - } - - // inject GhidraScript methods once into Python; we ASSUME all SharedInterpreters can share the same methods - if (ghidraScriptMethodsInitialized.get() == false) { - file = - Application.getModuleDataFile(GhidrathonUtils.THIS_EXTENSION_NAME, "python/jepinject.py"); - jep_.set("__ghidra_script__", script); - jep_.runScript(file.getAbsolutePath()); - - ghidraScriptMethodsInitialized.set(true); - } - } - /** * Create a new GhidrathonInterpreter instance. * @@ -287,7 +250,9 @@ public void close() { try { if (jep_ != null) { + jep_.invoke("jepwrappers.remove_state"); jep_.close(); + jep_ = null; } @@ -330,8 +295,7 @@ public boolean eval(String line) { try { - setSysPath(); - setStreams(); + extendPythonSysPath(); return (boolean) jep_.invoke("jepeval", line); @@ -351,25 +315,13 @@ public boolean eval(String line) { * @param line Python statement * @param script GhidrathonScript with desired state. * @return True (need more input), False (no more input needed) - * @throws FileNotFoundException */ public boolean eval(String line, GhidrathonScript script) { try { - injectScriptHierarchy(script); - - } catch (JepException | FileNotFoundException e) { - - // we made it here; something bad went wrong, raise to caller - e.printStackTrace(this.err); - throw new RuntimeException(e); - } - - try { - - setSysPath(); - setStreams(); + jep_.invoke("jepwrappers.set_script", script); + extendPythonSysPath(); return (boolean) jep_.invoke("jepeval", line); @@ -392,8 +344,7 @@ public void runScript(ResourceFile file) { try { - setSysPath(); - setStreams(); + extendPythonSysPath(); jep_.invoke("jep_runscript", file.getAbsolutePath()); @@ -412,20 +363,17 @@ public void runScript(ResourceFile file) { * * @param file Python script to execute * @param script GhidrathonScript with desired state. - * @throws FileNotFoundException */ public void runScript(ResourceFile file, GhidraScript script) { try { - injectScriptHierarchy(script); - - setSysPath(); - setStreams(); + jep_.invoke("jepwrappers.set_script", script); + extendPythonSysPath(); jep_.invoke("jep_runscript", file.getAbsolutePath()); - } catch (JepException | FileNotFoundException e) { + } catch (JepException e) { // Python exceptions should be handled in Python land; something bad must have happened e.printStackTrace(this.err); @@ -433,35 +381,6 @@ public void runScript(ResourceFile file, GhidraScript script) { } } - /** - * Set output and error streams for Jep instance. - * - *

Output and error streams from Python interpreter are redirected to the specified streams. If - * these are not set, this data is lost. - * - * @param out output stream - * @param err error stream - */ - public void setStreams() { - - try { - - ResourceFile file = - Application.getModuleDataFile(GhidrathonUtils.THIS_EXTENSION_NAME, "python/jepstream.py"); - - jep_.set("GhidraPluginToolConsoleOutWriter", this.out); - jep_.set("GhidraPluginToolConsoleErrWriter", this.err); - - jep_.runScript(file.getAbsolutePath()); - - } catch (JepException | FileNotFoundException e) { - - // ensure stack trace prints to err stream for user - e.printStackTrace(this.err); - throw new RuntimeException(e); - } - } - public void printWelcome() { try { @@ -470,7 +389,7 @@ public void printWelcome() { Application.getModuleDataFile( GhidrathonUtils.THIS_EXTENSION_NAME, "python/jepwelcome.py"); - jep_.set("GhidraVersion", Application.getApplicationVersion()); + jep_.set("ghidra_version", Application.getApplicationVersion()); runScript(file);