#!/usr/bin/env python3 """Agent for hunting suspicious Windows service installations (T1543.003) via Event ID 7045.""" import json import re import argparse from datetime import datetime from collections import Counter try: from lxml import etree except ImportError: etree = None try: import Evtx.Evtx as evtx except ImportError: evtx = None SUSPICIOUS_PATH_PATTERNS = [ (r"\\temp\\", "temp_directory"), (r"\\tmp\\", "temp_directory"), (r"\\appdata\\", "appdata_directory"), (r"\\users\\public\\", "public_user_directory"), (r"\\programdata\\[^\\]+\.[^\\]+$", "loose_file_in_programdata"), (r"powershell\.exe", "powershell_execution"), (r"cmd\.exe\s*/c", "cmd_execution"), (r"-enc\s+", "encoded_command"), (r"-encodedcommand", "encoded_command"), (r"frombase64string", "base64_decode"), (r"downloadstring|downloadfile|webclient", "download_pattern"), (r"invoke-expression|iex\s", "invoke_expression"), (r"\\windows\\temp\\", "windows_temp"), (r"mshta\.exe|regsvr32\.exe|rundll32\.exe|msiexec\.exe", "lolbin_execution"), (r"sc\.exe\s+create", "sc_create_chain"), (r"\\\$recycle\.bin\\", "recycle_bin"), (r"\\perflog", "perflog_abuse"), ] LEGITIMATE_SERVICE_PATHS = [ r"^\"?C:\\Windows\\System32\\", r"^\"?C:\\Windows\\SysWOW64\\", r"^\"?C:\\Program Files\\", r"^\"?C:\\Program Files \(x86\)\\", r"^\"?C:\\Windows\\Microsoft\.NET\\", ] def parse_evtx_events(evtx_path): """Parse System.evtx and extract Event ID 7045 records.""" if not evtx: raise RuntimeError("python-evtx not installed: pip install python-evtx") events = [] ns = {"e": "http://schemas.microsoft.com/win/2004/08/events/event"} with evtx.Evtx(evtx_path) as log: for record in log.records(): try: xml = record.xml() root = etree.fromstring(xml.encode("utf-8") if isinstance(xml, str) else xml) event_id_el = root.find(".//e:System/e:EventID", ns) if event_id_el is None or event_id_el.text != "7045": continue system = root.find(".//e:System", ns) event_data = root.find(".//e:EventData", ns) data_fields = {} if event_data is not None: for data in event_data.findall("e:Data", ns): name = data.get("Name", "") data_fields[name] = data.text or "" time_el = system.find("e:TimeCreated", ns) timestamp = time_el.get("SystemTime", "") if time_el is not None else "" computer_el = system.find("e:Computer", ns) computer = computer_el.text if computer_el is not None else "" events.append({ "timestamp": timestamp, "event_id": 7045, "computer": computer, "service_name": data_fields.get("ServiceName", ""), "image_path": data_fields.get("ImagePath", ""), "service_type": data_fields.get("ServiceType", ""), "start_type": data_fields.get("StartType", ""), "account_name": data_fields.get("AccountName", ""), }) except Exception: continue return events def analyze_service_path(image_path): """Analyze a service binary path for suspicious indicators.""" findings = [] path_lower = image_path.lower() for pattern, indicator in SUSPICIOUS_PATH_PATTERNS: if re.search(pattern, path_lower, re.IGNORECASE): findings.append(indicator) is_legitimate = any(re.match(p, image_path, re.IGNORECASE) for p in LEGITIMATE_SERVICE_PATHS) risk_score = 0 if not is_legitimate: risk_score += 20 risk_score += len(findings) * 15 risk_score = min(risk_score, 100) return { "suspicious_indicators": findings, "legitimate_path": is_legitimate, "risk_score": risk_score, "risk_level": "CRITICAL" if risk_score >= 70 else "HIGH" if risk_score >= 50 else "MEDIUM" if risk_score >= 20 else "LOW", } def hunt_suspicious_services(evtx_path): """Main hunting function: parse events and analyze each service.""" events = parse_evtx_events(evtx_path) results = [] for event in events: analysis = analyze_service_path(event.get("image_path", "")) entry = {**event, **analysis} if entry.get("account_name", "").lower() == "localsystem" and not analysis["legitimate_path"]: entry["risk_score"] = min(entry["risk_score"] + 20, 100) entry["risk_level"] = "CRITICAL" if entry["risk_score"] >= 70 else entry["risk_level"] entry["suspicious_indicators"].append("localsystem_nonstandard_path") results.append(entry) results.sort(key=lambda x: x.get("risk_score", 0), reverse=True) return results def generate_statistics(results): """Generate summary statistics from hunting results.""" total = len(results) risk_counts = Counter(r.get("risk_level", "LOW") for r in results) indicator_counts = Counter() account_counts = Counter() for r in results: for ind in r.get("suspicious_indicators", []): indicator_counts[ind] += 1 account_counts[r.get("account_name", "Unknown")] += 1 return { "total_service_installations": total, "risk_distribution": dict(risk_counts), "top_indicators": dict(indicator_counts.most_common(10)), "service_accounts": dict(account_counts.most_common(10)), "critical_count": risk_counts.get("CRITICAL", 0), "high_count": risk_counts.get("HIGH", 0), } def full_hunt(evtx_path): """Run comprehensive service installation threat hunt.""" results = hunt_suspicious_services(evtx_path) stats = generate_statistics(results) suspicious = [r for r in results if r.get("risk_score", 0) >= 20] return { "hunt_type": "Unusual Service Installation (T1543.003)", "timestamp": datetime.utcnow().isoformat(), "evtx_file": evtx_path, "statistics": stats, "suspicious_services": suspicious[:30], "mitre_technique": { "id": "T1543.003", "name": "Create or Modify System Process: Windows Service", "tactic": "Persistence, Privilege Escalation", }, "recommendation": "Investigate CRITICAL and HIGH services. Verify binary hashes against known-good baselines." if stats["critical_count"] + stats["high_count"] > 0 else "No high-risk service installations detected.", } def main(): parser = argparse.ArgumentParser(description="Service Installation Threat Hunting Agent (T1543.003)") parser.add_argument("evtx", help="Path to System.evtx file") sub = parser.add_subparsers(dest="command") sub.add_parser("parse", help="Parse and list all Event ID 7045 records") sub.add_parser("hunt", help="Hunt for suspicious service installations") sub.add_parser("stats", help="Generate hunting statistics") sub.add_parser("full", help="Full threat hunt report") args = parser.parse_args() if args.command == "parse": result = parse_evtx_events(args.evtx) elif args.command == "hunt": result = hunt_suspicious_services(args.evtx) elif args.command == "stats": results = hunt_suspicious_services(args.evtx) result = generate_statistics(results) elif args.command == "full" or args.command is None: result = full_hunt(args.evtx) else: parser.print_help() return print(json.dumps(result, indent=2, default=str)) if __name__ == "__main__": main()