Skip to content

Commit

Permalink
1.1.0 Release
Browse files Browse the repository at this point in the history
  • Loading branch information
dc3-tsd committed Apr 24, 2024
1 parent edbdd64 commit dab72b9
Show file tree
Hide file tree
Showing 12 changed files with 297 additions and 65 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [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.
Expand Down Expand Up @@ -96,7 +103,8 @@
## 0.1.0 - 2021-06-14
- Initial release

[Unreleased]: https://github.com/dod-cyber-crime-center/pyhidra/compare/1.0.2...HEAD
[Unreleased]: https://github.com/dod-cyber-crime-center/pyhidra/compare/1.1.0...HEAD
[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
[1.0.0]: https://github.com/dod-cyber-crime-center/pyhidra/compare/0.5.4...1.0.0
Expand Down
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Pyhidra was initially developed for use with Dragodis and is designed to be inst
1. Install pyhidra.

```console
> pip install pyhidra
pip install pyhidra
```
### Enabling the Ghidra User Interface Plugin

Expand All @@ -27,6 +27,24 @@ Pyhidra was initially developed for use with Dragodis and is designed to be inst
5. Check and enable Pyhidra as seen in the image below.
![](https://raw.githubusercontent.com/Defense-Cyber-Crime-Center/pyhidra/master/images/image-20220111154120531.png)

### Desktop Entry

If on linux or windows, a desktop entry can be created to launch an instance of Ghidra with pyhidra attached.

```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.


To remove, run the following:

```console
python -m pyhidra.uninstall_desktop
```

### Manual Plugin Installation

If pyhidra is planned to be used in a multiprocessing deployed server, the following must be run to allow the Ghidra plugins to be compiled and installed before use.
Expand Down
2 changes: 1 addition & 1 deletion pyhidra/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

__version__ = "1.0.2"
__version__ = "1.1.0"

# Expose API
from .core import run_script, start, started, open_program
Expand Down
14 changes: 0 additions & 14 deletions pyhidra/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@
logger = logging.getLogger("pyhidra")


def _create_shortcut():
from pyhidra.win_shortcut import create_shortcut
create_shortcut(Path(sys.argv[-1]))


def _interpreter(interpreter_globals: dict):
from ghidra.framework import Application
version = Application.getApplicationVersion()
Expand Down Expand Up @@ -182,15 +177,6 @@ def _get_parser():
dest="gui",
help="Start Ghidra GUI"
)
if is_win32:
parser.add_argument(
"-s",
"--shortcut",
action="store_const",
dest="func",
const=_create_shortcut,
help="Creates a shortcut that can be pinned to the taskbar (Windows only)"
)
parser.add_argument(
"--install-dir",
type=Path,
Expand Down
58 changes: 48 additions & 10 deletions pyhidra/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,11 @@ def _setup_project(
project_location: Union[str, Path] = None,
project_name: str = None,
language: str = None,
compiler: 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)
Expand All @@ -80,6 +82,19 @@ def _setup_project(
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:
Expand All @@ -90,15 +105,24 @@ def _setup_project(
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:
program = project.importProgram(binary_path)
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)
program = project.importProgram(binary_path, lang, comp)
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:
Expand Down Expand Up @@ -158,7 +182,8 @@ def open_program(
analyze=True,
language: str = None,
compiler: str = None,
) -> ContextManager["FlatProgramAPI"]:
loader: Union[str, JClass] = None
) -> ContextManager["FlatProgramAPI"]: # type: ignore
"""
Opens given binary path in Ghidra and returns FlatProgramAPI object.
Expand All @@ -172,8 +197,11 @@ def open_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 or compiler is invalid.
: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
Expand All @@ -189,7 +217,8 @@ def open_program(
project_location,
project_name,
language,
compiler
compiler,
loader
)
GhidraScriptUtil.acquireBundleHostReference()

Expand All @@ -215,6 +244,7 @@ def _flat_api(
analyze=True,
language: str = None,
compiler: str = None,
loader: Union[str, JClass] = None,
*,
install_dir: Path = None
):
Expand All @@ -234,10 +264,13 @@ def _flat_api(
(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 or compiler is invalid.
: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

Expand All @@ -251,7 +284,8 @@ def _flat_api(
project_location,
project_name,
language,
compiler
compiler,
loader
)

from ghidra.app.script import GhidraScriptUtil
Expand Down Expand Up @@ -282,6 +316,7 @@ def run_script(
analyze=True,
lang: str = None,
compiler: str = None,
loader: Union[str, JClass] = None,
*,
install_dir: Path = None
):
Expand All @@ -301,12 +336,15 @@ def run_script(
(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 or compiler is invalid.
: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
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)
87 changes: 56 additions & 31 deletions pyhidra/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import platform
import sys
import traceback
from typing import NoReturn
import warnings

import pyhidra
Expand Down Expand Up @@ -37,20 +38,68 @@ def print_help(self, file=None):
self._print_message(self.format_help(), file)


def _gui_mac() -> NoReturn:
args = _parse_args()
install_dir = args.install_dir
path = Path(sys.base_exec_prefix) / "Resources/Python.app/Contents/MacOS/Python"
if path.exists():
# the python launcher app will correctly start the venv if sys.executable is in a venv
argv = [sys.executable, "-m", "pyhidra", "-g"]
if install_dir is not None:
argv += ["--install-dir", str(install_dir)]
actions = ((os.POSIX_SPAWN_CLOSE, 0), (os.POSIX_SPAWN_CLOSE, 1), (os.POSIX_SPAWN_CLOSE, 2))
os.posix_spawn(str(path), argv, os.environ, file_actions=actions)
else:
print("could not find the Python.app path, launch failed")
sys.exit(0)


def _parse_args():
parser = _GuiArgumentParser(prog="pyhidraw")
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)"
)
return parser.parse_args()


def _gui_default(install_dir: Path):
pid = os.fork()
if pid != 0:
# original process can exit
return

fd = os.open(os.devnull, os.O_RDWR)
# redirect stdin, stdout and stderr to /dev/null so the jvm can't use the terminal
# this also prevents errors from attempting to write to a closed sys.stdout #21
os.dup2(fd, sys.stdin.fileno(), inheritable=False)
os.dup2(fd, sys.stdout.fileno(), inheritable=False)
os.dup2(fd, sys.stderr.fileno(), inheritable=False)

# run the application
gui(install_dir)


def _gui():
# this is the entry from the gui script
# there may or may not be an attached terminal
# depending on the current operating system

if platform.system() == "Darwin":
_gui_mac()

# This check handles the edge case of having a corrupt Python installation
# where tkinter can't be imported. Since there may not be an attached
# terminal, the problem still needs to be reported somehow.
try:
# This import creates problems for macOS
if platform.system() != 'Darwin':
import tkinter.messagebox as _
import tkinter.messagebox as _
except ImportError as e:
if platform.system() == 'Windows':
if platform.system() == "Windows":
# there is no console/terminal to report the error
import ctypes
MessageBox = ctypes.windll.user32.MessageBoxW
Expand All @@ -61,17 +110,7 @@ def _gui():
raise

try:
parser = _GuiArgumentParser(prog="pyhidraw")
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)"
)
args = parser.parse_args()
args = _parse_args()
install_dir = args.install_dir
except Exception as e:
import tkinter.messagebox
Expand All @@ -82,22 +121,8 @@ def _gui():
if platform.system() == 'Windows':
# gui_script works like it is supposed to on windows
gui(install_dir)
return

pid = os.fork()
if pid != 0:
# original process can exit
return

fd = os.open(os.devnull, os.O_RDWR)
# redirect stdin, stdout and stderr to /dev/null so the jvm can't use the terminal
# this also prevents errors from attempting to write to a closed sys.stdout #21
os.dup2(fd, sys.stdin.fileno(), inheritable=False)
os.dup2(fd, sys.stdout.fileno(), inheritable=False)
os.dup2(fd, sys.stderr.fileno(), inheritable=False)

# run the application
gui(install_dir)
else:
_gui_default(install_dir)


def gui(install_dir: Path = None):
Expand Down
Loading

0 comments on commit dab72b9

Please sign in to comment.