#!/usr/bin/env python3 """Agent for detecting T1003 credential dumping via EDR telemetry analysis.""" import argparse import json import re import subprocess import sys from datetime import datetime, timezone LSASS_ACCESS_MASKS = { 0x1010: "PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ", 0x1FFFFF: "PROCESS_ALL_ACCESS", 0x1410: "PROCESS_QUERY_INFORMATION | PROCESS_VM_READ", 0x0040: "PROCESS_DUP_HANDLE", } CREDENTIAL_DUMP_TOOLS = [ "mimikatz", "procdump", "sqldumper", "comsvcs.dll", "nanodump", "pypykatz", "lazagne", "secretsdump", "gsecdump", "wce.exe", "fgdump", "pwdump", "ntdsutil", "reg save hklm\\sam", "reg save hklm\\system", ] SYSMON_EVENTS = { 1: "Process Creation", 10: "Process Access (LSASS read)", 11: "File Create (credential dump file)", 7: "Image Loaded (suspicious DLL)", } def check_lsass_access_sysmon(): """Query Sysmon Event ID 10 for LSASS process access.""" findings = [] if sys.platform != "win32": return findings ps_cmd = ( "Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Sysmon/Operational';" "Id=10} -MaxEvents 500 " "| Where-Object {$_.Properties[8].Value -match 'lsass'} " "| Select-Object TimeCreated," "@{N='SourceProcess';E={$_.Properties[4].Value}}," "@{N='SourcePID';E={$_.Properties[3].Value}}," "@{N='TargetProcess';E={$_.Properties[8].Value}}," "@{N='GrantedAccess';E={$_.Properties[10].Value}} " "| ConvertTo-Json -Depth 3" ) try: result = subprocess.check_output( ["powershell", "-NoProfile", "-Command", ps_cmd], text=True, errors="replace", timeout=30 ) data = json.loads(result) if result.strip() else [] if not isinstance(data, list): data = [data] for evt in data: source = evt.get("SourceProcess", "") access = evt.get("GrantedAccess", "") if not any(safe in source.lower() for safe in ["svchost", "csrss", "lsass", "wmiprvse"]): findings.append({ "time": evt.get("TimeCreated", ""), "source": source, "target": evt.get("TargetProcess", ""), "access_mask": access, "suspicious": True, }) except (subprocess.SubprocessError, json.JSONDecodeError): pass return findings def check_credential_dump_processes(): """Check for known credential dumping tool processes.""" findings = [] if sys.platform != "win32": return findings ps_cmd = ( "Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Sysmon/Operational';" "Id=1} -MaxEvents 1000 " "| Select-Object TimeCreated," "@{N='Image';E={$_.Properties[4].Value}}," "@{N='CommandLine';E={$_.Properties[10].Value}}," "@{N='ParentImage';E={$_.Properties[20].Value}} " "| ConvertTo-Json -Depth 3" ) try: result = subprocess.check_output( ["powershell", "-NoProfile", "-Command", ps_cmd], text=True, errors="replace", timeout=30 ) data = json.loads(result) if result.strip() else [] if not isinstance(data, list): data = [data] for evt in data: cmdline = (evt.get("CommandLine", "") or "").lower() image = (evt.get("Image", "") or "").lower() for tool in CREDENTIAL_DUMP_TOOLS: if tool.lower() in cmdline or tool.lower() in image: findings.append({ "time": evt.get("TimeCreated", ""), "image": evt.get("Image", ""), "commandline": evt.get("CommandLine", "")[:200], "tool_match": tool, }) break except (subprocess.SubprocessError, json.JSONDecodeError): pass return findings def check_sam_ntds_access(): """Check for SAM/NTDS.dit/SYSTEM registry hive access.""" findings = [] patterns = [ r"reg\s+save\s+hklm\\sam", r"reg\s+save\s+hklm\\system", r"reg\s+save\s+hklm\\security", r"ntdsutil.*\"ac\s+i\s+ntds\"", r"vssadmin.*create\s+shadow", r"copy.*ntds\.dit", ] if sys.platform != "win32": return findings ps_cmd = ( "Get-WinEvent -FilterHashtable @{LogName='Security';Id=4688} -MaxEvents 500 " "| Select-Object TimeCreated," "@{N='CommandLine';E={$_.Properties[8].Value}} " "| ConvertTo-Json" ) try: result = subprocess.check_output( ["powershell", "-NoProfile", "-Command", ps_cmd], text=True, errors="replace", timeout=30 ) data = json.loads(result) if result.strip() else [] if not isinstance(data, list): data = [data] for evt in data: cmdline = (evt.get("CommandLine", "") or "").lower() for pat in patterns: if re.search(pat, cmdline): findings.append({ "time": evt.get("TimeCreated", ""), "commandline": cmdline[:200], "pattern": pat, }) except (subprocess.SubprocessError, json.JSONDecodeError): pass return findings def main(): parser = argparse.ArgumentParser( description="Detect T1003 credential dumping via EDR telemetry" ) parser.add_argument("--output", "-o", help="Output JSON report") parser.add_argument("--verbose", "-v", action="store_true") args = parser.parse_args() print("[*] T1003 Credential Dumping Detection Agent") report = {"timestamp": datetime.now(timezone.utc).isoformat(), "findings": {}} lsass = check_lsass_access_sysmon() report["findings"]["lsass_access"] = lsass print(f"[*] Suspicious LSASS access events: {len(lsass)}") tools = check_credential_dump_processes() report["findings"]["dump_tools"] = tools print(f"[*] Credential dump tool detections: {len(tools)}") sam = check_sam_ntds_access() report["findings"]["sam_ntds_access"] = sam print(f"[*] SAM/NTDS access events: {len(sam)}") total = len(lsass) + len(tools) + len(sam) report["risk_level"] = "CRITICAL" if total >= 5 else "HIGH" if total >= 2 else "MEDIUM" if total > 0 else "LOW" if args.output: with open(args.output, "w") as f: json.dump(report, f, indent=2) print(f"[*] Report saved to {args.output}") else: print(json.dumps(report, indent=2)) if __name__ == "__main__": main()