threat hunting

Hunting for Startup Folder Persistence

Detect T1547.001 startup folder persistence by monitoring Windows startup directories for suspicious file creation, analyzing autoruns entries, and using Python watchdog for real-time filesystem monitoring.

autorunsfilesystem-monitoringpersistencestartup-foldert1547.001threat-huntingwatchdog
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

Attackers use Windows startup folders for persistence (MITRE ATT&CK T1547.001 — Boot or Logon Autostart Execution: Registry Run Keys / Startup Folder). Files placed in %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup or C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup execute automatically at user logon. This skill scans startup directories for suspicious files, monitors for real-time changes using Python watchdog, and analyzes file metadata to detect persistence implants.

When to Use

  • When investigating security incidents that require hunting for startup folder persistence
  • When building detection rules or threat hunting queries for this domain
  • When SOC analysts need structured procedures for this analysis type
  • When validating security monitoring coverage for related attack techniques

Prerequisites

  • Python 3.9+ with watchdog, pefile (optional for PE analysis)
  • Access to Windows startup folders (user and all-users)
  • Windows Event Logs for Event ID 4663 correlation (optional)

Steps

  1. Enumerate all files in user and system startup directories
  2. Analyze file types, creation timestamps, and digital signatures
  3. Flag suspicious file extensions (.bat, .vbs, .ps1, .lnk, .exe)
  4. Check for recently created files (< 7 days) as potential implants
  5. Monitor startup folders in real-time using watchdog FileSystemEventHandler
  6. Correlate with known legitimate startup entries
  7. Generate threat hunting report with T1547.001 MITRE mapping

Expected Output

  • JSON report listing all startup folder contents with risk scores, file metadata, and suspicious indicators
  • Real-time monitoring alerts for new file creation in startup directories
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md2.8 KB

API Reference — Hunting for Startup Folder Persistence

Libraries Used

  • watchdog: Real-time filesystem monitoring — Observer, FileSystemEventHandler
  • hashlib: SHA-256 file hashing
  • subprocess: Registry Run key queries via reg query
  • pathlib: Cross-platform path handling and file metadata

CLI Interface

python agent.py scan
python agent.py registry
python agent.py monitor --duration 120
python agent.py full

Core Functions

get_startup_paths() — Enumerate startup directories

Returns user startup (%APPDATA%\...\Startup) and all-users startup (%PROGRAMDATA%\...\Startup) paths.

analyze_file(filepath, scope) — Single file risk analysis

Computes SHA-256 hash, checks extension against risk table, evaluates file age, size, baseline membership. Risk scoring by extension, recency, and scope.

scan_startup_folders() — Full startup directory scan

Iterates all files in both startup paths. Returns sorted by risk score.

check_registry_run_keys() — Registry autostart audit

Queries 4 Registry Run keys via reg query:

  • HKCU\...\Run, HKCU\...\RunOnce
  • HKLM\...\Run, HKLM\...\RunOnce Flags entries containing powershell, cmd.exe, temp paths, encoded commands.

StartupMonitorHandler — Watchdog event handler

Subclasses FileSystemEventHandler. Handles on_created, on_modified, on_deleted. Runs analyze_file() on new files and prints JSON alerts.

monitor_startup(duration_seconds) — Real-time monitoring

Creates Observer, schedules handler on all startup paths. Monitors for specified duration. Returns detected events.

full_hunt() — Comprehensive persistence hunt

Startup Folder Paths

Scope Path
Current User %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup
All Users %PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs\Startup

File Extension Risk Scores

Extension Base Score Notes
.ps1 45 PowerShell script
.hta 45 HTML Application
.pif 45 Program Information File
.vbs, .vbe 40 VBScript
.js, .jse 40 JScript
.wsf, .wsh 35-40 Windows Script
.bat, .cmd 35 Batch file
.exe 30 Executable
.scr 40 Screen saver (executable)
.url 20 Internet shortcut
.lnk 15 Shortcut (often legitimate)

Additional Risk Factors

Factor Points
Created within 7 days +25
Created within 24 hours +15
Zero-byte file +10
File > 10 MB +10
Not in baseline +10
All-users scope +10

MITRE ATT&CK Mapping

  • T1547.001 — Boot or Logon Autostart Execution: Registry Run Keys / Startup Folder
  • Tactics: Persistence, Privilege Escalation

Dependencies

  • watchdog >= 3.0.0
  • Windows OS (startup folder paths are Windows-specific)

Scripts 1

agent.py10.2 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Agent for hunting T1547.001 startup folder persistence on Windows."""

import json
import os
import hashlib
import argparse
import time
from datetime import datetime
from pathlib import Path

try:
    from watchdog.observers import Observer
    from watchdog.events import FileSystemEventHandler
except ImportError:
    Observer = None
    FileSystemEventHandler = object

SUSPICIOUS_EXTENSIONS = {
    ".exe": 30, ".bat": 35, ".cmd": 35, ".vbs": 40, ".vbe": 40,
    ".js": 40, ".jse": 40, ".wsf": 40, ".wsh": 35, ".ps1": 45,
    ".pif": 45, ".scr": 40, ".hta": 45, ".lnk": 15, ".url": 20,
}

LEGITIMATE_ENTRIES = [
    "desktop.ini", "Send to OneNote.lnk", "OneNote 2016.lnk",
    "Microsoft Teams.lnk", "Outlook.lnk", "OneDrive.lnk",
    "Cisco AnyConnect Secure Mobility Client.lnk",
    "Skype for Business.lnk", "Zoom.lnk",
]


def get_startup_paths():
    """Return user-specific and all-users startup folder paths."""
    paths = []
    user_startup = os.path.join(
        os.environ.get("APPDATA", ""),
        "Microsoft", "Windows", "Start Menu", "Programs", "Startup"
    )
    if os.path.isdir(user_startup):
        paths.append({"path": user_startup, "scope": "current_user"})
    all_users_startup = os.path.join(
        os.environ.get("PROGRAMDATA", r"C:\ProgramData"),
        "Microsoft", "Windows", "Start Menu", "Programs", "Startup"
    )
    if os.path.isdir(all_users_startup):
        paths.append({"path": all_users_startup, "scope": "all_users"})
    return paths


def compute_file_hash(filepath):
    """Compute SHA-256 hash of a file."""
    sha256 = hashlib.sha256()
    try:
        with open(filepath, "rb") as f:
            for chunk in iter(lambda: f.read(8192), b""):
                sha256.update(chunk)
        return sha256.hexdigest()
    except (PermissionError, OSError):
        return "access_denied"


def analyze_file(filepath, scope="unknown"):
    """Analyze a single file in a startup directory."""
    path = Path(filepath)
    name = path.name
    ext = path.suffix.lower()
    try:
        stat = path.stat()
        created = datetime.fromtimestamp(stat.st_ctime)
        modified = datetime.fromtimestamp(stat.st_mtime)
        size = stat.st_size
    except (PermissionError, OSError):
        return {"file": str(filepath), "error": "access_denied"}

    is_legitimate = name in LEGITIMATE_ENTRIES
    age_days = (datetime.now() - created).days

    risk = 0
    indicators = []
    risk += SUSPICIOUS_EXTENSIONS.get(ext, 0)
    if ext in SUSPICIOUS_EXTENSIONS and ext != ".lnk":
        indicators.append(f"suspicious_extension_{ext}")
    if age_days < 7:
        risk += 25
        indicators.append("recently_created")
    if age_days < 1:
        risk += 15
        indicators.append("created_within_24h")
    if size == 0:
        risk += 10
        indicators.append("zero_byte_file")
    if size > 10 * 1024 * 1024:
        risk += 10
        indicators.append("large_file_over_10mb")
    if not is_legitimate:
        risk += 10
        indicators.append("not_in_baseline")
    if scope == "all_users" and ext in SUSPICIOUS_EXTENSIONS:
        risk += 10
        indicators.append("all_users_startup")

    risk = min(risk, 100)

    return {
        "file": str(filepath),
        "filename": name,
        "extension": ext,
        "scope": scope,
        "size_bytes": size,
        "created": created.isoformat(),
        "modified": modified.isoformat(),
        "age_days": age_days,
        "sha256": compute_file_hash(filepath),
        "is_legitimate_baseline": is_legitimate,
        "suspicious_indicators": indicators,
        "risk_score": risk,
        "risk_level": "CRITICAL" if risk >= 70 else "HIGH" if risk >= 50 else "MEDIUM" if risk >= 25 else "LOW",
    }


def scan_startup_folders():
    """Scan all startup directories and analyze contents."""
    startup_paths = get_startup_paths()
    results = []
    for sp in startup_paths:
        folder = sp["path"]
        scope = sp["scope"]
        try:
            for entry in os.listdir(folder):
                full_path = os.path.join(folder, entry)
                if os.path.isfile(full_path):
                    analysis = analyze_file(full_path, scope)
                    results.append(analysis)
        except PermissionError:
            results.append({"path": folder, "error": "access_denied"})
    results.sort(key=lambda x: x.get("risk_score", 0), reverse=True)
    return results


def check_registry_run_keys():
    """Check Registry Run keys for autostart entries."""
    import subprocess
    run_keys = [
        r"HKCU\Software\Microsoft\Windows\CurrentVersion\Run",
        r"HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce",
        r"HKLM\Software\Microsoft\Windows\CurrentVersion\Run",
        r"HKLM\Software\Microsoft\Windows\CurrentVersion\RunOnce",
    ]
    entries = []
    for key in run_keys:
        try:
            result = subprocess.run(
                ["reg", "query", key],
                capture_output=True, text=True, timeout=10
            )
            if result.returncode == 0:
                for line in result.stdout.strip().split("\n"):
                    line = line.strip()
                    if line and not line.startswith("HK") and "REG_" in line:
                        parts = line.split("REG_SZ", 1) if "REG_SZ" in line else line.split("REG_EXPAND_SZ", 1)
                        name = parts[0].strip() if parts else line
                        value = parts[1].strip() if len(parts) > 1 else ""
                        entries.append({
                            "registry_key": key,
                            "name": name,
                            "value": value,
                            "suspicious": any(p in value.lower() for p in
                                             ["powershell", "cmd.exe", "\\temp\\", "\\appdata\\",
                                              "mshta", "-enc", "downloadstring"]),
                        })
        except Exception:
            pass
    return entries


class StartupMonitorHandler(FileSystemEventHandler):
    """Watchdog handler for monitoring startup folder changes."""

    def __init__(self):
        self.events = []

    def on_created(self, event):
        if not event.is_directory:
            analysis = analyze_file(event.src_path)
            alert = {
                "event": "FILE_CREATED",
                "timestamp": datetime.now().isoformat(),
                "file": event.src_path,
                "risk_score": analysis.get("risk_score", 0),
                "risk_level": analysis.get("risk_level", "UNKNOWN"),
                "indicators": analysis.get("suspicious_indicators", []),
            }
            self.events.append(alert)
            print(json.dumps(alert, indent=2))

    def on_modified(self, event):
        if not event.is_directory:
            alert = {
                "event": "FILE_MODIFIED",
                "timestamp": datetime.now().isoformat(),
                "file": event.src_path,
            }
            self.events.append(alert)
            print(json.dumps(alert, indent=2))

    def on_deleted(self, event):
        if not event.is_directory:
            alert = {
                "event": "FILE_DELETED",
                "timestamp": datetime.now().isoformat(),
                "file": event.src_path,
            }
            self.events.append(alert)
            print(json.dumps(alert, indent=2))


def monitor_startup(duration_seconds=60):
    """Monitor startup folders in real-time using watchdog."""
    if not Observer:
        return {"error": "watchdog not installed: pip install watchdog"}
    handler = StartupMonitorHandler()
    observer = Observer()
    startup_paths = get_startup_paths()
    for sp in startup_paths:
        observer.schedule(handler, sp["path"], recursive=False)
    observer.start()
    try:
        time.sleep(duration_seconds)
    except KeyboardInterrupt:
        pass
    observer.stop()
    observer.join()
    return {"monitored_seconds": duration_seconds, "events_detected": handler.events}


def full_hunt():
    """Run comprehensive startup persistence threat hunt."""
    scan_results = scan_startup_folders()
    registry_entries = check_registry_run_keys()
    suspicious_files = [r for r in scan_results if r.get("risk_score", 0) >= 25]
    suspicious_reg = [e for e in registry_entries if e.get("suspicious")]
    return {
        "hunt_type": "Startup Folder Persistence (T1547.001)",
        "timestamp": datetime.now().isoformat(),
        "startup_paths": get_startup_paths(),
        "statistics": {
            "total_startup_files": len(scan_results),
            "suspicious_files": len(suspicious_files),
            "registry_run_entries": len(registry_entries),
            "suspicious_registry_entries": len(suspicious_reg),
        },
        "file_analysis": scan_results[:30],
        "registry_analysis": registry_entries[:20],
        "mitre_technique": {
            "id": "T1547.001",
            "name": "Boot or Logon Autostart Execution: Registry Run Keys / Startup Folder",
            "tactic": "Persistence, Privilege Escalation",
        },
        "recommendation": "Investigate CRITICAL and HIGH files. Verify hashes against known-good baselines."
            if suspicious_files else "No suspicious startup entries detected.",
    }


def main():
    parser = argparse.ArgumentParser(description="Startup Folder Persistence Hunting Agent (T1547.001)")
    sub = parser.add_subparsers(dest="command")
    sub.add_parser("scan", help="Scan startup folders for suspicious files")
    sub.add_parser("registry", help="Check Registry Run keys")
    p_mon = sub.add_parser("monitor", help="Monitor startup folders in real-time")
    p_mon.add_argument("--duration", type=int, default=60, help="Monitor duration in seconds")
    sub.add_parser("full", help="Full persistence threat hunt")
    args = parser.parse_args()

    if args.command == "scan":
        result = scan_startup_folders()
    elif args.command == "registry":
        result = check_registry_run_keys()
    elif args.command == "monitor":
        result = monitor_startup(args.duration)
    elif args.command == "full" or args.command is None:
        result = full_hunt()
    else:
        parser.print_help()
        return
    print(json.dumps(result, indent=2, default=str))


if __name__ == "__main__":
    main()
Keep exploring