#!/usr/bin/env python3 """ Advanced string extraction and categorization for malware analysis. Extracts ASCII and Unicode strings from binary files, then categorizes findings into IPs, URLs, emails, file paths, registry keys, and suspicious API names using regex pattern matching. Usage: python3 extract_strings.py --file [--output report.json] python3 extract_strings.py --file --min-length 6 python3 extract_strings.py --file --category urls """ from __future__ import annotations import argparse import json import os import re import sys from collections import defaultdict from pathlib import Path # --------------------------------------------------------------------------- # String extraction # --------------------------------------------------------------------------- # Printable ASCII characters range ASCII_PATTERN = re.compile(rb"[\x20-\x7e]{4,}") # UTF-16LE strings (common in Windows binaries) UNICODE_PATTERN = re.compile(rb"(?:[\x20-\x7e]\x00){4,}") def extract_ascii_strings(data: bytes, min_length: int = 4) -> list: """Extract ASCII strings from binary data.""" pattern = re.compile(rb"[\x20-\x7e]{%d,}" % min_length) return [match.group().decode("ascii") for match in pattern.finditer(data)] def extract_unicode_strings(data: bytes, min_length: int = 4) -> list: """Extract UTF-16LE strings from binary data.""" pattern = re.compile(rb"(?:[\x20-\x7e]\x00){%d,}" % min_length) results = [] for match in pattern.finditer(data): try: decoded = match.group().decode("utf-16-le") results.append(decoded) except UnicodeDecodeError: continue return results # --------------------------------------------------------------------------- # Categorization patterns # --------------------------------------------------------------------------- CATEGORY_PATTERNS = { "ipv4_addresses": re.compile( r"\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b" ), "ipv6_addresses": re.compile( r"\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b" r"|\b(?:[0-9a-fA-F]{1,4}:){1,7}:\b" r"|\b::(?:[0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4}\b" ), "urls": re.compile( r"https?://[^\s<>\"'`\x00-\x1f]{4,200}" ), "domains": re.compile( r"\b(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)" r"{1,5}(?:com|net|org|info|biz|io|co|us|uk|ru|cn|de|fr|top|xyz|tk|ml|ga|cf|gq|pw)\b" ), "email_addresses": re.compile( r"\b[a-zA-Z0-9._%+\-]{1,64}@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}\b" ), "file_paths_windows": re.compile( r"[A-Za-z]:\\(?:[^\\\/:*?\"<>|\r\n]{1,200}\\)*[^\\\/:*?\"<>|\r\n]{0,200}" ), "file_paths_unix": re.compile( r"(?:/(?:usr|etc|tmp|var|home|opt|bin|sbin|dev|proc|sys|root|mnt|media)" r"(?:/[^\s/\x00-\x1f]{1,100}){0,10})" ), "registry_keys": re.compile( r"(?:HKEY_(?:LOCAL_MACHINE|CURRENT_USER|CLASSES_ROOT|USERS|CURRENT_CONFIG)" r"|HKLM|HKCU|HKCR|HKU|HKCC)" r"(?:\\[^\s\\]{1,200}){1,15}", re.IGNORECASE, ), "base64_blocks": re.compile( r"(?:[A-Za-z0-9+/]{4}){8,}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?" ), "hex_strings": re.compile( r"\b(?:0x)?[0-9a-fA-F]{32,}\b" ), "crypto_wallet_bitcoin": re.compile( r"\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b" ), "crypto_wallet_ethereum": re.compile( r"\b0x[0-9a-fA-F]{40}\b" ), } # Suspicious API patterns (Windows) SUSPICIOUS_API_PATTERNS = [ # Process injection r"CreateRemoteThread(?:Ex)?", r"NtCreateThreadEx", r"RtlCreateUserThread", r"Virtual(?:Alloc|Protect|Free)(?:Ex)?", r"(?:Nt)?WriteProcessMemory", r"(?:Nt)?ReadProcessMemory", r"NtUnmapViewOfSection", r"(?:Nt)?QueueApcThread", r"QueueUserAPC", r"(?:Nt)?(?:Get|Set)ContextThread", # Execution r"CreateProcess[AW]?", r"WinExec", r"ShellExecute(?:Ex)?[AW]?", r"system\b", # DLL operations r"LoadLibrary(?:Ex)?[AW]?", r"GetProcAddress", r"LdrLoadDll", # File operations r"CreateFile[AW]", r"WriteFile", r"DeleteFile[AW]", r"MoveFile(?:Ex)?[AW]?", r"CopyFile[AW]", # Network r"Internet(?:Open|Connect|ReadFile|WriteFile)[AW]?", r"Http(?:Open|Send|Query)Request[AW]?", r"URLDownloadToFile[AW]", r"URLDownloadToCacheFile[AW]", r"WinHttp(?:Open|Connect|SendRequest|ReadData)", r"WSAStartup", r"(?:connect|send|recv|bind|listen|accept)\b", r"getaddrinfo", r"gethostbyname", # Registry r"Reg(?:Open|Create|Set|Delete|Query)(?:Key|Value)(?:Ex)?[AW]?", # Service r"(?:Create|Open|Start|Control|Delete)Service[AW]?", r"OpenSCManager[AW]", # Crypto r"Crypt(?:Encrypt|Decrypt|GenKey|ImportKey|ExportKey|AcquireContext)[AW]?", r"BCrypt(?:Encrypt|Decrypt|GenerateSymmetricKey)", r"CryptCreateHash", r"CryptHashData", # Anti-debug r"IsDebuggerPresent", r"CheckRemoteDebuggerPresent", r"NtQueryInformationProcess", r"NtSetInformationThread", r"OutputDebugString[AW]", r"GetTickCount(?:64)?", r"QueryPerformanceCounter", # Privilege r"AdjustTokenPrivileges", r"OpenProcessToken", r"ImpersonateLoggedOnUser", r"DuplicateToken(?:Ex)?", # Keylog/Screen r"GetAsyncKeyState", r"GetKeyState", r"SetWindowsHookEx[AW]", r"GetDC", r"BitBlt", r"GetForegroundWindow", r"GetClipboardData", # Evasion r"Sleep\b", r"NtDelayExecution", r"GetSystemTime(?:AsFileTime)?", r"GetComputerName[AW]?", r"GetUserName[AW]?", r"GetSystemInfo", r"GlobalMemoryStatusEx", r"EnumProcesses", ] SUSPICIOUS_API_RE = re.compile( "|".join(f"(?:{p})" for p in SUSPICIOUS_API_PATTERNS) ) # --------------------------------------------------------------------------- # String categorization # --------------------------------------------------------------------------- def categorize_strings(strings: list) -> dict: """Categorize extracted strings by type.""" categories = defaultdict(set) full_text = "\n".join(strings) # Apply regex patterns for category, pattern in CATEGORY_PATTERNS.items(): for match in pattern.finditer(full_text): value = match.group().strip() if value: categories[category].add(value) # Filter out common false positives from IP addresses if "ipv4_addresses" in categories: filtered = set() for ip in categories["ipv4_addresses"]: parts = ip.split(".") # Skip version-like strings (e.g., 1.0.0.0) and broadcast/loopback if parts == ["0", "0", "0", "0"]: continue if parts == ["255", "255", "255", "255"]: continue filtered.add(ip) categories["ipv4_addresses"] = filtered # Find suspicious APIs for match in SUSPICIOUS_API_RE.finditer(full_text): categories["suspicious_apis"].add(match.group()) # Convert sets to sorted lists return {k: sorted(v) for k, v in categories.items() if v} def filter_interesting_strings(strings: list, min_length: int = 4) -> list: """Filter strings to only keep potentially interesting ones.""" boring_patterns = re.compile( r"^(?:" r"\.text|\.data|\.rdata|\.reloc|\.rsrc|\.bss" # Section names r"|!This program" # DOS stub r"|Rich\d" # Rich header r"|[.!@#$%^&*()_+=\-]{4,}" # Punctuation-only strings r"|[0-9a-fA-F]{4,8}$" # Short hex strings r")$" ) interesting = [] for s in strings: s = s.strip() if len(s) < min_length: continue if boring_patterns.match(s): continue interesting.append(s) return interesting # --------------------------------------------------------------------------- # Main analysis function # --------------------------------------------------------------------------- def analyze_strings(file_path: str, min_length: int = 4, max_file_size: int = 50 * 1024 * 1024) -> dict: """Extract and categorize strings from a binary file.""" file_size = os.path.getsize(file_path) if file_size > max_file_size: # For very large files, only read the first portion with open(file_path, "rb") as f: data = f.read(max_file_size) truncated = True else: with open(file_path, "rb") as f: data = f.read() truncated = False ascii_strings = extract_ascii_strings(data, min_length) unicode_strings = extract_unicode_strings(data, min_length) # Deduplicate all_strings = list(dict.fromkeys(ascii_strings + unicode_strings)) interesting = filter_interesting_strings(all_strings, min_length) categories = categorize_strings(all_strings) report = { "file": os.path.basename(file_path), "file_size": file_size, "truncated": truncated, "statistics": { "total_ascii": len(ascii_strings), "total_unicode": len(unicode_strings), "total_unique": len(all_strings), "total_interesting": len(interesting), }, "categories": categories, } # Include sample of interesting strings (limit for report size) if len(interesting) > 500: report["interesting_strings"] = interesting[:500] report["interesting_strings_note"] = f"Showing 500 of {len(interesting)} interesting strings" else: report["interesting_strings"] = interesting return report # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- def main() -> None: parser = argparse.ArgumentParser( description="Extract and categorize strings from binary files", formatter_class=argparse.RawDescriptionHelpFormatter, epilog="Examples:\n" " %(prog)s --file sample.exe\n" " %(prog)s --file sample.exe --min-length 6\n" " %(prog)s --file sample.exe --category urls\n" " %(prog)s --file sample.exe --output strings_report.json\n", ) parser.add_argument("--input", "--file", "-f", dest="file", required=True, help="Path to binary file") parser.add_argument("--output", "-o", help="Write JSON report to file") parser.add_argument("--min-length", type=int, default=4, help="Minimum string length (default: 4)") parser.add_argument("--category", "-c", choices=["urls", "ips", "emails", "paths", "registry", "apis", "base64", "domains", "crypto_wallets", "all"], default="all", help="Show only a specific category") parser.add_argument("--raw", action="store_true", help="Output raw strings (one per line) instead of JSON") parser.add_argument("--format", default="json", choices=["json", "text", "csv"], help="Output format (default: json)") args = parser.parse_args() if not os.path.isfile(args.file): print(f"Error: File not found: {args.file}", file=sys.stderr) sys.exit(1) report = analyze_strings(args.file, min_length=args.min_length) # Filter to specific category if requested if args.category != "all": category_map = { "urls": "urls", "ips": "ipv4_addresses", "emails": "email_addresses", "paths": "file_paths_windows", "registry": "registry_keys", "apis": "suspicious_apis", "base64": "base64_blocks", "domains": "domains", "crypto_wallets": "crypto_wallet_bitcoin", } key = category_map.get(args.category, args.category) items = report["categories"].get(key, []) if args.raw: for item in items: print(item) return report = { "file": report["file"], "category": args.category, "count": len(items), "items": items, } if args.raw and args.category == "all": for s in report.get("interesting_strings", []): print(s) return result = json.dumps(report, indent=2, default=str) if args.output: Path(args.output).write_text(result) print(f"Report written to {args.output}") else: print(result) if __name__ == "__main__": main()