Files
Anthropic-Cybersecurity-Skills/skills/detecting-credential-dumping-with-edr/scripts/process.py
T

384 lines
14 KiB
Python

#!/usr/bin/env python3
"""
Credential Dumping Detection Script
Analyzes process access logs for LSASS memory access, SAM extraction,
DCSync activity, and other credential theft indicators.
"""
import json
import csv
import argparse
import datetime
import re
import sys
from collections import defaultdict
from pathlib import Path
# Suspicious LSASS access masks indicating credential dumping
SUSPICIOUS_ACCESS_MASKS = {
"0x1FFFFF": {"risk": "CRITICAL", "description": "PROCESS_ALL_ACCESS - full process access"},
"0x1010": {"risk": "HIGH", "description": "PROCESS_VM_READ + PROCESS_QUERY_INFORMATION (Mimikatz default)"},
"0x1038": {"risk": "HIGH", "description": "Common credential dumping access mask"},
"0x0410": {"risk": "MEDIUM", "description": "PROCESS_QUERY_INFORMATION + PROCESS_VM_READ"},
"0x1400": {"risk": "MEDIUM", "description": "PROCESS_QUERY_INFORMATION + PROCESS_QUERY_LIMITED"},
"0x0040": {"risk": "HIGH", "description": "PROCESS_DUP_HANDLE - handle duplication"},
"0x0810": {"risk": "HIGH", "description": "PROCESS_SUSPEND_RESUME + PROCESS_VM_READ"},
"0x1fffff": {"risk": "CRITICAL", "description": "PROCESS_ALL_ACCESS (lowercase)"},
}
# Legitimate processes that commonly access LSASS
LSASS_WHITELIST = {
"csrss.exe", "svchost.exe", "services.exe", "lsass.exe", "wininit.exe",
"smss.exe", "wmiprvse.exe", "taskmgr.exe", "procexp.exe", "procexp64.exe",
"msmpsvc.exe", "msmpeng.exe", "nissrv.exe", "mssense.exe", "sensecncproxy.exe",
"csfalconservice.exe", "csfalconcontainer.exe",
"sentinelagent.exe", "sentinelone.exe",
"cb.exe", "carbonblack.exe",
"logrhythmagent.exe",
}
# Known credential dumping tool command-line patterns
CRED_DUMP_TOOL_PATTERNS = {
"mimikatz": {
"patterns": [
r"sekurlsa::",
r"lsadump::",
r"kerberos::list",
r"crypto::cng",
r"privilege::debug",
r"token::elevate",
r"dpapi::",
r"vault::cred",
],
"technique": "T1003.001/T1003.006",
},
"comsvcs_minidump": {
"patterns": [
r"comsvcs\.dll.*MiniDump",
r"comsvcs\.dll.*#24",
],
"technique": "T1003.001",
},
"procdump": {
"patterns": [
r"procdump.*-ma.*lsass",
r"procdump.*lsass.*-ma",
r"procdump.*-accepteula.*lsass",
],
"technique": "T1003.001",
},
"reg_save": {
"patterns": [
r"reg\s+(save|export)\s+HKLM\\SAM",
r"reg\s+(save|export)\s+HKLM\\SECURITY",
r"reg\s+(save|export)\s+HKLM\\SYSTEM",
],
"technique": "T1003.002",
},
"ntdsutil": {
"patterns": [
r"ntdsutil.*ifm",
r"ntdsutil.*\"activate instance ntds\"",
r"ntdsutil.*create full",
],
"technique": "T1003.003",
},
"vssadmin_shadow": {
"patterns": [
r"vssadmin.*create\s+shadow",
r"copy.*GLOBALROOT.*Device.*HarddiskVolumeShadowCopy",
r"wmic.*shadowcopy.*create",
],
"technique": "T1003.003",
},
"secretsdump": {
"patterns": [
r"secretsdump",
r"impacket.*dump",
],
"technique": "T1003.002/T1003.003/T1003.006",
},
"lazagne": {
"patterns": [
r"lazagne",
r"LaZagne\.exe",
],
"technique": "T1003.001/T1555",
},
"sharpdump": {
"patterns": [
r"SharpDump",
r"sharpdump",
],
"technique": "T1003.001",
},
"nanodump": {
"patterns": [
r"nanodump",
],
"technique": "T1003.001",
},
}
# DCSync detection GUIDs
DCSYNC_GUIDS = {
"1131f6aa-9c07-11d1-f79f-00c04fc2dcd2": "DS-Replication-Get-Changes",
"1131f6ad-9c07-11d1-f79f-00c04fc2dcd2": "DS-Replication-Get-Changes-All",
"89e95b76-444d-4c62-991a-0facbeda640c": "DS-Replication-Get-Changes-In-Filtered-Set",
}
def parse_logs(input_path: str) -> list[dict]:
"""Parse log files in JSON or CSV format."""
events = []
path = Path(input_path)
if path.suffix == ".json":
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
events = data if isinstance(data, list) else data.get("events", data.get("hits", {}).get("hits", []))
if events and isinstance(events[0], dict) and "_source" in events[0]:
events = [e["_source"] for e in events]
elif path.suffix == ".csv":
with open(path, "r", encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
events = [dict(row) for row in reader]
return events
def normalize_event(event: dict) -> dict:
"""Normalize event field names."""
field_map = {
"source_image": ["SourceImage", "source_image", "InitiatingProcessFileName", "process.executable"],
"target_image": ["TargetImage", "target_image", "FileName", "target.process.executable"],
"granted_access": ["GrantedAccess", "granted_access", "AccessMask"],
"command_line": ["CommandLine", "command_line", "ProcessCommandLine", "process.command_line"],
"user": ["User", "user", "AccountName", "SubjectUserName", "user.name"],
"hostname": ["Computer", "hostname", "DeviceName", "host.name"],
"timestamp": ["UtcTime", "timestamp", "Timestamp", "@timestamp"],
"event_id": ["EventID", "EventCode", "event_id", "event.code"],
"parent_image": ["ParentImage", "parent_image", "InitiatingProcessParentFileName"],
"properties": ["Properties", "properties", "ObjectType"],
}
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 detect_lsass_access(event: dict) -> dict | None:
"""Detect suspicious LSASS process access."""
target = event.get("target_image", "").lower()
if "lsass.exe" not in target:
return None
source = event.get("source_image", "").lower()
source_name = source.split("\\")[-1].split("/")[-1]
access = event.get("granted_access", "").lower()
# Skip whitelisted processes
if source_name in LSASS_WHITELIST:
return None
risk_info = SUSPICIOUS_ACCESS_MASKS.get(access, SUSPICIOUS_ACCESS_MASKS.get(access.upper()))
if not risk_info:
risk_info = {"risk": "LOW", "description": f"Unknown access mask: {access}"}
return {
"detection_type": "LSASS_ACCESS",
"technique": "T1003.001",
"source_process": event.get("source_image", ""),
"target_process": event.get("target_image", ""),
"granted_access": access,
"access_description": risk_info["description"],
"risk_level": risk_info["risk"],
"user": event.get("user", "unknown"),
"hostname": event.get("hostname", "unknown"),
"timestamp": event.get("timestamp", "unknown"),
"indicators": [f"LSASS access from {source_name} with mask {access}"],
}
def detect_credential_tool(event: dict) -> dict | None:
"""Detect known credential dumping tool execution."""
cmd = event.get("command_line", "")
if not cmd:
return None
for tool_name, tool_info in CRED_DUMP_TOOL_PATTERNS.items():
for pattern in tool_info["patterns"]:
if re.search(pattern, cmd, re.IGNORECASE):
return {
"detection_type": "CREDENTIAL_TOOL",
"technique": tool_info["technique"],
"tool": tool_name,
"command_line": cmd,
"source_process": event.get("source_image", ""),
"parent_process": event.get("parent_image", ""),
"risk_level": "CRITICAL",
"user": event.get("user", "unknown"),
"hostname": event.get("hostname", "unknown"),
"timestamp": event.get("timestamp", "unknown"),
"indicators": [f"Credential tool detected: {tool_name}", f"Pattern matched: {pattern}"],
}
return None
def detect_dcsync(event: dict) -> dict | None:
"""Detect DCSync activity from non-DC sources."""
props = event.get("properties", "")
for guid, name in DCSYNC_GUIDS.items():
if guid.lower() in props.lower():
return {
"detection_type": "DCSYNC",
"technique": "T1003.006",
"replication_right": name,
"guid": guid,
"risk_level": "CRITICAL",
"user": event.get("user", "unknown"),
"hostname": event.get("hostname", "unknown"),
"timestamp": event.get("timestamp", "unknown"),
"indicators": [f"DCSync activity: {name}", f"GUID: {guid}"],
}
return None
def run_hunt(input_path: str, output_dir: str, dc_list: list[str] | None = None) -> None:
"""Execute credential dumping hunt."""
print(f"[*] Credential Dumping Hunt - {datetime.datetime.now().isoformat()}")
print(f"[*] Input: {input_path}")
events = parse_logs(input_path)
print(f"[*] Loaded {len(events)} events")
findings = []
stats = defaultdict(int)
for raw_event in events:
event = normalize_event(raw_event)
# Check for LSASS access
result = detect_lsass_access(event)
if result:
findings.append(result)
stats["LSASS_ACCESS"] += 1
stats[result["risk_level"]] += 1
# Check for credential dumping tools
result = detect_credential_tool(event)
if result:
findings.append(result)
stats["CREDENTIAL_TOOL"] += 1
stats[result["risk_level"]] += 1
# Check for DCSync
result = detect_dcsync(event)
if result:
if dc_list and result["hostname"].lower() in [dc.lower() for dc in dc_list]:
continue # Skip legitimate DC replication
findings.append(result)
stats["DCSYNC"] += 1
stats[result["risk_level"]] += 1
# Write output
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
findings_file = output_path / "credential_dump_findings.json"
with open(findings_file, "w", encoding="utf-8") as f:
json.dump({
"hunt_id": f"TH-CRED-DUMP-{datetime.date.today().isoformat()}",
"timestamp": datetime.datetime.now().isoformat(),
"total_events": len(events),
"total_findings": len(findings),
"statistics": dict(stats),
"findings": findings,
}, f, indent=2)
# Write report
report_file = output_path / "hunt_report.md"
with open(report_file, "w", encoding="utf-8") as f:
f.write(f"# Credential Dumping Hunt Report\n\n")
f.write(f"**Date**: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"**Events Analyzed**: {len(events)}\n")
f.write(f"**Findings**: {len(findings)}\n\n")
f.write("## Detection Breakdown\n\n")
for key, count in sorted(stats.items()):
f.write(f"- {key}: {count}\n")
f.write("\n## Critical Findings\n\n")
for finding in sorted(findings, key=lambda x: ("CRITICAL", "HIGH", "MEDIUM", "LOW").index(x["risk_level"])):
if finding["risk_level"] in ("CRITICAL", "HIGH"):
f.write(f"### [{finding['risk_level']}] {finding['detection_type']} - {finding['technique']}\n")
f.write(f"- **Host**: {finding['hostname']}\n")
f.write(f"- **User**: {finding['user']}\n")
f.write(f"- **Indicators**: {', '.join(finding['indicators'])}\n\n")
print(f"[+] Output written to {output_dir}")
print(f"\n{'='*60}")
print(f"FINDINGS: {len(findings)} | CRITICAL: {stats.get('CRITICAL',0)} | HIGH: {stats.get('HIGH',0)}")
print(f"{'='*60}")
def generate_queries(platform: str) -> None:
"""Generate hunting queries for specified platform."""
if platform in ("splunk", "all"):
print("\n=== SPLUNK QUERIES ===\n")
print("--- LSASS Access Detection ---")
print("""index=sysmon EventCode=10 TargetImage="*\\\\lsass.exe"
| where NOT match(SourceImage, "(?i)(csrss|svchost|services|lsass|wininit|MsMpEng)")
| stats count by SourceImage GrantedAccess Computer User
| sort -count""")
print("\n--- Credential Tool Detection ---")
print("""index=sysmon EventCode=1
| where match(CommandLine, "(?i)(sekurlsa|lsadump|comsvcs.*MiniDump|procdump.*lsass|reg save.*SAM)")
| table _time Computer User Image CommandLine ParentImage""")
print("\n--- DCSync Detection ---")
print("""index=wineventlog EventCode=4662
| where match(Properties, "(?i)(1131f6aa|1131f6ad|89e95b76)")
| table _time SubjectUserName SubjectDomainName Computer Properties""")
if platform in ("kql", "all"):
print("\n=== KQL QUERIES ===\n")
print("--- LSASS Access ---")
print("""DeviceEvents
| where ActionType == "OpenProcessApiCall"
| where FileName == "lsass.exe"
| where InitiatingProcessFileName !in~ ("csrss.exe","svchost.exe","MsMpEng.exe")
| project Timestamp, DeviceName, AccountName, InitiatingProcessFileName, AdditionalFields""")
def main():
parser = argparse.ArgumentParser(description="Credential Dumping Detection Hunt")
subparsers = parser.add_subparsers(dest="command")
hunt_parser = subparsers.add_parser("hunt", help="Run credential dumping hunt")
hunt_parser.add_argument("--input", "-i", required=True, help="Log file path")
hunt_parser.add_argument("--output", "-o", default="./cred_dump_output", help="Output directory")
hunt_parser.add_argument("--dc-list", nargs="*", help="List of known DCs to exclude from DCSync alerts")
query_parser = subparsers.add_parser("queries", help="Generate hunting queries")
query_parser.add_argument("--platform", "-p", choices=["splunk", "kql", "all"], default="all")
subparsers.add_parser("signatures", help="List detection signatures")
args = parser.parse_args()
if args.command == "hunt":
run_hunt(args.input, args.output, args.dc_list)
elif args.command == "queries":
generate_queries(args.platform)
elif args.command == "signatures":
print("\n=== Credential Dumping Tool Signatures ===\n")
for tool, info in CRED_DUMP_TOOL_PATTERNS.items():
print(f"{tool:<25} {info['technique']:<25} Patterns: {len(info['patterns'])}")
else:
parser.print_help()
if __name__ == "__main__":
main()