diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e55d2f..c7b08f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,20 @@ # Changelog +## [1.2.0] - 2024-05-31 +- Fixed Python interpreter completion results for members of currentAddress, currentProgram, etc. +- Fixed handling of complex Python interpreter completions. +- Use configured theme colors for Python interpreter completions. +- Removed Mac specific pyobjc dependency. +- Fixed bug causing Mac specific properties from launch.properties to be omitted. +- Fixed icon bug with the Windows shortcut. +- Added Mac support to the script for installing a desktop launcher. + ## [1.1.0] - 2024-04-23 - Improved `pyhidraw` compatibility on Mac. - Added loader parameter to `open_program` and `run_script` (#37). - Added script to install a desktop launcher for Windows and Linux. (see [docs](./README.md#desktop-entry)) - Removed `--shortcut` option on `pyhidra` command. - ## [1.0.2] - 2024-02-14 - Added `--debug` switch to `pyhidra` command line to set the `pyhidra` logging level to `DEBUG`. - Warnings when compiling Java code are now logged at the `INFO` logging level. @@ -100,10 +108,11 @@ - Fixed noise produced from an exception during analysis due to an analyzer using a script without acquiring a bundle host reference - Fixed exception in open_program from attempting to use a non-public field in `FlatProgramAPI` -## 0.1.0 - 2021-06-14 +## [0.1.0] - 2021-06-14 - Initial release -[Unreleased]: https://github.com/dod-cyber-crime-center/pyhidra/compare/1.1.0...HEAD +[Unreleased]: https://github.com/dod-cyber-crime-center/pyhidra/compare/1.2.0...HEAD +[1.2.0]: https://github.com/dod-cyber-crime-center/pyhidra/compare/1.1.0...1.2.0 [1.1.0]: https://github.com/dod-cyber-crime-center/pyhidra/compare/1.0.2...1.1.0 [1.0.2]: https://github.com/dod-cyber-crime-center/pyhidra/compare/1.0.1...1.0.2 [1.0.1]: https://github.com/dod-cyber-crime-center/pyhidra/compare/1.0.0...1.0.1 diff --git a/README.md b/README.md index 189cdf9..7ca83e3 100644 --- a/README.md +++ b/README.md @@ -29,14 +29,17 @@ pip install pyhidra ### Desktop Entry -If on linux or windows, a desktop entry can be created to launch an instance of Ghidra with pyhidra attached. +If on linux, mac or windows, a desktop entry can be created to launch an instance of Ghidra with pyhidra attached. +When this script is run from a virtual environment (ie. venv), pyhidra will be started in this virtual environment +when launched. ```console python -m pyhidra.install_desktop ``` On windows, this will install a shortcut file on the user's desktop. On linux, this will create an entry -that can be found in the applications launcher. +that can be found in the applications launcher. On mac this will create an entry that can be found in +the Launchpad. To remove, run the following: diff --git a/pyhidra/__init__.py b/pyhidra/__init__.py index 99bedfd..a0778ac 100644 --- a/pyhidra/__init__.py +++ b/pyhidra/__init__.py @@ -1,5 +1,5 @@ -__version__ = "1.1.0" +__version__ = "1.2.0" # Expose API from .core import run_script, start, started, open_program diff --git a/pyhidra/install_desktop.py b/pyhidra/install_desktop.py index b7a6723..0ce61dd 100644 --- a/pyhidra/install_desktop.py +++ b/pyhidra/install_desktop.py @@ -26,6 +26,8 @@ from pyhidra.win_shortcut import create_shortcut elif sys.platform == "linux": from pyhidra.linux_shortcut import create_shortcut + elif sys.platform == "darwin": + from pyhidra.mac_shortcut import create_shortcut else: sys.exit("Unsupported platform") diff --git a/pyhidra/java/plugin/completions.py b/pyhidra/java/plugin/completions.py index ee8f7ab..5635fc5 100644 --- a/pyhidra/java/plugin/completions.py +++ b/pyhidra/java/plugin/completions.py @@ -14,16 +14,30 @@ NoneType = type(None) -CLASS_COLOR = Color(0, 0, 255) -CODE_COLOR = Color(0, 64, 0) -FUNCTION_COLOR = Color(0, 128, 0) -INSTANCE_COLOR = Color(128, 0, 128) -MAP_COLOR = Color(64, 96, 128) -METHOD_COLOR = Color(0, 128, 128) -NULL_COLOR = Color(255, 0, 0) -NUMBER_COLOR = Color(64, 64, 64) -PACKAGE_COLOR = Color(128, 0, 0) -SEQUENCE_COLOR = Color(128, 96, 64) +try: + from generic.theme import GColor + CLASS_COLOR = GColor("color.fg.plugin.python.syntax.class") + CODE_COLOR = GColor("color.fg.plugin.python.syntax.code") + FUNCTION_COLOR = GColor("color.fg.plugin.python.syntax.function") + INSTANCE_COLOR = GColor("color.fg.plugin.python.syntax.instance") + MAP_COLOR = GColor("color.fg.plugin.python.syntax.map") + METHOD_COLOR = GColor("color.fg.plugin.python.syntax.method") + NULL_COLOR = GColor("color.fg.plugin.python.syntax.null") + NUMBER_COLOR = GColor("color.fg.plugin.python.syntax.number") + PACKAGE_COLOR = GColor("color.fg.plugin.python.syntax.package") + SEQUENCE_COLOR = GColor("color.fg.plugin.python.syntax.sequence") +except: + # no custom theme support yet, fall back to hardcoded values + CLASS_COLOR = Color(0, 0, 255) + CODE_COLOR = Color(0, 64, 0) + FUNCTION_COLOR = Color(0, 128, 0) + INSTANCE_COLOR = Color(128, 0, 128) + MAP_COLOR = Color(64, 96, 128) + METHOD_COLOR = Color(0, 128, 128) + NULL_COLOR = Color(255, 0, 0) + NUMBER_COLOR = Color(64, 64, 64) + PACKAGE_COLOR = Color(128, 0, 0) + SEQUENCE_COLOR = Color(128, 96, 64) _TYPE_COLORS = { type: CLASS_COLOR, @@ -83,8 +97,9 @@ def _get_label(self, i: int) -> GLabel: return label def _supplier(self, i: int) -> CodeCompletion: - insertion = self.matches[i][len(self.cmd):] - return CodeCompletion(self.cmd, insertion, self._get_label(i)) + match = self.matches[i] + insertion = match[len(self.cmd):] + return CodeCompletion(match, insertion, self._get_label(i)) def get_completions(self, cmd: str): """ diff --git a/pyhidra/java/plugin/plugin.py b/pyhidra/java/plugin/plugin.py index e5a3462..2689704 100644 --- a/pyhidra/java/plugin/plugin.py +++ b/pyhidra/java/plugin/plugin.py @@ -1,13 +1,11 @@ import contextlib import ctypes -import itertools import logging -import rlcompleter +import re import sys import threading from code import InteractiveConsole -from ghidra.app.plugin.core.console import CodeCompletion from ghidra.app.plugin.core.interpreter import InterpreterConsole, InterpreterPanelService from ghidra.framework import Application from java.io import BufferedReader, InputStreamReader, PushbackReader @@ -170,6 +168,8 @@ class PyPhidraPlugin: """ The Python side PyhidraPlugin """ + + _WORD_PATTERN = re.compile(r".*?([\w\.]+)\Z") # get the last word, including '.', from the right def __init__(self, plugin): if hasattr(self, '_plugin'): @@ -224,6 +224,9 @@ def getCompletions(self, *args): else: # older versions of Ghidra don't have the `end` argument. line, = args + match = self._WORD_PATTERN.match(line) + if match: + line = match.group(1) return self.completer.get_completions(line) except Exception as e: if not self._logged_completions_change: diff --git a/pyhidra/launcher.py b/pyhidra/launcher.py index cd3d306..462836b 100644 --- a/pyhidra/launcher.py +++ b/pyhidra/launcher.py @@ -1,4 +1,6 @@ import contextlib +import ctypes +import ctypes.util import importlib.metadata import inspect import logging @@ -121,6 +123,8 @@ def __init__(self, verbose=False, *, install_dir: Path = None): @classmethod def _jvm_args(cls, install_dir: Path) -> List[str]: suffix = "_" + platform.system().upper() + if suffix == "_DARWIN": + suffix = "_MACOS" option_pattern: re.Pattern = re.compile(fr"VMARGS(?:{suffix})?=(.+)") properties = [] @@ -554,7 +558,6 @@ def _get_thread(name: str): return None def _launch(self): - import ctypes from ghidra import Ghidra from java.lang import Runtime, Thread @@ -565,10 +568,27 @@ def _launch(self): stdout = _PyhidraStdOut(sys.stdout) stderr = _PyhidraStdOut(sys.stderr) with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): - jpype.setupGuiEnvironment(lambda: Ghidra.main(["ghidra.GhidraRun", *self.args])) + Thread(lambda: Ghidra.main(["ghidra.GhidraRun", *self.args])).start() is_exiting = threading.Event() Runtime.getRuntime().addShutdownHook(Thread(is_exiting.set)) - try: - is_exiting.wait() - finally: - jpype.shutdownGuiEnvironment() + if sys.platform == "darwin": + _run_mac_app() + is_exiting.wait() + + +def _run_mac_app(): + # this runs the main event loop + # it is required for the GUI to show up + objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("libobjc")) + ctypes.cdll.LoadLibrary(ctypes.util.find_library("AppKit")) # required + msgSend = objc.objc_msgSend + msgSend.restype = ctypes.c_void_p + msgSend.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + registerName = objc.sel_registerName + registerName.restype = ctypes.c_void_p + registerName.argtypes = [ctypes.c_char_p] + getClass = objc.objc_getClass + getClass.restype = ctypes.c_void_p + NSApplication = getClass(b"NSApplication") + sharedApplication = msgSend(NSApplication, registerName(b"sharedApplication")) + msgSend(sharedApplication, registerName(b"run")) diff --git a/pyhidra/linux_shortcut.py b/pyhidra/linux_shortcut.py index 6db5736..159982e 100644 --- a/pyhidra/linux_shortcut.py +++ b/pyhidra/linux_shortcut.py @@ -38,12 +38,14 @@ def extract_png(install_dir: Path) -> Path: def create_shortcut(install_dir: Path = None): """Install a desktop entry on Linux machine.""" pyhidra_exec = Path(sysconfig.get_path("scripts")) / "pyhidra" + if not pyhidra_exec.exists(): + # User install + pyhidra_exec = Path(sysconfig.get_path("scripts", "posix_user")) / "pyhidra" if not pyhidra_exec.exists(): sys.exit("pyhidra executable is not installed.") - command = [str(pyhidra_exec), "--gui"] if install_dir: - command += ["--install-dir", str(install_dir.expanduser())] + pass elif install_dir := os.environ.get("GHIDRA_INSTALL_DIR"): install_dir = Path(install_dir) else: @@ -51,8 +53,11 @@ def create_shortcut(install_dir: Path = None): "Unable to determine Ghidra installation directory. " "Please set the GHIDRA_INSTALL_DIR environment variable." ) + + command = [str(pyhidra_exec), "--gui", "--install-dir", str(install_dir.expanduser())] icon = extract_png(install_dir) + desktop_path.parent.mkdir(parents=True, exist_ok=True) desktop_path.write_text(desktop_entry.format(icon=icon, exec=shlex.join(command))) print(f"Installed {desktop_path}") diff --git a/pyhidra/mac_shortcut.py b/pyhidra/mac_shortcut.py new file mode 100644 index 0000000..89d8151 --- /dev/null +++ b/pyhidra/mac_shortcut.py @@ -0,0 +1,186 @@ +from base64 import b64encode +from functools import cached_property +from hashlib import sha1, sha256 +import os +from pathlib import Path +import shutil +import stat +import subprocess +import sys +from tempfile import TemporaryDirectory + +from pyhidra.linux_shortcut import extract_png + + +applications = Path("~/Applications").expanduser() + + +class AppBuilder: + + APP_NAME = "Ghidra (pyhidra).app" + ICON_NAME = "ghidra.icns" + + def __init__(self, install_dir: Path): + self._tmpdir = TemporaryDirectory() + self.tmpdir = Path(self._tmpdir.name) + self.install_dir = install_dir + + @property + def desktop_path(self) -> Path: + app_dir = applications + app_dir.mkdir(exist_ok=True) + return app_dir / self.APP_NAME + + @cached_property + def contents(self) -> Path: + return self.tmpdir / self.APP_NAME / "Contents" + + @cached_property + def icon_path(self) -> Path: + return self.contents / "Resources" / self.ICON_NAME + + def create_icon(self): + icon_dir = self.tmpdir / "ghidra.iconset" + icon_dir.mkdir() + extract_png(self.install_dir).rename(icon_dir / "icon_256x256.png") + cmd = ["/usr/bin/iconutil", "--convert", "icns", "ghidra.iconset"] + subprocess.check_call(cmd, cwd=self.tmpdir) + resources = self.contents / "Resources" + resources.mkdir(parents=True) + icon = self.tmpdir / self.ICON_NAME + icon.rename(self.icon_path) + + def create_code_resources(self): + icon_data = self.icon_path.read_bytes() + hash1 = b64encode(sha1(icon_data).digest()).decode("utf-8") + hash2 = b64encode(sha256(icon_data).digest()).decode("utf-8") + sigdir = self.contents / "_CodeSignature" + sigdir.mkdir(parents=True) + code_resources = sigdir / "CodeResources" + data = CODE_RESOURCES.format(hash1=hash1, hash2=hash2) + code_resources.write_text(data, encoding="utf-8") + + def create_info_plist(self): + info_plist = self.contents / "Info.plist" + data = INFO_PLIST.format(install_dir=self.install_dir) + info_plist.write_text(data, encoding="utf-8") + + def create_exe(self): + exe_dir = self.contents / "MacOS" + exe_dir.mkdir(parents=True) + script_path = exe_dir / "pyhidra" + + # NOTE: using sys.executable allows venv to work properly + data = PYHIDRA_SCRIPT.format(python=sys.executable) + script_path.write_text(data, encoding="utf-8") + + # chmod +x + mode = script_path.stat().st_mode | stat.S_IXUSR + script_path.chmod(mode) + + def move(self): + # remove the existing one first if present + if self.desktop_path.exists(): + shutil.rmtree(self.desktop_path) + app_dir = self.tmpdir / self.APP_NAME + app_dir.rename(self.desktop_path) + + def __enter__(self): + self._tmpdir.__enter__() + return self + + def __exit__(self, *args): + return self._tmpdir.__exit__(*args) + + +def create_shortcut(install_dir: Path = None): + """Install a desktop entry on Mac machine.""" + if install_dir is None: + install_dir = os.environ.get("GHIDRA_INSTALL_DIR") + if install_dir is None: + sys.exit( + "Unable to determine Ghidra installation directory. " + "Please set the GHIDRA_INSTALL_DIR environment variable." + ) + install_dir = Path(install_dir) + + with AppBuilder(install_dir) as builder: + builder.create_icon() + builder.create_code_resources() + builder.create_info_plist() + builder.create_exe() + builder.move() + + +def remove_shortcut(): + desktop_path = applications / AppBuilder.APP_NAME + if desktop_path.exists(): + shutil.rmtree(desktop_path) + print(f"Removed {desktop_path}") + + +CODE_RESOURCES = """ + + + + files + + Resources/ghidra.icns + + {hash1} + + + files2 + + Resources/ghidra.icns + + hash + + {hash1} + + hash2 + + {hash2} + + + + + +""" + +INFO_PLIST = """ + + + + CFBundleExecutable + pyhidra + CFBundleGetInfoString + Ghidra (pyhidra) + CFBundleIconFile + ghidra.icns + CFBundleIdentifier + ghidra.Ghidra + CFBundleName + Ghidra + CFBundlePackageType + APPL + LSEnvironment + + GHIDRA_INSTALL_DIR + {install_dir} + + LSMultipleInstancesProhibited + + + +""" + +PYHIDRA_SCRIPT = """#!{python} +# -*- coding: utf-8 -*- +import pyhidra.gui + + +if __name__ == '__main__': + pyhidra.gui.gui() + +""" \ No newline at end of file diff --git a/pyhidra/script.py b/pyhidra/script.py index 70ad9ad..b2dbd5e 100644 --- a/pyhidra/script.py +++ b/pyhidra/script.py @@ -17,6 +17,7 @@ class _StaticMap(dict): + # this is a special view of the PyGhidraScript for use with rlcompleter __slots__ = ('script',) @@ -27,6 +28,14 @@ def __init__(self, script: "PyGhidraScript"): def __getitem__(self, key): res = self.get(key, _NO_ATTRIBUTE) if res is not _NO_ATTRIBUTE: + if isinstance(res, property): + # rlcompleter is attempting to use a property getter on the interpreter script + # allow the property magic to take place + # this is necessary for completions on currentAddress, currentProgram, etc. + try: + return getattr(self.script, key) + except AttributeError: + return res return res raise KeyError(key) diff --git a/pyhidra/uninstall_desktop.py b/pyhidra/uninstall_desktop.py index 15e7fcd..f090ad0 100644 --- a/pyhidra/uninstall_desktop.py +++ b/pyhidra/uninstall_desktop.py @@ -7,6 +7,8 @@ from pyhidra.win_shortcut import remove_shortcut elif sys.platform == "linux": from pyhidra.linux_shortcut import remove_shortcut + elif sys.platform == "darwin": + from pyhidra.mac_shortcut import remove_shortcut else: sys.exit("Unsupported platform") diff --git a/pyhidra/win_shortcut.py b/pyhidra/win_shortcut.py index 1b1c428..12b3b13 100644 --- a/pyhidra/win_shortcut.py +++ b/pyhidra/win_shortcut.py @@ -51,7 +51,7 @@ def __init__(self, key: str, pid: int) -> None: _Save = WINFUNCTYPE(ctypes.HRESULT, ctypes.c_wchar_p, ctypes.wintypes.BOOL)(6, "Save") _SetPath = WINFUNCTYPE(ctypes.HRESULT, ctypes.c_wchar_p)(20, "SetPath") _SetDescription = WINFUNCTYPE(ctypes.HRESULT, ctypes.c_wchar_p)(7, "SetDescription") - _SetIconLocation = WINFUNCTYPE(ctypes.HRESULT, ctypes.c_wchar_p)(17, "SetIconLocation") + _SetIconLocation = WINFUNCTYPE(ctypes.HRESULT, ctypes.c_wchar_p, ctypes.c_int)(17, "SetIconLocation") _SetValue = WINFUNCTYPE(ctypes.HRESULT, ctypes.c_void_p, ctypes.c_void_p)(6, "SetValue") link = str(link) @@ -67,7 +67,7 @@ def __init__(self, key: str, pid: int) -> None: _CoCreateInstance(_CLSID_ShellLink, None, _CLSCTX_INPROC_SERVER, _IID_IShellLinkW, ref) _SetPath(p_link, ctypes.c_wchar_p(str(target))) _SetDescription(p_link, p_app_id) - _SetIconLocation(p_link, ctypes.c_wchar_p(icon)) + _SetIconLocation(p_link, ctypes.c_wchar_p(icon), 0) _QueryInterface(p_link, _IID_IPropertyStore, ctypes.byref(p_store)) value = _PropertyVariant.pack(_VT_LPWSTR, ctypes.cast(p_app_id, ctypes.c_void_p).value) value = (ctypes.c_byte * len(value))(*value) diff --git a/setup.cfg b/setup.cfg index e487f0f..c87a186 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,7 +29,6 @@ zip_safe = False include_package_data = True install_requires = Jpype1>=1.3.0; python_version < '3.12' - pyobjc; sys_platform == "darwin" Jpype1>=1.5.0; python_version >= '3.12' [options.entry_points]