Files
Anthropic-Cybersecurity-Skills/skills/detecting-process-hollowing-technique/scripts/process.py
T

321 lines
12 KiB
Python

#!/usr/bin/env python3
"""
Process Hollowing Detection Script
Analyzes process creation, memory events, and parent-child relationships
to detect process hollowing (T1055.012) indicators.
"""
import json
import csv
import argparse
import datetime
import re
from collections import defaultdict
from pathlib import Path
# Legitimate parent-child process relationships on Windows
VALID_PARENT_CHILD = {
"smss.exe": {"parents": ["system", "smss.exe"], "user": "NT AUTHORITY\\SYSTEM"},
"csrss.exe": {"parents": ["smss.exe"], "user": "NT AUTHORITY\\SYSTEM"},
"wininit.exe": {"parents": ["smss.exe"], "user": "NT AUTHORITY\\SYSTEM"},
"winlogon.exe": {"parents": ["smss.exe"], "user": "NT AUTHORITY\\SYSTEM"},
"services.exe": {"parents": ["wininit.exe"], "user": "NT AUTHORITY\\SYSTEM"},
"lsass.exe": {"parents": ["wininit.exe"], "user": "NT AUTHORITY\\SYSTEM"},
"svchost.exe": {"parents": ["services.exe", "MsMpEng.exe"], "user": "NT AUTHORITY\\*"},
"taskhost.exe": {"parents": ["svchost.exe"], "user": "*"},
"taskhostw.exe": {"parents": ["svchost.exe"], "user": "*"},
"userinit.exe": {"parents": ["winlogon.exe"], "user": "*"},
"explorer.exe": {"parents": ["userinit.exe", "explorer.exe"], "user": "*"},
"dllhost.exe": {"parents": ["svchost.exe", "services.exe"], "user": "*"},
"conhost.exe": {"parents": ["csrss.exe"], "user": "*"},
"RuntimeBroker.exe": {"parents": ["svchost.exe"], "user": "*"},
"SearchIndexer.exe": {"parents": ["services.exe"], "user": "NT AUTHORITY\\SYSTEM"},
"spoolsv.exe": {"parents": ["services.exe"], "user": "NT AUTHORITY\\SYSTEM"},
}
# Common hollowing target processes
HOLLOWING_TARGETS = {
"svchost.exe", "explorer.exe", "rundll32.exe", "dllhost.exe",
"conhost.exe", "taskhost.exe", "taskhostw.exe", "RuntimeBroker.exe",
"RegAsm.exe", "MSBuild.exe", "RegSvcs.exe", "vbc.exe",
"AppLaunch.exe", "InstallUtil.exe", "aspnet_compiler.exe",
}
# Process behavior indicators that suggest hollowing
ANOMALOUS_BEHAVIORS = {
"svchost.exe": {
"no_cmdline_flag": True, # svchost should have -k flag
"required_arg": "-k",
"no_external_network": True, # unusual ports
},
"explorer.exe": {
"no_cmdline_flag": False,
"no_external_network": False,
"single_instance": True,
},
"dllhost.exe": {
"no_cmdline_flag": True,
"required_arg": "/Processid:",
},
}
def parse_logs(input_path: str) -> list[dict]:
"""Parse log files."""
path = Path(input_path)
if path.suffix == ".json":
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return data if isinstance(data, list) else data.get("events", [])
elif path.suffix == ".csv":
with open(path, "r", encoding="utf-8-sig") as f:
return [dict(row) for row in csv.DictReader(f)]
return []
def normalize_event(event: dict) -> dict:
"""Normalize process event fields."""
field_map = {
"event_id": ["EventCode", "EventID", "event_id"],
"image": ["Image", "FileName", "image", "process.executable"],
"command_line": ["CommandLine", "ProcessCommandLine", "command_line"],
"parent_image": ["ParentImage", "InitiatingProcessFileName", "parent_image"],
"parent_cmd": ["ParentCommandLine", "InitiatingProcessCommandLine", "parent_command_line"],
"user": ["User", "AccountName", "user.name"],
"hostname": ["Computer", "DeviceName", "host.name"],
"timestamp": ["UtcTime", "Timestamp", "@timestamp"],
"pid": ["ProcessId", "ProcessId", "process.pid"],
"parent_pid": ["ParentProcessId", "ppid", "process.parent.pid"],
"integrity": ["IntegrityLevel", "integrity_level"],
"hashes": ["Hashes", "SHA256", "hashes"],
"action_type": ["ActionType", "event_type"],
"dest_ip": ["DestinationIp", "RemoteIP"],
"dest_port": ["DestinationPort", "RemotePort"],
}
normalized = {}
for target, sources in field_map.items():
for src in sources:
if src in event and event[src]:
normalized[target] = str(event[src])
break
if target not in normalized:
normalized[target] = ""
return normalized
def get_process_name(path: str) -> str:
"""Extract process name from full path."""
if not path:
return ""
return path.split("\\")[-1].split("/")[-1].lower()
def check_parent_child(event: dict) -> dict | None:
"""Check for invalid parent-child process relationships."""
image = get_process_name(event.get("image", ""))
parent = get_process_name(event.get("parent_image", ""))
if image not in VALID_PARENT_CHILD:
return None
expected = VALID_PARENT_CHILD[image]
valid_parents = [p.lower() for p in expected["parents"]]
if parent and parent not in valid_parents:
return {
"detection_type": "INVALID_PARENT_CHILD",
"technique": "T1055.012",
"process": image,
"parent": parent,
"expected_parents": expected["parents"],
"full_image_path": event.get("image", ""),
"full_parent_path": event.get("parent_image", ""),
"command_line": event.get("command_line", ""),
"hostname": event.get("hostname", "unknown"),
"user": event.get("user", "unknown"),
"timestamp": event.get("timestamp", "unknown"),
"risk_score": 70,
"risk_level": "HIGH",
"indicators": [
f"Invalid parent: {parent} (expected: {', '.join(expected['parents'])})"
],
}
return None
def check_process_tampering(event: dict) -> dict | None:
"""Check for Sysmon Event ID 25 (ProcessTampering)."""
if event.get("event_id") != "25":
return None
return {
"detection_type": "PROCESS_TAMPERING",
"technique": "T1055.012",
"process": get_process_name(event.get("image", "")),
"full_image_path": event.get("image", ""),
"hostname": event.get("hostname", "unknown"),
"user": event.get("user", "unknown"),
"timestamp": event.get("timestamp", "unknown"),
"risk_score": 90,
"risk_level": "CRITICAL",
"indicators": ["Sysmon ProcessTampering event detected - image replaced in memory"],
}
def check_behavioral_anomaly(event: dict) -> dict | None:
"""Check for behavioral mismatches suggesting hollowing."""
if event.get("event_id") != "1":
return None
image = get_process_name(event.get("image", ""))
cmd = event.get("command_line", "")
if image not in ANOMALOUS_BEHAVIORS:
return None
behavior = ANOMALOUS_BEHAVIORS[image]
indicators = []
if behavior.get("required_arg") and behavior["required_arg"] not in cmd:
indicators.append(f"Missing required argument '{behavior['required_arg']}'")
if image in HOLLOWING_TARGETS:
# Check if process path is from unexpected location
expected_paths = ["\\windows\\system32\\", "\\windows\\syswow64\\"]
image_path = event.get("image", "").lower()
if not any(ep in image_path for ep in expected_paths):
indicators.append(f"Process running from unexpected path: {image_path}")
if not indicators:
return None
return {
"detection_type": "BEHAVIORAL_ANOMALY",
"technique": "T1055.012",
"process": image,
"full_image_path": event.get("image", ""),
"command_line": cmd,
"parent_process": get_process_name(event.get("parent_image", "")),
"hostname": event.get("hostname", "unknown"),
"user": event.get("user", "unknown"),
"timestamp": event.get("timestamp", "unknown"),
"risk_score": 50,
"risk_level": "MEDIUM",
"indicators": indicators,
}
def check_hollowing_target_network(event: dict) -> dict | None:
"""Detect hollowing targets making unusual network connections."""
if event.get("event_id") != "3":
return None
image = get_process_name(event.get("image", ""))
if image not in HOLLOWING_TARGETS:
return None
dest_ip = event.get("dest_ip", "")
dest_port = event.get("dest_port", "")
# Check for external connections from commonly hollowed processes
if dest_ip and not re.match(r"^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.)", dest_ip):
suspicious_ports = {"4444", "5555", "6666", "8888", "9090", "1234", "31337", "50050"}
risk = 40
indicators = [f"Hollowing target {image} connecting externally to {dest_ip}:{dest_port}"]
if dest_port in suspicious_ports:
risk += 20
indicators.append(f"Suspicious port: {dest_port}")
return {
"detection_type": "HOLLOWED_PROCESS_NETWORK",
"technique": "T1055.012",
"process": image,
"dest_ip": dest_ip,
"dest_port": dest_port,
"hostname": event.get("hostname", "unknown"),
"timestamp": event.get("timestamp", "unknown"),
"risk_score": risk,
"risk_level": "HIGH" if risk >= 50 else "MEDIUM",
"indicators": indicators,
}
return None
def run_hunt(input_path: str, output_dir: str) -> None:
"""Execute process hollowing hunt."""
print(f"[*] Process Hollowing Hunt - {datetime.datetime.now().isoformat()}")
events = parse_logs(input_path)
print(f"[*] Loaded {len(events)} events")
findings = []
stats = defaultdict(int)
detectors = [
check_process_tampering,
check_parent_child,
check_behavioral_anomaly,
check_hollowing_target_network,
]
for raw_event in events:
event = normalize_event(raw_event)
for detector in detectors:
result = detector(event)
if result:
findings.append(result)
stats[result["detection_type"]] += 1
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
with open(output_path / "hollowing_findings.json", "w", encoding="utf-8") as f:
json.dump({
"hunt_id": f"TH-HOLLOW-{datetime.date.today().isoformat()}",
"total_events": len(events),
"total_findings": len(findings),
"statistics": dict(stats),
"findings": sorted(findings, key=lambda x: x["risk_score"], reverse=True),
}, f, indent=2)
with open(output_path / "hunt_report.md", "w", encoding="utf-8") as f:
f.write(f"# Process Hollowing Hunt Report\n\n")
f.write(f"**Date**: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"**Findings**: {len(findings)}\n\n")
for finding in sorted(findings, key=lambda x: x["risk_score"], reverse=True)[:20]:
f.write(f"### [{finding['risk_level']}] {finding['detection_type']}\n")
f.write(f"- **Process**: {finding.get('process', '')}\n")
f.write(f"- **Host**: {finding['hostname']}\n")
f.write(f"- **Indicators**: {', '.join(finding['indicators'])}\n\n")
print(f"[+] {len(findings)} findings written to {output_dir}")
def main():
parser = argparse.ArgumentParser(description="Process Hollowing Detection")
subparsers = parser.add_subparsers(dest="command")
hunt_p = subparsers.add_parser("hunt")
hunt_p.add_argument("--input", "-i", required=True)
hunt_p.add_argument("--output", "-o", default="./hollowing_output")
subparsers.add_parser("queries")
args = parser.parse_args()
if args.command == "hunt":
run_hunt(args.input, args.output)
elif args.command == "queries":
print("=== Sysmon Queries ===")
print("--- Process Tampering ---")
print('index=sysmon EventCode=25\n| table _time Computer User Image Type')
print("\n--- Invalid Parent-Child ---")
print('index=sysmon EventCode=1 Image="*\\\\svchost.exe"\n| where NOT match(ParentImage, "(?i)services\\.exe")\n| table _time Computer Image ParentImage CommandLine')
else:
parser.print_help()
if __name__ == "__main__":
main()