-
Notifications
You must be signed in to change notification settings - Fork 710
Enhance ghidra backend with existing project feature #3087
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
6669204
b388cb1
1525906
957b854
45fbf0b
9338532
c58bb2b
9f7ee5a
58f211f
c4b5ab5
b8622c9
ef70aa8
4ba5dd7
5ee451b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,9 +32,11 @@ | |
| from capa.engine import MatchResults | ||
| from capa.helpers import assert_never | ||
| from capa.exceptions import ( | ||
| InvalidArgument, | ||
| UnsupportedOSError, | ||
| UnsupportedArchError, | ||
| UnsupportedFormatError, | ||
| LockedProjectDatabaseError, | ||
| ) | ||
| from capa.features.common import ( | ||
| OS_AUTO, | ||
|
|
@@ -434,24 +436,52 @@ def get_extractor( | |
| if not capa.ghidra.helpers.is_supported_ghidra_version(): | ||
| raise RuntimeError("unsupported Ghidra version") | ||
|
|
||
| import tempfile | ||
| project_path = input_path | ||
| tmpdir = None | ||
| if input_path.suffix.lower() == ".gpr": | ||
| try: | ||
| project_cm = pyghidra.open_project( | ||
| str(project_path.parent.resolve()), project_path.stem, create=False | ||
| ) | ||
| except Exception as e: | ||
| err = str(e) | ||
| if "LockException" in err or "Database is locked" in err: | ||
| msg = ( | ||
| f"Ghidra project database is locked. Ensure all programs accessing " | ||
| f"{project_path.name} are closed before proceeding." | ||
| ) | ||
| raise LockedProjectDatabaseError(msg) from e | ||
| raise | ||
| else: | ||
| import tempfile | ||
|
|
||
| tmpdir = tempfile.TemporaryDirectory() | ||
| tmpdir = tempfile.TemporaryDirectory() | ||
| project_cm = pyghidra.open_project(tmpdir.name, "CapaProject", create=True) | ||
|
|
||
| project_cm = pyghidra.open_project(tmpdir.name, "CapaProject", create=True) | ||
| project = project_cm.__enter__() | ||
| program, consumer = None, None | ||
| try: | ||
|
mike-hunhoff marked this conversation as resolved.
|
||
| from ghidra.util.task import TaskMonitor | ||
|
|
||
| monitor = TaskMonitor.DUMMY | ||
|
|
||
| # Import file | ||
| loader = pyghidra.program_loader().project(project).source(str(input_path)).name(input_path.name) | ||
| with loader.load() as load_results: | ||
| load_results.save(monitor) | ||
| if input_path.suffix.lower() == ".gpr": | ||
| try: | ||
| selected_program = capa.ghidra.helpers.select_project_file(project) | ||
| except ValueError as e: | ||
| raise InvalidArgument(str(e)) from e | ||
| program_path = selected_program.getPathname() | ||
| logger.debug("ghidra: selected program path: %s", program_path) | ||
| else: | ||
| # Import file | ||
| loader = pyghidra.program_loader().project(project).source(str(input_path)).name(input_path.name) | ||
| with loader.load() as load_results: | ||
| load_results.save(monitor) | ||
|
|
||
| program_path = "/" + input_path.name | ||
|
|
||
| # Open program | ||
| program, consumer = pyghidra.consume_program(project, "/" + input_path.name) | ||
| program, consumer = pyghidra.consume_program(project, program_path) | ||
|
|
||
| # Analyze | ||
| pyghidra.analyze(program, monitor) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed that analyzing an existing |
||
|
|
@@ -478,8 +508,18 @@ def __exit__(self, exc_type, exc_val, exc_tb): | |
| cm = GhidraContextWrapper(project_cm, program, consumer) | ||
|
|
||
| except Exception: | ||
| project_cm.__exit__(None, None, None) | ||
| tmpdir.cleanup() | ||
| if program is not None: | ||
| try: | ||
| program.release(consumer) | ||
| except Exception: | ||
| logger.warning("failed to release program handle", exc_info=True) | ||
| try: | ||
| project_cm.__exit__(None, None, None) | ||
| except Exception: | ||
| logger.warning("failed to close Ghidra project", exc_info=True) | ||
| if tmpdir: | ||
| with contextlib.suppress(Exception): | ||
| tmpdir.cleanup() | ||
| raise | ||
|
mike-hunhoff marked this conversation as resolved.
mike-hunhoff marked this conversation as resolved.
|
||
|
|
||
| import capa.features.extractors.ghidra.extractor | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -72,6 +72,7 @@ | |
| UnsupportedOSError, | ||
| UnsupportedArchError, | ||
| UnsupportedFormatError, | ||
| LockedProjectDatabaseError, | ||
| ) | ||
| from capa.features.common import ( | ||
| OS_AUTO, | ||
|
|
@@ -93,6 +94,7 @@ | |
| DYNAMIC_FORMATS, | ||
| FORMAT_BINJA_DB, | ||
| FORMAT_BINEXPORT2, | ||
| FORMAT_GHIDRA_PROJECT, | ||
| ) | ||
| from capa.capabilities.common import ( | ||
| Capabilities, | ||
|
|
@@ -130,6 +132,7 @@ | |
| E_UNSUPPORTED_GHIDRA_EXECUTION_MODE = 24 | ||
| E_INVALID_INPUT_FORMAT = 25 | ||
| E_INVALID_FEATURE_EXTRACTOR = 26 | ||
| E_GHIDRA_DB_LOCKED = 27 | ||
|
|
||
| logger = logging.getLogger("capa") | ||
|
|
||
|
|
@@ -279,6 +282,7 @@ def install_common_args(parser, wanted=None): | |
| (FORMAT_FREEZE, "features previously frozen by capa"), | ||
| (FORMAT_BINEXPORT2, "BinExport2"), | ||
| (FORMAT_BINJA_DB, "Binary Ninja Database"), | ||
| (FORMAT_GHIDRA_PROJECT, "Ghidra project"), | ||
| ] | ||
| format_help = ", ".join([f"{f[0]}: {f[1]}" for f in formats]) | ||
|
|
||
|
|
@@ -580,6 +584,9 @@ def get_backend_from_cli(args, input_format: str) -> str: | |
| if args.backend != BACKEND_AUTO: | ||
| return args.backend | ||
|
|
||
| if input_format == FORMAT_GHIDRA_PROJECT: | ||
| return BACKEND_GHIDRA | ||
|
|
||
| if input_format == FORMAT_CAPE: | ||
| return BACKEND_CAPE | ||
|
|
||
|
|
@@ -602,7 +609,7 @@ def get_backend_from_cli(args, input_format: str) -> str: | |
| return BACKEND_VIV | ||
|
|
||
|
|
||
| def get_sample_path_from_cli(args, backend: str) -> Optional[Path]: | ||
| def get_sample_path_from_cli(args, input_format, backend) -> Optional[Path]: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we remove
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or is there something else that can be done to avoid changing the arguments? |
||
| """ | ||
| Determine the path to the underlying sample, if it exists. | ||
|
|
||
|
|
@@ -611,13 +618,16 @@ def get_sample_path_from_cli(args, backend: str) -> Optional[Path]: | |
|
|
||
| args: | ||
| args: The parsed command line arguments from `install_common_args`. | ||
| input_format: The file format of the input file. | ||
| backend: The backend that will handle the input file. | ||
|
|
||
| raises: | ||
| ShouldExitError: if the program is invoked incorrectly and should exit. | ||
| """ | ||
| if backend in (BACKEND_CAPE, BACKEND_DRAKVUF, BACKEND_VMRAY): | ||
| return None | ||
| elif input_format == FORMAT_GHIDRA_PROJECT: | ||
| return None | ||
| elif backend == BACKEND_BINEXPORT2: | ||
| import capa.features.extractors.binexport2 | ||
|
|
||
|
|
@@ -629,14 +639,15 @@ def get_sample_path_from_cli(args, backend: str) -> Optional[Path]: | |
| return args.input_file | ||
|
|
||
|
|
||
| def get_os_from_cli(args, backend) -> str: | ||
| def get_os_from_cli(args, input_format, backend) -> str: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we remove
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or is there something else that can be done to avoid changing the arguments? |
||
| """ | ||
| Determine the OS for the given sample. | ||
| Respects an override provided by the user, otherwise, use heuristics and | ||
| algorithms to detect the OS. | ||
|
|
||
| args: | ||
| args: The parsed command line arguments from `install_common_args`. | ||
| input_format: The file format of the input file. | ||
| backend: The backend that will handle the input file. | ||
|
|
||
| raises: | ||
|
|
@@ -645,7 +656,7 @@ def get_os_from_cli(args, backend) -> str: | |
| if args.os: | ||
| return args.os | ||
|
|
||
| sample_path = get_sample_path_from_cli(args, backend) | ||
| sample_path = get_sample_path_from_cli(args, input_format, backend) | ||
| if sample_path is None: | ||
| return "unknown" | ||
| return capa.loader.get_os(sample_path) | ||
|
|
@@ -867,8 +878,8 @@ def get_extractor_from_cli(args, input_format: str, backend: str) -> FeatureExtr | |
| None, | ||
| ) | ||
|
|
||
| os_ = get_os_from_cli(args, backend) | ||
| sample_path = get_sample_path_from_cli(args, backend) | ||
| os_ = get_os_from_cli(args, input_format, backend) | ||
| sample_path = get_sample_path_from_cli(args, input_format, backend) | ||
| extractor_filters = get_extractor_filters_from_cli(args, input_format) | ||
|
|
||
| logger.debug("format: %s", input_format) | ||
|
|
@@ -886,6 +897,9 @@ def get_extractor_from_cli(args, input_format: str, backend: str) -> FeatureExtr | |
| sample_path=sample_path, | ||
| ) | ||
| return apply_extractor_filters(extractor, extractor_filters) | ||
| except InvalidArgument as e: | ||
| logger.error("%s", str(e)) | ||
| raise ShouldExitError(E_INVALID_INPUT_FORMAT) from e | ||
| except UnsupportedFormatError as e: | ||
| if input_format == FORMAT_CAPE: | ||
| log_unsupported_cape_report_error(str(e)) | ||
|
|
@@ -905,6 +919,9 @@ def get_extractor_from_cli(args, input_format: str, backend: str) -> FeatureExtr | |
| except capa.loader.CorruptFile as e: | ||
| logger.error("Input file '%s' is not a valid file: %s", args.input_file, str(e)) | ||
| raise ShouldExitError(E_CORRUPT_FILE) from e | ||
| except LockedProjectDatabaseError as e: | ||
| logger.error("%s", str(e)) | ||
| raise ShouldExitError(E_GHIDRA_DB_LOCKED) from e | ||
|
|
||
|
|
||
| def get_extractor_filters_from_cli(args, input_format) -> FilterConfig: | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's remove these for now. We can add in a more robust test for this specific feature if it becomes a problem in the future. |
Uh oh!
There was an error while loading. Please reload this page.