#!/usr/bin/env python3 """macOS malware analysis script. Performs Mach-O binary analysis, persistence mechanism enumeration, code signing verification, Gatekeeper bypass detection, credential theft investigation, sandbox escape analysis, and known family identification for macOS malware samples. """ from __future__ import annotations import argparse import hashlib import json import math import os import plistlib import re import subprocess import sys from datetime import datetime from pathlib import Path def compute_file_hashes(filepath) -> dict: """Compute MD5, SHA1, and SHA256 hashes of a file. Args: filepath: Path to the file to hash. Returns: Dictionary with md5, sha1, and sha256 hash strings. """ hashes = {"md5": hashlib.md5(), "sha1": hashlib.sha1(), "sha256": hashlib.sha256()} with open(filepath, "rb") as f: while chunk := f.read(8192): for h in hashes.values(): h.update(chunk) return {k: v.hexdigest() for k, v in hashes.items()} def calculate_entropy(data) -> dict: """Calculate Shannon entropy of a byte sequence. Args: data: Bytes object to analyze. Returns: Float representing the entropy value (0.0 - 8.0). """ if not data: return 0.0 freq = [0] * 256 for byte in data: freq[byte] += 1 length = len(data) return -sum( (count / length) * math.log2(count / length) for count in freq if count > 0 ) def analyze_macho_header(filepath) -> dict: """Parse and analyze a Mach-O binary header and load commands. Examines the Mach-O header fields, load commands, linked libraries, Objective-C class information, and flags suspicious characteristics like encryption, unusual entitlements, or signs of tampering. Args: filepath: Path to the Mach-O binary. Returns: Dictionary with Mach-O analysis results. """ results = { "architecture": "unknown", "universal_binary": False, "architectures": [], "sdk_version": "unknown", "min_os_version": "unknown", "load_commands": 0, "linked_libraries": [], "rpath_entries": [], "has_objc_classes": False, "class_names": [], "encrypted": False, "stripped": True, "compiler": "unknown", "entropy": 0.0, } try: with open(filepath, "rb") as f: magic = f.read(4) # Check for Mach-O magic numbers macho_magics = { b"\xfe\xed\xfa\xce": "Mach-O 32-bit", b"\xfe\xed\xfa\xcf": "Mach-O 64-bit", b"\xce\xfa\xed\xfe": "Mach-O 32-bit (reverse)", b"\xcf\xfa\xed\xfe": "Mach-O 64-bit (reverse)", b"\xca\xfe\xba\xbe": "Universal/Fat binary", b"\xbe\xba\xfe\xca": "Universal/Fat binary (reverse)", } if magic not in macho_magics: results["error"] = "Not a valid Mach-O binary" return results results["architecture"] = macho_magics[magic] # Check for universal binary lipo_out = subprocess.run( ["lipo", "-info", filepath], capture_output=True, text=True, timeout=10 ) if lipo_out.returncode == 0: output = lipo_out.stdout.strip() if "Non-fat" not in output and ("Architectures" in output or "are:" in output): results["universal_binary"] = True arch_part = output.split(":")[-1].strip() results["architectures"] = arch_part.split() elif "Non-fat" in output: arch = output.split(":")[-1].strip() results["architectures"] = [arch] # Use otool for header analysis header_out = subprocess.run( ["otool", "-h", filepath], capture_output=True, text=True, timeout=30 ) if header_out.returncode == 0: for line in header_out.stdout.splitlines(): if "ARM64" in line or "arm64" in line.lower(): results["architecture"] = "arm64" elif "X86_64" in line: results["architecture"] = "x86_64" # Get load commands load_cmd_out = subprocess.run( ["otool", "-l", filepath], capture_output=True, text=True, timeout=30 ) if load_cmd_out.returncode == 0: lc_text = load_cmd_out.stdout results["load_commands"] = lc_text.count("cmd LC_") # Check for encryption if "LC_ENCRYPTION_INFO" in lc_text: crypt_match = re.search(r"cryptid\s+(\d+)", lc_text) if crypt_match and int(crypt_match.group(1)) > 0: results["encrypted"] = True # Extract minimum OS version ver_match = re.search(r"minos\s+(\d+\.\d+)", lc_text) if ver_match: results["min_os_version"] = ver_match.group(1) sdk_match = re.search(r"sdk\s+(\d+\.\d+)", lc_text) if sdk_match: results["sdk_version"] = sdk_match.group(1) # Extract RPATH entries for rpath_match in re.finditer(r"path\s+(.+?)\s+\(offset", lc_text): results["rpath_entries"].append(rpath_match.group(1)) # Get linked libraries lib_out = subprocess.run( ["otool", "-L", filepath], capture_output=True, text=True, timeout=30 ) if lib_out.returncode == 0: for line in lib_out.stdout.splitlines()[1:]: # Skip first line (filename) lib_match = re.match(r"\s+(.+?)\s+\(", line) if lib_match: results["linked_libraries"].append(lib_match.group(1)) # Check for Objective-C classes class_dump_out = subprocess.run( ["class-dump", "-H", filepath], capture_output=True, text=True, timeout=30 ) if class_dump_out.returncode == 0 and class_dump_out.stdout.strip(): results["has_objc_classes"] = True for cls_match in re.finditer(r"@interface\s+(\w+)", class_dump_out.stdout): results["class_names"].append(cls_match.group(1)) else: # Fallback: check strings for ObjC class refs strings_out = subprocess.run( ["strings", "-a", filepath], capture_output=True, text=True, timeout=30 ) if strings_out.returncode == 0: objc_classes = re.findall( r"_OBJC_CLASS_\$_(\w+)", strings_out.stdout ) if objc_classes: results["has_objc_classes"] = True results["class_names"] = sorted(set(objc_classes))[:50] # Calculate entropy with open(filepath, "rb") as f: data = f.read() results["entropy"] = round(calculate_entropy(data), 2) # Detect compiler strings_out = subprocess.run( ["strings", "-a", filepath], capture_output=True, text=True, timeout=30 ) all_strings = strings_out.stdout if "Apple clang" in all_strings or "Apple LLVM" in all_strings: match = re.search(r"Apple (?:clang|LLVM) version (\d+\.\d+)", all_strings) results["compiler"] = f"Apple clang {match.group(1)}" if match else "Apple clang" elif "rustc" in all_strings or "core::panicking" in all_strings: results["compiler"] = "Rust" elif "swift" in all_strings.lower() and "Swift" in all_strings: results["compiler"] = "Swift" elif "GCC" in all_strings: results["compiler"] = "GCC" except FileNotFoundError as e: results["error"] = f"Required tool not found: {e}" except Exception as e: results["error"] = str(e) return results def analyze_code_signing(filepath) -> dict: """Analyze the code signature, certificates, and entitlements of a Mach-O binary. Verifies signature validity, extracts certificate chain information, checks for ad-hoc signing, and identifies suspicious entitlements. Args: filepath: Path to the Mach-O binary. Returns: Dictionary with code signing analysis results. """ results = { "signed": False, "signature_valid": False, "ad_hoc": False, "authority": [], "team_identifier": "unknown", "timestamp": None, "certificate_expired": False, "notarized": False, "entitlements": [], "suspicious_entitlements": [], } dangerous_entitlements = { "com.apple.security.cs.allow-dyld-environment-variables", "com.apple.security.cs.disable-library-validation", "com.apple.security.cs.allow-unsigned-executable-memory", "get-task-allow", "com.apple.security.cs.debugger", "com.apple.private.security.no-sandbox", } try: # Check code signature sig_out = subprocess.run( ["codesign", "-dv", "--verbose=4", filepath], capture_output=True, text=True, timeout=30 ) sig_text = sig_out.stdout + sig_out.stderr if "Signature=" in sig_text: results["signed"] = True if "Signature=adhoc" in sig_text: results["ad_hoc"] = True # Extract authorities for line in sig_text.splitlines(): if line.startswith("Authority="): results["authority"].append(line.split("=", 1)[1]) elif line.startswith("TeamIdentifier="): results["team_identifier"] = line.split("=", 1)[1] elif line.startswith("Timestamp="): results["timestamp"] = line.split("=", 1)[1] # Verify signature validity verify_out = subprocess.run( ["codesign", "--verify", "--verbose", filepath], capture_output=True, text=True, timeout=30 ) verify_text = verify_out.stdout + verify_out.stderr results["signature_valid"] = verify_out.returncode == 0 if "expired" in verify_text.lower(): results["certificate_expired"] = True # Check notarization spctl_out = subprocess.run( ["spctl", "--assess", "--verbose=4", "--type", "execute", filepath], capture_output=True, text=True, timeout=30 ) spctl_text = spctl_out.stdout + spctl_out.stderr results["notarized"] = "notarized" in spctl_text.lower() and "not notarized" not in spctl_text.lower() # Extract entitlements ent_out = subprocess.run( ["codesign", "-d", "--entitlements", "-", filepath], capture_output=True, text=True, timeout=30 ) ent_text = ent_out.stdout + ent_out.stderr for ent_match in re.finditer(r"(.+?)", ent_text): ent = ent_match.group(1) results["entitlements"].append(ent) if ent in dangerous_entitlements: results["suspicious_entitlements"].append(ent) except FileNotFoundError: results["error"] = "codesign/spctl not available (requires macOS)" except Exception as e: results["error"] = str(e) return results def scan_persistence_mechanisms() -> list: """Scan for macOS persistence mechanisms. Checks LaunchAgents, LaunchDaemons, Login Items, kernel extensions, cron jobs, and other persistence vectors on the system. Returns: Dictionary with findings for each persistence category. """ findings = { "launch_agents": {"found": False, "items": []}, "launch_daemons": {"found": False, "items": []}, "login_items": {"found": False, "items": []}, "kernel_extensions": {"found": False, "items": []}, "cron": {"found": False, "entries": []}, "authorization_plugins": {"found": False, "items": []}, } # Check LaunchAgents la_dirs = [ Path("/Library/LaunchAgents"), Path.home() / "Library" / "LaunchAgents", ] for la_dir in la_dirs: if la_dir.is_dir(): for plist_file in la_dir.glob("*.plist"): try: with open(plist_file, "rb") as f: plist_data = plistlib.load(f) item = { "path": str(plist_file), "label": plist_data.get("Label", "unknown"), "program": plist_data.get("Program", plist_data.get("ProgramArguments", ["unknown"])), "run_at_load": plist_data.get("RunAtLoad", False), "keep_alive": plist_data.get("KeepAlive", False), } # Flag non-Apple items if not str(plist_file.name).startswith("com.apple."): findings["launch_agents"]["found"] = True findings["launch_agents"]["items"].append(item) except (plistlib.InvalidFileException, PermissionError, OSError): findings["launch_agents"]["items"].append({ "path": str(plist_file), "error": "Could not parse plist", }) # Check LaunchDaemons ld_dir = Path("/Library/LaunchDaemons") if ld_dir.is_dir(): for plist_file in ld_dir.glob("*.plist"): try: with open(plist_file, "rb") as f: plist_data = plistlib.load(f) if not str(plist_file.name).startswith("com.apple."): findings["launch_daemons"]["found"] = True findings["launch_daemons"]["items"].append({ "path": str(plist_file), "label": plist_data.get("Label", "unknown"), "program": plist_data.get("Program", plist_data.get("ProgramArguments", ["unknown"])), "run_at_load": plist_data.get("RunAtLoad", False), }) except (plistlib.InvalidFileException, PermissionError, OSError): pass # Check kernel extensions kext_dirs = [Path("/Library/Extensions"), Path("/System/Library/Extensions")] for kext_dir in kext_dirs: if kext_dir.is_dir(): for kext in kext_dir.glob("*.kext"): kext_name = kext.name if not kext_name.startswith("com.apple."): findings["kernel_extensions"]["found"] = True info_plist = kext / "Contents" / "Info.plist" bundle_id = "unknown" if info_plist.exists(): try: with open(info_plist, "rb") as f: data = plistlib.load(f) bundle_id = data.get("CFBundleIdentifier", "unknown") except (plistlib.InvalidFileException, OSError): pass findings["kernel_extensions"]["items"].append({ "path": str(kext), "bundle_id": bundle_id, }) # Check cron try: cron_out = subprocess.run( ["crontab", "-l"], capture_output=True, text=True, timeout=10 ) if cron_out.returncode == 0 and cron_out.stdout.strip(): for line in cron_out.stdout.strip().splitlines(): line = line.strip() if line and not line.startswith("#"): findings["cron"]["found"] = True findings["cron"]["entries"].append(line) except (subprocess.TimeoutExpired, FileNotFoundError): pass # Check Authorization Plugins auth_plugin_dir = Path("/Library/Security/SecurityAgentPlugins") if auth_plugin_dir.is_dir(): for plugin in auth_plugin_dir.iterdir(): if plugin.suffix == ".bundle": findings["authorization_plugins"]["found"] = True findings["authorization_plugins"]["items"].append({ "path": str(plugin), "name": plugin.stem, }) return findings def analyze_credential_theft(filepath) -> dict: """Detect credential theft capabilities in a macOS binary. Searches for patterns indicating Keychain access, browser credential harvesting, cryptocurrency wallet targeting, and cookie theft. Args: filepath: Path to the binary to analyze. Returns: Dictionary with credential theft capability findings. """ results = { "keychain_access": False, "browser_credentials": [], "crypto_wallets": [], "ssh_keys": False, "cookies": False, "techniques": [], } try: strings_out = subprocess.run( ["strings", "-a", "-n", "6", filepath], capture_output=True, text=True, timeout=60 ) all_strings = strings_out.stdout except (subprocess.TimeoutExpired, FileNotFoundError): return results # Keychain access keychain_patterns = [ r"SecKeychain", r"SecItem", r"kSecClass", r"kSecAttr", r"security\s+find-", r"security\s+dump-", r"login\.keychain", ] for pattern in keychain_patterns: if re.search(pattern, all_strings, re.IGNORECASE): results["keychain_access"] = True results["techniques"].append(f"Keychain API: {pattern}") break # Browser credential patterns browser_patterns = { "Chrome": [r"Login Data", r"Chrome.*Safe Storage", r"Google/Chrome"], "Firefox": [r"logins\.json", r"key4\.db", r"cert9\.db"], "Safari": [r"Safari.*Password", r"Keychain.*Safari"], "Brave": [r"Brave-Browser", r"Brave.*Safe Storage"], "Edge": [r"Microsoft Edge", r"Edge.*Safe Storage"], } for browser, patterns in browser_patterns.items(): for pattern in patterns: if re.search(pattern, all_strings, re.IGNORECASE): results["browser_credentials"].append(browser) break # Cryptocurrency wallet patterns wallet_patterns = { "Exodus": r"Exodus", "Electrum": r"Electrum", "MetaMask": r"MetaMask|metamask", "Coinbase Wallet": r"Coinbase.*Wallet|coinbase-wallet", "Atomic Wallet": r"Atomic.*Wallet|atomic-wallet", "TronLink": r"TronLink", "Phantom": r"Phantom", "Trust Wallet": r"Trust.*Wallet|trustwallet", } for wallet, pattern in wallet_patterns.items(): if re.search(pattern, all_strings, re.IGNORECASE): results["crypto_wallets"].append(wallet) # SSH key access if re.search(r"id_rsa|id_ed25519|id_ecdsa|\.ssh/", all_strings, re.IGNORECASE): results["ssh_keys"] = True # Cookie theft if re.search(r"Cookies\.binarycookies|/Library/Cookies|cookie", all_strings, re.IGNORECASE): results["cookies"] = True return results def analyze_gatekeeper_bypass(filepath) -> dict: """Detect Gatekeeper and notarization bypass techniques. Checks for quarantine attribute removal, archive-based bypasses, and other techniques used to circumvent Gatekeeper. Args: filepath: Path to the binary to analyze. Returns: Dictionary with Gatekeeper bypass analysis results. """ results = { "quarantine_stripped": False, "bypass_technique": None, "notarization_status": "unknown", "quarantine_attribute": None, } try: # Check quarantine extended attribute xattr_out = subprocess.run( ["xattr", "-l", filepath], capture_output=True, text=True, timeout=10 ) if "com.apple.quarantine" in xattr_out.stdout: results["quarantine_attribute"] = "present" else: results["quarantine_stripped"] = True results["quarantine_attribute"] = "absent" except FileNotFoundError: pass try: strings_out = subprocess.run( ["strings", "-a", "-n", "6", filepath], capture_output=True, text=True, timeout=60 ) all_strings = strings_out.stdout except (subprocess.TimeoutExpired, FileNotFoundError): return results # Check for quarantine removal techniques bypass_patterns = { "Quarantine attribute removal (xattr -d)": r"xattr.*-d.*quarantine", "Quarantine clear (xattr -c)": r"xattr.*-c", "DMG/ISO mount bypass": r"hdiutil.*attach|diskutil.*mount", "AppleScript bypass": r"osascript.*do shell script", "Curl/wget pipe to shell": r"curl.*\|\s*(ba)?sh|wget.*\|\s*(ba)?sh", } for technique, pattern in bypass_patterns.items(): if re.search(pattern, all_strings, re.IGNORECASE): results["bypass_technique"] = technique break # Check notarization try: spctl_out = subprocess.run( ["spctl", "--assess", "--verbose", "--type", "execute", filepath], capture_output=True, text=True, timeout=30 ) spctl_text = spctl_out.stdout + spctl_out.stderr if "notarized" in spctl_text.lower() and "not notarized" not in spctl_text.lower(): results["notarization_status"] = "notarized" elif "rejected" in spctl_text.lower(): results["notarization_status"] = "rejected" else: results["notarization_status"] = "not notarized" except FileNotFoundError: pass return results def identify_malware_family(filepath) -> dict: """Identify known macOS malware families based on string signatures and behavioral markers. Checks against signature patterns for XCSSET, Atomic Stealer, RustBucket, Lazarus macOS tools, and other known macOS malware families. Args: filepath: Path to the binary to analyze. Returns: Dictionary with family identification results and confidence level. """ results = { "family": "unknown", "confidence": "low", "variant": None, "indicators": [], } try: strings_out = subprocess.run( ["strings", "-a", "-n", "4", filepath], capture_output=True, text=True, timeout=60 ) all_strings = strings_out.stdout except (subprocess.TimeoutExpired, FileNotFoundError): return results families = { "XCSSET": { "patterns": [ r"xcsset", r"\.xcodeproj", r"replicator", r"camtrigger", r"screen_sim", r"safari.*inject", ], "min_matches": 2, }, "Atomic Stealer": { "patterns": [ r"atomicstealer", r"amos", r"/Library/Keychains", r"Login Data.*Chrome", r"Exodus|Electrum|MetaMask", r"Cookies\.binarycookies", ], "min_matches": 3, }, "RustBucket": { "patterns": [ r"rustbucket", r"InternalPDF", r"PDFViewer", r"core::panicking", r"std::rt::lang_start", ], "min_matches": 2, }, "Lazarus macOS": { "patterns": [ r"TraderTraitor", r"AppleJeus", r"CryptoTrader", r"DeFiApp", r"binance|coinbase|blockchain\.com", ], "min_matches": 2, }, "CloudMensis": { "patterns": [ r"cloudmensis", r"pCloud", r"Yandex.*Disk", r"CFNetworkAgent", ], "min_matches": 2, }, "MacStealer": { "patterns": [ r"macstealer", r"login\.keychain-db", r"creditcard", r"Notes\.sqlite", ], "min_matches": 2, }, } best_match = None best_score = 0 for family_name, family_info in families.items(): matches = [] for pattern in family_info["patterns"]: if re.search(pattern, all_strings, re.IGNORECASE): matches.append(pattern) if len(matches) >= family_info["min_matches"]: score = len(matches) / len(family_info["patterns"]) if score > best_score: best_score = score best_match = family_name results["indicators"] = [ f"Matched pattern: {m}" for m in matches ] if best_match: results["family"] = best_match results["confidence"] = "high" if best_score > 0.5 else "medium" return results def extract_iocs(filepath) -> list: """Extract indicators of compromise from a macOS binary. Searches for IP addresses, domain names, URLs, file paths, and other indicators that can be used for detection and threat intelligence. Args: filepath: Path to the binary to analyze. Returns: Dictionary with categorized IOCs. """ iocs = { "ip_addresses": [], "domains": [], "urls": [], "file_paths": [], "team_identifiers": [], } try: strings_out = subprocess.run( ["strings", "-a", "-n", "6", filepath], capture_output=True, text=True, timeout=60 ) all_strings = strings_out.stdout except (subprocess.TimeoutExpired, FileNotFoundError): return iocs # Extract IP addresses ip_pattern = r"\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b" for match in re.finditer(ip_pattern, all_strings): ip = match.group() if not ip.startswith(("0.", "127.", "255.")): iocs["ip_addresses"].append(ip) iocs["ip_addresses"] = sorted(set(iocs["ip_addresses"])) # Extract URLs url_pattern = r"https?://[^\s\"'<>]{4,}" for match in re.finditer(url_pattern, all_strings): iocs["urls"].append(match.group()) iocs["urls"] = sorted(set(iocs["urls"])) # Extract suspicious macOS file paths path_pattern = r"(~/Library/LaunchAgents/[^\s\"']+|/Library/LaunchDaemons/[^\s\"']+|/tmp/[^\s\"']+|/private/tmp/[^\s\"']+)" for match in re.finditer(path_pattern, all_strings): iocs["file_paths"].append(match.group()) iocs["file_paths"] = sorted(set(iocs["file_paths"])) # Extract Team Identifiers team_pattern = r"\b[A-Z0-9]{10}\b" # Only look for team IDs near codesign-related strings for line in all_strings.splitlines(): if "TeamIdentifier" in line or "team-identifier" in line: for match in re.finditer(team_pattern, line): iocs["team_identifiers"].append(match.group()) return iocs def run_full_analysis(filepath) -> dict: """Run all analysis modules and produce a consolidated report. Args: filepath: Path to the malware sample. Returns: Dictionary with complete analysis results. """ file_hashes = compute_file_hashes(filepath) # Get file type try: file_out = subprocess.run( ["file", filepath], capture_output=True, text=True, timeout=10 ) file_type = file_out.stdout.split(":", 1)[-1].strip() except Exception: file_type = "unknown" macho = analyze_macho_header(filepath) report = { "sample": { "filename": os.path.basename(filepath), "sha256": file_hashes["sha256"], "md5": file_hashes["md5"], "file_type": file_type, "size_bytes": os.path.getsize(filepath), "universal_binary": macho.get("universal_binary", False), "architectures": macho.get("architectures", []), }, "macho_analysis": macho, "code_signing": analyze_code_signing(filepath), "family_identification": identify_malware_family(filepath), "persistence": scan_persistence_mechanisms(), "credential_theft": analyze_credential_theft(filepath), "gatekeeper_bypass": analyze_gatekeeper_bypass(filepath), "iocs": extract_iocs(filepath), "mitre_attack": [], "analysis_timestamp": datetime.utcnow().isoformat() + "Z", } # Map findings to MITRE ATT&CK techniques attack_mapping = [] if report["persistence"]["launch_agents"]["found"]: attack_mapping.append("T1543.001") # Launch Agent if report["persistence"]["launch_daemons"]["found"]: attack_mapping.append("T1543.004") # Launch Daemon if report["persistence"]["login_items"]["found"]: attack_mapping.append("T1547.015") # Login Items if report["persistence"]["kernel_extensions"]["found"]: attack_mapping.append("T1547.006") # Kernel Modules and Extensions if report["credential_theft"]["keychain_access"]: attack_mapping.append("T1555.001") # Keychain if report["credential_theft"]["browser_credentials"]: attack_mapping.append("T1555.003") # Credentials from Web Browsers if report["credential_theft"]["cookies"]: attack_mapping.append("T1539") # Steal Web Session Cookie if report["credential_theft"]["crypto_wallets"]: attack_mapping.append("T1005") # Data from Local System if report["gatekeeper_bypass"]["quarantine_stripped"]: attack_mapping.append("T1553.001") # Gatekeeper Bypass if report["code_signing"]["suspicious_entitlements"]: attack_mapping.append("T1574.004") # Dylib Hijacking report["mitre_attack"] = sorted(set(attack_mapping)) return report def main() -> None: parser = argparse.ArgumentParser( description="macOS Malware Analyzer - Analyze Mach-O binaries and detect macOS-specific threats", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: %(prog)s --sample malware.macho --mode macho-analysis --output macho.json %(prog)s --mode persistence --output persistence.json %(prog)s --sample malware.macho --mode code-signing --output signing.json %(prog)s --sample malware.macho --mode family-id --output family.json %(prog)s --sample malware.macho --mode full-analysis --output report.json """, ) parser.add_argument( "--input", "--sample", help="Path to the malware sample (Mach-O binary)", ) parser.add_argument( "--mode", choices=["macho-analysis", "persistence", "code-signing", "family-id", "full-analysis"], default="full-analysis", help="Analysis mode to run (default: full-analysis)", ) parser.add_argument( "--output", default="macos_analysis.json", help="Output file path for JSON results (default: macos_analysis.json)", ) parser.add_argument( "--format", default="json", choices=["json", "text", "csv"], help="Output format (default: json)", ) args = parser.parse_args() # Validate arguments if args.mode != "persistence" and not args.sample: parser.error("--sample is required for all modes except 'persistence'") if args.sample and not os.path.isfile(args.sample): print(f"Error: Sample file not found: {args.sample}", file=sys.stderr) sys.exit(1) # Run the selected analysis mode if args.mode == "macho-analysis": results = {"macho_analysis": analyze_macho_header(args.sample)} elif args.mode == "persistence": results = {"persistence": scan_persistence_mechanisms()} elif args.mode == "code-signing": results = {"code_signing": analyze_code_signing(args.sample)} elif args.mode == "family-id": results = {"family_identification": identify_malware_family(args.sample)} elif args.mode == "full-analysis": results = run_full_analysis(args.sample) # Write output output_path = Path(args.output) output_path.parent.mkdir(parents=True, exist_ok=True) with open(output_path, "w") as f: json.dump(results, f, indent=2, default=str) print(f"Analysis complete. Results written to: {output_path}") # Print summary to stdout if args.mode == "full-analysis" and "family_identification" in results: family = results["family_identification"] print(f" Family: {family.get('family', 'unknown')} " f"(confidence: {family.get('confidence', 'low')})") if "macho_analysis" in results: macho = results["macho_analysis"] print(f" Architecture: {macho.get('architecture', 'unknown')}") print(f" Universal binary: {macho.get('universal_binary', False)}") print(f" Encrypted: {macho.get('encrypted', False)}") if "code_signing" in results: cs = results["code_signing"] print(f" Signed: {cs.get('signed', False)}") print(f" Notarized: {cs.get('notarized', False)}") if __name__ == "__main__": main()