diff --git a/src/main/java/ghidrathon/interpreter/GhidrathonInterpreter.java b/src/main/java/ghidrathon/interpreter/GhidrathonInterpreter.java index 522df36..507d950 100644 --- a/src/main/java/ghidrathon/interpreter/GhidrathonInterpreter.java +++ b/src/main/java/ghidrathon/interpreter/GhidrathonInterpreter.java @@ -10,6 +10,7 @@ package ghidrathon.interpreter; +import com.google.gson.*; import generic.jar.ResourceFile; import ghidra.app.script.GhidraScript; import ghidra.app.script.GhidraScriptUtil; @@ -22,6 +23,8 @@ import java.io.*; import java.lang.reflect.*; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import jep.Jep; import jep.JepConfig; @@ -32,6 +35,11 @@ /** Utility class used to configure a Jep instance to access Ghidra */ public class GhidrathonInterpreter { + private class GhidrathonSave { + String executable; + String home; + } + private static final String GHIDRATHON_SAVE_FILENAME = "ghidrathon.save"; private static final String SUPPORTED_JEP_VERSION = "4.2.0"; @@ -50,7 +58,8 @@ public class GhidrathonInterpreter { private static File jepPythonPackageDir = null; private static File jepNativeFile = null; - private static File pythonFile = null; + private static File pythonExecutableFile = null; + private static File pythonHomeDir = null; /** * Create and configure a new GhidrathonInterpreter instance. @@ -89,7 +98,8 @@ private GhidrathonInterpreter(GhidrathonConfig config) throws JepException, IOEx Msg.info(GhidrathonInterpreter.class, "Configuring Python sys module."); jep_.eval( - String.format("import sys;sys.executable=sys._base_executable=r\"%s\"", this.pythonFile)); + String.format( + "import sys;sys.executable=sys._base_executable=r\"%s\"", this.pythonExecutableFile)); // site module configures other necessary sys vars, e.g. sys.prefix, using sys.executable jep_.eval("import site;site.main()"); jep_.eval( @@ -198,28 +208,61 @@ private void configureJepMainInterpreter() throws JepException, FileNotFoundExce GhidrathonInterpreter.class, String.format("Using save file at %s.", ghidrathonSaveFile.getAbsolutePath())); - // read absolute path of Python interpreter from save file + GhidrathonSave ghidrathonSave = null; try (BufferedReader reader = new BufferedReader(new FileReader(ghidrathonSaveFile))) { - String pythonFilePath = reader.readLine().trim(); - if (pythonFilePath != null && !pythonFilePath.isEmpty()) { - this.pythonFile = new File(pythonFilePath); + String json = reader.readLine().trim(); + if (json != null && !json.isEmpty()) { + try { + ghidrathonSave = new Gson().fromJson(json, GhidrathonSave.class); + } catch (JsonSyntaxException e) { + throw new JepException( + String.format( + "Failed to parse JSON from %s (%s). Please configure Ghidrathon before running" + + " it.", + ghidrathonSaveFile.getAbsolutePath(), e)); + } } } catch (IOException e) { throw new JepException( String.format("Failed to read %s (%s)", ghidrathonSaveFile.getAbsolutePath(), e)); } - // validate Python file path exists and is a file - if (this.pythonFile == null || !(this.pythonFile.exists() && this.pythonFile.isFile())) { + if (ghidrathonSave.home == null || ghidrathonSave.executable == null) { + throw new JepException( + String.format( + "%s JSON is not valid. Please configure Ghidrathon before running it.", + ghidrathonSaveFile.getAbsolutePath())); + } + + Msg.info( + GhidrathonInterpreter.class, + String.format( + "ghidrathonSave.home = \"%s\", ghidrathonSave.executable = \"%s\"", + ghidrathonSave.home, ghidrathonSave.executable)); + + // validate Python home directory exists and is a directory + this.pythonHomeDir = new File(ghidrathonSave.home); + if (!(this.pythonHomeDir.exists() && this.pythonHomeDir.isDirectory())) { + throw new JepException( + String.format( + "Python home path %s is not valid. Please configure Ghidrathon before running it.", + this.pythonHomeDir.getAbsolutePath())); + } + + // validate Python executable path exists and is a file + this.pythonExecutableFile = new File(ghidrathonSave.executable); + if (!(this.pythonExecutableFile.exists() && this.pythonExecutableFile.isFile())) { throw new JepException( String.format( - "Python path %s is not valid. Please configure Ghidrathon before running it.", - this.pythonFile.getAbsolutePath())); + "Python executable path %s is not valid. Please configure Ghidrathon before running" + + " it.", + this.pythonExecutableFile.getAbsolutePath())); } Msg.info( GhidrathonInterpreter.class, - String.format("Using Python interpreter at %s.", this.pythonFile.getAbsolutePath())); + String.format( + "Using Python interpreter at %s.", this.pythonExecutableFile.getAbsolutePath())); String jepPythonPackagePath = findJepPackageDir(); if (jepPythonPackagePath.isEmpty()) { @@ -301,26 +344,42 @@ private void configureJepMainInterpreter() throws JepException, FileNotFoundExce GhidrathonInterpreter.class, String.format("Using Jep version %s.", GhidrathonInterpreter.SUPPORTED_JEP_VERSION)); + /* + * We need to ensure Jep nativate can link its dependencies, namely + * Python. This must be done before jep.MainInterpreter is initialized so we attempt + * to load Jep native here and resolve any linking issues. Linking issues are most common + * when a non-standard Python install is used. + */ try { - MainInterpreter.setJepLibraryPath(this.jepNativeFile.getAbsolutePath()); + System.load(this.jepNativeFile.getAbsolutePath()); + } catch (UnsatisfiedLinkError e) { + Msg.info( + GhidrathonInterpreter.class, + String.format("Link error encountered when loading Jep native (%s)", e)); + + // https://github.com/ninia/jep/blob/dd2bf345392b1b66fd6c9aeb12c234a557690ba1/src/main/java/jep/LibraryLocator.java#L244 + Matcher m = Pattern.compile("libpython[\\w\\.]*").matcher(e.getMessage()); + if (!(m.find() && findPythonLibrary(m.group(0)))) { + if (!findPythonLibraryWindows()) { + // failed to resolve link error + throw new JepException(String.format("Failed to load native Jep (%s).", e)); + } + } + } - PyConfig config = new PyConfig(); + MainInterpreter.setJepLibraryPath(this.jepNativeFile.getAbsolutePath()); - // we can't auto import the site module becuase we are running an embedded Python interpreter - config.setNoSiteFlag(1); - // config.setIgnoreEnvironmentFlag(1); + // delay site module import + PyConfig config = new PyConfig(); + config.setNoSiteFlag(1); - MainInterpreter.setInitParams(config); - } catch (IllegalStateException e) { - e.printStackTrace(this.err); - throw new RuntimeException(e); - } + MainInterpreter.setInitParams(config); } private String findJepPackageDir() { String output = execCmd( - this.pythonFile.getAbsolutePath(), + this.pythonExecutableFile.getAbsolutePath(), "-c", "import importlib.util;import" + " pathlib;print(pathlib.Path(importlib.util.find_spec('jep').origin).parent)"); @@ -353,6 +412,52 @@ private String execCmd(String... commands) { return output; } + /** + * Attempt to load libpython from within PYTHONHOME + * + * @param libraryName the full file name of libpython + * @return true if libpython was found and loaded. + */ + private boolean findPythonLibrary(String libraryName) { + // https://github.com/ninia/jep/blob/dd2bf345392b1b66fd6c9aeb12c234a557690ba1/src/main/java/jep/LibraryLocator.java#L275 + if (this.pythonHomeDir != null) { + for (String libDirName : new String[] {"lib", "lib64", "Lib"}) { + File libDir = new File(this.pythonHomeDir, libDirName); + if (!libDir.isDirectory()) { + continue; + } + File libraryFile = new File(libDir, libraryName); + if (libraryFile.exists()) { + System.load(libraryFile.getAbsolutePath()); + return true; + } + } + } + return false; + } + + /** + * Attempt to load pythonXX.dll from within PYTHONHOME + * + * @return true if pythonXX.dll was found and loaded. + */ + private boolean findPythonLibraryWindows() { + // https://github.com/ninia/jep/blob/dd2bf345392b1b66fd6c9aeb12c234a557690ba1/src/main/java/jep/LibraryLocator.java#L297 + if (this.pythonHomeDir != null) { + Pattern re = Pattern.compile("^python\\d\\d+\\.dll$"); + for (File file : this.pythonHomeDir.listFiles()) { + if (!file.isFile()) { + continue; + } + if (re.matcher(file.getName()).matches() && file.exists()) { + System.load(file.getAbsolutePath()); + return true; + } + } + } + return false; + } + /** * Configure wrapper functions in Python land. * diff --git a/util/ghidrathon_configure.py b/util/ghidrathon_configure.py index 59e71be..fe2e015 100644 --- a/util/ghidrathon_configure.py +++ b/util/ghidrathon_configure.py @@ -7,12 +7,16 @@ # See the License for the specific language governing permissions and limitations under the License. import sys +import json import logging import pathlib import argparse import importlib.util +from typing import Dict SUPPORTED_JEP_VERSION = "4.2.0" +PYTHON_HOME_DIR_KEY = "home" +PYTHON_EXECUTABLE_FILE_KEY = "executable" logger = logging.getLogger(__name__) @@ -62,6 +66,8 @@ def main(args): ) return -1 + ghidrathon_save: Dict[str, str] = {} + python_path: pathlib.Path = pathlib.Path("None" if not sys.executable else sys.executable) if not all((python_path.exists(), python_path.is_file())): logger.error( @@ -70,13 +76,26 @@ def main(args): ) return -1 + ghidrathon_save[PYTHON_EXECUTABLE_FILE_KEY] = str(python_path) logger.debug('Using Python interpreter located at "%s".', python_path) + home_path: pathlib.Path = pathlib.Path("None" if not sys.base_prefix else sys.base_prefix) + if not all((home_path.exists(), home_path.is_dir())): + logger.error( + 'sys.base_prefix value "%s" is not valid. Please verify your Python environment is correct before configuring Ghidrathon.', + home_path, + ) + return -1 + + ghidrathon_save[PYTHON_HOME_DIR_KEY] = str(home_path) + logger.debug('Using Python home located at "%s".', home_path) + + json_: str = json.dumps(ghidrathon_save) save_path: pathlib.Path = install_path / "ghidrathon.save" try: - save_path.write_text(str(python_path), encoding="utf-8") + save_path.write_text(json_, encoding="utf-8") except Exception as e: - logger.error('Failed to write "%s" to "%s" (%s).', python_path, save_path, e) + logger.error('Failed to write "%s" to "%s" (%s).', json_, save_path, e) return -1 try: @@ -99,7 +118,7 @@ def main(args): ) return -1 - logger.debug('Wrote "%s" to "%s".', python_path, save_path) + logger.debug('Wrote "%s" to "%s".', json_, save_path) logger.info( 'Ghidrathon has been configured to use the Python interpreter located at "%s". Please restart Ghidra for these changes to take effect.', python_path,