mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-16 16:03:17 +03:00
494 lines
19 KiB
Python
494 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Ransomware Precursor Detection Engine
|
|
|
|
Analyzes network logs (Zeek format) to detect ransomware precursor patterns:
|
|
- C2 beaconing detection via statistical interval analysis
|
|
- Internal reconnaissance scanning
|
|
- Kerberoasting and credential harvesting indicators
|
|
- Admin share enumeration
|
|
- Data staging via large SMB transfers
|
|
|
|
Reads Zeek TSV logs and generates structured alerts.
|
|
"""
|
|
|
|
import csv
|
|
import json
|
|
import math
|
|
import os
|
|
import sys
|
|
import statistics
|
|
from collections import defaultdict
|
|
from dataclasses import dataclass, field, asdict
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
|
|
@dataclass
|
|
class PrecursorAlert:
|
|
alert_id: str
|
|
timestamp: str
|
|
source_ip: str
|
|
dest_ip: str
|
|
category: str
|
|
confidence: str
|
|
kill_chain_phase: str
|
|
description: str
|
|
mitre_technique: str
|
|
evidence: list = field(default_factory=list)
|
|
|
|
|
|
class BeaconDetector:
|
|
"""Detects C2 beaconing by analyzing connection interval patterns."""
|
|
|
|
def __init__(self, min_connections: int = 20, beacon_score_threshold: float = 0.7):
|
|
self.min_connections = min_connections
|
|
self.beacon_score_threshold = beacon_score_threshold
|
|
self.connections = defaultdict(list)
|
|
|
|
def add_connection(self, src_ip: str, dst_ip: str, timestamp: float, orig_bytes: int, resp_bytes: int):
|
|
key = (src_ip, dst_ip)
|
|
self.connections[key].append({
|
|
"ts": timestamp,
|
|
"orig_bytes": orig_bytes,
|
|
"resp_bytes": resp_bytes,
|
|
})
|
|
|
|
def calculate_beacon_score(self, timestamps: list) -> dict:
|
|
"""Calculate beacon score based on connection interval regularity."""
|
|
if len(timestamps) < self.min_connections:
|
|
return {"score": 0.0, "interval": 0, "jitter": 0}
|
|
|
|
sorted_ts = sorted(timestamps)
|
|
intervals = [sorted_ts[i + 1] - sorted_ts[i] for i in range(len(sorted_ts) - 1)]
|
|
|
|
if not intervals:
|
|
return {"score": 0.0, "interval": 0, "jitter": 0}
|
|
|
|
median_interval = statistics.median(intervals)
|
|
if median_interval == 0:
|
|
return {"score": 0.0, "interval": 0, "jitter": 0}
|
|
|
|
# Calculate coefficient of variation (lower = more regular = more likely beacon)
|
|
try:
|
|
stdev = statistics.stdev(intervals)
|
|
cv = stdev / median_interval
|
|
except statistics.StatisticsError:
|
|
cv = 0
|
|
|
|
# Beacon score: inverse of coefficient of variation, capped at 1.0
|
|
# Perfect beacon (cv=0) scores 1.0, high variation scores low
|
|
if cv == 0:
|
|
score = 1.0
|
|
else:
|
|
score = max(0, min(1.0, 1.0 - cv))
|
|
|
|
# Penalize very short intervals (likely legitimate keep-alives under 5s)
|
|
if median_interval < 5:
|
|
score *= 0.5
|
|
|
|
# Penalize very long intervals (over 1 hour - less likely active C2)
|
|
if median_interval > 3600:
|
|
score *= 0.7
|
|
|
|
return {
|
|
"score": round(score, 3),
|
|
"interval": round(median_interval, 1),
|
|
"jitter": round(stdev, 1) if stdev else 0,
|
|
"connection_count": len(timestamps),
|
|
}
|
|
|
|
def detect(self) -> list:
|
|
"""Detect beaconing patterns in collected connections."""
|
|
alerts = []
|
|
for (src_ip, dst_ip), conns in self.connections.items():
|
|
timestamps = [c["ts"] for c in conns]
|
|
result = self.calculate_beacon_score(timestamps)
|
|
|
|
if result["score"] >= self.beacon_score_threshold:
|
|
# Check for consistent payload sizes (another beacon indicator)
|
|
orig_sizes = [c["orig_bytes"] for c in conns if c["orig_bytes"] > 0]
|
|
size_consistency = 0.0
|
|
if len(orig_sizes) >= 5:
|
|
try:
|
|
size_cv = statistics.stdev(orig_sizes) / statistics.mean(orig_sizes)
|
|
size_consistency = max(0, 1.0 - size_cv)
|
|
except (statistics.StatisticsError, ZeroDivisionError):
|
|
pass
|
|
|
|
combined_score = (result["score"] * 0.7) + (size_consistency * 0.3)
|
|
|
|
if combined_score >= self.beacon_score_threshold:
|
|
confidence = "High" if combined_score >= 0.9 else "Medium"
|
|
alert = PrecursorAlert(
|
|
alert_id=f"BEACON-{src_ip}-{dst_ip}",
|
|
timestamp=datetime.fromtimestamp(max(timestamps)).isoformat(),
|
|
source_ip=src_ip,
|
|
dest_ip=dst_ip,
|
|
category="C2 Beaconing",
|
|
confidence=confidence,
|
|
kill_chain_phase="Command and Control",
|
|
description=(
|
|
f"Beaconing pattern detected: {result['connection_count']} connections "
|
|
f"at {result['interval']}s intervals (jitter: {result['jitter']}s, "
|
|
f"beacon score: {combined_score:.3f})"
|
|
),
|
|
mitre_technique="T1071 - Application Layer Protocol",
|
|
evidence=[
|
|
f"Beacon score: {combined_score:.3f}",
|
|
f"Interval: {result['interval']}s",
|
|
f"Jitter: {result['jitter']}s",
|
|
f"Payload size consistency: {size_consistency:.3f}",
|
|
f"Connections: {result['connection_count']}",
|
|
],
|
|
)
|
|
alerts.append(alert)
|
|
return alerts
|
|
|
|
|
|
class ScanDetector:
|
|
"""Detects internal reconnaissance scanning."""
|
|
|
|
def __init__(self, unique_dest_threshold: int = 30, time_window_seconds: int = 300):
|
|
self.threshold = unique_dest_threshold
|
|
self.window = time_window_seconds
|
|
self.connections = defaultdict(list)
|
|
|
|
def add_connection(self, src_ip: str, dst_ip: str, dst_port: int, timestamp: float):
|
|
self.connections[src_ip].append({
|
|
"dst_ip": dst_ip,
|
|
"dst_port": dst_port,
|
|
"ts": timestamp,
|
|
})
|
|
|
|
def detect(self) -> list:
|
|
alerts = []
|
|
for src_ip, conns in self.connections.items():
|
|
sorted_conns = sorted(conns, key=lambda c: c["ts"])
|
|
|
|
# Sliding window analysis
|
|
window_start = 0
|
|
for window_end in range(len(sorted_conns)):
|
|
while (sorted_conns[window_end]["ts"] - sorted_conns[window_start]["ts"]) > self.window:
|
|
window_start += 1
|
|
|
|
window_conns = sorted_conns[window_start:window_end + 1]
|
|
unique_dests = set(c["dst_ip"] for c in window_conns)
|
|
unique_ports = set(c["dst_port"] for c in window_conns)
|
|
|
|
if len(unique_dests) >= self.threshold:
|
|
alert = PrecursorAlert(
|
|
alert_id=f"SCAN-{src_ip}-{int(sorted_conns[window_start]['ts'])}",
|
|
timestamp=datetime.fromtimestamp(sorted_conns[window_end]["ts"]).isoformat(),
|
|
source_ip=src_ip,
|
|
dest_ip="Multiple",
|
|
category="Internal Reconnaissance",
|
|
confidence="High" if len(unique_dests) >= self.threshold * 2 else "Medium",
|
|
kill_chain_phase="Discovery",
|
|
description=(
|
|
f"Internal scan: {len(unique_dests)} unique destinations on "
|
|
f"{len(unique_ports)} ports within {self.window}s window"
|
|
),
|
|
mitre_technique="T1046 - Network Service Discovery",
|
|
evidence=[
|
|
f"Unique destinations: {len(unique_dests)}",
|
|
f"Unique ports: {sorted(unique_ports)}",
|
|
f"Total connections: {len(window_conns)}",
|
|
f"Time window: {self.window}s",
|
|
],
|
|
)
|
|
alerts.append(alert)
|
|
break # One alert per source IP per window
|
|
|
|
return alerts
|
|
|
|
|
|
class CredentialHarvestDetector:
|
|
"""Detects Kerberoasting and credential harvesting patterns."""
|
|
|
|
def __init__(self, kerberoast_threshold: int = 5):
|
|
self.threshold = kerberoast_threshold
|
|
self.tgs_requests = defaultdict(list)
|
|
self.smb_failures = defaultdict(int)
|
|
|
|
def add_kerberos_event(self, src_ip: str, service_name: str, encryption_type: str, timestamp: float):
|
|
self.tgs_requests[src_ip].append({
|
|
"service": service_name,
|
|
"enc_type": encryption_type,
|
|
"ts": timestamp,
|
|
})
|
|
|
|
def add_smb_failure(self, src_ip: str, dst_ip: str):
|
|
self.smb_failures[src_ip] += 1
|
|
|
|
def detect(self) -> list:
|
|
alerts = []
|
|
|
|
# Kerberoasting: multiple TGS requests for unique services with RC4 encryption
|
|
for src_ip, requests in self.tgs_requests.items():
|
|
rc4_requests = [r for r in requests if "rc4" in r.get("enc_type", "").lower()
|
|
or "23" in str(r.get("enc_type", ""))]
|
|
unique_services = set(r["service"] for r in rc4_requests)
|
|
|
|
if len(unique_services) >= self.threshold:
|
|
alert = PrecursorAlert(
|
|
alert_id=f"KERB-{src_ip}",
|
|
timestamp=datetime.fromtimestamp(max(r["ts"] for r in rc4_requests)).isoformat(),
|
|
source_ip=src_ip,
|
|
dest_ip="Domain Controller",
|
|
category="Kerberoasting",
|
|
confidence="High",
|
|
kill_chain_phase="Credential Access",
|
|
description=(
|
|
f"Possible Kerberoasting: {len(unique_services)} unique service ticket "
|
|
f"requests with RC4 encryption from single host"
|
|
),
|
|
mitre_technique="T1558.003 - Kerberoasting",
|
|
evidence=[
|
|
f"Unique services targeted: {len(unique_services)}",
|
|
f"RC4 encryption requests: {len(rc4_requests)}",
|
|
f"Services: {list(unique_services)[:10]}",
|
|
],
|
|
)
|
|
alerts.append(alert)
|
|
|
|
# SMB brute force
|
|
for src_ip, count in self.smb_failures.items():
|
|
if count >= 10:
|
|
alert = PrecursorAlert(
|
|
alert_id=f"SMB-BRUTE-{src_ip}",
|
|
timestamp=datetime.now().isoformat(),
|
|
source_ip=src_ip,
|
|
dest_ip="Multiple",
|
|
category="SMB Brute Force",
|
|
confidence="Medium",
|
|
kill_chain_phase="Credential Access",
|
|
description=f"SMB authentication failures: {count} failed attempts",
|
|
mitre_technique="T1110 - Brute Force",
|
|
evidence=[f"Failed SMB auth count: {count}"],
|
|
)
|
|
alerts.append(alert)
|
|
|
|
return alerts
|
|
|
|
|
|
class AdminShareDetector:
|
|
"""Detects suspicious access to administrative shares (C$, ADMIN$, IPC$)."""
|
|
|
|
def __init__(self, threshold: int = 5):
|
|
self.threshold = threshold
|
|
self.share_access = defaultdict(lambda: defaultdict(set))
|
|
|
|
def add_share_access(self, src_ip: str, dst_ip: str, share_name: str, timestamp: float):
|
|
admin_shares = {"ADMIN$", "C$", "IPC$", "D$", "E$"}
|
|
normalized_share = share_name.split("\\")[-1].upper()
|
|
if normalized_share in admin_shares:
|
|
self.share_access[src_ip][normalized_share].add(dst_ip)
|
|
|
|
def detect(self) -> list:
|
|
alerts = []
|
|
for src_ip, shares in self.share_access.items():
|
|
total_targets = set()
|
|
for share, targets in shares.items():
|
|
total_targets.update(targets)
|
|
|
|
if len(total_targets) >= self.threshold:
|
|
alert = PrecursorAlert(
|
|
alert_id=f"ADMINSHARE-{src_ip}",
|
|
timestamp=datetime.now().isoformat(),
|
|
source_ip=src_ip,
|
|
dest_ip="Multiple",
|
|
category="Admin Share Enumeration",
|
|
confidence="High" if len(total_targets) >= self.threshold * 2 else "Medium",
|
|
kill_chain_phase="Lateral Movement",
|
|
description=(
|
|
f"Admin share access to {len(total_targets)} hosts: "
|
|
f"shares accessed: {list(shares.keys())}"
|
|
),
|
|
mitre_technique="T1021.002 - SMB/Windows Admin Shares",
|
|
evidence=[
|
|
f"Unique targets: {len(total_targets)}",
|
|
f"Shares accessed: {dict((s, len(t)) for s, t in shares.items())}",
|
|
],
|
|
)
|
|
alerts.append(alert)
|
|
|
|
return alerts
|
|
|
|
|
|
class RansomwarePrecursorEngine:
|
|
"""Orchestrates all detection modules."""
|
|
|
|
def __init__(self):
|
|
self.beacon_detector = BeaconDetector()
|
|
self.scan_detector = ScanDetector()
|
|
self.cred_detector = CredentialHarvestDetector()
|
|
self.share_detector = AdminShareDetector()
|
|
self.alerts = []
|
|
|
|
def load_zeek_conn_log(self, filepath: str):
|
|
"""Parse Zeek conn.log for beacon and scan detection."""
|
|
with open(filepath, "r") as f:
|
|
for line in f:
|
|
if line.startswith("#"):
|
|
continue
|
|
fields = line.strip().split("\t")
|
|
if len(fields) < 20:
|
|
continue
|
|
try:
|
|
ts = float(fields[0])
|
|
src_ip = fields[2]
|
|
src_port = int(fields[3]) if fields[3] != "-" else 0
|
|
dst_ip = fields[4]
|
|
dst_port = int(fields[5]) if fields[5] != "-" else 0
|
|
proto = fields[6]
|
|
orig_bytes = int(fields[9]) if fields[9] != "-" else 0
|
|
resp_bytes = int(fields[10]) if fields[10] != "-" else 0
|
|
|
|
# Feed to beacon detector (external destinations)
|
|
if not self._is_internal(dst_ip):
|
|
self.beacon_detector.add_connection(src_ip, dst_ip, ts, orig_bytes, resp_bytes)
|
|
|
|
# Feed to scan detector (internal destinations)
|
|
if self._is_internal(src_ip) and self._is_internal(dst_ip):
|
|
self.scan_detector.add_connection(src_ip, dst_ip, dst_port, ts)
|
|
except (ValueError, IndexError):
|
|
continue
|
|
|
|
def _is_internal(self, ip: str) -> bool:
|
|
"""Check if IP is in RFC1918 private range."""
|
|
parts = ip.split(".")
|
|
if len(parts) != 4:
|
|
return False
|
|
try:
|
|
first = int(parts[0])
|
|
second = int(parts[1])
|
|
if first == 10:
|
|
return True
|
|
if first == 172 and 16 <= second <= 31:
|
|
return True
|
|
if first == 192 and second == 168:
|
|
return True
|
|
except ValueError:
|
|
pass
|
|
return False
|
|
|
|
def run_detection(self) -> list:
|
|
"""Run all detectors and return combined alerts."""
|
|
self.alerts = []
|
|
self.alerts.extend(self.beacon_detector.detect())
|
|
self.alerts.extend(self.scan_detector.detect())
|
|
self.alerts.extend(self.cred_detector.detect())
|
|
self.alerts.extend(self.share_detector.detect())
|
|
|
|
# Sort by confidence (High first)
|
|
confidence_order = {"High": 0, "Medium": 1, "Low": 2}
|
|
self.alerts.sort(key=lambda a: confidence_order.get(a.confidence, 3))
|
|
|
|
return self.alerts
|
|
|
|
def generate_report(self) -> str:
|
|
"""Generate formatted detection report."""
|
|
if not self.alerts:
|
|
self.run_detection()
|
|
|
|
lines = []
|
|
lines.append("=" * 70)
|
|
lines.append("RANSOMWARE PRECURSOR DETECTION REPORT")
|
|
lines.append("=" * 70)
|
|
lines.append(f"Generated: {datetime.now().isoformat()}")
|
|
lines.append(f"Total Alerts: {len(self.alerts)}")
|
|
|
|
by_category = defaultdict(list)
|
|
for alert in self.alerts:
|
|
by_category[alert.category].append(alert)
|
|
|
|
lines.append(f"\nAlert Categories:")
|
|
for cat, cat_alerts in sorted(by_category.items()):
|
|
lines.append(f" - {cat}: {len(cat_alerts)}")
|
|
|
|
lines.append("")
|
|
for i, alert in enumerate(self.alerts, 1):
|
|
lines.append("-" * 50)
|
|
lines.append(f"Alert #{i}: {alert.alert_id}")
|
|
lines.append(f" Category: {alert.category}")
|
|
lines.append(f" Confidence: {alert.confidence}")
|
|
lines.append(f" Kill Chain: {alert.kill_chain_phase}")
|
|
lines.append(f" Source: {alert.source_ip}")
|
|
lines.append(f" Destination: {alert.dest_ip}")
|
|
lines.append(f" MITRE: {alert.mitre_technique}")
|
|
lines.append(f" Description: {alert.description}")
|
|
lines.append(f" Evidence:")
|
|
for e in alert.evidence:
|
|
lines.append(f" - {e}")
|
|
|
|
lines.append("")
|
|
lines.append("=" * 70)
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main():
|
|
"""Run detection engine with sample data or Zeek log file."""
|
|
engine = RansomwarePrecursorEngine()
|
|
|
|
# Check for Zeek conn.log argument
|
|
if len(sys.argv) > 1:
|
|
log_file = sys.argv[1]
|
|
if os.path.exists(log_file):
|
|
print(f"Loading Zeek conn.log: {log_file}")
|
|
engine.load_zeek_conn_log(log_file)
|
|
else:
|
|
print(f"File not found: {log_file}")
|
|
sys.exit(1)
|
|
else:
|
|
# Demo with simulated data
|
|
print("No Zeek log provided. Running with simulated beacon data...")
|
|
import time
|
|
|
|
base_time = time.time() - 3600 # 1 hour ago
|
|
|
|
# Simulate Cobalt Strike beacon (60-second interval)
|
|
for i in range(40):
|
|
jitter = (i % 3) * 2 # Small jitter
|
|
engine.beacon_detector.add_connection(
|
|
"10.1.5.42", "185.220.101.42",
|
|
base_time + (i * 60) + jitter,
|
|
orig_bytes=48, resp_bytes=128,
|
|
)
|
|
|
|
# Simulate internal port scan
|
|
for i in range(50):
|
|
engine.scan_detector.add_connection(
|
|
"10.1.5.42", f"10.1.5.{100 + i}", 445,
|
|
base_time + 1800 + (i * 2),
|
|
)
|
|
|
|
# Simulate Kerberoasting
|
|
for i in range(8):
|
|
engine.cred_detector.add_kerberos_event(
|
|
"10.1.5.42", f"MSSQLSvc/sql{i}.corp.local:1433",
|
|
"rc4-hmac", base_time + 2000 + (i * 5),
|
|
)
|
|
|
|
# Simulate admin share access
|
|
for i in range(12):
|
|
engine.share_detector.add_share_access(
|
|
"10.1.5.42", f"10.1.5.{200 + i}", "ADMIN$",
|
|
base_time + 2500 + (i * 10),
|
|
)
|
|
|
|
report = engine.generate_report()
|
|
print(report)
|
|
|
|
# Export alerts as JSON
|
|
alerts_json = [asdict(a) for a in engine.alerts]
|
|
output_path = Path(__file__).parent / "precursor_alerts.json"
|
|
with open(output_path, "w") as f:
|
|
json.dump(alerts_json, f, indent=2)
|
|
print(f"\nAlerts exported to: {output_path}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|