diff --git a/Ghidra/Features/PyGhidra/src/main/java/ghidra/pyghidra/PyGhidraProject.java b/Ghidra/Features/PyGhidra/src/main/java/ghidra/pyghidra/PyGhidraProject.java new file mode 100644 index 0000000000..8f01041c06 --- /dev/null +++ b/Ghidra/Features/PyGhidra/src/main/java/ghidra/pyghidra/PyGhidraProject.java @@ -0,0 +1,29 @@ +/* ### + * IP: GHIDRA + * + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package ghidra.pyghidra; + +import ghidra.framework.data.DefaultProjectData; +import ghidra.framework.main.AppInfo; +import ghidra.framework.model.ProjectData; +import ghidra.framework.project.DefaultProject; + +public class PyGhidraProject extends DefaultProject { + + public PyGhidraProject(PyGhidraProjectManager projectManager, ProjectData projectData) { + super(projectManager, (DefaultProjectData) projectData); + AppInfo.setActiveProject(this); + } +} diff --git a/Ghidra/Features/PyGhidra/src/main/java/ghidra/pyghidra/interpreter/InterpreterGhidraScript.java b/Ghidra/Features/PyGhidra/src/main/java/ghidra/pyghidra/interpreter/InterpreterGhidraScript.java index a1961aaf95..fb6f36d3ee 100644 --- a/Ghidra/Features/PyGhidra/src/main/java/ghidra/pyghidra/interpreter/InterpreterGhidraScript.java +++ b/Ghidra/Features/PyGhidra/src/main/java/ghidra/pyghidra/interpreter/InterpreterGhidraScript.java @@ -17,7 +17,7 @@ package ghidra.pyghidra.interpreter; import java.io.PrintWriter; -import ghidra.app.script.*; +import ghidra.app.script.GhidraScript; import ghidra.program.model.address.Address; import ghidra.program.model.listing.Program; import ghidra.program.util.ProgramLocation; @@ -83,8 +83,4 @@ public final class InterpreterGhidraScript extends GhidraScript { currentHighlight = highlight; state.setCurrentHighlight(highlight); } - - public void set(GhidraState state, PrintWriter writer) { - set(state, new ScriptControls(writer, writer, new InterpreterTaskMonitor(writer))); - } } diff --git a/Ghidra/Features/PyGhidra/src/main/py/README.md b/Ghidra/Features/PyGhidra/src/main/py/README.md index 4550be5b4b..cb3df45fb8 100644 --- a/Ghidra/Features/PyGhidra/src/main/py/README.md +++ b/Ghidra/Features/PyGhidra/src/main/py/README.md @@ -21,33 +21,28 @@ instructions in this document focus on this type of usage. To install the PyGhidra Python library: 1. Download and install - [Ghidra 11.3 or later](https://github.com/NationalSecurityAgency/ghidra/releases) to a desired + [Ghidra 12.0 or later](https://github.com/NationalSecurityAgency/ghidra/releases) to a desired location. -2. Set the `GHIDRA_INSTALL_DIR` environment variable to point to the directory where Ghidra is - installed. -3. Install PyGhidra: +2. Install PyGhidra: * Online: `pip install pyghidra` * Offline: `python3 -m pip install --no-index -f /Ghidra/Features/PyGhidra/pypkg/dist pyghidra` - -Optionally, you can also install the Ghidra type stubs to improve your development experience -(assuming your Python editor supports it). The type stubs module is specific to each version of -Ghidra: -* Online: `pip install ghidra-stubs==` -* Offline: `python3 -m pip install --no-index -f /docs/ghidra_stubs ghidra-stubs` +3. Optionally install the Ghidra type stubs to improve your development experience (assuming your + Python editor supports it). The type stubs module is specific to each version of Ghidra: + * Online: `pip install ghidra-stubs==` + * Offline: `python3 -m pip install --no-index -f /docs/ghidra_stubs ghidra-stubs` +4. Optionally point PyGhidra at your Ghidra installation by setting the `GHIDRA_INSTALL_DIR` + environment variable. If not set, PyGhidra will point itself at the last used installation of + Ghidra. Alternatively, you can point PyGhidra at a Ghidra installation with + `pyghidra.start(install_dir=)` (see below). ## API -The current version of PyGhidra inherits an API from the original "Pyhidra" project that provides an -excellent starting point for interacting with a Ghidra installation. __NOTE:__ These functions are -subject to change in the future as more thought and feedback is collected on PyGhidra's role in the -greater Ghidra ecosystem: +The current version of PyGhidra introduces many new API methods with the goal of making the most +common Ghidra tasks quick and easy, such as opening a project, getting a program, running a +GhidraScript, etc. The inherited API from the original "Pyhidra" project is still available, but at +this point it will only receive bug fixes. ### pyghidra.start() -To get a raw connection to Ghidra use the `start()` function. This will setup a JPype connection and -initialize Ghidra in headless mode, which will allow you to directly import `ghidra` and `java`. - -__NOTE:__ No projects or programs get setup in this mode. - ```python def start(verbose=False, *, install_dir: Path = None) -> "PyGhidraLauncher": """ @@ -60,23 +55,7 @@ def start(verbose=False, *, install_dir: Path = None) -> "PyGhidraLauncher": """ ``` -#### Example: -```python -import pyghidra -pyghidra.start() - -import ghidra -from ghidra.app.util.headless import HeadlessAnalyzer -from ghidra.program.flatapi import FlatProgramAPI -from ghidra.base.project import GhidraProject -from java.lang import String - -# do things -``` - ### pyghidra.started() -To check to see if PyGhidra has been started, use the `started()` function. - ```python def started() -> bool: """ @@ -84,14 +63,256 @@ def started() -> bool: """ ``` -#### Example: +### pyghidra.open_project() ```python -import pyghidra +def open_project( + path: Union[str, Path], + name: str, + create: bool = False +) -> "Project": # type: ignore + """ + Opens the Ghidra project at the given location, optionally creating it if it doesn't exist. -if pyghidra.started(): - ... + :param path: Path of Ghidra project parent directory. + :param name: Name of Ghidra project to open/create. + :param create: Whether to create the project if it doesn't exist + :return: A Ghidra "Project" object. + :raises FileNotFoundError: If the project to open was not found and it shouldn't be created. + """ ``` +### pyghidra.open_filesystem() +```python +def open_filesystem( + path: Union[str, Path] + ) -> "GFileSystem": + """ + Opens a filesystem in Ghidra. + + :param path: Path of filesystem to open in Ghidra. + :return: A Ghidra "GFileSystem" object. + :raises ValueError: If the filesystem to open is not supported by Ghidra. + """ +``` + +### pyghidra.consume_program() +```python +def consume_program( + project: "Project", + path: Union[str, Path], + consumer: Any = None + ) -> Tuple["Program", "Object"]: + """ + Gets the Ghidra program from the given project with the given project path. The returned program + must be manually released when it is no longer needed. + + :param project: The Ghidra project that has the program. + :param path: The project path of the program (should start with "/") + :param consumer: An optional reference to the Java object "consuming" the returned program, used + to ensure the underlying DomainObject is only closed when every consumer is done with it. If + a consumer is not provided, one will be generated by this function. + :return: A 2-element tuple containing the program and a consumer object that must be used to + release the program when finished with it (i.e., program.release(consumer). If a consumer + object was provided, the same consumer object is returned. Otherwise, a new consumer object + is created and returned. + :raises FileNotFoundError: If the path does not exist in the project. + :raises TypeError: If the path in the project exists but is not a Program. + """ +``` + +### pyghidra.program_context() +```python +@contextlib.contextmanager +def program_context( + project: "Project", + path: Union[str, Path], + ) -> "Program": + """ + Gets the Ghidra program from the given project with the given project path. The returned + program's resource cleanup is performed by a context manager. + + :param project: The Ghidra project that has the program. + :param path: The project path of the program (should start with "/"). + :return: The Ghidra program. + :raises FileNotFoundError: If the path does not exist in the project. + :raises TypeError: If the path in the project exists but is not a Program. + """ +``` + +### pyghidra.analyze() +```python +def analyze(program: "Program"): + """ + Analyzes the given program. + + :param program: The Ghidra program to analyze. + """ +``` + +### pyghidra.ghidra_script() +```python +def ghidra_script( + path: Union[str, Path], + project: "Project", + program: "Program" = None, + echo_stdout = True, + echo_stderr = True + ) -> Tuple[str, str]: + """ + Runs any type of GhidraScript (Java, PyGhidra, Jython, etc). + + :param path: The GhidraScript's path. + :param project: The Ghidra project to run the GhidraScript in. + :param program: An optional Ghidra program that the GhidraScript will see as its "currentProgram". + :param echo_stdout: Whether or not to echo the GhidraScript's standard output. + :param echo_stderr: Whether or not to echo the GhidraScript's standard error. + :return: A 2 element tuple consisting of the GhidraScript's standard output and standard error. + """ +``` + +### pyghidra.transaction() +```python +@contextlib.contextmanager +def transaction( + program: "Program", + description: str = "Unnamed Transaction" + ): + """ + Creates a context for running a Ghidra transaction. + + :param program: The Ghidra program that will be affected. + :param description: The transaction description + :return: The transaction ID. + """ +``` + +### pyghidra.analysis_properties() +```python +def analysis_properties(program: "Program") -> "Options": + """ + Convenience function to get the Ghidra "Program.ANALYSIS_PROPERTIES" options. + + :return: the Ghidra "Program.ANALYSIS_PROPERTIES" options. + """ +``` + +### pyghidra.program_info() +```python +def program_info(program: "Program") -> "Options": + """ + Convenience function to get the Ghidra "Program.PROGRAM_INFO" options. + + :return: the Ghidra "Program.PROGRAM_INFO" options. + """ +``` + +### pyghidra.program_loader() +```python +def program_loader() -> "ProgramLoader.Builder": + """ + Convenience function to get a Ghidra "ProgramLoader.Builder" object. + + :return: A Ghidra "ProgramLoader.Builder" object. + """ +``` + +### pyghidra.dummy_monitor() +```python +def dummy_monitor() -> "TaskMonitor": + """ + Convenience function to get the Ghidra "TaskMonitor.DUMMY" object. + + :return: The Ghidra "TaskMonitor.DUMMY" object. + """ +``` + +### pyghidra.walk_project() +```python +def walk_project( + project: "Project", + callback: Callable[["DomainFile"], None], + start: Union[str, Path] = "/", + file_filter: Callable[["DomainFile"], bool] = lambda _f: True + ): + """ + Walks the the given Ghidra project, calling the provided function when each domain file is + encountered. + + :param project: The Ghidra project to walk. + :param callback: The callback to process each domain file. + :param start: An optional starting project folder path. + :param file_filter: A filter used to limit what domain files get processed. + :raises FileNotFoundError: If the starting folder is not found in the project. + """ +``` + +### pyghidra.walk_programs() +```python +def walk_programs( + project: "Project", + callback: Callable[["DomainFile", "Program"], None], + start: Union[str, Path] = "/", + program_filter: Callable[["DomainFile", "Program"], bool] = lambda _f, _p: True + ): + """ + Walks the the given Ghidra project, calling the provided function when each program is + encountered. Non-programs in the project are skipped. + + :param project: The Ghidra project to walk. + :param callback: The callback to process each program. + :param start: An optional starting project folder path. + :param program_filter: A filter used to limit what programs get processed. + :raises FileNotFoundError: If the starting folder is not found in the project. + """ +``` + +## Example +The following example, while not very useful, showcases much of the API: +```python +import os, jpype, pyghidra +pyghidra.start() + +# Open/create a project +with pyghidra.open_project(os.environ["GHIDRA_PROJECT_DIR"], "ExampleProject", create=True) as project: + + # Walk a Ghidra release zip file, load every decompiler binary, and save them to the project + with pyghidra.open_filesystem(f"{os.environ['DOWNLOADS_DIR']}/ghidra_11.4_PUBLIC_20250620.zip") as fs: + loader = pyghidra.program_loader().project(project) + for f in fs.files(lambda f: "os/" in f.path and f.name.startswith("decompile")): + loader.source(f.getFSRL()).projectFolderPath("/" + f.parentFile.name) + with loader.load() as load_results: + load_results.save(pyghidra.dummy_monitor()) + + # Analyze the windows decompiler program + with pyghidra.program_context(project, "/win_x86_64/decompile.exe") as program: + analysis_props = pyghidra.analysis_properties(program) + with pyghidra.transaction(program): + analysis_props.setBoolean("Non-Returning Functions - Discovered", False) + pyghidra.analyze(program) + program.save("Analyzed", pyghidra.dummy_monitor()) + + # Walk the project and set a propery in each decompiler program + def set_property(domain_file, program): + with pyghidra.transaction(program): + program_info = pyghidra.program_info(program) + program_info.setString("PyGhidra Property", "Set by PyGhidra!") + program.save("Setting property", pyghidra.dummy_monitor()) + pyghidra.walk_programs(project, set_property, program_filter=lambda f, p: p.name.startswith("decompile")) + + # Load some bytes as a new program + ByteArrayCls = jpype.JArray(jpype.JByte) + my_bytes = ByteArrayCls(b"\xaa\xbb\xcc\xdd\xee\xff") + loader = pyghidra.program_loader().project(project).source(my_bytes).name("my_bytes") + loader.loaders("BinaryLoader").language("DATA:LE:64:default") + with loader.load() as load_results: + load_results.save(pyghidra.dummy_monitor()) + + # Run a GhidraScript + pyghidra.ghidra_script(f"{os.environ['GHIDRA_SCRIPTS_DIR']}/HelloWorldScript.java", project) +``` + +## Legacy API + ### pyghidra.open_program() To have PyGhidra setup a binary file for you, use the `open_program()` function. This will setup a Ghidra project and import the given binary file as a program for you. @@ -325,6 +546,10 @@ import pdb # imports Python's pdb import pdb_ # imports Ghidra's pdb ``` ## Change History +__3.0.0:__ +* Introduced many new functions to the PyGhidra API. PyGhidra 3.0.0 requires Ghidra 12.0 or later + to run. + __2.2.1:__ * PyGhidra now launches with the current working directory removed from `sys.path` to prevent the potential for importing invalid modules from random `ghidra/` or `java/` directories that may diff --git a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/__init__.py b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/__init__.py index acff535673..713e00f538 100644 --- a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/__init__.py +++ b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/__init__.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. ## -__version__ = "2.2.1" +__version__ = "3.0.0" # stub for documentation and typing # this is mostly to hide the function parameter @@ -55,6 +55,9 @@ debug_callback = _debug_callback # Expose API from pyghidra.core import run_script, start, started, open_program +from pyghidra.api import open_project, open_filesystem, consume_program, program_context, analyze +from pyghidra.api import ghidra_script, transaction, analysis_properties, program_info +from pyghidra.api import program_loader, dummy_monitor, walk_project, walk_programs from pyghidra.launcher import DeferredPyGhidraLauncher, GuiPyGhidraLauncher, HeadlessPyGhidraLauncher from pyghidra.script import get_current_interpreter from pyghidra.version import ApplicationInfo, ExtensionDetails @@ -63,5 +66,8 @@ from pyghidra.version import ApplicationInfo, ExtensionDetails __all__ = [ "debug_callback", "get_current_interpreter", "open_program", "run_script", "start", "started", "ApplicationInfo", "DeferredPyGhidraLauncher", "ExtensionDetails", - "GuiPyGhidraLauncher", "HeadlessPyGhidraLauncher" + "GuiPyGhidraLauncher", "HeadlessPyGhidraLauncher", "open_project", "open_filesystem", + "consume_program", "program_conext", "analyze", "ghidra_script", "transaction", + "analysis_properties", "program_info", "program_loader", "dummy_monitor", "walk_project", + "walk_programs" ] diff --git a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/api.py b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/api.py new file mode 100644 index 0000000000..7bcd4e3798 --- /dev/null +++ b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/api.py @@ -0,0 +1,313 @@ +## ### +# IP: GHIDRA +# +# 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 +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# 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 contextlib +from typing import Union, TYPE_CHECKING, Tuple, Callable, Any + +from pyghidra.converters import * # pylint: disable=wildcard-import, unused-wildcard-import + +if TYPE_CHECKING: + from ghidra.program.model.listing import Program + from ghidra.program.util import GhidraProgramUtilities + from ghidra.framework.model import Project, DomainFile + from ghidra.framework.options import Options + from ghidra.formats.gfilesystem import GFileSystem + from ghidra.formats.gfilesystem import FSUtilities + from ghidra.util.task import TaskMonitor + from ghidra.app.script import GhidraScript + from ghidra.app.util.importer import ProgramLoader + from generic.jar import ResourceFile + from java.lang import Object # type:ignore @UnresolvedImport + +def open_project( + path: Union[str, Path], + name: str, + create: bool = False +) -> "Project": # type: ignore + """ + Opens the Ghidra project at the given location, optionally creating it if it doesn't exist. + + :param path: Path of Ghidra project parent directory. + :param name: Name of Ghidra project to open/create. + :param create: Whether to create the project if it doesn't exist + :return: A Ghidra "Project" object. + :raises FileNotFoundError: If the project to open was not found and it shouldn't be created. + """ + from ghidra.framework.model import ProjectLocator + from ghidra.pyghidra import PyGhidraProjectManager + + projectLocator = ProjectLocator(path, name); + projectManager = PyGhidraProjectManager() + if projectLocator.exists(): + return projectManager.openProject(projectLocator, False, True); + elif create: + return projectManager.createProject(projectLocator, None, True) + raise FileNotFoundError(f'Project "{name}" not found at "{path}"!') + +def open_filesystem( + path: Union[str, Path] + ) -> "GFileSystem": + """ + Opens a filesystem in Ghidra. + + :param path: Path of filesystem to open in Ghidra. + :return: A Ghidra "GFileSystem" object. + :raises ValueError: If the filesystem to open is not supported by Ghidra. + """ + from java.io import File # type:ignore @UnresolvedImport + from ghidra.formats.gfilesystem import FileSystemService + + service = FileSystemService.getInstance() + fsrl = service.getLocalFS().getLocalFSRL(File(path)) + fs = service.openFileSystemContainer(fsrl, dummy_monitor()) + if fs is None: + raise ValueError(f'"{fsrl}" is not a supported GFileSystem!') + return fs + +def consume_program( + project: "Project", + path: Union[str, Path], + consumer: Any = None + ) -> Tuple["Program", "Object"]: + """ + Gets the Ghidra program from the given project with the given project path. The returned program + must be manually released when it is no longer needed. + + :param project: The Ghidra project that has the program. + :param path: The project path of the program (should start with "/") + :param consumer: An optional reference to the Java object "consuming" the returned program, used + to ensure the underlying DomainObject is only closed when every consumer is done with it. If + a consumer is not provided, one will be generated by this function. + :return: A 2-element tuple containing the program and a consumer object that must be used to + release the program when finished with it (i.e., program.release(consumer). If a consumer + object was provided, the same consumer object is returned. Otherwise, a new consumer object + is created and returned. + :raises FileNotFoundError: If the path does not exist in the project. + :raises TypeError: If the path in the project exists but is not a Program. + """ + from ghidra.program.model.listing import Program + from java.lang import Object # type:ignore @UnresolvedImport + if consumer is None: + consumer = Object() + project_data = project.getProjectData() + df = project_data.getFile(path) + if df is None: + raise FileNotFoundError(f'"{path}" does not exist in the Project') + dobj = df.getDomainObject(consumer, True, False, dummy_monitor()) + program_cls = Program.class_ + if not program_cls.isAssignableFrom(dobj.getClass()): + dobj.release(consumer) + raise TypeError(f'"{path}" exists but is not a Program') + return dobj, consumer + +@contextlib.contextmanager +def program_context( + project: "Project", + path: Union[str, Path], + ) -> "Program": + """ + Gets the Ghidra program from the given project with the given project path. The returned + program's resource cleanup is performed by a context manager. + + :param project: The Ghidra project that has the program. + :param path: The project path of the program (should start with "/"). + :return: The Ghidra program. + :raises FileNotFoundError: If the path does not exist in the project. + :raises TypeError: If the path in the project exists but is not a Program. + """ + program, consumer = consume_program(project, path) + try: + yield program + finally: + program.release(consumer) + +def analyze(program: "Program"): + """ + Analyzes the given program. + + :param program: The Ghidra program to analyze. + """ + from ghidra.app.script import GhidraScriptUtil + from ghidra.program.flatapi import FlatProgramAPI + from ghidra.program.util import GhidraProgramUtilities + with transaction(program, "Analyze"): + GhidraScriptUtil.acquireBundleHostReference() + try: + FlatProgramAPI(program).analyzeAll(program) + GhidraProgramUtilities.markProgramAnalyzed(program) + finally: + GhidraScriptUtil.releaseBundleHostReference() + +def ghidra_script( + path: Union[str, Path], + project: "Project", + program: "Program" = None, + echo_stdout = True, + echo_stderr = True + ) -> Tuple[str, str]: + """ + Runs any type of GhidraScript (Java, PyGhidra, Jython, etc). + + :param path: The GhidraScript's path. + :param project: The Ghidra project to run the GhidraScript in. + :param program: An optional Ghidra program that the GhidraScript will see as its "currentProgram". + :param echo_stdout: Whether or not to echo the GhidraScript's standard output. + :param echo_stderr: Whether or not to echo the GhidraScript's standard error. + :return: A 2 element tuple consisting of the GhidraScript's standard output and standard error. + """ + from generic.jar import ResourceFile + from ghidra.app.script import GhidraScriptUtil, GhidraState, ScriptControls + from java.io import File, PrintWriter, StringWriter # type:ignore @UnresolvedImport + from java.lang import System # type:ignore @UnresolvedImport + + GhidraScriptUtil.acquireBundleHostReference() + try: + source_file = ResourceFile(File(path)) + if not source_file.exists(): + raise TypeError(f'"{str(source_file)}" was not found') + provider = GhidraScriptUtil.getProvider(source_file) + if provider is None: + raise TypeError(f'"{path}" is not a supported GhidraScript') + script = provider.getScriptInstance(source_file, PrintWriter(System.out)) + if script is None: + raise TypeError(f'"{str(source_file)}" was not found') + state = GhidraState(None, project, program, None, None, None) + stdout_string_writer = StringWriter() + stderr_string_writer = StringWriter() + controls = ScriptControls( + PrintWriter(stdout_string_writer, True), + PrintWriter(stderr_string_writer, True), + dummy_monitor() + ) + script.execute(state, controls) + stdout_str = str(stdout_string_writer) + stderr_str = str(stderr_string_writer) + if echo_stdout: + sys.stdout.write(stdout_str) + sys.stdout.flush() + if echo_stderr: + sys.stderr.write(stderr_str) + sys.stderr.flush() + return stdout_str, stderr_str + finally: + GhidraScriptUtil.releaseBundleHostReference() + +@contextlib.contextmanager +def transaction( + program: "Program", + description: str = "Unnamed Transaction" + ): + """ + Creates a context for running a Ghidra transaction. + + :param program: The Ghidra program that will be affected. + :param description: The transaction description + :return: The transaction ID. + """ + transaction_id = program.startTransaction(description) + success = True + try: + yield transaction_id + except: + success = False + finally: + program.endTransaction(transaction_id, success) + +def analysis_properties(program: "Program") -> "Options": + """ + Convenience function to get the Ghidra "Program.ANALYSIS_PROPERTIES" options. + + :return: the Ghidra "Program.ANALYSIS_PROPERTIES" options. + """ + from ghidra.program.model.listing import Program + return program.getOptions(Program.ANALYSIS_PROPERTIES) + +def program_info(program: "Program") -> "Options": + """ + Convenience function to get the Ghidra "Program.PROGRAM_INFO" options. + + :return: the Ghidra "Program.PROGRAM_INFO" options. + """ + from ghidra.program.model.listing import Program + return program.getOptions(Program.PROGRAM_INFO) + +def program_loader() -> "ProgramLoader.Builder": + """ + Convenience function to get a Ghidra "ProgramLoader.Builder" object. + + :return: A Ghidra "ProgramLoader.Builder" object. + """ + from ghidra.app.util.importer import ProgramLoader + return ProgramLoader.builder() + +def dummy_monitor() -> "TaskMonitor": + """ + Convenience function to get the Ghidra "TaskMonitor.DUMMY" object. + + :return: The Ghidra "TaskMonitor.DUMMY" object. + """ + from ghidra.util.task import TaskMonitor + return TaskMonitor.DUMMY + +def walk_project( + project: "Project", + callback: Callable[["DomainFile"], None], + start: Union[str, Path] = "/", + file_filter: Callable[["DomainFile"], bool] = lambda _f: True + ): + """ + Walks the the given Ghidra project, calling the provided function when each domain file is + encountered. + + :param project: The Ghidra project to walk. + :param callback: The callback to process each domain file. + :param start: An optional starting project folder path. + :param file_filter: A filter used to limit what domain files get processed. + :raises FileNotFoundError: If the starting folder is not found in the project. + """ + from ghidra.framework.model import ProjectDataUtils + start_folder = project.projectData.getFolder(start) + if start_folder is None: + raise FileNotFoundError(f'Starting folder "{start}" does not exist in the Project') + for file in ProjectDataUtils.DomainFileIterator(start_folder): + if file_filter(file): + callback(file) + +def walk_programs( + project: "Project", + callback: Callable[["DomainFile", "Program"], None], + start: Union[str, Path] = "/", + program_filter: Callable[["DomainFile", "Program"], bool] = lambda _f, _p: True + ): + """ + Walks the the given Ghidra project, calling the provided function when each program is + encountered. Non-programs in the project are skipped. + + :param project: The Ghidra project to walk. + :param callback: The callback to process each program. + :param start: An optional starting project folder path. + :param program_filter: A filter used to limit what programs get processed. + :raises FileNotFoundError: If the starting folder is not found in the project. + """ + def process(file: "DomainFile"): + try: + with program_context(project, file.getPathname()) as program: + if program_filter(file, program): + callback(file, program) + except TypeError: + pass # skip over non-programs + + walk_project(project, process, start=start) diff --git a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/core.py b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/core.py index e813fe41a2..3c426dd02b 100644 --- a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/core.py +++ b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/core.py @@ -176,7 +176,7 @@ def _setup_script(project: "GhidraProject", program: "Program"): location = ProgramLocation(program, mem.getMinAddress()) state = GhidraState(None, project, program, location, None, None) script = PyGhidraScript() - script.set(state, TaskMonitor.DUMMY, PrintWriter(System.out)) + script.set(state, TaskMonitor.DUMMY, PrintWriter(System.out), PrintWriter(System.err)) return script @@ -187,10 +187,7 @@ def _analyze_program(flat_api, program): GhidraScriptUtil.acquireBundleHostReference() try: flat_api.analyzeAll(program) - if hasattr(GhidraProgramUtilities, "markProgramAnalyzed"): - GhidraProgramUtilities.markProgramAnalyzed(program) - else: - GhidraProgramUtilities.setAnalyzedFlag(program, True) # @UndefinedVariable + GhidraProgramUtilities.markProgramAnalyzed(program) finally: GhidraScriptUtil.releaseBundleHostReference() diff --git a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/internal/plugin/plugin.py b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/internal/plugin/plugin.py index 44bb9e8207..913b6e143e 100644 --- a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/internal/plugin/plugin.py +++ b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/internal/plugin/plugin.py @@ -23,9 +23,11 @@ import threading import types from code import InteractiveConsole +from ghidra.app.script import ScriptControls from ghidra.framework import Application from ghidra.pyghidra import PyGhidraScriptProvider, PyGhidraPlugin from ghidra.pyghidra.interpreter import PyGhidraConsole +from ghidra.util.task import TaskMonitor from java.io import BufferedReader, InputStreamReader # type:ignore @UnresolvedImport from java.lang import String # type:ignore @UnresolvedImport from java.lang import Thread as JThread # type:ignore @UnresolvedImport @@ -160,11 +162,12 @@ class PyConsole(InteractiveConsole): self._out = console.getOutWriter() self._err = console.getErrWriter() self._writer = self._out + self._error_writer = self._err self._thread = None self._interact_thread = None self._script = self.locals._script state = self._script.getState() - self._script.set(state, self._out) + self._script.set(state, ScriptControls(console, TaskMonitor.DUMMY)) self._state = ConsoleState.RESET self._completer = PythonCodeCompleter(self) diff --git a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/script.py b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/script.py index 6609295689..70cadcd7c4 100644 --- a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/script.py +++ b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/script.py @@ -205,12 +205,6 @@ class PyGhidraScript(dict): def get_static_view(self): return _StaticMap(self) - def set(self, state, monitor, writer): - """ - see GhidraScript.set - """ - self._script.set(state, monitor, writer) - def run(self, script_path: str = None, script_args: List[str] = None): """ Run this GhidraScript diff --git a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/version.py b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/version.py index fbfd44342b..0f70b751cf 100644 --- a/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/version.py +++ b/Ghidra/Features/PyGhidra/src/main/py/src/pyghidra/version.py @@ -19,7 +19,7 @@ from pathlib import Path import re -MINIMUM_GHIDRA_VERSION = "11.2" +MINIMUM_GHIDRA_VERSION = "12.0" _APPLICATION_PATTERN = re.compile(r"^application\.(\S+?)=(.*)$")