mirror of
https://github.com/SHOGGOTH-SECTOR/sica-fondt.git
synced 2026-08-01 08:30:20 +00:00
Place useful parts of the surrounding repos into sica-fondt by layer, per the
body model (Ada = membrane; brain/endocrine/capabilities/knowledge non-Ada):
- brain/ LLM reasoning + providers (dapr, hermes, MoMoA)
- capabilities/ REPRAG sidecars: hermes tools/skills, dapr tools, parallel
dispatch, A51 channels, and the OSINT cluster
- knowledge/ LORAG corpus: 754 cyber-skills, agency personas, secure-coding,
MITRE ATT&CK data
- reference/ defensive threat-reference (C3, shhbruh doc) + AdaYaml parser
License handling: AGPL sources (worldosint, advanced_evolution, mercury,
Reticulum) and GPL DeTTECT are SPEC-only clean-room/port descriptions — no
copyleft code copied. MIT/Apache/data parts copied as working trees.
Safety: shhbruh escape/persistence material and C3 covert-C2 kept as reference
only, not wired into the running organism. See CONSOLIDATION.md.
https://claude.ai/code/session_01UehUqEXXJJCsHoA4voCU5c
512 lines
19 KiB
Python
512 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
LOLBin Threat Hunt Automation Script
|
|
Queries Windows Event Logs and Sysmon for suspicious LOLBin activity.
|
|
Supports Splunk API, Elastic API, and local Windows Event Log parsing.
|
|
"""
|
|
|
|
import json
|
|
import csv
|
|
import argparse
|
|
import datetime
|
|
import re
|
|
import sys
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
try:
|
|
import xml.etree.ElementTree as ET
|
|
except ImportError:
|
|
pass
|
|
|
|
# Known LOLBins and their suspicious argument patterns
|
|
LOLBIN_SIGNATURES = {
|
|
"certutil.exe": {
|
|
"attack_id": "T1140",
|
|
"suspicious_args": [
|
|
r"-urlcache\s+-split\s+-f",
|
|
r"-encode",
|
|
r"-decode",
|
|
r"-verifyctl",
|
|
r"http[s]?://",
|
|
],
|
|
"description": "Certificate utility abused for download/encode",
|
|
},
|
|
"mshta.exe": {
|
|
"attack_id": "T1218.005",
|
|
"suspicious_args": [
|
|
r"javascript:",
|
|
r"vbscript:",
|
|
r"http[s]?://",
|
|
r"\.hta",
|
|
],
|
|
"description": "HTML Application host for script execution",
|
|
},
|
|
"rundll32.exe": {
|
|
"attack_id": "T1218.011",
|
|
"suspicious_args": [
|
|
r"javascript:",
|
|
r"shell32\.dll.*ShellExec_RunDLL",
|
|
r"url\.dll.*FileProtocolHandler",
|
|
r"advpack\.dll.*RegisterOCX",
|
|
r"pcwutl\.dll.*LaunchApplication",
|
|
],
|
|
"description": "DLL loader abused for proxy execution",
|
|
},
|
|
"regsvr32.exe": {
|
|
"attack_id": "T1218.010",
|
|
"suspicious_args": [
|
|
r"/s\s+/n\s+/u\s+/i:",
|
|
r"scrobj\.dll",
|
|
r"http[s]?://",
|
|
],
|
|
"description": "COM registration utility for Squiblydoo attacks",
|
|
},
|
|
"msiexec.exe": {
|
|
"attack_id": "T1218.007",
|
|
"suspicious_args": [
|
|
r"/q.*http[s]?://",
|
|
r"/quiet.*http[s]?://",
|
|
r"/i\s+http[s]?://",
|
|
],
|
|
"description": "Windows Installer abused for remote MSI execution",
|
|
},
|
|
"bitsadmin.exe": {
|
|
"attack_id": "T1197",
|
|
"suspicious_args": [
|
|
r"/transfer",
|
|
r"/create.*http[s]?://",
|
|
r"/addfile.*http[s]?://",
|
|
r"/SetNotifyCmdLine",
|
|
],
|
|
"description": "BITS service abused for download and persistence",
|
|
},
|
|
"cmstp.exe": {
|
|
"attack_id": "T1218.003",
|
|
"suspicious_args": [
|
|
r"/s\s+/ns",
|
|
r"\.inf",
|
|
r"/au",
|
|
],
|
|
"description": "Connection Manager for UAC bypass and execution",
|
|
},
|
|
"wmic.exe": {
|
|
"attack_id": "T1047",
|
|
"suspicious_args": [
|
|
r"process\s+call\s+create",
|
|
r"/node:",
|
|
r"os\s+get",
|
|
r"format:",
|
|
r"http[s]?://",
|
|
],
|
|
"description": "WMI command-line for remote execution",
|
|
},
|
|
"msbuild.exe": {
|
|
"attack_id": "T1127.001",
|
|
"suspicious_args": [
|
|
r"\.xml",
|
|
r"\.csproj",
|
|
r"\.proj",
|
|
r"inline.*task",
|
|
],
|
|
"description": "Build tool abused for inline task execution",
|
|
},
|
|
"installutil.exe": {
|
|
"attack_id": "T1218.004",
|
|
"suspicious_args": [
|
|
r"/logfile=",
|
|
r"/LogToConsole=false",
|
|
r"/U",
|
|
],
|
|
"description": ".NET utility for managed code execution",
|
|
},
|
|
"forfiles.exe": {
|
|
"attack_id": "T1202",
|
|
"suspicious_args": [
|
|
r"/c\s+cmd",
|
|
r"/c\s+powershell",
|
|
r"/p\s+c:\\windows",
|
|
],
|
|
"description": "Indirect command execution utility",
|
|
},
|
|
"pcalua.exe": {
|
|
"attack_id": "T1202",
|
|
"suspicious_args": [
|
|
r"-a\s+.*\.exe",
|
|
r"-a\s+.*\.dll",
|
|
],
|
|
"description": "Program Compatibility Assistant for proxy execution",
|
|
},
|
|
}
|
|
|
|
# Suspicious parent processes for LOLBins
|
|
SUSPICIOUS_PARENTS = {
|
|
"winword.exe", "excel.exe", "powerpnt.exe", "outlook.exe",
|
|
"wmiprvse.exe", "w3wp.exe", "php-cgi.exe", "httpd.exe",
|
|
"nginx.exe", "tomcat.exe", "sqlservr.exe", "python.exe",
|
|
"wscript.exe", "cscript.exe",
|
|
}
|
|
|
|
|
|
def parse_sysmon_xml(xml_path: str) -> list[dict]:
|
|
"""Parse exported Sysmon Event ID 1 (Process Creation) XML logs."""
|
|
events = []
|
|
try:
|
|
tree = ET.parse(xml_path)
|
|
root = tree.getroot()
|
|
ns = {"ns": "http://schemas.microsoft.com/win/2004/08/events/event"}
|
|
|
|
for event in root.findall(".//ns:Event", ns):
|
|
event_data = {}
|
|
for data in event.findall(".//ns:Data", ns):
|
|
name = data.get("Name", "")
|
|
event_data[name] = data.text or ""
|
|
if event_data.get("Image"):
|
|
events.append(event_data)
|
|
except ET.ParseError as e:
|
|
print(f"[ERROR] Failed to parse XML: {e}")
|
|
except FileNotFoundError:
|
|
print(f"[ERROR] File not found: {xml_path}")
|
|
return events
|
|
|
|
|
|
def parse_csv_logs(csv_path: str) -> list[dict]:
|
|
"""Parse CSV-exported process creation logs."""
|
|
events = []
|
|
try:
|
|
with open(csv_path, "r", encoding="utf-8-sig") as f:
|
|
reader = csv.DictReader(f)
|
|
for row in reader:
|
|
events.append(dict(row))
|
|
except FileNotFoundError:
|
|
print(f"[ERROR] File not found: {csv_path}")
|
|
return events
|
|
|
|
|
|
def parse_json_logs(json_path: str) -> list[dict]:
|
|
"""Parse JSON-exported process creation logs."""
|
|
events = []
|
|
try:
|
|
with open(json_path, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
if isinstance(data, list):
|
|
events = data
|
|
elif isinstance(data, dict) and "events" in data:
|
|
events = data["events"]
|
|
elif isinstance(data, dict) and "hits" in data:
|
|
events = [h.get("_source", h) for h in data["hits"].get("hits", [])]
|
|
except (FileNotFoundError, json.JSONDecodeError) as e:
|
|
print(f"[ERROR] Failed to parse JSON: {e}")
|
|
return events
|
|
|
|
|
|
def normalize_event(event: dict) -> dict:
|
|
"""Normalize event fields across different log formats."""
|
|
normalized = {}
|
|
field_mappings = {
|
|
"image": ["Image", "image", "process_name", "FileName", "process.executable"],
|
|
"command_line": ["CommandLine", "command_line", "ProcessCommandLine", "process.command_line", "cmdline"],
|
|
"parent_image": ["ParentImage", "parent_image", "InitiatingProcessFileName", "process.parent.executable"],
|
|
"user": ["User", "user", "AccountName", "user.name", "SubjectUserName"],
|
|
"timestamp": ["UtcTime", "timestamp", "Timestamp", "@timestamp", "TimeCreated"],
|
|
"hostname": ["Computer", "hostname", "DeviceName", "host.name", "ComputerName"],
|
|
"pid": ["ProcessId", "pid", "process_id", "process.pid"],
|
|
"parent_pid": ["ParentProcessId", "parent_pid", "ppid", "process.parent.pid"],
|
|
}
|
|
for target_field, source_fields in field_mappings.items():
|
|
for src in source_fields:
|
|
if src in event and event[src]:
|
|
normalized[target_field] = str(event[src])
|
|
break
|
|
if target_field not in normalized:
|
|
normalized[target_field] = ""
|
|
return normalized
|
|
|
|
|
|
def analyze_lolbin_event(event: dict) -> dict | None:
|
|
"""Analyze a single process event for LOLBin abuse indicators."""
|
|
image = event.get("image", "").lower()
|
|
command_line = event.get("command_line", "")
|
|
parent_image = event.get("parent_image", "").lower()
|
|
|
|
binary_name = image.split("\\")[-1].split("/")[-1] if image else ""
|
|
|
|
if binary_name not in LOLBIN_SIGNATURES:
|
|
return None
|
|
|
|
sig = LOLBIN_SIGNATURES[binary_name]
|
|
finding = {
|
|
"binary": binary_name,
|
|
"attack_id": sig["attack_id"],
|
|
"description": sig["description"],
|
|
"command_line": command_line,
|
|
"parent_process": parent_image.split("\\")[-1].split("/")[-1] if parent_image else "unknown",
|
|
"user": event.get("user", "unknown"),
|
|
"hostname": event.get("hostname", "unknown"),
|
|
"timestamp": event.get("timestamp", "unknown"),
|
|
"risk_score": 0,
|
|
"indicators": [],
|
|
}
|
|
|
|
# Check for suspicious command-line arguments
|
|
for pattern in sig["suspicious_args"]:
|
|
if re.search(pattern, command_line, re.IGNORECASE):
|
|
finding["risk_score"] += 30
|
|
finding["indicators"].append(f"Suspicious argument pattern: {pattern}")
|
|
|
|
# Check for suspicious parent processes
|
|
parent_name = finding["parent_process"]
|
|
if parent_name in SUSPICIOUS_PARENTS:
|
|
finding["risk_score"] += 25
|
|
finding["indicators"].append(f"Suspicious parent process: {parent_name}")
|
|
|
|
# Check for network-related indicators in command line
|
|
if re.search(r"http[s]?://", command_line, re.IGNORECASE):
|
|
finding["risk_score"] += 20
|
|
finding["indicators"].append("Contains URL - possible download/C2")
|
|
|
|
# Check for encoded content
|
|
if re.search(r"(base64|encode|decode|-enc\s)", command_line, re.IGNORECASE):
|
|
finding["risk_score"] += 15
|
|
finding["indicators"].append("Contains encoding/decoding operation")
|
|
|
|
# Check for execution from unusual paths
|
|
unusual_paths = [r"\\temp\\", r"\\tmp\\", r"\\appdata\\", r"\\programdata\\", r"\\public\\", r"\\downloads\\"]
|
|
for path_pattern in unusual_paths:
|
|
if re.search(path_pattern, command_line, re.IGNORECASE):
|
|
finding["risk_score"] += 10
|
|
finding["indicators"].append(f"References unusual path: {path_pattern}")
|
|
|
|
# Only return if there are actual indicators
|
|
if finding["indicators"]:
|
|
finding["risk_level"] = (
|
|
"CRITICAL" if finding["risk_score"] >= 60
|
|
else "HIGH" if finding["risk_score"] >= 40
|
|
else "MEDIUM" if finding["risk_score"] >= 20
|
|
else "LOW"
|
|
)
|
|
return finding
|
|
return None
|
|
|
|
|
|
def generate_splunk_queries() -> dict[str, str]:
|
|
"""Generate Splunk SPL queries for LOLBin hunting."""
|
|
queries = {}
|
|
|
|
# General LOLBin network activity
|
|
queries["lolbin_network_activity"] = """index=sysmon EventCode=3
|
|
| where match(Image, "(?i)(certutil|mshta|rundll32|regsvr32|msiexec|bitsadmin|cmstp|wmic)\.exe$")
|
|
| stats count values(DestinationIp) as dest_ips values(DestinationPort) as dest_ports by Image Computer User
|
|
| where count > 0
|
|
| sort -count"""
|
|
|
|
# Certutil download cradle
|
|
queries["certutil_download"] = """index=sysmon EventCode=1 Image="*\\certutil.exe"
|
|
| where match(CommandLine, "(?i)(urlcache|split|decode|encode|http)")
|
|
| table _time Computer User Image CommandLine ParentImage"""
|
|
|
|
# Regsvr32 Squiblydoo
|
|
queries["regsvr32_squiblydoo"] = """index=sysmon EventCode=1 Image="*\\regsvr32.exe"
|
|
| where match(CommandLine, "(?i)(scrobj|/i:http|/s /n /u)")
|
|
| table _time Computer User CommandLine ParentImage"""
|
|
|
|
# Mshta script execution
|
|
queries["mshta_script_exec"] = """index=sysmon EventCode=1 Image="*\\mshta.exe"
|
|
| where match(CommandLine, "(?i)(javascript|vbscript|http)")
|
|
| table _time Computer User CommandLine ParentImage"""
|
|
|
|
# LOLBin with suspicious parent
|
|
queries["lolbin_suspicious_parent"] = """index=sysmon EventCode=1
|
|
| where match(Image, "(?i)(certutil|mshta|rundll32|regsvr32|bitsadmin)\.exe$")
|
|
| where match(ParentImage, "(?i)(winword|excel|powerpnt|outlook|wmiprvse|w3wp)\.exe$")
|
|
| table _time Computer User Image CommandLine ParentImage"""
|
|
|
|
return queries
|
|
|
|
|
|
def generate_kql_queries() -> dict[str, str]:
|
|
"""Generate KQL queries for Microsoft Defender for Endpoint."""
|
|
queries = {}
|
|
|
|
queries["lolbin_suspicious_execution"] = """DeviceProcessEvents
|
|
| where Timestamp > ago(7d)
|
|
| where FileName in~ ("certutil.exe","mshta.exe","rundll32.exe","regsvr32.exe","bitsadmin.exe","cmstp.exe","msiexec.exe","wmic.exe")
|
|
| where ProcessCommandLine has_any ("http","ftp","urlcache","-decode","-encode","scrobj","javascript","vbscript","transfer","/i:")
|
|
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine, InitiatingProcessFileName
|
|
| order by Timestamp desc"""
|
|
|
|
queries["lolbin_network_connections"] = """DeviceNetworkEvents
|
|
| where Timestamp > ago(7d)
|
|
| where InitiatingProcessFileName in~ ("certutil.exe","mshta.exe","rundll32.exe","regsvr32.exe","bitsadmin.exe")
|
|
| where RemoteIPType == "Public"
|
|
| summarize ConnectionCount=count(), RemoteIPs=make_set(RemoteIP) by InitiatingProcessFileName, DeviceName
|
|
| order by ConnectionCount desc"""
|
|
|
|
queries["lolbin_from_office"] = """DeviceProcessEvents
|
|
| where Timestamp > ago(7d)
|
|
| where InitiatingProcessFileName in~ ("winword.exe","excel.exe","powerpnt.exe","outlook.exe")
|
|
| where FileName in~ ("certutil.exe","mshta.exe","rundll32.exe","regsvr32.exe","powershell.exe","cmd.exe","wscript.exe","cscript.exe")
|
|
| project Timestamp, DeviceName, AccountName, InitiatingProcessFileName, FileName, ProcessCommandLine"""
|
|
|
|
return queries
|
|
|
|
|
|
def run_hunt(input_path: str, output_dir: str, log_format: str = "auto") -> None:
|
|
"""Execute the LOLBin threat hunt against provided log data."""
|
|
print(f"[*] LOLBin Threat Hunt Starting - {datetime.datetime.now().isoformat()}")
|
|
print(f"[*] Input: {input_path}")
|
|
print(f"[*] Output: {output_dir}")
|
|
|
|
# Parse input logs
|
|
if log_format == "auto":
|
|
if input_path.endswith(".xml"):
|
|
log_format = "xml"
|
|
elif input_path.endswith(".csv"):
|
|
log_format = "csv"
|
|
elif input_path.endswith(".json"):
|
|
log_format = "json"
|
|
else:
|
|
print("[ERROR] Cannot determine log format. Use --format flag.")
|
|
sys.exit(1)
|
|
|
|
print(f"[*] Parsing {log_format} logs...")
|
|
if log_format == "xml":
|
|
raw_events = parse_sysmon_xml(input_path)
|
|
elif log_format == "csv":
|
|
raw_events = parse_csv_logs(input_path)
|
|
elif log_format == "json":
|
|
raw_events = parse_json_logs(input_path)
|
|
else:
|
|
print(f"[ERROR] Unsupported format: {log_format}")
|
|
sys.exit(1)
|
|
|
|
print(f"[*] Parsed {len(raw_events)} events")
|
|
|
|
# Normalize and analyze
|
|
findings = []
|
|
stats = defaultdict(int)
|
|
|
|
for raw_event in raw_events:
|
|
event = normalize_event(raw_event)
|
|
result = analyze_lolbin_event(event)
|
|
if result:
|
|
findings.append(result)
|
|
stats[result["binary"]] += 1
|
|
stats[result["risk_level"]] += 1
|
|
|
|
print(f"[*] Analysis complete - {len(findings)} suspicious events found")
|
|
|
|
# Output results
|
|
output_path = Path(output_dir)
|
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Write findings JSON
|
|
findings_file = output_path / "lolbin_findings.json"
|
|
with open(findings_file, "w", encoding="utf-8") as f:
|
|
json.dump({
|
|
"hunt_id": f"TH-LOLBIN-{datetime.date.today().isoformat()}",
|
|
"timestamp": datetime.datetime.now().isoformat(),
|
|
"total_events_analyzed": len(raw_events),
|
|
"total_findings": len(findings),
|
|
"statistics": dict(stats),
|
|
"findings": findings,
|
|
}, f, indent=2)
|
|
print(f"[+] Findings written to {findings_file}")
|
|
|
|
# Write CSV summary
|
|
csv_file = output_path / "lolbin_findings.csv"
|
|
if findings:
|
|
with open(csv_file, "w", newline="", encoding="utf-8") as f:
|
|
writer = csv.DictWriter(f, fieldnames=findings[0].keys())
|
|
writer.writeheader()
|
|
for finding in findings:
|
|
row = dict(finding)
|
|
row["indicators"] = "; ".join(row["indicators"])
|
|
writer.writerow(row)
|
|
print(f"[+] CSV summary written to {csv_file}")
|
|
|
|
# Write hunt report
|
|
report_file = output_path / "hunt_report.md"
|
|
with open(report_file, "w", encoding="utf-8") as f:
|
|
f.write(f"# LOLBin Threat Hunt Report\n\n")
|
|
f.write(f"**Hunt ID**: TH-LOLBIN-{datetime.date.today().isoformat()}\n")
|
|
f.write(f"**Date**: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
|
f.write(f"**Events Analyzed**: {len(raw_events)}\n")
|
|
f.write(f"**Findings**: {len(findings)}\n\n")
|
|
f.write("## Statistics\n\n")
|
|
f.write("| Metric | Count |\n|--------|-------|\n")
|
|
for key, count in sorted(stats.items()):
|
|
f.write(f"| {key} | {count} |\n")
|
|
f.write("\n## Top Findings\n\n")
|
|
for finding in sorted(findings, key=lambda x: x["risk_score"], reverse=True)[:20]:
|
|
f.write(f"### [{finding['risk_level']}] {finding['binary']} - {finding['attack_id']}\n")
|
|
f.write(f"- **Host**: {finding['hostname']}\n")
|
|
f.write(f"- **User**: {finding['user']}\n")
|
|
f.write(f"- **Parent**: {finding['parent_process']}\n")
|
|
f.write(f"- **Command**: `{finding['command_line'][:200]}`\n")
|
|
f.write(f"- **Indicators**: {', '.join(finding['indicators'])}\n\n")
|
|
print(f"[+] Hunt report written to {report_file}")
|
|
|
|
# Print summary
|
|
print("\n" + "=" * 60)
|
|
print("HUNT SUMMARY")
|
|
print("=" * 60)
|
|
print(f"Total Events Analyzed: {len(raw_events)}")
|
|
print(f"Suspicious Findings: {len(findings)}")
|
|
print(f" CRITICAL: {stats.get('CRITICAL', 0)}")
|
|
print(f" HIGH: {stats.get('HIGH', 0)}")
|
|
print(f" MEDIUM: {stats.get('MEDIUM', 0)}")
|
|
print(f" LOW: {stats.get('LOW', 0)}")
|
|
print("=" * 60)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="LOLBin Threat Hunt - Detect abuse of Living-off-the-Land Binaries"
|
|
)
|
|
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
|
|
# Hunt command
|
|
hunt_parser = subparsers.add_parser("hunt", help="Run LOLBin hunt against log data")
|
|
hunt_parser.add_argument("--input", "-i", required=True, help="Path to log file (XML, CSV, JSON)")
|
|
hunt_parser.add_argument("--output", "-o", default="./hunt_output", help="Output directory")
|
|
hunt_parser.add_argument("--format", "-f", default="auto", choices=["auto", "xml", "csv", "json"])
|
|
|
|
# Queries command
|
|
queries_parser = subparsers.add_parser("queries", help="Generate hunting queries")
|
|
queries_parser.add_argument("--platform", "-p", choices=["splunk", "kql", "all"], default="all")
|
|
queries_parser.add_argument("--output", "-o", help="Output file for queries")
|
|
|
|
# Signatures command
|
|
subparsers.add_parser("signatures", help="List LOLBin signatures and indicators")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.command == "hunt":
|
|
run_hunt(args.input, args.output, args.format)
|
|
elif args.command == "queries":
|
|
if args.platform in ("splunk", "all"):
|
|
print("\n=== SPLUNK SPL QUERIES ===\n")
|
|
for name, query in generate_splunk_queries().items():
|
|
print(f"--- {name} ---")
|
|
print(query)
|
|
print()
|
|
if args.platform in ("kql", "all"):
|
|
print("\n=== KQL QUERIES (Microsoft Defender) ===\n")
|
|
for name, query in generate_kql_queries().items():
|
|
print(f"--- {name} ---")
|
|
print(query)
|
|
print()
|
|
elif args.command == "signatures":
|
|
print("\n=== LOLBin Signatures ===\n")
|
|
print(f"{'Binary':<20} {'ATT&CK ID':<15} {'Description'}")
|
|
print("-" * 80)
|
|
for binary, sig in LOLBIN_SIGNATURES.items():
|
|
print(f"{binary:<20} {sig['attack_id']:<15} {sig['description']}")
|
|
else:
|
|
parser.print_help()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|