#!/usr/bin/env python3 """Agent for hunting T1547.001 startup folder persistence on Windows.""" import json import os import hashlib import argparse import time from datetime import datetime from pathlib import Path try: from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler except ImportError: Observer = None FileSystemEventHandler = object SUSPICIOUS_EXTENSIONS = { ".exe": 30, ".bat": 35, ".cmd": 35, ".vbs": 40, ".vbe": 40, ".js": 40, ".jse": 40, ".wsf": 40, ".wsh": 35, ".ps1": 45, ".pif": 45, ".scr": 40, ".hta": 45, ".lnk": 15, ".url": 20, } LEGITIMATE_ENTRIES = [ "desktop.ini", "Send to OneNote.lnk", "OneNote 2016.lnk", "Microsoft Teams.lnk", "Outlook.lnk", "OneDrive.lnk", "Cisco AnyConnect Secure Mobility Client.lnk", "Skype for Business.lnk", "Zoom.lnk", ] def get_startup_paths(): """Return user-specific and all-users startup folder paths.""" paths = [] user_startup = os.path.join( os.environ.get("APPDATA", ""), "Microsoft", "Windows", "Start Menu", "Programs", "Startup" ) if os.path.isdir(user_startup): paths.append({"path": user_startup, "scope": "current_user"}) all_users_startup = os.path.join( os.environ.get("PROGRAMDATA", r"C:\ProgramData"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup" ) if os.path.isdir(all_users_startup): paths.append({"path": all_users_startup, "scope": "all_users"}) return paths def compute_file_hash(filepath): """Compute SHA-256 hash of a file.""" sha256 = hashlib.sha256() try: with open(filepath, "rb") as f: for chunk in iter(lambda: f.read(8192), b""): sha256.update(chunk) return sha256.hexdigest() except (PermissionError, OSError): return "access_denied" def analyze_file(filepath, scope="unknown"): """Analyze a single file in a startup directory.""" path = Path(filepath) name = path.name ext = path.suffix.lower() try: stat = path.stat() created = datetime.fromtimestamp(stat.st_ctime) modified = datetime.fromtimestamp(stat.st_mtime) size = stat.st_size except (PermissionError, OSError): return {"file": str(filepath), "error": "access_denied"} is_legitimate = name in LEGITIMATE_ENTRIES age_days = (datetime.now() - created).days risk = 0 indicators = [] risk += SUSPICIOUS_EXTENSIONS.get(ext, 0) if ext in SUSPICIOUS_EXTENSIONS and ext != ".lnk": indicators.append(f"suspicious_extension_{ext}") if age_days < 7: risk += 25 indicators.append("recently_created") if age_days < 1: risk += 15 indicators.append("created_within_24h") if size == 0: risk += 10 indicators.append("zero_byte_file") if size > 10 * 1024 * 1024: risk += 10 indicators.append("large_file_over_10mb") if not is_legitimate: risk += 10 indicators.append("not_in_baseline") if scope == "all_users" and ext in SUSPICIOUS_EXTENSIONS: risk += 10 indicators.append("all_users_startup") risk = min(risk, 100) return { "file": str(filepath), "filename": name, "extension": ext, "scope": scope, "size_bytes": size, "created": created.isoformat(), "modified": modified.isoformat(), "age_days": age_days, "sha256": compute_file_hash(filepath), "is_legitimate_baseline": is_legitimate, "suspicious_indicators": indicators, "risk_score": risk, "risk_level": "CRITICAL" if risk >= 70 else "HIGH" if risk >= 50 else "MEDIUM" if risk >= 25 else "LOW", } def scan_startup_folders(): """Scan all startup directories and analyze contents.""" startup_paths = get_startup_paths() results = [] for sp in startup_paths: folder = sp["path"] scope = sp["scope"] try: for entry in os.listdir(folder): full_path = os.path.join(folder, entry) if os.path.isfile(full_path): analysis = analyze_file(full_path, scope) results.append(analysis) except PermissionError: results.append({"path": folder, "error": "access_denied"}) results.sort(key=lambda x: x.get("risk_score", 0), reverse=True) return results def check_registry_run_keys(): """Check Registry Run keys for autostart entries.""" import subprocess run_keys = [ r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run", r"HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce", r"HKLM\Software\Microsoft\Windows\CurrentVersion\Run", r"HKLM\Software\Microsoft\Windows\CurrentVersion\RunOnce", ] entries = [] for key in run_keys: try: result = subprocess.run( ["reg", "query", key], capture_output=True, text=True, timeout=10 ) if result.returncode == 0: for line in result.stdout.strip().split("\n"): line = line.strip() if line and not line.startswith("HK") and "REG_" in line: parts = line.split("REG_SZ", 1) if "REG_SZ" in line else line.split("REG_EXPAND_SZ", 1) name = parts[0].strip() if parts else line value = parts[1].strip() if len(parts) > 1 else "" entries.append({ "registry_key": key, "name": name, "value": value, "suspicious": any(p in value.lower() for p in ["powershell", "cmd.exe", "\\temp\\", "\\appdata\\", "mshta", "-enc", "downloadstring"]), }) except Exception: pass return entries class StartupMonitorHandler(FileSystemEventHandler): """Watchdog handler for monitoring startup folder changes.""" def __init__(self): self.events = [] def on_created(self, event): if not event.is_directory: analysis = analyze_file(event.src_path) alert = { "event": "FILE_CREATED", "timestamp": datetime.now().isoformat(), "file": event.src_path, "risk_score": analysis.get("risk_score", 0), "risk_level": analysis.get("risk_level", "UNKNOWN"), "indicators": analysis.get("suspicious_indicators", []), } self.events.append(alert) print(json.dumps(alert, indent=2)) def on_modified(self, event): if not event.is_directory: alert = { "event": "FILE_MODIFIED", "timestamp": datetime.now().isoformat(), "file": event.src_path, } self.events.append(alert) print(json.dumps(alert, indent=2)) def on_deleted(self, event): if not event.is_directory: alert = { "event": "FILE_DELETED", "timestamp": datetime.now().isoformat(), "file": event.src_path, } self.events.append(alert) print(json.dumps(alert, indent=2)) def monitor_startup(duration_seconds=60): """Monitor startup folders in real-time using watchdog.""" if not Observer: return {"error": "watchdog not installed: pip install watchdog"} handler = StartupMonitorHandler() observer = Observer() startup_paths = get_startup_paths() for sp in startup_paths: observer.schedule(handler, sp["path"], recursive=False) observer.start() try: time.sleep(duration_seconds) except KeyboardInterrupt: pass observer.stop() observer.join() return {"monitored_seconds": duration_seconds, "events_detected": handler.events} def full_hunt(): """Run comprehensive startup persistence threat hunt.""" scan_results = scan_startup_folders() registry_entries = check_registry_run_keys() suspicious_files = [r for r in scan_results if r.get("risk_score", 0) >= 25] suspicious_reg = [e for e in registry_entries if e.get("suspicious")] return { "hunt_type": "Startup Folder Persistence (T1547.001)", "timestamp": datetime.now().isoformat(), "startup_paths": get_startup_paths(), "statistics": { "total_startup_files": len(scan_results), "suspicious_files": len(suspicious_files), "registry_run_entries": len(registry_entries), "suspicious_registry_entries": len(suspicious_reg), }, "file_analysis": scan_results[:30], "registry_analysis": registry_entries[:20], "mitre_technique": { "id": "T1547.001", "name": "Boot or Logon Autostart Execution: Registry Run Keys / Startup Folder", "tactic": "Persistence, Privilege Escalation", }, "recommendation": "Investigate CRITICAL and HIGH files. Verify hashes against known-good baselines." if suspicious_files else "No suspicious startup entries detected.", } def main(): parser = argparse.ArgumentParser(description="Startup Folder Persistence Hunting Agent (T1547.001)") sub = parser.add_subparsers(dest="command") sub.add_parser("scan", help="Scan startup folders for suspicious files") sub.add_parser("registry", help="Check Registry Run keys") p_mon = sub.add_parser("monitor", help="Monitor startup folders in real-time") p_mon.add_argument("--duration", type=int, default=60, help="Monitor duration in seconds") sub.add_parser("full", help="Full persistence threat hunt") args = parser.parse_args() if args.command == "scan": result = scan_startup_folders() elif args.command == "registry": result = check_registry_run_keys() elif args.command == "monitor": result = monitor_startup(args.duration) elif args.command == "full" or args.command is None: result = full_hunt() else: parser.print_help() return print(json.dumps(result, indent=2, default=str)) if __name__ == "__main__": main()