-
-
Notifications
You must be signed in to change notification settings - Fork 578
Machofile analyzer #3268
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
Machofile analyzer #3268
Changes from all commits
6f01a28
a185d08
40a9ebc
b4cd43b
3e6bc3d
a66781c
946e43a
933989b
3da2d8e
8c7f00f
520cf62
94a3c65
395a2a3
964400c
6bce4ff
e1b3822
11d1bab
3907083
c6526c5
5a9b7db
6185f41
f65ca9f
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 |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| # This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl | ||
| # See the file 'LICENSE' for copying permission. | ||
|
|
||
| import logging | ||
| from typing import Any, Dict | ||
|
|
||
| import machofile | ||
|
|
||
| from api_app.analyzers_manager.classes import FileAnalyzer | ||
| from api_app.analyzers_manager.exceptions import AnalyzerRunException | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def _safe_decode(value: Any) -> str: | ||
| """Helper to safely decode bytes to string.""" | ||
| if isinstance(value, bytes): | ||
| return value.decode("utf-8", "ignore") | ||
| return str(value) | ||
|
|
||
|
|
||
| class MachoInfo(FileAnalyzer): | ||
| """ | ||
| Analyzer for Mach-O binary files (macOS/iOS executables). | ||
| Uses the machofile library to parse and extract information. | ||
| """ | ||
|
|
||
| @classmethod | ||
| def update(cls) -> bool: | ||
| return False | ||
|
|
||
| def _parse_macho(self): | ||
| """Attempts to parse the file as Single or Universal Mach-O.""" | ||
| try: | ||
| macho = machofile.MachO(self.filepath) | ||
| try: | ||
| macho.parse() | ||
| except AttributeError: | ||
| pass | ||
| return macho | ||
| except Exception as e: | ||
| try: | ||
| macho = machofile.UniversalMachO(self.filepath) | ||
| try: | ||
| macho.parse() | ||
| except AttributeError: | ||
|
||
| pass | ||
| return macho | ||
| except Exception as universal_error: | ||
| raise AnalyzerRunException( | ||
| f"Failed to parse as both single and universal binary. " | ||
| f"Single: {e}, Universal: {universal_error}" | ||
| ) | ||
|
|
||
| def _extract_basic_info(self, macho, results: Dict[str, Any]): | ||
| """Extract basic information like headers and hashes.""" | ||
| results["general_info"] = macho.get_general_info() | ||
| results["header"] = macho.get_macho_header() | ||
| results["hashes"] = macho.get_similarity_hashes() | ||
| results["code_signature"] = macho.code_signature_info | ||
| try: | ||
| results["architectures"] = macho.get_architectures() | ||
| except AttributeError: | ||
| results["architectures"] = [] | ||
| results["uuid"] = macho.uuid | ||
| results["entrypoint"] = macho.entry_point | ||
| results["version_info"] = macho.version_info | ||
|
|
||
| def _extract_lists(self, macho, results: Dict[str, Any]): | ||
| """Extract list-based structures like load commands and segments.""" | ||
| is_universal = isinstance(macho, machofile.UniversalMachO) | ||
|
|
||
| def get_macho_lists(m): | ||
| return { | ||
| "load_commands": [str(lc) for lc in m.load_commands], | ||
| "segments": [str(s) for s in m.segments], | ||
| "dylib_names": [_safe_decode(d) for d in m.dylib_names], | ||
| } | ||
|
|
||
| if is_universal: | ||
| for k in ["load_commands", "segments", "dylib_names"]: | ||
| results[k] = {} | ||
| for arch, m in macho.architectures.items(): | ||
| sub_lists = get_macho_lists(m) | ||
| for k, v in sub_lists.items(): | ||
| results[k][arch] = v | ||
| else: | ||
| results.update(get_macho_lists(macho)) | ||
|
|
||
| def _extract_symbols(self, macho, results: Dict[str, Any]): | ||
| """Extract imported and exported symbols.""" | ||
| results["imports"] = macho.get_imported_functions() | ||
| results["exports"] = macho.get_exported_symbols() | ||
|
|
||
| def run(self) -> Dict[str, Any]: | ||
| results: Dict[str, Any] = {} | ||
|
|
||
| try: | ||
| macho = self._parse_macho() | ||
| self._extract_basic_info(macho, results) | ||
| self._extract_lists(macho, results) | ||
| self._extract_symbols(macho, results) | ||
|
|
||
| except Exception as e: | ||
| error_msg = ( | ||
| f"job_id:{self.job_id} analyzer:{self.analyzer_name} " | ||
| f"md5:{self.md5} filename:{self.filename} " | ||
| f"MachoInfo parsing error: {e}" | ||
| ) | ||
| logger.error(error_msg, exc_info=True) | ||
| self.report.errors.append(error_msg) | ||
| raise AnalyzerRunException(error_msg) | ||
|
|
||
| return results | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,130 @@ | ||||||
| # This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl | ||||||
| # See the file 'LICENSE' for copying permission. | ||||||
|
|
||||||
| from django.db import migrations | ||||||
| from django.db.models.fields.related_descriptors import ( | ||||||
| ForwardManyToOneDescriptor, | ||||||
IshaanXCoder marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| ForwardOneToOneDescriptor, | ||||||
| ManyToManyDescriptor, | ||||||
| ReverseManyToOneDescriptor, | ||||||
| ReverseOneToOneDescriptor, | ||||||
| ) | ||||||
|
|
||||||
| plugin = { | ||||||
| "name": "MachoInfo", | ||||||
| "python_module": { | ||||||
| "module": "macho_info.MachoInfo", | ||||||
| "base_path": "api_app.analyzers_manager.file_analyzers", | ||||||
| }, | ||||||
| "description": "Parse Mach-O binary files (macOS/iOS executables) using [machofile](https://github.com/pstirparo/machofile) library. Extracts headers, segments, dylibs, imports, exports, hashes and code signatures.", | ||||||
| "disabled": False, | ||||||
| "soft_time_limit": 60, | ||||||
| "routing_key": "local", | ||||||
| "health_check_status": True, | ||||||
| "type": "file", | ||||||
| "docker_based": False, | ||||||
| "maximum_tlp": "RED", | ||||||
| "observable_supported": [], | ||||||
| "supported_filetypes": [ | ||||||
| "application/x-mach-binary", | ||||||
mlodic marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| "application/mac-binary", | ||||||
| "application/x-binary", | ||||||
| "application/x-executable", | ||||||
|
Comment on lines
+31
to
+32
|
||||||
| "application/x-binary", | |
| "application/x-executable", |
Copilot
AI
Feb 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The supported_filetypes list includes very broad MIME types like "application/x-executable" and "application/x-binary", which are commonly used for non–Mach-O binaries (e.g., ELF). This will cause MachoInfo to run (and likely fail) on unrelated files; restrict supported_filetypes to Mach-O-specific MIME types (and/or add explicit signature checks + early exit) to avoid noisy failures and unnecessary work.
Copilot
AI
Feb 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
supported_filetypes includes generic MIME types like application/x-binary and application/x-executable. Since this analyzer is Mach-O specific (macOS/iOS), this configuration will make MachoInfo run on many non–Mach-O samples (e.g., ELF binaries commonly labeled application/x-executable), producing avoidable failures/noise in default playbooks. Consider restricting supported_filetypes to Mach-O specific MIME types (e.g., application/x-mach-binary and/or application/mac-binary) and removing the generic ones, or moving generic types to a separate opt-in config.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl | ||
| # See the file 'LICENSE' for copying permission. | ||
|
|
||
|
|
||
| from django.db import migrations | ||
|
|
||
|
|
||
| def migrate(apps, schema_editor): | ||
| playbook_config = apps.get_model("playbooks_manager", "PlaybookConfig") | ||
| AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") | ||
| pc = playbook_config.objects.get(name="FREE_TO_USE_ANALYZERS") | ||
| pc.analyzers.add(AnalyzerConfig.objects.get(name="MachoInfo").id) | ||
| pc.full_clean() | ||
| pc.save() | ||
|
|
||
|
|
||
| def reverse_migrate(apps, schema_editor): | ||
| playbook_config = apps.get_model("playbooks_manager", "PlaybookConfig") | ||
| AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") | ||
| pc = playbook_config.objects.get(name="FREE_TO_USE_ANALYZERS") | ||
IshaanXCoder marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| pc.analyzers.remove(AnalyzerConfig.objects.get(name="MachoInfo").id) | ||
| pc.full_clean() | ||
| pc.save() | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("playbooks_manager", "0062_add_cleanbrowsing_to_free_to_use"), | ||
| ("analyzers_manager", "0176_analyzer_config_macho_info"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.RunPython(migrate, reverse_migrate), | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl | ||
| # See the file 'LICENSE' for copying permission. | ||
|
|
||
|
|
||
| from django.db import migrations | ||
|
|
||
|
|
||
| def migrate(apps, schema_editor): | ||
| playbook_config = apps.get_model("playbooks_manager", "PlaybookConfig") | ||
| AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") | ||
| pc = playbook_config.objects.get(name="Sample_Static_Analysis") | ||
| pc.analyzers.add(AnalyzerConfig.objects.get(name="MachoInfo").id) | ||
| pc.full_clean() | ||
| pc.save() | ||
|
|
||
|
|
||
| def reverse_migrate(apps, schema_editor): | ||
| playbook_config = apps.get_model("playbooks_manager", "PlaybookConfig") | ||
| AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") | ||
| pc = playbook_config.objects.get(name="Sample_Static_Analysis") | ||
| pc.analyzers.remove(AnalyzerConfig.objects.get(name="MachoInfo").id) | ||
| pc.full_clean() | ||
| pc.save() | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("playbooks_manager", "0063_add_machofile_to_free_to_use"), | ||
| ("analyzers_manager", "0176_analyzer_config_macho_info"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.RunPython(migrate, reverse_migrate), | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.