mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-10 21:24:56 +03:00
Add folder anatomy (scripts/agent.py + references/api-reference.md) for 648 cybersecurity skills
Complete skill folder anatomy across all cybersecurity skills: - scripts/agent.py: 80-150 line Python agents using real libraries (impacket, boto3, azure-mgmt-*, kubernetes, pefile, yara, scapy, shodan, stix2, etc.) - references/api-reference.md: real API documentation with method signatures - LICENSE: MIT license for all skill folders
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,99 @@
|
||||
# API Reference: dd and dcfldd Disk Imaging
|
||||
|
||||
## dd - Standard Unix Disk Duplication
|
||||
|
||||
### Basic Syntax
|
||||
```bash
|
||||
dd if=<source> of=<destination> [options]
|
||||
```
|
||||
|
||||
### Key Options
|
||||
| Flag | Description | Example |
|
||||
|------|-------------|---------|
|
||||
| `if=` | Input file (source device) | `if=/dev/sdb` |
|
||||
| `of=` | Output file (destination image) | `of=evidence.dd` |
|
||||
| `bs=` | Block size for read/write | `bs=4096` (forensic standard) |
|
||||
| `count=` | Number of blocks to copy | `count=1024` |
|
||||
| `skip=` | Skip N blocks from input start | `skip=2048` |
|
||||
| `conv=` | Conversion options | `conv=noerror,sync` |
|
||||
| `status=` | Transfer statistics level | `status=progress` |
|
||||
|
||||
### conv= Values
|
||||
- `noerror` - Continue on read errors (do not abort)
|
||||
- `sync` - Pad input blocks with zeros on error (preserves offset alignment)
|
||||
- `notrunc` - Do not truncate output file
|
||||
|
||||
### Output Format
|
||||
```
|
||||
500107862016 bytes (500 GB, 466 GiB) copied, 8132.45 s, 61.5 MB/s
|
||||
976773168+0 records in
|
||||
976773168+0 records out
|
||||
```
|
||||
|
||||
## dcfldd - DoD Forensic dd
|
||||
|
||||
### Basic Syntax
|
||||
```bash
|
||||
dcfldd if=<source> of=<destination> [options]
|
||||
```
|
||||
|
||||
### Extended Options
|
||||
| Flag | Description | Example |
|
||||
|------|-------------|---------|
|
||||
| `hash=` | Hash algorithm(s) | `hash=sha256,md5` |
|
||||
| `hashlog=` | File for hash output | `hashlog=hashes.txt` |
|
||||
| `hashwindow=` | Hash every N bytes | `hashwindow=1G` |
|
||||
| `hashconv=` | Hash before or after conversion | `hashconv=after` |
|
||||
| `errlog=` | Error log file | `errlog=errors.log` |
|
||||
| `split=` | Split output into chunks | `split=2G` |
|
||||
| `splitformat=` | Suffix format for split files | `splitformat=aa` |
|
||||
| `vf=` | Verification file | `vf=evidence.dd` |
|
||||
| `verifylog=` | Verification result log | `verifylog=verify.log` |
|
||||
|
||||
### Output Format
|
||||
```
|
||||
Total (sha256): a3f2b8c9d4e5f6a7b8c9d0e1f2a3b4c5...
|
||||
1024+0 records in
|
||||
1024+0 records out
|
||||
```
|
||||
|
||||
## sha256sum - Hash Verification
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
sha256sum <file_or_device>
|
||||
sha256sum -c <checksum_file>
|
||||
```
|
||||
|
||||
### Output Format
|
||||
```
|
||||
a3f2b8c9d4e5f6... /dev/sdb
|
||||
a3f2b8c9d4e5f6... evidence.dd
|
||||
```
|
||||
|
||||
## blockdev - Write Protection
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
blockdev --setro <device> # Set read-only
|
||||
blockdev --setrw <device> # Set read-write
|
||||
blockdev --getro <device> # Check: 1=RO, 0=RW
|
||||
blockdev --getsize64 <device> # Size in bytes
|
||||
```
|
||||
|
||||
## lsblk - Block Device Enumeration
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,MODEL,SERIAL,RO
|
||||
lsblk -J # JSON output
|
||||
lsblk -p # Full device paths
|
||||
```
|
||||
|
||||
## hdparm - Drive Identification
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
hdparm -I <device> # Detailed drive info
|
||||
hdparm -i <device> # Summary identification
|
||||
```
|
||||
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Forensic disk image acquisition agent using dd and dcfldd with hash verification."""
|
||||
|
||||
import subprocess
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
import datetime
|
||||
import json
|
||||
|
||||
|
||||
def run_cmd(cmd, capture=True):
|
||||
"""Execute a shell command and return output."""
|
||||
result = subprocess.run(cmd, shell=True, capture_output=capture, text=True)
|
||||
return result.stdout.strip(), result.stderr.strip(), result.returncode
|
||||
|
||||
|
||||
def list_block_devices():
|
||||
"""Enumerate connected block devices."""
|
||||
stdout, _, rc = run_cmd("lsblk -J -o NAME,SIZE,TYPE,MOUNTPOINT,MODEL,SERIAL,RO")
|
||||
if rc == 0 and stdout:
|
||||
return json.loads(stdout)
|
||||
return {"blockdevices": []}
|
||||
|
||||
|
||||
def check_write_protection(device):
|
||||
"""Verify a device is set to read-only mode."""
|
||||
stdout, _, rc = run_cmd(f"blockdev --getro {device}")
|
||||
if rc == 0:
|
||||
return stdout.strip() == "1"
|
||||
return False
|
||||
|
||||
|
||||
def enable_write_protection(device):
|
||||
"""Enable software write-blocking on the target device."""
|
||||
_, _, rc = run_cmd(f"blockdev --setro {device}")
|
||||
if rc != 0:
|
||||
print(f"[ERROR] Failed to set {device} read-only. Run as root.")
|
||||
return False
|
||||
if check_write_protection(device):
|
||||
print(f"[OK] Write protection enabled on {device}")
|
||||
return True
|
||||
print(f"[ERROR] Write protection verification failed for {device}")
|
||||
return False
|
||||
|
||||
|
||||
def compute_hash(path, algorithm="sha256", block_size=65536):
|
||||
"""Compute the SHA-256 or MD5 hash of a file or device."""
|
||||
h = hashlib.new(algorithm)
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
while True:
|
||||
block = f.read(block_size)
|
||||
if not block:
|
||||
break
|
||||
h.update(block)
|
||||
except PermissionError:
|
||||
print(f"[ERROR] Permission denied reading {path}. Run as root.")
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
print(f"[ERROR] Path not found: {path}")
|
||||
return None
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def acquire_with_dd(source, destination, block_size=4096, log_file=None):
|
||||
"""Acquire a forensic image using dd with error handling."""
|
||||
cmd = (
|
||||
f"dd if={source} of={destination} bs={block_size} "
|
||||
f"conv=noerror,sync status=progress"
|
||||
)
|
||||
if log_file:
|
||||
cmd += f" 2>&1 | tee {log_file}"
|
||||
print(f"[*] Starting dd acquisition: {source} -> {destination}")
|
||||
print(f"[*] Block size: {block_size}")
|
||||
start = datetime.datetime.utcnow()
|
||||
_, stderr, rc = run_cmd(cmd, capture=False)
|
||||
elapsed = (datetime.datetime.utcnow() - start).total_seconds()
|
||||
print(f"[*] Acquisition completed in {elapsed:.1f} seconds (rc={rc})")
|
||||
return rc == 0
|
||||
|
||||
|
||||
def acquire_with_dcfldd(source, destination, hash_alg="sha256", hash_log=None,
|
||||
error_log=None, block_size=4096, split_size=None):
|
||||
"""Acquire a forensic image using dcfldd with built-in hashing."""
|
||||
cmd = f"dcfldd if={source} of={destination} bs={block_size} conv=noerror,sync"
|
||||
cmd += f" hash={hash_alg}"
|
||||
if hash_log:
|
||||
cmd += f" hashlog={hash_log}"
|
||||
cmd += " hashwindow=1G"
|
||||
if error_log:
|
||||
cmd += f" errlog={error_log}"
|
||||
if split_size:
|
||||
cmd += f" split={split_size} splitformat=aa"
|
||||
print(f"[*] Starting dcfldd acquisition: {source} -> {destination}")
|
||||
start = datetime.datetime.utcnow()
|
||||
_, stderr, rc = run_cmd(cmd, capture=False)
|
||||
elapsed = (datetime.datetime.utcnow() - start).total_seconds()
|
||||
print(f"[*] dcfldd completed in {elapsed:.1f} seconds (rc={rc})")
|
||||
return rc == 0
|
||||
|
||||
|
||||
def verify_image(source, image_path, algorithm="sha256"):
|
||||
"""Verify image integrity by comparing hashes of source and acquired image."""
|
||||
print(f"[*] Computing {algorithm} hash of source: {source}")
|
||||
source_hash = compute_hash(source, algorithm)
|
||||
print(f" Source hash: {source_hash}")
|
||||
print(f"[*] Computing {algorithm} hash of image: {image_path}")
|
||||
image_hash = compute_hash(image_path, algorithm)
|
||||
print(f" Image hash: {image_hash}")
|
||||
if source_hash and image_hash:
|
||||
match = source_hash == image_hash
|
||||
status = "PASSED" if match else "FAILED"
|
||||
print(f"[{'OK' if match else 'FAIL'}] Verification: {status}")
|
||||
return match, source_hash, image_hash
|
||||
return False, source_hash, image_hash
|
||||
|
||||
|
||||
def generate_report(case_dir, source_device, image_path, tool_used,
|
||||
source_hash, image_hash, verified, elapsed_seconds=0):
|
||||
"""Generate a forensic acquisition report."""
|
||||
report = {
|
||||
"report_type": "Disk Image Acquisition",
|
||||
"timestamp": datetime.datetime.utcnow().isoformat() + "Z",
|
||||
"case_directory": case_dir,
|
||||
"source_device": source_device,
|
||||
"image_file": image_path,
|
||||
"acquisition_tool": tool_used,
|
||||
"block_size": 4096,
|
||||
"source_hash_sha256": source_hash,
|
||||
"image_hash_sha256": image_hash,
|
||||
"hash_verified": verified,
|
||||
"duration_seconds": elapsed_seconds,
|
||||
}
|
||||
report_path = os.path.join(case_dir, "acquisition_report.json")
|
||||
with open(report_path, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
print(f"[*] Report saved to {report_path}")
|
||||
return report
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Forensic Disk Image Acquisition Agent")
|
||||
print("Tools: dd / dcfldd with SHA-256 verification")
|
||||
print("=" * 60)
|
||||
|
||||
# Demo: list block devices
|
||||
print("\n[*] Enumerating block devices...")
|
||||
devices = list_block_devices()
|
||||
for dev in devices.get("blockdevices", []):
|
||||
name = dev.get("name", "?")
|
||||
size = dev.get("size", "?")
|
||||
dtype = dev.get("type", "?")
|
||||
model = dev.get("model", "N/A")
|
||||
ro = "RO" if dev.get("ro") else "RW"
|
||||
print(f" /dev/{name} {size} {dtype} {model} [{ro}]")
|
||||
|
||||
# Demo workflow (dry run)
|
||||
demo_source = "/dev/sdb"
|
||||
demo_case = "/cases/demo-case/images"
|
||||
demo_image = os.path.join(demo_case, "evidence.dd")
|
||||
|
||||
print(f"\n[DEMO] Acquisition workflow for {demo_source}:")
|
||||
print(f" 1. Enable write protection: blockdev --setro {demo_source}")
|
||||
print(f" 2. Acquire with dcfldd: dcfldd if={demo_source} of={demo_image} "
|
||||
f"hash=sha256 hashwindow=1G bs=4096 conv=noerror,sync")
|
||||
print(f" 3. Verify: compare SHA-256 of {demo_source} and {demo_image}")
|
||||
print(f" 4. Generate acquisition report with chain-of-custody metadata")
|
||||
print("\n[*] Agent ready. Provide a source device and case directory to begin.")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: analyzing-api-gateway-access-logs
|
||||
description: >
|
||||
Parses API Gateway access logs (AWS API Gateway, Kong, Nginx) to detect BOLA/IDOR
|
||||
attacks, rate limit bypass, credential scanning, and injection attempts. Uses pandas
|
||||
for statistical analysis of request patterns and anomaly detection. Use when
|
||||
investigating API abuse or building API-specific threat detection rules.
|
||||
---
|
||||
|
||||
# Analyzing API Gateway Access Logs
|
||||
|
||||
## Instructions
|
||||
|
||||
Parse API gateway access logs to identify attack patterns including broken object
|
||||
level authorization (BOLA), excessive data exposure, and injection attempts.
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
df = pd.read_json("api_gateway_logs.json", lines=True)
|
||||
# Detect BOLA: same user accessing many different resource IDs
|
||||
bola = df.groupby(["user_id", "endpoint"]).agg(
|
||||
unique_ids=("resource_id", "nunique")).reset_index()
|
||||
suspicious = bola[bola["unique_ids"] > 50]
|
||||
```
|
||||
|
||||
Key detection patterns:
|
||||
1. BOLA/IDOR: sequential resource ID enumeration
|
||||
2. Rate limit bypass via header manipulation
|
||||
3. Credential scanning (401 surges from single source)
|
||||
4. SQL/NoSQL injection in query parameters
|
||||
5. Unusual HTTP methods (DELETE, PATCH) on read-only endpoints
|
||||
|
||||
## Examples
|
||||
|
||||
```python
|
||||
# Detect 401 surges indicating credential scanning
|
||||
auth_failures = df[df["status_code"] == 401]
|
||||
scanner_ips = auth_failures.groupby("source_ip").size()
|
||||
scanners = scanner_ips[scanner_ips > 100]
|
||||
```
|
||||
@@ -0,0 +1,58 @@
|
||||
# API Reference: Analyzing API Gateway Access Logs
|
||||
|
||||
## AWS API Gateway Log Fields
|
||||
|
||||
```json
|
||||
{
|
||||
"requestId": "abc-123",
|
||||
"ip": "203.0.113.50",
|
||||
"httpMethod": "GET",
|
||||
"resourcePath": "/api/users/{id}",
|
||||
"status": 200,
|
||||
"requestTime": "2025-03-15T14:00:00Z",
|
||||
"responseLength": 1024
|
||||
}
|
||||
```
|
||||
|
||||
## Pandas Log Analysis
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
df = pd.read_json("access_logs.json", lines=True)
|
||||
|
||||
# BOLA detection
|
||||
df.groupby("user_id")["resource_id"].nunique()
|
||||
|
||||
# Auth failure surge
|
||||
df[df["status_code"] == 401].groupby("source_ip").size()
|
||||
|
||||
# Request velocity
|
||||
df.set_index("timestamp").resample("1min").size()
|
||||
```
|
||||
|
||||
## OWASP API Top 10 Patterns
|
||||
|
||||
| Risk | Detection Pattern |
|
||||
|------|-------------------|
|
||||
| BOLA (API1) | User accessing > 50 unique resource IDs |
|
||||
| Broken Auth (API2) | > 100 401/403 from single IP |
|
||||
| Excessive Data (API3) | Response size > 10x average |
|
||||
| Rate Limit (API4) | > 100 req/min from single IP |
|
||||
| BFLA (API5) | DELETE/PUT on read-only endpoints |
|
||||
| Injection (API8) | SQL/NoSQL patterns in params |
|
||||
|
||||
## Injection Regex Patterns
|
||||
|
||||
```python
|
||||
sql = r"union\s+select|drop\s+table|'\s*or\s+'1'"
|
||||
nosql = r"\$ne|\$gt|\$regex|\$where"
|
||||
xss = r"<script|javascript:|onerror="
|
||||
path_traversal = r"\.\./\.\./|/etc/passwd"
|
||||
```
|
||||
|
||||
### References
|
||||
|
||||
- OWASP API Security Top 10: https://owasp.org/API-Security/
|
||||
- AWS API Gateway logging: https://docs.aws.amazon.com/apigateway/latest/developerguide/
|
||||
- pandas: https://pandas.pydata.org/docs/
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Agent for analyzing API Gateway access logs for security threats."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
|
||||
def load_api_logs(log_path):
|
||||
"""Load API gateway logs from JSON lines or CSV."""
|
||||
if log_path.endswith(".csv"):
|
||||
return pd.read_csv(log_path, parse_dates=["timestamp"])
|
||||
return pd.read_json(log_path, lines=True)
|
||||
|
||||
|
||||
def detect_bola_attacks(df, threshold=50):
|
||||
"""Detect Broken Object Level Authorization (BOLA/IDOR) attacks."""
|
||||
findings = []
|
||||
if "resource_id" not in df.columns:
|
||||
path_col = "request_path" if "request_path" in df.columns else "path"
|
||||
df["resource_id"] = df[path_col].str.extract(r'/(\d+)(?:/|$|\?)')
|
||||
df_with_ids = df.dropna(subset=["resource_id"])
|
||||
if df_with_ids.empty:
|
||||
return findings
|
||||
user_col = "user_id" if "user_id" in df.columns else "source_ip"
|
||||
grouped = df_with_ids.groupby([user_col]).agg(
|
||||
unique_resources=("resource_id", "nunique"),
|
||||
total_requests=("resource_id", "count"),
|
||||
).reset_index()
|
||||
bola_suspects = grouped[grouped["unique_resources"] >= threshold]
|
||||
for _, row in bola_suspects.iterrows():
|
||||
findings.append({
|
||||
"user": row[user_col],
|
||||
"unique_resources_accessed": int(row["unique_resources"]),
|
||||
"total_requests": int(row["total_requests"]),
|
||||
"type": "BOLA/IDOR",
|
||||
"severity": "CRITICAL",
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def detect_auth_scanning(df, threshold=100):
|
||||
"""Detect credential scanning via 401/403 response surges."""
|
||||
findings = []
|
||||
auth_failures = df[df["status_code"].isin([401, 403])]
|
||||
if auth_failures.empty:
|
||||
return findings
|
||||
ip_col = "source_ip" if "source_ip" in df.columns else "client_ip"
|
||||
ip_failures = auth_failures.groupby(ip_col).agg(
|
||||
failure_count=("status_code", "count"),
|
||||
unique_endpoints=("request_path", "nunique") if "request_path" in df.columns
|
||||
else ("path", "nunique"),
|
||||
).reset_index()
|
||||
scanners = ip_failures[ip_failures["failure_count"] >= threshold]
|
||||
for _, row in scanners.iterrows():
|
||||
findings.append({
|
||||
"source_ip": row[ip_col],
|
||||
"auth_failures": int(row["failure_count"]),
|
||||
"endpoints_probed": int(row["unique_endpoints"]),
|
||||
"type": "credential_scanning",
|
||||
"severity": "HIGH",
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def detect_injection_attempts(df):
|
||||
"""Detect SQL/NoSQL injection attempts in request parameters."""
|
||||
injection_patterns = [
|
||||
r"(?:union\s+select|select\s+.*\s+from|drop\s+table|insert\s+into)",
|
||||
r"(?:'\s*or\s+'1'\s*=\s*'1|'\s*or\s+1\s*=\s*1)",
|
||||
r'(?:\$ne|\$gt|\$lt|\$regex|\$where)',
|
||||
r'(?:<script|javascript:|onerror=|onload=)',
|
||||
r'(?:\.\./\.\./|/etc/passwd|/proc/self)',
|
||||
]
|
||||
findings = []
|
||||
path_col = "request_path" if "request_path" in df.columns else "path"
|
||||
query_col = "query_string" if "query_string" in df.columns else path_col
|
||||
for _, row in df.iterrows():
|
||||
request_str = str(row.get(query_col, "")) + str(row.get("request_body", ""))
|
||||
for pattern in injection_patterns:
|
||||
if re.search(pattern, request_str, re.IGNORECASE):
|
||||
findings.append({
|
||||
"source_ip": row.get("source_ip", row.get("client_ip", "")),
|
||||
"path": row.get(path_col, ""),
|
||||
"pattern_matched": pattern,
|
||||
"type": "injection_attempt",
|
||||
"severity": "HIGH",
|
||||
})
|
||||
break
|
||||
return findings[:500]
|
||||
|
||||
|
||||
def detect_rate_limit_bypass(df, window="1min", threshold=100):
|
||||
"""Detect rate limit bypass attempts."""
|
||||
findings = []
|
||||
ip_col = "source_ip" if "source_ip" in df.columns else "client_ip"
|
||||
df_copy = df.copy()
|
||||
df_copy["timestamp"] = pd.to_datetime(df_copy["timestamp"])
|
||||
df_copy = df_copy.set_index("timestamp")
|
||||
for ip, group in df_copy.groupby(ip_col):
|
||||
resampled = group.resample(window).size()
|
||||
bursts = resampled[resampled > threshold]
|
||||
if len(bursts) > 0:
|
||||
findings.append({
|
||||
"source_ip": ip,
|
||||
"max_requests_per_min": int(resampled.max()),
|
||||
"burst_periods": len(bursts),
|
||||
"type": "rate_limit_bypass",
|
||||
"severity": "MEDIUM",
|
||||
})
|
||||
return sorted(findings, key=lambda x: x["max_requests_per_min"], reverse=True)[:50]
|
||||
|
||||
|
||||
def detect_unusual_methods(df):
|
||||
"""Detect unusual HTTP methods on typically read-only endpoints."""
|
||||
findings = []
|
||||
dangerous_methods = {"DELETE", "PUT", "PATCH"}
|
||||
method_col = "method" if "method" in df.columns else "http_method"
|
||||
path_col = "request_path" if "request_path" in df.columns else "path"
|
||||
unusual = df[df[method_col].str.upper().isin(dangerous_methods)]
|
||||
for _, row in unusual.iterrows():
|
||||
findings.append({
|
||||
"source_ip": row.get("source_ip", row.get("client_ip", "")),
|
||||
"method": row[method_col],
|
||||
"path": row[path_col],
|
||||
"status_code": int(row.get("status_code", 0)),
|
||||
"type": "unusual_method",
|
||||
"severity": "MEDIUM",
|
||||
})
|
||||
return findings[:200]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="API Gateway Log Analysis Agent")
|
||||
parser.add_argument("--log-file", required=True, help="API gateway log file")
|
||||
parser.add_argument("--output", default="api_gateway_report.json")
|
||||
parser.add_argument("--action", choices=[
|
||||
"bola", "auth_scan", "injection", "rate_limit", "full_analysis"
|
||||
], default="full_analysis")
|
||||
args = parser.parse_args()
|
||||
|
||||
df = load_api_logs(args.log_file)
|
||||
report = {"generated_at": datetime.utcnow().isoformat(), "total_requests": len(df),
|
||||
"findings": {}}
|
||||
print(f"[+] Loaded {len(df)} API requests")
|
||||
|
||||
if args.action in ("bola", "full_analysis"):
|
||||
findings = detect_bola_attacks(df)
|
||||
report["findings"]["bola"] = findings
|
||||
print(f"[+] BOLA suspects: {len(findings)}")
|
||||
|
||||
if args.action in ("auth_scan", "full_analysis"):
|
||||
findings = detect_auth_scanning(df)
|
||||
report["findings"]["auth_scanning"] = findings
|
||||
print(f"[+] Auth scanners: {len(findings)}")
|
||||
|
||||
if args.action in ("injection", "full_analysis"):
|
||||
findings = detect_injection_attempts(df)
|
||||
report["findings"]["injection_attempts"] = findings
|
||||
print(f"[+] Injection attempts: {len(findings)}")
|
||||
|
||||
if args.action in ("rate_limit", "full_analysis"):
|
||||
findings = detect_rate_limit_bypass(df)
|
||||
report["findings"]["rate_limit_bypass"] = findings
|
||||
print(f"[+] Rate limit bypasses: {len(findings)}")
|
||||
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2, default=str)
|
||||
print(f"[+] Report saved to {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,97 @@
|
||||
# API Reference: MITRE ATT&CK Navigator APT Analysis
|
||||
|
||||
## ATT&CK Navigator Layer Format
|
||||
|
||||
### Layer JSON Structure
|
||||
```json
|
||||
{
|
||||
"name": "APT29 - TTPs",
|
||||
"versions": {"attack": "14", "navigator": "4.9.1", "layer": "4.5"},
|
||||
"domain": "enterprise-attack",
|
||||
"techniques": [
|
||||
{
|
||||
"techniqueID": "T1566.001",
|
||||
"tactic": "initial-access",
|
||||
"color": "#ff6666",
|
||||
"score": 100,
|
||||
"comment": "Used by APT29",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"gradient": {"colors": ["#ffffff", "#ff6666"], "minValue": 0, "maxValue": 100}
|
||||
}
|
||||
```
|
||||
|
||||
## ATT&CK STIX Data Access
|
||||
|
||||
### Download Enterprise ATT&CK Bundle
|
||||
```bash
|
||||
curl -o enterprise-attack.json \
|
||||
https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json
|
||||
```
|
||||
|
||||
### STIX Object Types
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `intrusion-set` | APT groups / threat actors |
|
||||
| `attack-pattern` | Techniques and sub-techniques |
|
||||
| `relationship` | Links groups to techniques (`uses`) |
|
||||
| `malware` | Malware families |
|
||||
| `tool` | Legitimate tools used by adversaries |
|
||||
|
||||
## mitreattack-python Library
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
pip install mitreattack-python
|
||||
```
|
||||
|
||||
### Query Group Techniques
|
||||
```python
|
||||
from mitreattack.stix20 import MitreAttackData
|
||||
|
||||
attack = MitreAttackData("enterprise-attack.json")
|
||||
groups = attack.get_groups()
|
||||
for g in groups:
|
||||
techs = attack.get_techniques_used_by_group(g)
|
||||
print(f"{g.name}: {len(techs)} techniques")
|
||||
```
|
||||
|
||||
### Get Technique Details
|
||||
```python
|
||||
technique = attack.get_object_by_attack_id("T1566.001", "attack-pattern")
|
||||
print(technique.name) # Spearphishing Attachment
|
||||
print(technique.x_mitre_platforms) # ['Windows', 'macOS', 'Linux']
|
||||
```
|
||||
|
||||
## Navigator CLI (attack-navigator)
|
||||
|
||||
### Export Layer to SVG
|
||||
```bash
|
||||
npx attack-navigator-export \
|
||||
--layer layer.json \
|
||||
--output output.svg \
|
||||
--theme dark
|
||||
```
|
||||
|
||||
## ATT&CK API (TAXII)
|
||||
```python
|
||||
from stix2 import TAXIICollectionSource, Filter
|
||||
from taxii2client.v20 import Collection
|
||||
|
||||
collection = Collection(
|
||||
"https://cti-taxii.mitre.org/stix/collections/95ecc380-afe9-11e4-9b6c-751b66dd541e/"
|
||||
)
|
||||
tc_source = TAXIICollectionSource(collection)
|
||||
groups = tc_source.query([Filter("type", "=", "intrusion-set")])
|
||||
```
|
||||
|
||||
## Key APT Groups Reference
|
||||
| ID | Name | Known Aliases |
|
||||
|----|------|--------------|
|
||||
| G0016 | APT29 | Cozy Bear, The Dukes, NOBELIUM |
|
||||
| G0007 | APT28 | Fancy Bear, Sofacy, Strontium |
|
||||
| G0022 | APT3 | Gothic Panda, UPS |
|
||||
| G0032 | Lazarus Group | HIDDEN COBRA, Zinc |
|
||||
| G0074 | Dragonfly 2.0 | Energetic Bear, Berserk Bear |
|
||||
| G0010 | Turla | Waterbug, Venomous Bear |
|
||||
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
"""APT group analysis agent using MITRE ATT&CK Navigator layers.
|
||||
|
||||
Queries ATT&CK data, maps APT techniques to Navigator layers,
|
||||
performs detection gap analysis, and generates threat-informed reports.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import hashlib
|
||||
from collections import Counter
|
||||
|
||||
try:
|
||||
import requests
|
||||
HAS_REQUESTS = True
|
||||
except ImportError:
|
||||
HAS_REQUESTS = False
|
||||
|
||||
ATTACK_ENTERPRISE_URL = "https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json"
|
||||
|
||||
NAVIGATOR_LAYER_TEMPLATE = {
|
||||
"name": "",
|
||||
"versions": {"attack": "14", "navigator": "4.9.1", "layer": "4.5"},
|
||||
"domain": "enterprise-attack",
|
||||
"description": "",
|
||||
"filters": {"platforms": ["Windows", "Linux", "macOS", "Cloud"]},
|
||||
"sorting": 0,
|
||||
"layout": {"layout": "side", "aggregateFunction": "average", "showID": False,
|
||||
"showName": True, "showAggregateScores": False, "countUnscored": False},
|
||||
"hideDisabled": False,
|
||||
"techniques": [],
|
||||
"gradient": {"colors": ["#ffffff", "#ff6666"], "minValue": 0, "maxValue": 100},
|
||||
"legendItems": [],
|
||||
"metadata": [],
|
||||
"links": [],
|
||||
"showTacticRowBackground": False,
|
||||
"tacticRowBackground": "#dddddd",
|
||||
"selectTechniquesAcrossTactics": True,
|
||||
"selectSubtechniquesWithParent": False,
|
||||
"selectVisibleTechniques": False,
|
||||
}
|
||||
|
||||
|
||||
def load_attack_data(filepath=None):
|
||||
"""Load ATT&CK STIX bundle from file or download."""
|
||||
if filepath and os.path.exists(filepath):
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
if HAS_REQUESTS:
|
||||
print("[*] Downloading ATT&CK Enterprise data...")
|
||||
resp = requests.get(ATTACK_ENTERPRISE_URL, timeout=60)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
return None
|
||||
|
||||
|
||||
def extract_groups(bundle):
|
||||
"""Extract intrusion-set (APT group) objects from STIX bundle."""
|
||||
groups = {}
|
||||
for obj in bundle.get("objects", []):
|
||||
if obj.get("type") == "intrusion-set":
|
||||
name = obj.get("name", "Unknown")
|
||||
aliases = obj.get("aliases", [])
|
||||
ext_refs = obj.get("external_references", [])
|
||||
attack_id = ""
|
||||
for ref in ext_refs:
|
||||
if ref.get("source_name") == "mitre-attack":
|
||||
attack_id = ref.get("external_id", "")
|
||||
break
|
||||
groups[obj["id"]] = {
|
||||
"name": name, "id": attack_id, "aliases": aliases,
|
||||
"description": obj.get("description", "")[:200],
|
||||
}
|
||||
return groups
|
||||
|
||||
|
||||
def extract_techniques(bundle):
|
||||
"""Extract attack-pattern (technique) objects from STIX bundle."""
|
||||
techniques = {}
|
||||
for obj in bundle.get("objects", []):
|
||||
if obj.get("type") == "attack-pattern" and not obj.get("revoked", False):
|
||||
ext_refs = obj.get("external_references", [])
|
||||
attack_id = ""
|
||||
for ref in ext_refs:
|
||||
if ref.get("source_name") == "mitre-attack":
|
||||
attack_id = ref.get("external_id", "")
|
||||
break
|
||||
if attack_id:
|
||||
tactics = [p["phase_name"] for p in obj.get("kill_chain_phases", [])]
|
||||
techniques[obj["id"]] = {
|
||||
"id": attack_id, "name": obj.get("name", ""),
|
||||
"tactics": tactics, "platforms": obj.get("x_mitre_platforms", []),
|
||||
}
|
||||
return techniques
|
||||
|
||||
|
||||
def map_group_techniques(bundle, group_stix_id, techniques):
|
||||
"""Map techniques used by a specific group via relationship objects."""
|
||||
group_techniques = []
|
||||
for obj in bundle.get("objects", []):
|
||||
if (obj.get("type") == "relationship" and
|
||||
obj.get("relationship_type") == "uses" and
|
||||
obj.get("source_ref") == group_stix_id and
|
||||
obj.get("target_ref", "").startswith("attack-pattern--")):
|
||||
tech_id = obj["target_ref"]
|
||||
if tech_id in techniques:
|
||||
group_techniques.append(techniques[tech_id])
|
||||
return group_techniques
|
||||
|
||||
|
||||
def build_navigator_layer(group_name, group_techniques, color="#ff6666", score=100):
|
||||
"""Build ATT&CK Navigator JSON layer for a group's techniques."""
|
||||
layer = json.loads(json.dumps(NAVIGATOR_LAYER_TEMPLATE))
|
||||
layer["name"] = f"{group_name} - TTPs"
|
||||
layer["description"] = f"ATT&CK techniques attributed to {group_name}"
|
||||
for tech in group_techniques:
|
||||
entry = {
|
||||
"techniqueID": tech["id"],
|
||||
"tactic": tech["tactics"][0] if tech["tactics"] else "",
|
||||
"color": color,
|
||||
"comment": f"Used by {group_name}",
|
||||
"enabled": True,
|
||||
"metadata": [],
|
||||
"links": [],
|
||||
"showSubtechniques": False,
|
||||
"score": score,
|
||||
}
|
||||
layer["techniques"].append(entry)
|
||||
return layer
|
||||
|
||||
|
||||
def detection_gap_analysis(group_techniques, detection_rules):
|
||||
"""Compare group TTPs against existing detection rules to find gaps."""
|
||||
covered = set()
|
||||
for rule in detection_rules:
|
||||
tech_id = rule.get("technique_id", "")
|
||||
if tech_id:
|
||||
covered.add(tech_id)
|
||||
gaps = []
|
||||
for tech in group_techniques:
|
||||
if tech["id"] not in covered:
|
||||
gaps.append({
|
||||
"technique_id": tech["id"],
|
||||
"technique_name": tech["name"],
|
||||
"tactics": tech["tactics"],
|
||||
"status": "NO DETECTION",
|
||||
})
|
||||
coverage_pct = (len(covered & {t["id"] for t in group_techniques}) /
|
||||
len(group_techniques) * 100) if group_techniques else 0
|
||||
return gaps, round(coverage_pct, 1)
|
||||
|
||||
|
||||
def tactic_heatmap(group_techniques):
|
||||
"""Generate tactic-level heatmap showing technique distribution."""
|
||||
tactic_counts = Counter()
|
||||
for tech in group_techniques:
|
||||
for tactic in tech["tactics"]:
|
||||
tactic_counts[tactic] += 1
|
||||
return dict(tactic_counts.most_common())
|
||||
|
||||
|
||||
def compare_groups(group_a_techs, group_b_techs):
|
||||
"""Compare two groups' technique sets for overlap analysis."""
|
||||
set_a = {t["id"] for t in group_a_techs}
|
||||
set_b = {t["id"] for t in group_b_techs}
|
||||
overlap = set_a & set_b
|
||||
only_a = set_a - set_b
|
||||
only_b = set_b - set_a
|
||||
jaccard = len(overlap) / len(set_a | set_b) if (set_a | set_b) else 0
|
||||
return {
|
||||
"overlap_count": len(overlap), "overlap_ids": sorted(overlap),
|
||||
"only_group_a": len(only_a), "only_group_b": len(only_b),
|
||||
"jaccard_similarity": round(jaccard, 4),
|
||||
}
|
||||
|
||||
|
||||
def save_layer(layer, output_path):
|
||||
"""Save Navigator layer to JSON file."""
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
json.dump(layer, f, indent=2)
|
||||
print(f"[+] Layer saved: {output_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("APT Group Analysis Agent - MITRE ATT&CK Navigator")
|
||||
print("TTP mapping, detection gap analysis, group comparison")
|
||||
print("=" * 60)
|
||||
|
||||
group_name = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
attack_file = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
|
||||
bundle = load_attack_data(attack_file)
|
||||
if not bundle:
|
||||
print("\n[!] Cannot load ATT&CK data. Provide STIX bundle path or install requests.")
|
||||
print("[DEMO] Usage:")
|
||||
print(" python agent.py APT29 enterprise-attack.json")
|
||||
print(" python agent.py APT28 # downloads from GitHub")
|
||||
sys.exit(1)
|
||||
|
||||
groups = extract_groups(bundle)
|
||||
techniques = extract_techniques(bundle)
|
||||
print(f"[*] Loaded {len(groups)} groups, {len(techniques)} techniques")
|
||||
|
||||
if not group_name:
|
||||
print("\n--- Available APT Groups (sample) ---")
|
||||
for gid, g in list(groups.items())[:20]:
|
||||
print(f" {g['id']:8s} {g['name']:30s} aliases={g['aliases'][:3]}")
|
||||
sys.exit(0)
|
||||
|
||||
target_group = None
|
||||
for gid, g in groups.items():
|
||||
if (g["name"].lower() == group_name.lower() or
|
||||
g["id"].lower() == group_name.lower() or
|
||||
group_name.lower() in [a.lower() for a in g["aliases"]]):
|
||||
target_group = (gid, g)
|
||||
break
|
||||
|
||||
if not target_group:
|
||||
print(f"[!] Group '{group_name}' not found")
|
||||
sys.exit(1)
|
||||
|
||||
gid, ginfo = target_group
|
||||
print(f"\n[*] Group: {ginfo['name']} ({ginfo['id']})")
|
||||
print(f" Aliases: {', '.join(ginfo['aliases'][:5])}")
|
||||
|
||||
group_techs = map_group_techniques(bundle, gid, techniques)
|
||||
print(f" Techniques: {len(group_techs)}")
|
||||
|
||||
heatmap = tactic_heatmap(group_techs)
|
||||
print("\n--- Tactic Heatmap ---")
|
||||
for tactic, count in heatmap.items():
|
||||
bar = "#" * count
|
||||
print(f" {tactic:35s} {count:3d} {bar}")
|
||||
|
||||
layer = build_navigator_layer(ginfo["name"], group_techs)
|
||||
out_file = f"{ginfo['name'].replace(' ', '_')}_layer.json"
|
||||
save_layer(layer, out_file)
|
||||
|
||||
sample_rules = [{"technique_id": t["id"]} for t in group_techs[:len(group_techs)//2]]
|
||||
gaps, coverage = detection_gap_analysis(group_techs, sample_rules)
|
||||
print(f"\n--- Detection Gap Analysis (demo: {coverage}% coverage) ---")
|
||||
for gap in gaps[:10]:
|
||||
print(f" [GAP] {gap['technique_id']:12s} {gap['technique_name']}")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: analyzing-azure-activity-logs-for-threats
|
||||
description: >
|
||||
Queries Azure Monitor activity logs and sign-in logs via azure-monitor-query to
|
||||
detect suspicious administrative operations, impossible travel, privilege escalation,
|
||||
and resource modifications. Builds KQL queries for threat hunting in Azure environments.
|
||||
Use when investigating suspicious Azure tenant activity or building cloud SIEM detections.
|
||||
---
|
||||
|
||||
# Analyzing Azure Activity Logs for Threats
|
||||
|
||||
## Instructions
|
||||
|
||||
Use azure-monitor-query to execute KQL queries against Azure Log Analytics workspaces,
|
||||
detecting suspicious admin operations and sign-in anomalies.
|
||||
|
||||
```python
|
||||
from azure.identity import DefaultAzureCredential
|
||||
from azure.monitor.query import LogsQueryClient
|
||||
from datetime import timedelta
|
||||
|
||||
credential = DefaultAzureCredential()
|
||||
client = LogsQueryClient(credential)
|
||||
|
||||
response = client.query_workspace(
|
||||
workspace_id="WORKSPACE_ID",
|
||||
query="AzureActivity | where OperationNameValue has 'MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE' | take 10",
|
||||
timespan=timedelta(hours=24),
|
||||
)
|
||||
```
|
||||
|
||||
Key detection queries:
|
||||
1. Role assignment changes (privilege escalation)
|
||||
2. Resource group and subscription modifications
|
||||
3. Key vault secret access from new IPs
|
||||
4. Network security group rule changes
|
||||
5. Conditional access policy modifications
|
||||
|
||||
## Examples
|
||||
|
||||
```python
|
||||
# Detect new Global Admin role assignments
|
||||
query = '''
|
||||
AuditLogs
|
||||
| where OperationName == "Add member to role"
|
||||
| where TargetResources[0].modifiedProperties[0].newValue has "Global Administrator"
|
||||
'''
|
||||
```
|
||||
@@ -0,0 +1,54 @@
|
||||
# API Reference: Analyzing Azure Activity Logs for Threats
|
||||
|
||||
## azure-monitor-query
|
||||
|
||||
```python
|
||||
from azure.identity import DefaultAzureCredential
|
||||
from azure.monitor.query import LogsQueryClient, LogsQueryStatus
|
||||
from datetime import timedelta
|
||||
|
||||
credential = DefaultAzureCredential()
|
||||
client = LogsQueryClient(credential)
|
||||
|
||||
response = client.query_workspace(
|
||||
workspace_id="WORKSPACE_ID",
|
||||
query="AzureActivity | take 10",
|
||||
timespan=timedelta(hours=24),
|
||||
)
|
||||
if response.status == LogsQueryStatus.SUCCESS:
|
||||
for table in response.tables:
|
||||
columns = [col.name for col in table.columns]
|
||||
for row in table.rows:
|
||||
print(dict(zip(columns, row)))
|
||||
```
|
||||
|
||||
## Key Azure Log Tables
|
||||
|
||||
| Table | Content |
|
||||
|-------|---------|
|
||||
| `AzureActivity` | Control plane operations (ARM) |
|
||||
| `SigninLogs` | Azure AD sign-in events |
|
||||
| `AuditLogs` | Azure AD audit trail |
|
||||
| `AzureDiagnostics` | Resource diagnostics (Key Vault, NSG) |
|
||||
| `SecurityAlert` | Defender for Cloud alerts |
|
||||
|
||||
## Threat Detection KQL Patterns
|
||||
|
||||
```kql
|
||||
// Privilege escalation
|
||||
AzureActivity | where OperationNameValue has "ROLEASSIGNMENTS/WRITE"
|
||||
|
||||
// Impossible travel
|
||||
SigninLogs | where ResultType == 0
|
||||
| extend Distance = geo_distance_2points(...)
|
||||
|
||||
// Mass deletion
|
||||
AzureActivity | where OperationNameValue endswith "/DELETE"
|
||||
| summarize count() by Caller, bin(TimeGenerated, 1h)
|
||||
```
|
||||
|
||||
### References
|
||||
|
||||
- azure-monitor-query: https://pypi.org/project/azure-monitor-query/
|
||||
- KQL reference: https://learn.microsoft.com/en-us/azure/data-explorer/kusto/query/
|
||||
- Azure Activity Log schema: https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/activity-log-schema
|
||||
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Agent for analyzing Azure activity logs for threat detection."""
|
||||
|
||||
import os
|
||||
import json
|
||||
import argparse
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from azure.identity import DefaultAzureCredential, ClientSecretCredential
|
||||
from azure.monitor.query import LogsQueryClient, LogsQueryStatus
|
||||
|
||||
|
||||
def get_credential(tenant_id=None, client_id=None, client_secret=None):
|
||||
"""Get Azure credential."""
|
||||
if client_id and client_secret and tenant_id:
|
||||
return ClientSecretCredential(tenant_id, client_id, client_secret)
|
||||
return DefaultAzureCredential()
|
||||
|
||||
|
||||
def run_kql(credential, workspace_id, query, hours=24):
|
||||
"""Execute KQL query against Log Analytics workspace."""
|
||||
client = LogsQueryClient(credential)
|
||||
response = client.query_workspace(
|
||||
workspace_id, query, timespan=timedelta(hours=hours)
|
||||
)
|
||||
rows = []
|
||||
if response.status == LogsQueryStatus.SUCCESS:
|
||||
for table in response.tables:
|
||||
columns = [col.name for col in table.columns]
|
||||
for row in table.rows:
|
||||
rows.append(dict(zip(columns, row)))
|
||||
return rows
|
||||
|
||||
|
||||
def detect_privilege_escalation(credential, workspace_id):
|
||||
"""Detect role assignment changes indicating privilege escalation."""
|
||||
query = """
|
||||
AzureActivity
|
||||
| where OperationNameValue has_any (
|
||||
"MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE",
|
||||
"MICROSOFT.AUTHORIZATION/ROLEDEFINITIONS/WRITE"
|
||||
)
|
||||
| where ActivityStatusValue == "Success"
|
||||
| project TimeGenerated, Caller, CallerIpAddress,
|
||||
OperationNameValue, ResourceGroup, Properties_d
|
||||
| order by TimeGenerated desc
|
||||
"""
|
||||
return run_kql(credential, workspace_id, query)
|
||||
|
||||
|
||||
def detect_nsg_changes(credential, workspace_id):
|
||||
"""Detect Network Security Group rule modifications."""
|
||||
query = """
|
||||
AzureActivity
|
||||
| where OperationNameValue has_any (
|
||||
"MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/SECURITYRULES/WRITE",
|
||||
"MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/SECURITYRULES/DELETE"
|
||||
)
|
||||
| where ActivityStatusValue == "Success"
|
||||
| project TimeGenerated, Caller, CallerIpAddress,
|
||||
OperationNameValue, ResourceGroup
|
||||
| order by TimeGenerated desc
|
||||
"""
|
||||
return run_kql(credential, workspace_id, query)
|
||||
|
||||
|
||||
def detect_keyvault_access(credential, workspace_id):
|
||||
"""Detect Key Vault secret access from unusual sources."""
|
||||
query = """
|
||||
AzureDiagnostics
|
||||
| where ResourceProvider == "MICROSOFT.KEYVAULT"
|
||||
| where OperationName in ("SecretGet", "SecretList", "SecretSet")
|
||||
| summarize AccessCount = count(), DistinctIPs = dcount(CallerIPAddress),
|
||||
IPList = make_set(CallerIPAddress, 10)
|
||||
by identity_claim_upn_s, OperationName, Resource
|
||||
| where DistinctIPs > 2 or AccessCount > 50
|
||||
| order by AccessCount desc
|
||||
"""
|
||||
return run_kql(credential, workspace_id, query)
|
||||
|
||||
|
||||
def detect_impossible_travel(credential, workspace_id):
|
||||
"""Detect sign-ins from geographically distant locations in short time."""
|
||||
query = """
|
||||
SigninLogs
|
||||
| where ResultType == 0
|
||||
| project TimeGenerated, UserPrincipalName, IPAddress,
|
||||
Lat = toreal(LocationDetails.geoCoordinates.latitude),
|
||||
Lon = toreal(LocationDetails.geoCoordinates.longitude)
|
||||
| sort by UserPrincipalName asc, TimeGenerated asc
|
||||
| extend PrevLat = prev(Lat), PrevLon = prev(Lon),
|
||||
PrevTime = prev(TimeGenerated), PrevUser = prev(UserPrincipalName)
|
||||
| where UserPrincipalName == PrevUser
|
||||
| extend TimeDiffMin = datetime_diff('minute', TimeGenerated, PrevTime)
|
||||
| where TimeDiffMin < 60 and TimeDiffMin > 0
|
||||
| extend DistKm = geo_distance_2points(Lon, Lat, PrevLon, PrevLat) / 1000
|
||||
| where DistKm > 500
|
||||
| project TimeGenerated, UserPrincipalName, IPAddress, DistKm, TimeDiffMin
|
||||
"""
|
||||
return run_kql(credential, workspace_id, query)
|
||||
|
||||
|
||||
def detect_resource_deletion(credential, workspace_id):
|
||||
"""Detect mass resource deletion events."""
|
||||
query = """
|
||||
AzureActivity
|
||||
| where OperationNameValue endswith "/DELETE"
|
||||
| where ActivityStatusValue == "Success"
|
||||
| summarize DeleteCount = count(), Resources = make_set(Resource, 20)
|
||||
by Caller, bin(TimeGenerated, 1h)
|
||||
| where DeleteCount > 10
|
||||
| order by DeleteCount desc
|
||||
"""
|
||||
return run_kql(credential, workspace_id, query)
|
||||
|
||||
|
||||
def detect_conditional_access_changes(credential, workspace_id):
|
||||
"""Detect modifications to Conditional Access policies."""
|
||||
query = """
|
||||
AuditLogs
|
||||
| where OperationName has_any (
|
||||
"Update conditional access policy",
|
||||
"Delete conditional access policy"
|
||||
)
|
||||
| project TimeGenerated, InitiatedBy, OperationName,
|
||||
TargetResources, Result
|
||||
| order by TimeGenerated desc
|
||||
"""
|
||||
return run_kql(credential, workspace_id, query)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Azure Activity Log Threat Detection Agent")
|
||||
parser.add_argument("--workspace-id", default=os.getenv("AZURE_WORKSPACE_ID"))
|
||||
parser.add_argument("--tenant-id", default=os.getenv("AZURE_TENANT_ID"))
|
||||
parser.add_argument("--client-id", default=os.getenv("AZURE_CLIENT_ID"))
|
||||
parser.add_argument("--client-secret", default=os.getenv("AZURE_CLIENT_SECRET"))
|
||||
parser.add_argument("--output", default="azure_threat_report.json")
|
||||
parser.add_argument("--action", choices=[
|
||||
"privesc", "nsg", "keyvault", "travel", "deletion", "full_hunt"
|
||||
], default="full_hunt")
|
||||
args = parser.parse_args()
|
||||
|
||||
cred = get_credential(args.tenant_id, args.client_id, args.client_secret)
|
||||
report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}}
|
||||
|
||||
if args.action in ("privesc", "full_hunt"):
|
||||
results = detect_privilege_escalation(cred, args.workspace_id)
|
||||
report["findings"]["privilege_escalation"] = results
|
||||
print(f"[+] Privilege escalation events: {len(results)}")
|
||||
|
||||
if args.action in ("nsg", "full_hunt"):
|
||||
results = detect_nsg_changes(cred, args.workspace_id)
|
||||
report["findings"]["nsg_changes"] = results
|
||||
print(f"[+] NSG changes: {len(results)}")
|
||||
|
||||
if args.action in ("keyvault", "full_hunt"):
|
||||
results = detect_keyvault_access(cred, args.workspace_id)
|
||||
report["findings"]["keyvault_anomalies"] = results
|
||||
print(f"[+] Key Vault anomalies: {len(results)}")
|
||||
|
||||
if args.action in ("travel", "full_hunt"):
|
||||
results = detect_impossible_travel(cred, args.workspace_id)
|
||||
report["findings"]["impossible_travel"] = results
|
||||
print(f"[+] Impossible travel: {len(results)}")
|
||||
|
||||
if args.action in ("deletion", "full_hunt"):
|
||||
results = detect_resource_deletion(cred, args.workspace_id)
|
||||
report["findings"]["mass_deletion"] = results
|
||||
print(f"[+] Mass deletion events: {len(results)}")
|
||||
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2, default=str)
|
||||
print(f"[+] Report saved to {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,97 @@
|
||||
# API Reference: Bootkit and Rootkit Analysis Tools
|
||||
|
||||
## dd - Boot Sector Extraction
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
dd if=/dev/sda of=mbr.bin bs=512 count=1 # MBR
|
||||
dd if=/dev/sda of=first_track.bin bs=512 count=63 # First track
|
||||
dd if=/dev/sda1 of=vbr.bin bs=512 count=1 # VBR
|
||||
```
|
||||
|
||||
## ndisasm - 16-bit Disassembly
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
ndisasm -b16 mbr.bin > mbr_disasm.txt
|
||||
ndisasm -b16 -o 0x7C00 mbr.bin # Set origin to MBR load address
|
||||
```
|
||||
|
||||
### Key Flags
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-b16` | 16-bit real-mode disassembly |
|
||||
| `-b32` | 32-bit protected-mode |
|
||||
| `-o` | Origin address offset |
|
||||
|
||||
## UEFITool - Firmware Analysis
|
||||
|
||||
### CLI Syntax
|
||||
```bash
|
||||
UEFIExtract firmware.rom all # Extract all modules
|
||||
UEFIExtract firmware.rom <GUID> body # Extract specific module body
|
||||
```
|
||||
|
||||
### Output
|
||||
Extracts firmware volumes into a directory tree with each DXE driver, PEI module, and option ROM as separate files identified by GUID.
|
||||
|
||||
## chipsec - Hardware Security Assessment
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
python chipsec_main.py -m common.secureboot.variables # Check Secure Boot
|
||||
python chipsec_main.py -m common.bios_wp # SPI write protection
|
||||
python chipsec_main.py -m common.spi_lock # SPI lock status
|
||||
python chipsec_util.py spi dump firmware.rom # Dump SPI flash
|
||||
```
|
||||
|
||||
### Key Modules
|
||||
| Module | Purpose |
|
||||
|--------|---------|
|
||||
| `common.secureboot.variables` | Verify Secure Boot configuration |
|
||||
| `common.bios_wp` | Check BIOS write protection |
|
||||
| `common.spi_lock` | Verify SPI flash lock bits |
|
||||
| `common.smm` | SMM protection verification |
|
||||
|
||||
## Volatility 3 - Rootkit Detection Plugins
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
vol3 -f memory.dmp <plugin>
|
||||
```
|
||||
|
||||
### Rootkit Detection Plugins
|
||||
| Plugin | Purpose |
|
||||
|--------|---------|
|
||||
| `windows.ssdt` | System Service Descriptor Table hooks |
|
||||
| `windows.callbacks` | Kernel callback registrations |
|
||||
| `windows.driverscan` | Scan for driver objects |
|
||||
| `windows.modules` | List loaded kernel modules |
|
||||
| `windows.psscan` | Pool-tag scan for processes (finds hidden) |
|
||||
| `windows.pslist` | Active process list (DKOM-affected) |
|
||||
| `windows.idt` | Interrupt Descriptor Table hooks |
|
||||
|
||||
### Output Format
|
||||
```
|
||||
Offset Order Module Section Owner
|
||||
------- ----- ------ ------- -----
|
||||
0x... 0 ntoskrnl.exe .text ntoskrnl.exe
|
||||
0x... 73 UNKNOWN - rootkit.sys ← suspicious
|
||||
```
|
||||
|
||||
## flashrom - SPI Flash Dumping
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
flashrom -p internal -r firmware.rom # Read/dump
|
||||
flashrom -p internal -w clean.rom # Write/reflash
|
||||
flashrom -p internal --verify clean.rom # Verify flash contents
|
||||
```
|
||||
|
||||
## YARA - Firmware Pattern Scanning
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
yara -r uefi_malware.yar firmware.rom
|
||||
yara -s -r rules.yar firmware.rom # Show matching strings
|
||||
```
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bootkit and rootkit analysis agent for MBR/VBR/UEFI inspection and rootkit detection."""
|
||||
|
||||
import struct
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import math
|
||||
from collections import Counter
|
||||
|
||||
|
||||
def read_mbr(disk_path_or_file):
|
||||
"""Read and parse the first 512 bytes (MBR) from a disk image or device."""
|
||||
with open(disk_path_or_file, "rb") as f:
|
||||
mbr = f.read(512)
|
||||
return mbr
|
||||
|
||||
|
||||
def validate_mbr_signature(mbr_data):
|
||||
"""Check the MBR boot signature at bytes 510-511 (should be 0x55AA)."""
|
||||
sig = mbr_data[510:512]
|
||||
valid = sig == b"\x55\xAA"
|
||||
return valid, sig.hex()
|
||||
|
||||
|
||||
def parse_partition_table(mbr_data):
|
||||
"""Parse the four 16-byte partition table entries starting at offset 446."""
|
||||
partitions = []
|
||||
for i in range(4):
|
||||
offset = 446 + (i * 16)
|
||||
entry = mbr_data[offset:offset + 16]
|
||||
if entry == b"\x00" * 16:
|
||||
continue
|
||||
boot_flag = entry[0]
|
||||
part_type = entry[4]
|
||||
start_lba = struct.unpack_from("<I", entry, 8)[0]
|
||||
size_lba = struct.unpack_from("<I", entry, 12)[0]
|
||||
partitions.append({
|
||||
"index": i + 1,
|
||||
"active": boot_flag == 0x80,
|
||||
"type_id": f"0x{part_type:02X}",
|
||||
"start_lba": start_lba,
|
||||
"size_sectors": size_lba,
|
||||
"size_mb": round(size_lba * 512 / (1024 * 1024), 1),
|
||||
})
|
||||
return partitions
|
||||
|
||||
|
||||
BOOTKIT_SIGNATURES = {
|
||||
b"\xE8\x00\x00\x5E\x81\xEE": "TDL4/Alureon bootkit",
|
||||
b"\xFA\x33\xC0\x8E\xD0\xBC\x00\x7C\x8B\xF4\x50\x07": "Standard Windows MBR (clean)",
|
||||
b"\xEB\x5A\x90\x4E\x54\x46\x53": "Standard NTFS VBR (clean)",
|
||||
b"\xEB\x52\x90\x4E\x54\x46\x53": "NTFS VBR variant (clean)",
|
||||
b"\x33\xC0\x8E\xD0\xBC\x00\x7C": "Windows 10 MBR (clean)",
|
||||
}
|
||||
|
||||
|
||||
def scan_bootkit_signatures(data):
|
||||
"""Scan boot sector data against known bootkit signatures."""
|
||||
matches = []
|
||||
for sig, name in BOOTKIT_SIGNATURES.items():
|
||||
if sig in data:
|
||||
offset = data.find(sig)
|
||||
matches.append({"signature": name, "offset": offset, "clean": "clean" in name})
|
||||
return matches
|
||||
|
||||
|
||||
def calculate_entropy(data):
|
||||
"""Calculate Shannon entropy of binary data."""
|
||||
if not data:
|
||||
return 0.0
|
||||
counter = Counter(data)
|
||||
length = len(data)
|
||||
entropy = -sum(
|
||||
(count / length) * math.log2(count / length)
|
||||
for count in counter.values()
|
||||
)
|
||||
return round(entropy, 4)
|
||||
|
||||
|
||||
def read_first_track(disk_path, num_sectors=63):
|
||||
"""Read the first track (typically 63 sectors) for extended bootkit code."""
|
||||
with open(disk_path, "rb") as f:
|
||||
data = f.read(num_sectors * 512)
|
||||
return data
|
||||
|
||||
|
||||
def analyze_boot_code(mbr_data):
|
||||
"""Analyze MBR bootstrap code (bytes 0-445) for suspicious patterns."""
|
||||
boot_code = mbr_data[:446]
|
||||
entropy = calculate_entropy(boot_code)
|
||||
sha256 = hashlib.sha256(boot_code).hexdigest()
|
||||
suspicious_patterns = []
|
||||
# Check for INT 13h hooking (common bootkit technique)
|
||||
if b"\xCD\x13" in boot_code:
|
||||
count = boot_code.count(b"\xCD\x13")
|
||||
suspicious_patterns.append(f"INT 13h calls: {count}")
|
||||
# Check for far jumps to unusual addresses
|
||||
if b"\xEA" in boot_code:
|
||||
suspicious_patterns.append("Far JMP instruction found")
|
||||
# Check for self-modifying code patterns
|
||||
if b"\xF3\xA4" in boot_code or b"\xF3\xA5" in boot_code:
|
||||
suspicious_patterns.append("REP MOVSB/MOVSW (memory copy, possible code relocation)")
|
||||
return {
|
||||
"entropy": entropy,
|
||||
"sha256": sha256,
|
||||
"high_entropy": entropy > 6.5,
|
||||
"suspicious_patterns": suspicious_patterns,
|
||||
}
|
||||
|
||||
|
||||
def run_volatility_rootkit_scan(memory_dump, plugin):
|
||||
"""Run a Volatility 3 plugin for rootkit detection via subprocess."""
|
||||
cmd = f"vol3 -f {memory_dump} {plugin}"
|
||||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
||||
return result.stdout, result.stderr, result.returncode
|
||||
|
||||
|
||||
def detect_kernel_rootkit(memory_dump):
|
||||
"""Run multiple Volatility plugins to detect kernel-level rootkit artifacts."""
|
||||
plugins = [
|
||||
"windows.ssdt",
|
||||
"windows.callbacks",
|
||||
"windows.driverscan",
|
||||
"windows.modules",
|
||||
"windows.psscan",
|
||||
"windows.pslist",
|
||||
]
|
||||
results = {}
|
||||
for plugin in plugins:
|
||||
stdout, stderr, rc = run_volatility_rootkit_scan(memory_dump, plugin)
|
||||
results[plugin] = {"output": stdout, "error": stderr, "return_code": rc}
|
||||
return results
|
||||
|
||||
|
||||
def compare_process_lists(pslist_output, psscan_output):
|
||||
"""Compare pslist and psscan output to find hidden processes (DKOM)."""
|
||||
pslist_pids = set()
|
||||
psscan_pids = set()
|
||||
for line in pslist_output.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) >= 2 and parts[1].isdigit():
|
||||
pslist_pids.add(int(parts[1]))
|
||||
for line in psscan_output.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) >= 2 and parts[1].isdigit():
|
||||
psscan_pids.add(int(parts[1]))
|
||||
hidden = psscan_pids - pslist_pids
|
||||
return hidden
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Bootkit & Rootkit Analysis Agent")
|
||||
print("MBR/VBR inspection, UEFI firmware analysis, rootkit detection")
|
||||
print("=" * 60)
|
||||
|
||||
# Demo with a sample MBR file if available
|
||||
demo_mbr = "mbr.bin"
|
||||
if len(sys.argv) > 1:
|
||||
demo_mbr = sys.argv[1]
|
||||
|
||||
if os.path.exists(demo_mbr):
|
||||
print(f"\n[*] Analyzing: {demo_mbr}")
|
||||
mbr = read_mbr(demo_mbr)
|
||||
valid, sig_hex = validate_mbr_signature(mbr)
|
||||
print(f"[*] MBR Signature: 0x{sig_hex.upper()} ({'Valid' if valid else 'INVALID'})")
|
||||
|
||||
partitions = parse_partition_table(mbr)
|
||||
print(f"[*] Partition entries: {len(partitions)}")
|
||||
for p in partitions:
|
||||
active = "Active" if p["active"] else "Inactive"
|
||||
print(f" Part {p['index']}: Type={p['type_id']} {active} "
|
||||
f"Start=LBA {p['start_lba']} Size={p['size_mb']} MB")
|
||||
|
||||
sigs = scan_bootkit_signatures(mbr)
|
||||
for s in sigs:
|
||||
tag = "[*]" if s["clean"] else "[!]"
|
||||
print(f"{tag} Signature match: {s['signature']} at offset {s['offset']}")
|
||||
|
||||
analysis = analyze_boot_code(mbr)
|
||||
print(f"[*] Boot code entropy: {analysis['entropy']}"
|
||||
f" ({'HIGH - possible encryption' if analysis['high_entropy'] else 'Normal'})")
|
||||
print(f"[*] Boot code SHA-256: {analysis['sha256']}")
|
||||
for pat in analysis["suspicious_patterns"]:
|
||||
print(f"[!] {pat}")
|
||||
else:
|
||||
print(f"\n[DEMO] No MBR file provided. Usage: {sys.argv[0]} <mbr.bin | /dev/sda>")
|
||||
print("[DEMO] Provide a 512-byte MBR dump or disk device for analysis.")
|
||||
print("\n[*] Supported analysis:")
|
||||
print(" - MBR/VBR signature validation and bootkit detection")
|
||||
print(" - Partition table parsing and anomaly detection")
|
||||
print(" - Boot code entropy and pattern analysis")
|
||||
print(" - Volatility-based kernel rootkit detection (SSDT, callbacks, DKOM)")
|
||||
print(" - UEFI firmware module inspection via chipsec subprocess")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,92 @@
|
||||
# API Reference: Browser Forensics with Hindsight
|
||||
|
||||
## Hindsight CLI
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
hindsight.py -i <profile_path> # Analyze Chrome profile
|
||||
hindsight.py -i <path> -o <output_dir> # Save results
|
||||
hindsight.py -i <path> -f xlsx # Export as Excel
|
||||
hindsight.py -i <path> -f sqlite # Export as SQLite
|
||||
hindsight.py -i <path> -b <browser_type> # Specify browser type
|
||||
```
|
||||
|
||||
### Browser Types
|
||||
| Flag | Browser |
|
||||
|------|---------|
|
||||
| `Chrome` | Google Chrome |
|
||||
| `Edge` | Microsoft Edge (Chromium) |
|
||||
| `Brave` | Brave Browser |
|
||||
| `Opera` | Opera (Chromium) |
|
||||
|
||||
### Output Artifacts
|
||||
| Table | Description |
|
||||
|-------|-------------|
|
||||
| `urls` | Browsing history with visit counts |
|
||||
| `downloads` | File downloads with source URLs |
|
||||
| `cookies` | Cookie values, domains, expiry |
|
||||
| `autofill` | Form autofill entries |
|
||||
| `bookmarks` | Saved bookmarks |
|
||||
| `preferences` | Browser configuration |
|
||||
| `local_storage` | Site local storage data |
|
||||
| `login_data` | Saved credential metadata |
|
||||
| `extensions` | Installed extensions with permissions |
|
||||
|
||||
## Chrome SQLite Databases
|
||||
|
||||
### History Database
|
||||
```sql
|
||||
-- Browsing history
|
||||
SELECT u.url, u.title, v.visit_time, v.transition
|
||||
FROM visits v JOIN urls u ON v.url = u.id
|
||||
ORDER BY v.visit_time DESC;
|
||||
|
||||
-- Downloads
|
||||
SELECT target_path, tab_url, total_bytes, start_time, danger_type, mime_type
|
||||
FROM downloads ORDER BY start_time DESC;
|
||||
```
|
||||
|
||||
### Cookies Database
|
||||
```sql
|
||||
SELECT host_key, name, value, creation_utc, expires_utc, is_secure, is_httponly
|
||||
FROM cookies ORDER BY creation_utc DESC;
|
||||
```
|
||||
|
||||
### Web Data Database (Autofill)
|
||||
```sql
|
||||
SELECT name, value, count, date_created, date_last_used
|
||||
FROM autofill ORDER BY date_last_used DESC;
|
||||
```
|
||||
|
||||
## Chrome Timestamp Conversion
|
||||
|
||||
### Format
|
||||
Microseconds since January 1, 1601 (Windows FILETIME base)
|
||||
|
||||
### Python Conversion
|
||||
```python
|
||||
import datetime
|
||||
def chrome_to_datetime(chrome_time):
|
||||
epoch = datetime.datetime(1601, 1, 1)
|
||||
return epoch + datetime.timedelta(microseconds=chrome_time)
|
||||
```
|
||||
|
||||
## Browser Profile Paths
|
||||
|
||||
| OS | Browser | Default Path |
|
||||
|----|---------|-------------|
|
||||
| Windows | Chrome | `%LOCALAPPDATA%\Google\Chrome\User Data\Default` |
|
||||
| Windows | Edge | `%LOCALAPPDATA%\Microsoft\Edge\User Data\Default` |
|
||||
| Linux | Chrome | `~/.config/google-chrome/Default` |
|
||||
| macOS | Chrome | `~/Library/Application Support/Google/Chrome/Default` |
|
||||
|
||||
## Transition Types (visit_transition & 0xFF)
|
||||
| Value | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| 0 | LINK | Clicked a link |
|
||||
| 1 | TYPED | Typed URL in address bar |
|
||||
| 2 | AUTO_BOOKMARK | Via bookmark |
|
||||
| 3 | AUTO_SUBFRAME | Subframe navigation |
|
||||
| 5 | GENERATED | Generated (e.g., search) |
|
||||
| 7 | FORM_SUBMIT | Form submission |
|
||||
| 8 | RELOAD | Page reload |
|
||||
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Browser forensics analysis agent using Hindsight concepts.
|
||||
|
||||
Parses Chromium-based browser artifacts (Chrome, Edge, Brave) including
|
||||
history, downloads, cookies, autofill, and extensions from SQLite databases.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import sqlite3
|
||||
import datetime
|
||||
import hashlib
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
def chrome_time_to_datetime(chrome_time):
|
||||
"""Convert Chrome timestamp (microseconds since 1601-01-01) to datetime."""
|
||||
if not chrome_time or chrome_time == 0:
|
||||
return None
|
||||
try:
|
||||
epoch = datetime.datetime(1601, 1, 1)
|
||||
delta = datetime.timedelta(microseconds=chrome_time)
|
||||
return (epoch + delta).isoformat() + "Z"
|
||||
except (OverflowError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def find_browser_profiles(base_path=None):
|
||||
"""Locate Chromium-based browser profile directories."""
|
||||
if base_path and os.path.isdir(base_path):
|
||||
return [base_path]
|
||||
profiles = []
|
||||
home = os.path.expanduser("~")
|
||||
candidates = [
|
||||
os.path.join(home, "AppData", "Local", "Google", "Chrome", "User Data", "Default"),
|
||||
os.path.join(home, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default"),
|
||||
os.path.join(home, "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data", "Default"),
|
||||
os.path.join(home, ".config", "google-chrome", "Default"),
|
||||
os.path.join(home, ".config", "chromium", "Default"),
|
||||
os.path.join(home, ".config", "microsoft-edge", "Default"),
|
||||
]
|
||||
for path in candidates:
|
||||
if os.path.isdir(path):
|
||||
profiles.append(path)
|
||||
return profiles
|
||||
|
||||
|
||||
def parse_history(profile_path):
|
||||
"""Parse browsing history from History SQLite database."""
|
||||
db_path = os.path.join(profile_path, "History")
|
||||
if not os.path.exists(db_path):
|
||||
return []
|
||||
entries = []
|
||||
try:
|
||||
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT u.url, u.title, v.visit_time, v.transition, u.visit_count
|
||||
FROM visits v JOIN urls u ON v.url = u.id
|
||||
ORDER BY v.visit_time DESC LIMIT 5000
|
||||
""")
|
||||
for url, title, visit_time, transition, count in cursor.fetchall():
|
||||
entries.append({
|
||||
"url": url, "title": title or "",
|
||||
"visit_time": chrome_time_to_datetime(visit_time),
|
||||
"transition": transition & 0xFF,
|
||||
"visit_count": count,
|
||||
})
|
||||
conn.close()
|
||||
except sqlite3.Error as e:
|
||||
entries.append({"error": str(e)})
|
||||
return entries
|
||||
|
||||
|
||||
def parse_downloads(profile_path):
|
||||
"""Parse download history from History database."""
|
||||
db_path = os.path.join(profile_path, "History")
|
||||
if not os.path.exists(db_path):
|
||||
return []
|
||||
downloads = []
|
||||
try:
|
||||
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT target_path, tab_url, total_bytes, start_time, end_time,
|
||||
danger_type, interrupt_reason, mime_type
|
||||
FROM downloads ORDER BY start_time DESC LIMIT 1000
|
||||
""")
|
||||
for row in cursor.fetchall():
|
||||
downloads.append({
|
||||
"target_path": row[0], "source_url": row[1],
|
||||
"total_bytes": row[2],
|
||||
"start_time": chrome_time_to_datetime(row[3]),
|
||||
"end_time": chrome_time_to_datetime(row[4]),
|
||||
"danger_type": row[5], "interrupt_reason": row[6],
|
||||
"mime_type": row[7],
|
||||
})
|
||||
conn.close()
|
||||
except sqlite3.Error as e:
|
||||
downloads.append({"error": str(e)})
|
||||
return downloads
|
||||
|
||||
|
||||
def parse_cookies(profile_path):
|
||||
"""Parse cookies from Cookies database."""
|
||||
db_path = os.path.join(profile_path, "Cookies")
|
||||
if not os.path.exists(db_path):
|
||||
db_path = os.path.join(profile_path, "Network", "Cookies")
|
||||
if not os.path.exists(db_path):
|
||||
return []
|
||||
cookies = []
|
||||
try:
|
||||
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT host_key, name, path, creation_utc, expires_utc,
|
||||
is_secure, is_httponly, samesite
|
||||
FROM cookies ORDER BY creation_utc DESC LIMIT 2000
|
||||
""")
|
||||
for row in cursor.fetchall():
|
||||
cookies.append({
|
||||
"host": row[0], "name": row[1], "path": row[2],
|
||||
"created": chrome_time_to_datetime(row[3]),
|
||||
"expires": chrome_time_to_datetime(row[4]),
|
||||
"secure": bool(row[5]), "httponly": bool(row[6]),
|
||||
"samesite": row[7],
|
||||
})
|
||||
conn.close()
|
||||
except sqlite3.Error as e:
|
||||
cookies.append({"error": str(e)})
|
||||
return cookies
|
||||
|
||||
|
||||
def parse_autofill(profile_path):
|
||||
"""Parse autofill data from Web Data database."""
|
||||
db_path = os.path.join(profile_path, "Web Data")
|
||||
if not os.path.exists(db_path):
|
||||
return []
|
||||
entries = []
|
||||
try:
|
||||
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT name, value, count, date_created, date_last_used
|
||||
FROM autofill ORDER BY date_last_used DESC LIMIT 500
|
||||
""")
|
||||
for row in cursor.fetchall():
|
||||
entries.append({
|
||||
"field_name": row[0], "value": row[1][:50] + "..." if len(row[1]) > 50 else row[1],
|
||||
"usage_count": row[2],
|
||||
"created": chrome_time_to_datetime(row[3] * 1000000 if row[3] else 0),
|
||||
"last_used": chrome_time_to_datetime(row[4] * 1000000 if row[4] else 0),
|
||||
})
|
||||
conn.close()
|
||||
except sqlite3.Error as e:
|
||||
entries.append({"error": str(e)})
|
||||
return entries
|
||||
|
||||
|
||||
def parse_extensions(profile_path):
|
||||
"""Parse installed browser extensions."""
|
||||
ext_dir = os.path.join(profile_path, "Extensions")
|
||||
extensions = []
|
||||
if not os.path.isdir(ext_dir):
|
||||
return extensions
|
||||
for ext_id in os.listdir(ext_dir):
|
||||
ext_path = os.path.join(ext_dir, ext_id)
|
||||
if os.path.isdir(ext_path):
|
||||
versions = sorted(os.listdir(ext_path))
|
||||
manifest_path = os.path.join(ext_path, versions[-1], "manifest.json") if versions else None
|
||||
name = ext_id
|
||||
if manifest_path and os.path.exists(manifest_path):
|
||||
try:
|
||||
with open(manifest_path, "r", encoding="utf-8") as f:
|
||||
manifest = json.load(f)
|
||||
name = manifest.get("name", ext_id)
|
||||
extensions.append({
|
||||
"id": ext_id, "name": name,
|
||||
"version": manifest.get("version", "?"),
|
||||
"permissions": manifest.get("permissions", [])[:10],
|
||||
})
|
||||
except (json.JSONDecodeError, IOError):
|
||||
extensions.append({"id": ext_id, "name": name, "version": "unknown"})
|
||||
return extensions
|
||||
|
||||
|
||||
def detect_suspicious_activity(history, downloads):
|
||||
"""Flag suspicious browsing and download patterns."""
|
||||
findings = []
|
||||
suspicious_domains = ["pastebin.com", "ngrok.io", "raw.githubusercontent.com",
|
||||
"transfer.sh", "file.io", "temp.sh", "anonfiles.com"]
|
||||
for entry in history:
|
||||
url = entry.get("url", "").lower()
|
||||
for domain in suspicious_domains:
|
||||
if domain in url:
|
||||
findings.append({
|
||||
"type": "suspicious_url", "url": entry["url"],
|
||||
"domain": domain, "time": entry.get("visit_time"),
|
||||
})
|
||||
dangerous_mimes = ["application/x-msdownload", "application/x-msdos-program",
|
||||
"application/x-executable", "application/vnd.ms-excel.sheet.macroEnabled"]
|
||||
for dl in downloads:
|
||||
if dl.get("danger_type", 0) > 0:
|
||||
findings.append({
|
||||
"type": "dangerous_download", "path": dl.get("target_path"),
|
||||
"source": dl.get("source_url"), "danger_type": dl.get("danger_type"),
|
||||
})
|
||||
if dl.get("mime_type", "") in dangerous_mimes:
|
||||
findings.append({
|
||||
"type": "suspicious_mime", "mime": dl.get("mime_type"),
|
||||
"path": dl.get("target_path"),
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Browser Forensics Analysis Agent")
|
||||
print("Chromium history, downloads, cookies, extensions")
|
||||
print("=" * 60)
|
||||
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
profiles = find_browser_profiles(target)
|
||||
|
||||
if not profiles:
|
||||
print("\n[!] No browser profiles found.")
|
||||
print("[DEMO] Usage: python agent.py <profile_path>")
|
||||
print(" e.g. python agent.py ~/AppData/Local/Google/Chrome/User\\ Data/Default")
|
||||
sys.exit(0)
|
||||
|
||||
for profile in profiles:
|
||||
print(f"\n[*] Profile: {profile}")
|
||||
|
||||
history = parse_history(profile)
|
||||
print(f" History entries: {len(history)}")
|
||||
for h in history[:5]:
|
||||
print(f" {h.get('visit_time', '?')} | {h.get('title', '')[:50]} | {h.get('url', '')[:60]}")
|
||||
|
||||
downloads = parse_downloads(profile)
|
||||
print(f" Downloads: {len(downloads)}")
|
||||
for d in downloads[:5]:
|
||||
print(f" {d.get('start_time', '?')} | {d.get('mime_type', '?')} | {os.path.basename(d.get('target_path', ''))}")
|
||||
|
||||
cookies = parse_cookies(profile)
|
||||
print(f" Cookies: {len(cookies)}")
|
||||
|
||||
extensions = parse_extensions(profile)
|
||||
print(f" Extensions: {len(extensions)}")
|
||||
for ext in extensions[:5]:
|
||||
print(f" {ext.get('name', '?')} v{ext.get('version', '?')} [{ext.get('id', '')[:20]}]")
|
||||
|
||||
findings = detect_suspicious_activity(history, downloads)
|
||||
print(f"\n --- Suspicious Activity: {len(findings)} findings ---")
|
||||
for f in findings[:10]:
|
||||
print(f" [{f['type']}] {f.get('url', f.get('path', ''))}")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,110 @@
|
||||
# API Reference: Campaign Attribution Evidence Analysis
|
||||
|
||||
## Diamond Model of Intrusion Analysis
|
||||
|
||||
### Four Core Features
|
||||
| Feature | Description | Attribution Value |
|
||||
|---------|-------------|-------------------|
|
||||
| Adversary | Threat actor identity | Direct attribution |
|
||||
| Capability | Malware, exploits, tools | Indirect - shared tooling |
|
||||
| Infrastructure | C2, domains, IPs | Strong - operational overlap |
|
||||
| Victim | Targets, sectors, regions | Contextual - targeting pattern |
|
||||
|
||||
### Pivot Analysis
|
||||
```
|
||||
Adversary ←→ Capability ←→ Infrastructure ←→ Victim
|
||||
↕ ↕ ↕ ↕
|
||||
(HUMINT) (Malware DB) (WHOIS/DNS) (Victimology)
|
||||
```
|
||||
|
||||
## Analysis of Competing Hypotheses (ACH)
|
||||
|
||||
### Matrix Format
|
||||
```
|
||||
Evidence \ Hypothesis | APT28 | APT29 | Lazarus | Unknown
|
||||
-----------------------------------------------------------------
|
||||
Infrastructure overlap | ++ | - | - | N
|
||||
TTP consistency | ++ | ++ | - | N
|
||||
Malware similarity | + | - | - | N
|
||||
Timing (UTC+3) | ++ | ++ | - | N
|
||||
Language (Russian) | ++ | ++ | - | N
|
||||
```
|
||||
|
||||
### Scoring
|
||||
| Symbol | Meaning | Weight |
|
||||
|--------|---------|--------|
|
||||
| `++` | Strongly consistent | +2 |
|
||||
| `+` | Consistent | +1 |
|
||||
| `N` | Neutral | 0 |
|
||||
| `-` | Inconsistent | -1 |
|
||||
| `--` | Strongly inconsistent | -2 |
|
||||
|
||||
## MITRE ATT&CK Group Queries
|
||||
|
||||
### Python (mitreattack-python)
|
||||
```python
|
||||
from mitreattack.stix20 import MitreAttackData
|
||||
attack = MitreAttackData("enterprise-attack.json")
|
||||
group = attack.get_group_by_alias("APT29")
|
||||
techniques = attack.get_techniques_used_by_group(group.id)
|
||||
```
|
||||
|
||||
### STIX2 Relationship Query
|
||||
```python
|
||||
from stix2 import Filter
|
||||
relationships = src.query([
|
||||
Filter("type", "=", "relationship"),
|
||||
Filter("source_ref", "=", group_id),
|
||||
Filter("relationship_type", "=", "uses"),
|
||||
])
|
||||
```
|
||||
|
||||
## Infrastructure Overlap Tools
|
||||
|
||||
### PassiveTotal / RiskIQ
|
||||
```bash
|
||||
# WHOIS history
|
||||
curl -u user:key "https://api.passivetotal.org/v2/whois?query=domain.com"
|
||||
|
||||
# Passive DNS
|
||||
curl -u user:key "https://api.passivetotal.org/v2/dns/passive?query=1.2.3.4"
|
||||
```
|
||||
|
||||
### VirusTotal Relations
|
||||
```bash
|
||||
curl -H "x-apikey: KEY" \
|
||||
"https://www.virustotal.com/api/v3/domains/example.com/communicating_files"
|
||||
```
|
||||
|
||||
## Confidence Assessment Framework
|
||||
|
||||
| Level | Score Range | Criteria |
|
||||
|-------|------------|---------|
|
||||
| HIGH | 0.8-1.0 | Multiple independent evidence types converge |
|
||||
| MEDIUM | 0.5-0.8 | Significant evidence with some gaps |
|
||||
| LOW | 0.2-0.5 | Limited evidence, alternative hypotheses remain |
|
||||
| NEGLIGIBLE | 0.0-0.2 | Insufficient evidence for attribution |
|
||||
|
||||
## STIX Attribution Objects
|
||||
|
||||
### Campaign Object
|
||||
```json
|
||||
{
|
||||
"type": "campaign",
|
||||
"name": "Operation DarkShadow",
|
||||
"first_seen": "2024-01-15T00:00:00Z",
|
||||
"last_seen": "2024-03-20T00:00:00Z",
|
||||
"objective": "Espionage targeting defense sector"
|
||||
}
|
||||
```
|
||||
|
||||
### Attribution Relationship
|
||||
```json
|
||||
{
|
||||
"type": "relationship",
|
||||
"relationship_type": "attributed-to",
|
||||
"source_ref": "campaign--abc123",
|
||||
"target_ref": "intrusion-set--def456",
|
||||
"confidence": 75
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Campaign attribution analysis agent using Diamond Model and ACH methodology.
|
||||
|
||||
Evaluates attribution evidence including infrastructure overlaps, TTP consistency,
|
||||
malware code similarity, timing patterns, and language artifacts.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import hashlib
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
DIAMOND_DIMENSIONS = {
|
||||
"adversary": "Threat actor identity, group attribution",
|
||||
"capability": "Malware, exploits, tools used",
|
||||
"infrastructure": "C2 servers, domains, IP addresses",
|
||||
"victim": "Targeted sectors, regions, organizations",
|
||||
}
|
||||
|
||||
EVIDENCE_WEIGHTS = {
|
||||
"infrastructure_overlap": 0.25,
|
||||
"ttp_consistency": 0.30,
|
||||
"malware_code_similarity": 0.25,
|
||||
"timing_pattern": 0.10,
|
||||
"language_artifact": 0.10,
|
||||
}
|
||||
|
||||
CONFIDENCE_LEVELS = {
|
||||
(0.8, 1.0): "HIGH - Strong attribution confidence",
|
||||
(0.5, 0.8): "MEDIUM - Moderate attribution, further analysis recommended",
|
||||
(0.2, 0.5): "LOW - Weak attribution, insufficient evidence",
|
||||
(0.0, 0.2): "NEGLIGIBLE - No meaningful attribution possible",
|
||||
}
|
||||
|
||||
|
||||
def diamond_model_analysis(adversary=None, capability=None, infrastructure=None, victim=None):
|
||||
"""Structure evidence using the Diamond Model of Intrusion Analysis."""
|
||||
model = {
|
||||
"adversary": {
|
||||
"identified": adversary is not None,
|
||||
"details": adversary or "Unknown",
|
||||
},
|
||||
"capability": {
|
||||
"tools": capability.get("tools", []) if capability else [],
|
||||
"exploits": capability.get("exploits", []) if capability else [],
|
||||
"malware": capability.get("malware", []) if capability else [],
|
||||
},
|
||||
"infrastructure": {
|
||||
"c2_servers": infrastructure.get("c2", []) if infrastructure else [],
|
||||
"domains": infrastructure.get("domains", []) if infrastructure else [],
|
||||
"ip_addresses": infrastructure.get("ips", []) if infrastructure else [],
|
||||
},
|
||||
"victim": {
|
||||
"sectors": victim.get("sectors", []) if victim else [],
|
||||
"regions": victim.get("regions", []) if victim else [],
|
||||
},
|
||||
"pivot_opportunities": [],
|
||||
}
|
||||
if infrastructure and infrastructure.get("c2"):
|
||||
model["pivot_opportunities"].append("Pivot from C2 infrastructure to related campaigns")
|
||||
if capability and capability.get("malware"):
|
||||
model["pivot_opportunities"].append("Pivot from malware samples to shared infrastructure")
|
||||
return model
|
||||
|
||||
|
||||
def evaluate_infrastructure_overlap(campaign_infra, known_actor_infra):
|
||||
"""Score infrastructure overlap between campaign and known actor."""
|
||||
campaign_set = set(campaign_infra)
|
||||
known_set = set(known_actor_infra)
|
||||
if not campaign_set or not known_set:
|
||||
return 0.0, []
|
||||
overlap = campaign_set & known_set
|
||||
score = len(overlap) / max(len(campaign_set), len(known_set))
|
||||
return round(score, 4), sorted(overlap)
|
||||
|
||||
|
||||
def evaluate_ttp_consistency(campaign_ttps, actor_ttps):
|
||||
"""Score TTP consistency using MITRE ATT&CK technique overlap."""
|
||||
campaign_set = set(campaign_ttps)
|
||||
actor_set = set(actor_ttps)
|
||||
if not campaign_set or not actor_set:
|
||||
return 0.0, []
|
||||
overlap = campaign_set & actor_set
|
||||
jaccard = len(overlap) / len(campaign_set | actor_set)
|
||||
return round(jaccard, 4), sorted(overlap)
|
||||
|
||||
|
||||
def evaluate_malware_similarity(sample_features, known_features):
|
||||
"""Score malware code similarity based on feature comparison."""
|
||||
if not sample_features or not known_features:
|
||||
return 0.0
|
||||
matches = 0
|
||||
total = max(len(sample_features), len(known_features))
|
||||
for feature in sample_features:
|
||||
if feature in known_features:
|
||||
matches += 1
|
||||
return round(matches / total, 4) if total > 0 else 0.0
|
||||
|
||||
|
||||
def evaluate_timing_pattern(campaign_timestamps, actor_timezone_offset=None):
|
||||
"""Analyze operational timing to infer timezone/working hours."""
|
||||
if not campaign_timestamps:
|
||||
return {"score": 0.0, "working_hours": None, "timezone_guess": None}
|
||||
hours = []
|
||||
for ts in campaign_timestamps:
|
||||
try:
|
||||
if isinstance(ts, str):
|
||||
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
||||
else:
|
||||
dt = ts
|
||||
adjusted = dt.hour + (actor_timezone_offset or 0)
|
||||
hours.append(adjusted % 24)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
if not hours:
|
||||
return {"score": 0.0}
|
||||
work_hours = sum(1 for h in hours if 8 <= h <= 18)
|
||||
work_ratio = work_hours / len(hours)
|
||||
avg_hour = sum(hours) / len(hours)
|
||||
return {
|
||||
"score": round(work_ratio, 4),
|
||||
"average_hour_utc": round(avg_hour, 1),
|
||||
"work_hour_ratio": round(work_ratio, 4),
|
||||
"sample_size": len(hours),
|
||||
}
|
||||
|
||||
|
||||
def evaluate_language_artifacts(strings_list):
|
||||
"""Detect language artifacts in malware strings or documents."""
|
||||
language_indicators = {
|
||||
"Russian": [r"[а-яА-Я]{3,}", r"codepage.*1251", r"locale.*ru"],
|
||||
"Chinese": [r"[\u4e00-\u9fff]{2,}", r"codepage.*936", r"GB2312"],
|
||||
"Korean": [r"[\uac00-\ud7af]{2,}", r"codepage.*949", r"EUC-KR"],
|
||||
"Farsi": [r"[\u0600-\u06ff]{3,}", r"codepage.*1256"],
|
||||
"English": [r"\b(the|and|for|with)\b"],
|
||||
}
|
||||
detections = defaultdict(int)
|
||||
for s in strings_list:
|
||||
for lang, patterns in language_indicators.items():
|
||||
for pattern in patterns:
|
||||
if re.search(pattern, s, re.IGNORECASE):
|
||||
detections[lang] += 1
|
||||
total = sum(detections.values()) or 1
|
||||
scored = {lang: round(count / total, 4) for lang, count in detections.items()}
|
||||
return scored
|
||||
|
||||
|
||||
def ach_analysis(hypotheses, evidence_items):
|
||||
"""Analysis of Competing Hypotheses (ACH) for attribution."""
|
||||
matrix = {}
|
||||
for hyp in hypotheses:
|
||||
hyp_name = hyp["name"]
|
||||
matrix[hyp_name] = {"consistent": 0, "inconsistent": 0, "neutral": 0, "score": 0}
|
||||
for evidence in evidence_items:
|
||||
ev_name = evidence["name"]
|
||||
consistency = evidence.get("hypotheses", {}).get(hyp_name, "neutral")
|
||||
if consistency == "consistent":
|
||||
matrix[hyp_name]["consistent"] += evidence.get("weight", 1)
|
||||
elif consistency == "inconsistent":
|
||||
matrix[hyp_name]["inconsistent"] += evidence.get("weight", 1)
|
||||
else:
|
||||
matrix[hyp_name]["neutral"] += evidence.get("weight", 1)
|
||||
c = matrix[hyp_name]["consistent"]
|
||||
i = matrix[hyp_name]["inconsistent"]
|
||||
matrix[hyp_name]["score"] = round((c - i) / (c + i + 0.01), 4)
|
||||
return matrix
|
||||
|
||||
|
||||
def compute_attribution_score(scores):
|
||||
"""Compute weighted attribution confidence score."""
|
||||
total = 0.0
|
||||
for evidence_type, weight in EVIDENCE_WEIGHTS.items():
|
||||
score = scores.get(evidence_type, 0.0)
|
||||
total += score * weight
|
||||
confidence = "UNKNOWN"
|
||||
for (low, high), label in CONFIDENCE_LEVELS.items():
|
||||
if low <= total < high:
|
||||
confidence = label
|
||||
break
|
||||
return round(total, 4), confidence
|
||||
|
||||
|
||||
def generate_attribution_report(campaign_name, candidate_actor, evidence):
|
||||
"""Generate structured attribution assessment report."""
|
||||
scores = {}
|
||||
details = {}
|
||||
|
||||
infra_score, infra_overlap = evaluate_infrastructure_overlap(
|
||||
evidence.get("campaign_infra", []), evidence.get("actor_infra", []))
|
||||
scores["infrastructure_overlap"] = infra_score
|
||||
details["infrastructure_overlap"] = infra_overlap
|
||||
|
||||
ttp_score, ttp_overlap = evaluate_ttp_consistency(
|
||||
evidence.get("campaign_ttps", []), evidence.get("actor_ttps", []))
|
||||
scores["ttp_consistency"] = ttp_score
|
||||
details["ttp_consistency"] = ttp_overlap
|
||||
|
||||
malware_score = evaluate_malware_similarity(
|
||||
evidence.get("sample_features", []), evidence.get("known_features", []))
|
||||
scores["malware_code_similarity"] = malware_score
|
||||
|
||||
timing = evaluate_timing_pattern(
|
||||
evidence.get("timestamps", []), evidence.get("tz_offset"))
|
||||
scores["timing_pattern"] = timing.get("score", 0.0)
|
||||
details["timing"] = timing
|
||||
|
||||
lang = evaluate_language_artifacts(evidence.get("strings", []))
|
||||
scores["language_artifact"] = max(lang.values()) if lang else 0.0
|
||||
details["language_artifacts"] = lang
|
||||
|
||||
total_score, confidence = compute_attribution_score(scores)
|
||||
|
||||
return {
|
||||
"campaign": campaign_name,
|
||||
"candidate_actor": candidate_actor,
|
||||
"attribution_score": total_score,
|
||||
"confidence_level": confidence,
|
||||
"evidence_scores": scores,
|
||||
"evidence_details": details,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Campaign Attribution Evidence Analysis Agent")
|
||||
print("Diamond Model, ACH, TTP/infrastructure/malware scoring")
|
||||
print("=" * 60)
|
||||
|
||||
demo_evidence = {
|
||||
"campaign_infra": ["185.220.101.1", "evil-domain.com", "c2.attacker.net"],
|
||||
"actor_infra": ["185.220.101.1", "c2.attacker.net", "other-domain.org"],
|
||||
"campaign_ttps": ["T1566.001", "T1059.001", "T1053.005", "T1071.001", "T1041"],
|
||||
"actor_ttps": ["T1566.001", "T1059.001", "T1053.005", "T1071.001", "T1021.001", "T1003.001"],
|
||||
"sample_features": ["xor_0x55", "mutex_Global\\QWE", "ua_Mozilla5", "rc4_key"],
|
||||
"known_features": ["xor_0x55", "mutex_Global\\QWE", "ua_Mozilla5", "aes_cbc"],
|
||||
"timestamps": ["2024-03-15T06:30:00Z", "2024-03-15T07:15:00Z",
|
||||
"2024-03-16T08:00:00Z", "2024-03-16T09:45:00Z"],
|
||||
"tz_offset": 3,
|
||||
"strings": ["Привет мир", "connect to server", "upload file"],
|
||||
}
|
||||
|
||||
report = generate_attribution_report("Operation DarkShadow", "APT29", demo_evidence)
|
||||
|
||||
print(f"\n[*] Campaign: {report['campaign']}")
|
||||
print(f"[*] Candidate: {report['candidate_actor']}")
|
||||
print(f"[*] Attribution Score: {report['attribution_score']}")
|
||||
print(f"[*] Confidence: {report['confidence_level']}")
|
||||
print("\n--- Evidence Scores ---")
|
||||
for ev, score in report["evidence_scores"].items():
|
||||
weight = EVIDENCE_WEIGHTS.get(ev, 0)
|
||||
print(f" {ev:30s} score={score:.4f} weight={weight}")
|
||||
print(f"\n[*] Full report:\n{json.dumps(report, indent=2, default=str)}")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,97 @@
|
||||
# API Reference: Certificate Transparency Phishing Detection
|
||||
|
||||
## crt.sh API
|
||||
|
||||
### Search Certificates
|
||||
```bash
|
||||
# JSON output
|
||||
curl "https://crt.sh/?q=%.example.com&output=json"
|
||||
|
||||
# Exclude expired
|
||||
curl "https://crt.sh/?q=%.example.com&output=json&exclude=expired"
|
||||
|
||||
# Exact match
|
||||
curl "https://crt.sh/?q=example.com&output=json"
|
||||
```
|
||||
|
||||
### Response Fields
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `id` | Certificate ID in crt.sh database |
|
||||
| `common_name` | Certificate CN |
|
||||
| `name_value` | All SANs (newline-separated) |
|
||||
| `issuer_name` | Certificate Authority |
|
||||
| `not_before` | Validity start |
|
||||
| `not_after` | Validity end |
|
||||
| `serial_number` | Certificate serial |
|
||||
|
||||
## Certstream - Real-time CT Monitoring
|
||||
|
||||
### Python Client
|
||||
```python
|
||||
import certstream
|
||||
|
||||
def callback(message, context):
|
||||
if message["message_type"] == "certificate_update":
|
||||
data = message["data"]
|
||||
domains = data["leaf_cert"]["all_domains"]
|
||||
for domain in domains:
|
||||
if "example" in domain:
|
||||
print(f"[ALERT] {domain}")
|
||||
|
||||
certstream.listen_for_events(callback, url="wss://certstream.calidog.io/")
|
||||
```
|
||||
|
||||
### Message Fields
|
||||
| Field | Path |
|
||||
|-------|------|
|
||||
| Domains | `data.leaf_cert.all_domains` |
|
||||
| Issuer | `data.leaf_cert.issuer.O` |
|
||||
| Subject | `data.leaf_cert.subject.CN` |
|
||||
| Fingerprint | `data.leaf_cert.fingerprint` |
|
||||
| Source | `data.source.name` |
|
||||
|
||||
## CT Log Servers
|
||||
|
||||
| Log | Operator | URL |
|
||||
|-----|----------|-----|
|
||||
| Argon | Google | `ct.googleapis.com/logs/argon2024` |
|
||||
| Xenon | Google | `ct.googleapis.com/logs/xenon2024` |
|
||||
| Nimbus | Cloudflare | `ct.cloudflare.com/logs/nimbus2024` |
|
||||
| Oak | Let's Encrypt | `oak.ct.letsencrypt.org/2024h1` |
|
||||
| Yeti | DigiCert | `yeti2024.ct.digicert.com/log` |
|
||||
|
||||
## Phishing Detection Techniques
|
||||
|
||||
### Homoglyph / IDN Attacks
|
||||
| Original | Lookalike | Technique |
|
||||
|----------|-----------|-----------|
|
||||
| example.com | examp1e.com | Character substitution (l→1) |
|
||||
| google.com | gооgle.com | Cyrillic о (U+043E) |
|
||||
| paypal.com | paypa1.com | l→1 substitution |
|
||||
| microsoft.com | mіcrosoft.com | Cyrillic і (U+0456) |
|
||||
|
||||
### dnstwist Integration
|
||||
```bash
|
||||
dnstwist -r -f json example.com # Generate and resolve permutations
|
||||
dnstwist -w wordlist.txt example.com # Dictionary-based
|
||||
```
|
||||
|
||||
## Certificate Details Lookup
|
||||
```bash
|
||||
# Get full certificate from crt.sh
|
||||
curl "https://crt.sh/?d=<cert_id>"
|
||||
|
||||
# OpenSSL inspection
|
||||
openssl s_client -connect domain.com:443 -servername domain.com </dev/null 2>/dev/null | \
|
||||
openssl x509 -noout -text
|
||||
```
|
||||
|
||||
## Suspicious Indicators
|
||||
| Pattern | Risk Level |
|
||||
|---------|-----------|
|
||||
| Free CA + new domain + brand keyword | HIGH |
|
||||
| Wildcard cert on recently registered domain | HIGH |
|
||||
| Multiple certs for slight domain variants | MEDIUM |
|
||||
| IDN/punycode domain mimicking brand | HIGH |
|
||||
| Cert issued same day as domain registration | MEDIUM |
|
||||
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Certificate Transparency monitoring agent for phishing detection.
|
||||
|
||||
Queries crt.sh for certificates matching target domains, detects lookalike
|
||||
certificates, and identifies potential phishing infrastructure.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
try:
|
||||
import requests
|
||||
HAS_REQUESTS = True
|
||||
except ImportError:
|
||||
HAS_REQUESTS = False
|
||||
|
||||
|
||||
def query_crtsh(domain, wildcard=True, expired=False):
|
||||
"""Query crt.sh for certificates matching a domain."""
|
||||
if not HAS_REQUESTS:
|
||||
return []
|
||||
query = f"%.{domain}" if wildcard else domain
|
||||
params = {"q": query, "output": "json"}
|
||||
if not expired:
|
||||
params["exclude"] = "expired"
|
||||
try:
|
||||
resp = requests.get("https://crt.sh/", params=params, timeout=30)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except (requests.RequestException, json.JSONDecodeError) as e:
|
||||
return [{"error": str(e)}]
|
||||
|
||||
|
||||
def find_lookalike_domains(target_domain, ct_results):
|
||||
"""Identify certificates for domains that look similar to the target."""
|
||||
base = target_domain.split(".")[0].lower()
|
||||
lookalikes = []
|
||||
for cert in ct_results:
|
||||
cn = cert.get("common_name", "").lower()
|
||||
names = cert.get("name_value", "").lower().split("\n")
|
||||
for name in [cn] + names:
|
||||
name = name.strip()
|
||||
if not name or name == target_domain:
|
||||
continue
|
||||
similarity = calculate_similarity(base, name.split(".")[0])
|
||||
if similarity > 0.6 and name != target_domain:
|
||||
lookalikes.append({
|
||||
"domain": name,
|
||||
"similarity": round(similarity, 3),
|
||||
"issuer": cert.get("issuer_name", ""),
|
||||
"not_before": cert.get("not_before", ""),
|
||||
"not_after": cert.get("not_after", ""),
|
||||
"cert_id": cert.get("id"),
|
||||
})
|
||||
seen = set()
|
||||
unique = []
|
||||
for l in sorted(lookalikes, key=lambda x: -x["similarity"]):
|
||||
if l["domain"] not in seen:
|
||||
seen.add(l["domain"])
|
||||
unique.append(l)
|
||||
return unique
|
||||
|
||||
|
||||
def calculate_similarity(s1, s2):
|
||||
"""Calculate string similarity using Levenshtein-like ratio."""
|
||||
if s1 == s2:
|
||||
return 1.0
|
||||
len1, len2 = len(s1), len(s2)
|
||||
if len1 == 0 or len2 == 0:
|
||||
return 0.0
|
||||
matrix = [[0] * (len2 + 1) for _ in range(len1 + 1)]
|
||||
for i in range(len1 + 1):
|
||||
matrix[i][0] = i
|
||||
for j in range(len2 + 1):
|
||||
matrix[0][j] = j
|
||||
for i in range(1, len1 + 1):
|
||||
for j in range(1, len2 + 1):
|
||||
cost = 0 if s1[i-1] == s2[j-1] else 1
|
||||
matrix[i][j] = min(matrix[i-1][j] + 1, matrix[i][j-1] + 1,
|
||||
matrix[i-1][j-1] + cost)
|
||||
distance = matrix[len1][len2]
|
||||
return 1.0 - distance / max(len1, len2)
|
||||
|
||||
|
||||
HOMOGLYPH_MAP = {
|
||||
"a": ["а", "@", "4"], "e": ["е", "3"], "o": ["о", "0"],
|
||||
"i": ["і", "1", "l"], "l": ["1", "i", "I"],
|
||||
"s": ["5", "$"], "t": ["7"], "g": ["9", "q"],
|
||||
}
|
||||
|
||||
|
||||
def detect_homoglyph_domains(target_domain, ct_results):
|
||||
"""Detect domains using homoglyph/IDN attacks against target."""
|
||||
findings = []
|
||||
base = target_domain.split(".")[0].lower()
|
||||
for cert in ct_results:
|
||||
names = cert.get("name_value", "").lower().split("\n")
|
||||
for name in names:
|
||||
name = name.strip()
|
||||
if not name or name == target_domain:
|
||||
continue
|
||||
name_base = name.split(".")[0]
|
||||
if len(name_base) == len(base):
|
||||
diffs = sum(1 for a, b in zip(base, name_base) if a != b)
|
||||
if 0 < diffs <= 2:
|
||||
findings.append({
|
||||
"domain": name,
|
||||
"char_differences": diffs,
|
||||
"cert_id": cert.get("id"),
|
||||
"issuer": cert.get("issuer_name", ""),
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def analyze_issuer_patterns(ct_results):
|
||||
"""Analyze certificate issuer patterns for anomalies."""
|
||||
issuer_counts = defaultdict(int)
|
||||
free_cas = ["Let's Encrypt", "ZeroSSL", "Buypass"]
|
||||
for cert in ct_results:
|
||||
issuer = cert.get("issuer_name", "Unknown")
|
||||
issuer_counts[issuer] += 1
|
||||
free_ca_certs = sum(
|
||||
count for issuer, count in issuer_counts.items()
|
||||
if any(ca.lower() in issuer.lower() for ca in free_cas)
|
||||
)
|
||||
return {
|
||||
"issuers": dict(issuer_counts),
|
||||
"total_certs": len(ct_results),
|
||||
"free_ca_count": free_ca_certs,
|
||||
"free_ca_ratio": round(free_ca_certs / max(len(ct_results), 1), 3),
|
||||
}
|
||||
|
||||
|
||||
def detect_wildcard_abuse(ct_results):
|
||||
"""Detect suspicious wildcard certificate patterns."""
|
||||
wildcards = []
|
||||
for cert in ct_results:
|
||||
cn = cert.get("common_name", "")
|
||||
if cn.startswith("*."):
|
||||
wildcards.append({
|
||||
"domain": cn,
|
||||
"issuer": cert.get("issuer_name", ""),
|
||||
"not_before": cert.get("not_before", ""),
|
||||
})
|
||||
return wildcards
|
||||
|
||||
|
||||
def generate_report(target_domain, ct_results):
|
||||
"""Generate comprehensive CT monitoring report."""
|
||||
lookalikes = find_lookalike_domains(target_domain, ct_results)
|
||||
homoglyphs = detect_homoglyph_domains(target_domain, ct_results)
|
||||
issuer_analysis = analyze_issuer_patterns(ct_results)
|
||||
wildcards = detect_wildcard_abuse(ct_results)
|
||||
|
||||
risk_score = 0
|
||||
risk_score += min(len(lookalikes) * 10, 40)
|
||||
risk_score += min(len(homoglyphs) * 15, 30)
|
||||
risk_score += 20 if issuer_analysis["free_ca_ratio"] > 0.8 else 0
|
||||
risk_score = min(risk_score, 100)
|
||||
|
||||
return {
|
||||
"target_domain": target_domain,
|
||||
"total_certificates": len(ct_results),
|
||||
"lookalike_domains": lookalikes[:20],
|
||||
"homoglyph_domains": homoglyphs[:20],
|
||||
"issuer_analysis": issuer_analysis,
|
||||
"wildcard_certs": wildcards[:10],
|
||||
"risk_score": risk_score,
|
||||
"risk_level": "HIGH" if risk_score >= 60 else "MEDIUM" if risk_score >= 30 else "LOW",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Certificate Transparency Phishing Detection Agent")
|
||||
print("crt.sh queries, lookalike detection, homoglyph analysis")
|
||||
print("=" * 60)
|
||||
|
||||
domain = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
|
||||
if not domain:
|
||||
print("\n[DEMO] Usage: python agent.py <target_domain>")
|
||||
print(" e.g. python agent.py example.com")
|
||||
sys.exit(0)
|
||||
|
||||
if not HAS_REQUESTS:
|
||||
print("[!] Install requests: pip install requests")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\n[*] Querying crt.sh for: {domain}")
|
||||
results = query_crtsh(domain)
|
||||
print(f"[*] Found {len(results)} certificates")
|
||||
|
||||
report = generate_report(domain, results)
|
||||
|
||||
print(f"\n--- Lookalike Domains ({len(report['lookalike_domains'])}) ---")
|
||||
for l in report["lookalike_domains"][:10]:
|
||||
print(f" [{l['similarity']:.3f}] {l['domain']} (issuer: {l['issuer'][:40]})")
|
||||
|
||||
print(f"\n--- Homoglyph Domains ({len(report['homoglyph_domains'])}) ---")
|
||||
for h in report["homoglyph_domains"][:10]:
|
||||
print(f" [diff={h['char_differences']}] {h['domain']}")
|
||||
|
||||
print(f"\n--- Issuer Analysis ---")
|
||||
for issuer, count in sorted(report["issuer_analysis"]["issuers"].items(),
|
||||
key=lambda x: -x[1])[:5]:
|
||||
print(f" {count:4d} | {issuer[:60]}")
|
||||
|
||||
print(f"\n[*] Risk Score: {report['risk_score']}/100 ({report['risk_level']})")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: analyzing-cloud-storage-access-patterns
|
||||
description: >-
|
||||
Detect abnormal access patterns in AWS S3, GCS, and Azure Blob Storage by analyzing CloudTrail
|
||||
Data Events, GCS audit logs, and Azure Storage Analytics. Identifies after-hours bulk downloads,
|
||||
access from new IP addresses, unusual API calls (GetObject spikes), and potential data exfiltration
|
||||
using statistical baselines and time-series anomaly detection.
|
||||
---
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Install dependencies: `pip install boto3 requests`
|
||||
2. Query CloudTrail for S3 Data Events using AWS CLI or boto3.
|
||||
3. Build access baselines: hourly request volume, per-user object counts, source IP history.
|
||||
4. Detect anomalies:
|
||||
- After-hours access (outside 8am-6pm local time)
|
||||
- Bulk downloads: >100 GetObject calls from single principal in 1 hour
|
||||
- New source IPs not seen in the prior 30 days
|
||||
- ListBucket enumeration spikes (reconnaissance indicator)
|
||||
5. Generate prioritized findings report.
|
||||
|
||||
```bash
|
||||
python scripts/agent.py --bucket my-sensitive-data --hours-back 24 --output s3_access_report.json
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### CloudTrail S3 Data Event
|
||||
```json
|
||||
{"eventName": "GetObject", "requestParameters": {"bucketName": "sensitive-data", "key": "financials/q4.xlsx"},
|
||||
"sourceIPAddress": "203.0.113.50", "userIdentity": {"arn": "arn:aws:iam::123456789012:user/analyst"}}
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
# API Reference: Cloud Storage Access Pattern Analysis
|
||||
|
||||
## AWS CLI - CloudTrail Lookup
|
||||
```bash
|
||||
aws cloudtrail lookup-events \
|
||||
--lookup-attributes AttributeKey=ResourceType,AttributeValue=AWS::S3::Object \
|
||||
--start-time 2024-01-15T00:00:00Z \
|
||||
--output json
|
||||
```
|
||||
|
||||
## CloudTrail S3 Data Event Structure
|
||||
```json
|
||||
{
|
||||
"EventTime": "2024-01-15T10:30:00Z",
|
||||
"EventName": "GetObject",
|
||||
"Username": "analyst",
|
||||
"CloudTrailEvent": "{\"sourceIPAddress\":\"10.0.0.1\",\"userAgent\":\"aws-cli\",\"requestParameters\":{\"bucketName\":\"data\",\"key\":\"file.csv\"},\"userIdentity\":{\"arn\":\"arn:aws:iam::123:user/analyst\"}}"
|
||||
}
|
||||
```
|
||||
|
||||
## Key S3 Event Names
|
||||
| Event | Meaning |
|
||||
|-------|---------|
|
||||
| GetObject | Object download |
|
||||
| PutObject | Object upload |
|
||||
| DeleteObject | Object deletion |
|
||||
| ListBucket / ListObjectsV2 | Bucket enumeration |
|
||||
| GetBucketPolicy | Policy read |
|
||||
| PutBucketPolicy | Policy modification |
|
||||
|
||||
## Detection Thresholds
|
||||
| Anomaly | Threshold | Severity |
|
||||
|---------|-----------|----------|
|
||||
| Bulk download | >100 GetObject/hr per user | Critical |
|
||||
| After-hours | Access outside 08:00-18:00 UTC | Medium |
|
||||
| New source IP | IP not in 30-day baseline | High |
|
||||
| Enumeration | >20 ListBucket per user | High |
|
||||
|
||||
## boto3 CloudTrail Client (alternative)
|
||||
```python
|
||||
import boto3
|
||||
client = boto3.client("cloudtrail")
|
||||
response = client.lookup_events(
|
||||
LookupAttributes=[{"AttributeKey":"ResourceType","AttributeValue":"AWS::S3::Object"}],
|
||||
StartTime=datetime(2024,1,15),
|
||||
MaxResults=50
|
||||
)
|
||||
events = response["Events"]
|
||||
```
|
||||
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cloud Storage Access Pattern Analyzer - Detects abnormal S3/GCS/Azure Blob access via CloudTrail."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import argparse
|
||||
import subprocess
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def query_cloudtrail_s3_events(bucket_name, hours_back=24):
|
||||
"""Query CloudTrail for S3 data events on a specific bucket."""
|
||||
start_time = (datetime.utcnow() - timedelta(hours=hours_back)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
cmd = [
|
||||
"aws", "cloudtrail", "lookup-events",
|
||||
"--lookup-attributes", f"AttributeKey=ResourceType,AttributeValue=AWS::S3::Object",
|
||||
"--start-time", start_time,
|
||||
"--output", "json",
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
logger.error("CloudTrail query failed: %s", result.stderr[:200])
|
||||
return []
|
||||
events = json.loads(result.stdout).get("Events", [])
|
||||
s3_events = []
|
||||
for event in events:
|
||||
ct_event = json.loads(event.get("CloudTrailEvent", "{}"))
|
||||
req_params = ct_event.get("requestParameters", {})
|
||||
if req_params.get("bucketName") == bucket_name or not bucket_name:
|
||||
s3_events.append({
|
||||
"timestamp": event.get("EventTime", ""),
|
||||
"event_name": event.get("EventName", ""),
|
||||
"username": event.get("Username", ""),
|
||||
"source_ip": ct_event.get("sourceIPAddress", ""),
|
||||
"user_agent": ct_event.get("userAgent", ""),
|
||||
"bucket": req_params.get("bucketName", ""),
|
||||
"key": req_params.get("key", ""),
|
||||
"user_arn": ct_event.get("userIdentity", {}).get("arn", ""),
|
||||
})
|
||||
logger.info("Retrieved %d S3 events for bucket '%s'", len(s3_events), bucket_name or "all")
|
||||
return s3_events
|
||||
|
||||
|
||||
def detect_bulk_downloads(events, threshold=100):
|
||||
"""Detect bulk GetObject operations from a single principal."""
|
||||
user_downloads = defaultdict(list)
|
||||
for event in events:
|
||||
if event["event_name"] == "GetObject":
|
||||
user_downloads[event["user_arn"]].append(event)
|
||||
alerts = []
|
||||
for user_arn, downloads in user_downloads.items():
|
||||
if len(downloads) >= threshold:
|
||||
keys = [d["key"] for d in downloads]
|
||||
alerts.append({
|
||||
"user_arn": user_arn,
|
||||
"download_count": len(downloads),
|
||||
"unique_keys": len(set(keys)),
|
||||
"source_ips": list({d["source_ip"] for d in downloads}),
|
||||
"first_access": downloads[0]["timestamp"],
|
||||
"last_access": downloads[-1]["timestamp"],
|
||||
"severity": "critical",
|
||||
"indicator": "Bulk download (potential exfiltration)",
|
||||
})
|
||||
logger.info("Found %d bulk download alerts", len(alerts))
|
||||
return alerts
|
||||
|
||||
|
||||
def detect_after_hours_access(events, business_start=8, business_end=18):
|
||||
"""Detect access outside business hours."""
|
||||
after_hours = []
|
||||
for event in events:
|
||||
try:
|
||||
ts = event["timestamp"]
|
||||
if isinstance(ts, str):
|
||||
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
||||
else:
|
||||
dt = ts
|
||||
hour = dt.hour
|
||||
if hour < business_start or hour >= business_end:
|
||||
event["indicator"] = f"After-hours access at {hour:02d}:00 UTC"
|
||||
event["severity"] = "medium"
|
||||
after_hours.append(event)
|
||||
except (ValueError, AttributeError):
|
||||
continue
|
||||
logger.info("Found %d after-hours access events", len(after_hours))
|
||||
return after_hours
|
||||
|
||||
|
||||
def detect_new_source_ips(events, known_ips=None):
|
||||
"""Detect access from IP addresses not in the known baseline."""
|
||||
if known_ips is None:
|
||||
known_ips = set()
|
||||
new_ip_events = []
|
||||
for event in events:
|
||||
ip = event["source_ip"]
|
||||
if ip and ip not in known_ips and not ip.startswith("AWS Internal"):
|
||||
event["indicator"] = f"New source IP: {ip}"
|
||||
event["severity"] = "high"
|
||||
new_ip_events.append(event)
|
||||
unique_new = len({e["source_ip"] for e in new_ip_events})
|
||||
logger.info("Found %d events from %d new source IPs", len(new_ip_events), unique_new)
|
||||
return new_ip_events
|
||||
|
||||
|
||||
def detect_enumeration(events, threshold=20):
|
||||
"""Detect ListBucket/ListObjects enumeration patterns."""
|
||||
user_listings = defaultdict(int)
|
||||
for event in events:
|
||||
if event["event_name"] in ("ListBucket", "ListObjects", "ListObjectsV2"):
|
||||
user_listings[event["user_arn"]] += 1
|
||||
alerts = []
|
||||
for user_arn, count in user_listings.items():
|
||||
if count >= threshold:
|
||||
alerts.append({
|
||||
"user_arn": user_arn,
|
||||
"list_count": count,
|
||||
"severity": "high",
|
||||
"indicator": "Bucket enumeration spike (reconnaissance)",
|
||||
})
|
||||
return alerts
|
||||
|
||||
|
||||
def build_access_baseline(events):
|
||||
"""Build statistical baseline of normal access patterns."""
|
||||
hourly_counts = defaultdict(int)
|
||||
user_counts = defaultdict(int)
|
||||
ip_set = set()
|
||||
for event in events:
|
||||
try:
|
||||
ts = event["timestamp"]
|
||||
if isinstance(ts, str):
|
||||
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
||||
hourly_counts[dt.hour] += 1
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
user_counts[event["user_arn"]] += 1
|
||||
if event["source_ip"]:
|
||||
ip_set.add(event["source_ip"])
|
||||
return {
|
||||
"hourly_distribution": dict(hourly_counts),
|
||||
"user_request_counts": dict(user_counts),
|
||||
"known_ips": list(ip_set),
|
||||
"total_events": len(events),
|
||||
}
|
||||
|
||||
|
||||
def generate_report(events, bulk_alerts, after_hours, new_ips, enum_alerts, baseline):
|
||||
"""Generate cloud storage access analysis report."""
|
||||
report = {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"total_events_analyzed": len(events),
|
||||
"bulk_download_alerts": bulk_alerts,
|
||||
"after_hours_access": len(after_hours),
|
||||
"new_source_ip_events": len(new_ips),
|
||||
"enumeration_alerts": enum_alerts,
|
||||
"baseline_summary": {
|
||||
"known_ips": len(baseline.get("known_ips", [])),
|
||||
"total_baseline_events": baseline.get("total_events", 0),
|
||||
},
|
||||
"sample_after_hours": after_hours[:10],
|
||||
"sample_new_ips": new_ips[:10],
|
||||
}
|
||||
total_alerts = len(bulk_alerts) + len(enum_alerts) + (1 if new_ips else 0)
|
||||
print(f"CLOUD STORAGE REPORT: {len(events)} events, {total_alerts} alerts")
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Cloud Storage Access Pattern Analyzer")
|
||||
parser.add_argument("--bucket", default="", help="S3 bucket name to analyze")
|
||||
parser.add_argument("--hours-back", type=int, default=24)
|
||||
parser.add_argument("--bulk-threshold", type=int, default=100)
|
||||
parser.add_argument("--known-ips-file", help="File with known IP baselines")
|
||||
parser.add_argument("--output", default="s3_access_report.json")
|
||||
args = parser.parse_args()
|
||||
|
||||
events = query_cloudtrail_s3_events(args.bucket, args.hours_back)
|
||||
baseline = build_access_baseline(events)
|
||||
known_ips = set(baseline.get("known_ips", []))
|
||||
if args.known_ips_file:
|
||||
with open(args.known_ips_file) as f:
|
||||
known_ips.update(line.strip() for line in f if line.strip())
|
||||
|
||||
bulk_alerts = detect_bulk_downloads(events, args.bulk_threshold)
|
||||
after_hours = detect_after_hours_access(events)
|
||||
new_ips = detect_new_source_ips(events, known_ips)
|
||||
enum_alerts = detect_enumeration(events)
|
||||
|
||||
report = generate_report(events, bulk_alerts, after_hours, new_ips, enum_alerts, baseline)
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2, default=str)
|
||||
logger.info("Report saved to %s", args.output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,112 @@
|
||||
# API Reference: Cobalt Strike Beacon Configuration Analysis
|
||||
|
||||
## Beacon Config TLV Format
|
||||
|
||||
### Structure
|
||||
```
|
||||
[Field ID: 2 bytes][Type: 2 bytes][Value: variable]
|
||||
Type 1 = short (2 bytes), Type 2 = int (4 bytes), Type 3 = string/blob (2-byte length + data)
|
||||
```
|
||||
|
||||
### XOR Encoding
|
||||
| Version | XOR Key |
|
||||
|---------|---------|
|
||||
| CS 3.x | `0x69` |
|
||||
| CS 4.x | `0x2E` |
|
||||
|
||||
### Key Configuration Fields
|
||||
| ID | Name | Description |
|
||||
|----|------|-------------|
|
||||
| 1 | BeaconType | 0=HTTP, 1=Hybrid, 2=SMB, 8=HTTPS |
|
||||
| 2 | Port | C2 communication port |
|
||||
| 3 | SleepTime | Beacon interval (ms) |
|
||||
| 5 | Jitter | Random sleep variation (%) |
|
||||
| 7 | PublicKey | RSA public key for encryption |
|
||||
| 8 | C2Server | Command and control server(s) |
|
||||
| 9 | UserAgent | HTTP User-Agent string |
|
||||
| 10 | PostURI | POST callback URI |
|
||||
| 37 | Watermark | License watermark (operator ID) |
|
||||
| 54 | PipeName | Named pipe for SMB beacons |
|
||||
|
||||
## 1768.py (Didier Stevens) - Config Extractor
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
python 1768.py <beacon_file> # Extract config
|
||||
python 1768.py -j <beacon_file> # JSON output
|
||||
python 1768.py -r <beacon_file> # Raw config dump
|
||||
```
|
||||
|
||||
## CobaltStrikeParser (SentinelOne)
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
python parse_beacon_config.py <file>
|
||||
python parse_beacon_config.py --json <file>
|
||||
```
|
||||
|
||||
### Output Fields
|
||||
```
|
||||
BeaconType: HTTPS
|
||||
Port: 443
|
||||
SleepTime: 60000
|
||||
Jitter: 37
|
||||
C2Server: update.microsoft-cdn.com,/api/v2
|
||||
UserAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
|
||||
Watermark: 305419896
|
||||
SpawnToX86: %windir%\syswow64\dllhost.exe
|
||||
SpawnToX64: %windir%\sysnative\dllhost.exe
|
||||
```
|
||||
|
||||
## JARM Fingerprinting
|
||||
|
||||
### Cobalt Strike Default JARM
|
||||
```bash
|
||||
# Default CS JARM hash (pre-4.7)
|
||||
07d14d16d21d21d07c42d41d00041d24a458a375eef0c576d23a7bab9a9fb1
|
||||
|
||||
# Scan with JARM
|
||||
python jarm.py <target_ip> -p 443
|
||||
```
|
||||
|
||||
## Known Watermark Values
|
||||
| Watermark | Attribution |
|
||||
|-----------|------------|
|
||||
| 0 | Trial/cracked version |
|
||||
| 305419896 | Common cracked version |
|
||||
| 1359593325 | Known threat actor toolkit |
|
||||
| 1580103824 | Known APT usage |
|
||||
|
||||
## Detection Signatures
|
||||
|
||||
### Suricata
|
||||
```
|
||||
alert http $HOME_NET any -> $EXTERNAL_NET any (
|
||||
msg:"ET MALWARE Cobalt Strike Beacon";
|
||||
content:"/submit.php"; http_uri;
|
||||
content:"Cookie:"; http_header;
|
||||
pcre:"/Cookie:\s[A-Za-z0-9+/=]{60,}/H";
|
||||
sid:2028591; rev:1;)
|
||||
```
|
||||
|
||||
### YARA
|
||||
```yara
|
||||
rule CobaltStrike_Beacon {
|
||||
strings:
|
||||
$config_v3 = { 00 01 00 01 00 02 ?? ?? 00 01 00 02 }
|
||||
$magic = "MSSE-%d-server"
|
||||
$pipe = "\\\\.\\pipe\\msagent_"
|
||||
condition:
|
||||
uint16(0) == 0x5A4D and any of them
|
||||
}
|
||||
```
|
||||
|
||||
## Malleable C2 Profile Elements
|
||||
| Element | Description |
|
||||
|---------|-------------|
|
||||
| `http-get` | GET request profile (URI, headers, metadata transform) |
|
||||
| `http-post` | POST request profile (URI, body transform) |
|
||||
| `set sleeptime` | Default beacon interval |
|
||||
| `set jitter` | Randomization percentage |
|
||||
| `set useragent` | HTTP User-Agent |
|
||||
| `set pipename` | SMB named pipe name |
|
||||
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cobalt Strike beacon configuration extraction and analysis agent.
|
||||
|
||||
Extracts C2 configuration from beacon payloads including server addresses,
|
||||
communication settings, malleable C2 profile details, and watermark values.
|
||||
"""
|
||||
|
||||
import struct
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import hashlib
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
|
||||
# Cobalt Strike beacon configuration field IDs (Type-Length-Value format)
|
||||
BEACON_CONFIG_FIELDS = {
|
||||
1: ("BeaconType", "short"),
|
||||
2: ("Port", "short"),
|
||||
3: ("SleepTime", "int"),
|
||||
4: ("MaxGetSize", "int"),
|
||||
5: ("Jitter", "short"),
|
||||
7: ("PublicKey", "bytes"),
|
||||
8: ("C2Server", "str"),
|
||||
9: ("UserAgent", "str"),
|
||||
10: ("PostURI", "str"),
|
||||
11: ("Malleable_C2_Instructions", "bytes"),
|
||||
12: ("HttpGet_Metadata", "bytes"),
|
||||
13: ("HttpPost_Metadata", "bytes"),
|
||||
14: ("SpawnToX86", "str"),
|
||||
15: ("SpawnToX64", "str"),
|
||||
19: ("CryptoScheme", "short"),
|
||||
26: ("GetVerb", "str"),
|
||||
27: ("PostVerb", "str"),
|
||||
28: ("HttpPostChunk", "int"),
|
||||
29: ("Spawnto_x86", "str"),
|
||||
30: ("Spawnto_x64", "str"),
|
||||
31: ("CryptoScheme2", "str"),
|
||||
37: ("Watermark", "int"),
|
||||
38: ("StageCleanup", "short"),
|
||||
39: ("CFGCaution", "short"),
|
||||
43: ("DNS_Idle", "int"),
|
||||
44: ("DNS_Sleep", "int"),
|
||||
50: ("HostHeader", "str"),
|
||||
54: ("PipeName", "str"),
|
||||
}
|
||||
|
||||
BEACON_TYPES = {0: "HTTP", 1: "Hybrid HTTP/DNS", 2: "SMB", 4: "TCP", 8: "HTTPS", 16: "DNS over HTTPS"}
|
||||
|
||||
XOR_KEY_V3 = 0x69
|
||||
XOR_KEY_V4 = 0x2E
|
||||
|
||||
|
||||
def compute_hash(filepath):
|
||||
"""Compute SHA-256 hash of file."""
|
||||
sha256 = hashlib.sha256()
|
||||
with open(filepath, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(65536), b""):
|
||||
sha256.update(chunk)
|
||||
return sha256.hexdigest()
|
||||
|
||||
|
||||
def find_config_offset(data):
|
||||
"""Find the beacon configuration blob in PE data or shellcode."""
|
||||
# Look for XOR-encoded config patterns
|
||||
for xor_key in [XOR_KEY_V3, XOR_KEY_V4]:
|
||||
# Config starts with 0x0001 (BeaconType field ID) XOR-encoded
|
||||
encoded_marker = bytes([0x00 ^ xor_key, 0x01 ^ xor_key, 0x00 ^ xor_key, 0x01 ^ xor_key])
|
||||
offset = data.find(encoded_marker)
|
||||
if offset != -1:
|
||||
return offset, xor_key
|
||||
# Try unencoded
|
||||
for offset in range(len(data) - 100):
|
||||
if data[offset:offset+4] == b"\x00\x01\x00\x01":
|
||||
return offset, None
|
||||
return -1, None
|
||||
|
||||
|
||||
def xor_decode(data, key):
|
||||
"""XOR decode data with single byte key."""
|
||||
if key is None:
|
||||
return data
|
||||
return bytes(b ^ key for b in data)
|
||||
|
||||
|
||||
def parse_config_field(data, offset):
|
||||
"""Parse a single TLV config field."""
|
||||
if offset + 6 > len(data):
|
||||
return None, None, None, offset
|
||||
field_id = struct.unpack_from(">H", data, offset)[0]
|
||||
field_type = struct.unpack_from(">H", data, offset + 2)[0]
|
||||
if field_type == 1: # short
|
||||
value = struct.unpack_from(">H", data, offset + 4)[0]
|
||||
return field_id, "short", value, offset + 6
|
||||
elif field_type == 2: # int
|
||||
value = struct.unpack_from(">I", data, offset + 4)[0]
|
||||
return field_id, "int", value, offset + 8
|
||||
elif field_type == 3: # str/bytes
|
||||
length = struct.unpack_from(">H", data, offset + 4)[0]
|
||||
if offset + 6 + length > len(data):
|
||||
return None, None, None, offset
|
||||
value = data[offset + 6:offset + 6 + length]
|
||||
return field_id, "str", value, offset + 6 + length
|
||||
return None, None, None, offset + 2
|
||||
|
||||
|
||||
def extract_beacon_config(filepath):
|
||||
"""Extract and parse Cobalt Strike beacon configuration."""
|
||||
with open(filepath, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
config_offset, xor_key = find_config_offset(data)
|
||||
if config_offset == -1:
|
||||
return {"error": "No beacon configuration found", "file": filepath}
|
||||
|
||||
config_data = xor_decode(data[config_offset:config_offset + 4096], xor_key)
|
||||
config = OrderedDict()
|
||||
config["_meta"] = {
|
||||
"config_offset": f"0x{config_offset:08X}",
|
||||
"xor_key": f"0x{xor_key:02X}" if xor_key else "none",
|
||||
"version_guess": "4.x" if xor_key == XOR_KEY_V4 else "3.x" if xor_key == XOR_KEY_V3 else "unknown",
|
||||
}
|
||||
|
||||
offset = 0
|
||||
max_fields = 100
|
||||
parsed = 0
|
||||
while offset < len(config_data) - 4 and parsed < max_fields:
|
||||
field_id, field_type, value, new_offset = parse_config_field(config_data, offset)
|
||||
if field_id is None or new_offset == offset:
|
||||
break
|
||||
offset = new_offset
|
||||
parsed += 1
|
||||
|
||||
field_info = BEACON_CONFIG_FIELDS.get(field_id)
|
||||
if field_info:
|
||||
field_name, expected_type = field_info
|
||||
if isinstance(value, bytes):
|
||||
try:
|
||||
str_value = value.rstrip(b"\x00").decode("utf-8", errors="replace")
|
||||
config[field_name] = str_value
|
||||
except Exception:
|
||||
config[field_name] = value.hex()[:100]
|
||||
elif field_id == 1:
|
||||
config[field_name] = BEACON_TYPES.get(value, f"Unknown({value})")
|
||||
else:
|
||||
config[field_name] = value
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def extract_c2_indicators(config):
|
||||
"""Extract C2 indicators from parsed config for threat intelligence."""
|
||||
indicators = {"c2_servers": [], "user_agents": [], "uris": [],
|
||||
"pipes": [], "watermark": None, "dns": []}
|
||||
c2 = config.get("C2Server", "")
|
||||
if c2:
|
||||
for server in c2.split(","):
|
||||
server = server.strip().rstrip("/")
|
||||
if server:
|
||||
indicators["c2_servers"].append(server)
|
||||
ua = config.get("UserAgent", "")
|
||||
if ua:
|
||||
indicators["user_agents"].append(ua)
|
||||
for key in ["PostURI"]:
|
||||
uri = config.get(key, "")
|
||||
if uri:
|
||||
indicators["uris"].append(uri)
|
||||
pipe = config.get("PipeName", "")
|
||||
if pipe:
|
||||
indicators["pipes"].append(pipe)
|
||||
wm = config.get("Watermark")
|
||||
if wm:
|
||||
indicators["watermark"] = wm
|
||||
return indicators
|
||||
|
||||
|
||||
def assess_operator_opsec(config):
|
||||
"""Assess operator OPSEC based on beacon configuration."""
|
||||
findings = []
|
||||
sleep = config.get("SleepTime", 0)
|
||||
jitter = config.get("Jitter", 0)
|
||||
if sleep < 30000:
|
||||
findings.append({"level": "INFO", "detail": f"Low sleep time: {sleep}ms - high beacon frequency"})
|
||||
if jitter == 0:
|
||||
findings.append({"level": "WARN", "detail": "No jitter configured - predictable beacon interval"})
|
||||
ua = config.get("UserAgent", "")
|
||||
if "Mozilla" not in ua and ua:
|
||||
findings.append({"level": "WARN", "detail": f"Non-standard User-Agent: {ua[:60]}"})
|
||||
spawn86 = config.get("SpawnToX86", config.get("Spawnto_x86", ""))
|
||||
if "rundll32" in spawn86.lower():
|
||||
findings.append({"level": "INFO", "detail": "Default spawn-to process (rundll32) - easy to detect"})
|
||||
cleanup = config.get("StageCleanup", 0)
|
||||
if cleanup == 0:
|
||||
findings.append({"level": "INFO", "detail": "Stage cleanup disabled - beacon stub remains in memory"})
|
||||
return findings
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Cobalt Strike Beacon Configuration Extractor")
|
||||
print("C2 extraction, watermark analysis, OPSEC assessment")
|
||||
print("=" * 60)
|
||||
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
|
||||
if not target or not os.path.exists(target):
|
||||
print("\n[DEMO] Usage: python agent.py <beacon_sample.exe>")
|
||||
print(" Extracts: C2 servers, sleep/jitter, watermark, malleable profile")
|
||||
sys.exit(0)
|
||||
|
||||
print(f"\n[*] Analyzing: {target}")
|
||||
print(f"[*] SHA-256: {compute_hash(target)}")
|
||||
print(f"[*] Size: {os.path.getsize(target)} bytes")
|
||||
|
||||
config = extract_beacon_config(target)
|
||||
|
||||
if "error" in config:
|
||||
print(f"\n[!] {config['error']}")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n--- Beacon Configuration ---")
|
||||
for key, value in config.items():
|
||||
if key == "_meta":
|
||||
for mk, mv in value.items():
|
||||
print(f" {mk}: {mv}")
|
||||
else:
|
||||
print(f" {key}: {value}")
|
||||
|
||||
indicators = extract_c2_indicators(config)
|
||||
print("\n--- C2 Indicators ---")
|
||||
for c2 in indicators["c2_servers"]:
|
||||
print(f" [C2] {c2}")
|
||||
if indicators["watermark"]:
|
||||
print(f" [Watermark] {indicators['watermark']}")
|
||||
for pipe in indicators["pipes"]:
|
||||
print(f" [Pipe] {pipe}")
|
||||
|
||||
opsec = assess_operator_opsec(config)
|
||||
print("\n--- Operator OPSEC Assessment ---")
|
||||
for f in opsec:
|
||||
print(f" [{f['level']}] {f['detail']}")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: analyzing-cobalt-strike-malleable-profiles
|
||||
description: >
|
||||
Parses Cobalt Strike malleable C2 profiles using pyMalleableC2 to extract beacon
|
||||
configuration, HTTP communication patterns, and sleep/jitter settings. Combines with
|
||||
JARM TLS fingerprinting to detect C2 servers on the network. Use when investigating
|
||||
suspected Cobalt Strike infrastructure or building detection signatures for C2 traffic.
|
||||
---
|
||||
|
||||
# Analyzing Cobalt Strike Malleable Profiles
|
||||
|
||||
## Instructions
|
||||
|
||||
Parse malleable C2 profiles to extract IOCs and detection opportunities using the
|
||||
pyMalleableC2 library. Combine with JARM fingerprinting to identify C2 servers.
|
||||
|
||||
```python
|
||||
from malleablec2 import Profile
|
||||
|
||||
# Parse a malleable profile from file
|
||||
profile = Profile.from_file("amazon.profile")
|
||||
|
||||
# Extract global options (sleep, jitter, user-agent)
|
||||
print(profile.ast.pretty())
|
||||
|
||||
# Access HTTP-GET block URIs and headers for network signatures
|
||||
# Access HTTP-POST block for data exfiltration patterns
|
||||
# Generate JARM fingerprints for known C2 infrastructure
|
||||
```
|
||||
|
||||
Key analysis steps:
|
||||
1. Parse the malleable profile to extract HTTP-GET/POST URI patterns
|
||||
2. Extract User-Agent strings and custom headers for IDS signatures
|
||||
3. Identify sleep time and jitter for beaconing detection thresholds
|
||||
4. Scan suspect IPs with JARM to match known C2 fingerprint hashes
|
||||
5. Cross-reference extracted IOCs with network traffic logs
|
||||
|
||||
## Examples
|
||||
|
||||
```python
|
||||
# Parse profile and extract detection indicators
|
||||
from malleablec2 import Profile
|
||||
p = Profile.from_file("cobaltstrike.profile")
|
||||
print(p) # Reconstructed source
|
||||
|
||||
# JARM scan a suspect C2 server
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
["python3", "jarm.py", "suspect-server.com"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
print(result.stdout)
|
||||
# Compare fingerprint against known CS JARM hashes
|
||||
```
|
||||
@@ -0,0 +1,69 @@
|
||||
# API Reference: Analyzing Cobalt Strike Malleable Profiles
|
||||
|
||||
## pyMalleableC2
|
||||
|
||||
```python
|
||||
from malleablec2 import Profile
|
||||
from malleablec2.components import HttpGetBlock, HttpPostBlock, ClientBlock, ServerBlock
|
||||
|
||||
# Parse from file or string
|
||||
p = Profile.from_file("amazon.profile")
|
||||
p = Profile.from_string(code_string)
|
||||
p = Profile.from_scratch()
|
||||
|
||||
# Set global options
|
||||
p.set_option("sleeptime", "3000")
|
||||
p.set_option("jitter", "0")
|
||||
p.set_option("pipename", "mojo__##")
|
||||
|
||||
# HTTP blocks
|
||||
http_get = HttpGetBlock()
|
||||
http_get.set_option("uri", "/updates")
|
||||
client = ClientBlock()
|
||||
client.add_statement("header", "Accept", "*/*")
|
||||
http_get.add_code_block(client)
|
||||
p.add_code_block(http_get)
|
||||
|
||||
# AST and reconstruction
|
||||
print(p.ast.pretty()) # Display AST
|
||||
print(p) # Reconstruct source
|
||||
```
|
||||
|
||||
## JARM TLS Fingerprinting
|
||||
|
||||
```bash
|
||||
# Scan a single host
|
||||
python3 jarm.py www.example.com
|
||||
|
||||
# Scan with specific port
|
||||
python3 jarm.py 192.168.1.1 -p 8443
|
||||
|
||||
# Batch scan from file
|
||||
python3 jarm.py -i targets.txt -o results.csv
|
||||
```
|
||||
|
||||
Fingerprint format: 62-char hybrid hash
|
||||
- First 30 chars: cipher + TLS version (10 handshakes x 3 chars)
|
||||
- Last 32 chars: truncated SHA256 of cumulative extensions
|
||||
|
||||
## Known Cobalt Strike JARM Hashes
|
||||
|
||||
| JARM Hash | Description |
|
||||
|-----------|-------------|
|
||||
| `07d14d16d21d21d07c42d41d00041d...` | CS default config |
|
||||
| `07d14d16d21d21d00042d41d00041d...` | CS with Java 11 |
|
||||
|
||||
## dissect.cobaltstrike (Alternative)
|
||||
|
||||
```python
|
||||
from dissect.cobaltstrike import beacon
|
||||
b = beacon.BeaconConfig.from_file("beacon.bin")
|
||||
print(b.protocol, b.port, b.sleeptime)
|
||||
```
|
||||
|
||||
### References
|
||||
|
||||
- pyMalleableC2: https://github.com/byt3bl33d3r/pyMalleableC2
|
||||
- JARM scanner: https://github.com/salesforce/jarm
|
||||
- dissect.cobaltstrike: https://github.com/fox-it/dissect.cobaltstrike
|
||||
- C2 JARM list: https://github.com/cedowens/C2-JARM
|
||||
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Agent for analyzing Cobalt Strike malleable C2 profiles and JARM fingerprinting."""
|
||||
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from malleablec2 import Profile
|
||||
|
||||
|
||||
def extract_profile_indicators(profile_path):
|
||||
"""Extract detection indicators from a malleable C2 profile."""
|
||||
with open(profile_path) as f:
|
||||
content = f.read()
|
||||
profile = Profile.from_string(content)
|
||||
indicators = {
|
||||
"file": str(profile_path),
|
||||
"source_lines": len(content.splitlines()),
|
||||
"reconstructed": str(profile),
|
||||
}
|
||||
keywords = ["sleeptime", "jitter", "useragent", "pipename", "host_stage",
|
||||
"dns_idle", "dns_sleep", "spawnto_x86", "spawnto_x64"]
|
||||
options = {}
|
||||
for kw in keywords:
|
||||
for line in content.splitlines():
|
||||
stripped = line.strip().rstrip(";").strip()
|
||||
if kw in stripped.lower() and "set " in stripped.lower():
|
||||
parts = stripped.split('"')
|
||||
if len(parts) >= 2:
|
||||
options[kw] = parts[1]
|
||||
indicators["global_options"] = options
|
||||
uris = []
|
||||
for line in content.splitlines():
|
||||
if "set uri" in line.strip().lower():
|
||||
parts = line.strip().split('"')
|
||||
if len(parts) >= 2:
|
||||
uris.append(parts[1])
|
||||
indicators["uris"] = uris
|
||||
headers = []
|
||||
for line in content.splitlines():
|
||||
stripped = line.strip()
|
||||
if "header " in stripped.lower() and '"' in stripped:
|
||||
parts = stripped.split('"')
|
||||
if len(parts) >= 4:
|
||||
headers.append({"name": parts[1], "value": parts[3]})
|
||||
indicators["custom_headers"] = headers
|
||||
return indicators
|
||||
|
||||
|
||||
def scan_directory_profiles(directory):
|
||||
"""Scan a directory for malleable C2 profiles and extract indicators."""
|
||||
results = []
|
||||
for path in Path(directory).rglob("*.profile"):
|
||||
try:
|
||||
indicators = extract_profile_indicators(str(path))
|
||||
results.append(indicators)
|
||||
except Exception as e:
|
||||
results.append({"file": str(path), "error": str(e)})
|
||||
return results
|
||||
|
||||
|
||||
KNOWN_CS_JARM = {
|
||||
"07d14d16d21d21d07c42d41d00041d24a458a375eef0c576d23a7bab9a9fb1":
|
||||
"Cobalt Strike (default)",
|
||||
"07d14d16d21d21d00042d41d00041de5fb3038104f457d92ba02e9311512c2":
|
||||
"Cobalt Strike (Java 11)",
|
||||
}
|
||||
|
||||
|
||||
def compute_jarm_fingerprint(host, port=443):
|
||||
"""Compute JARM fingerprint by invoking the salesforce/jarm scanner."""
|
||||
jarm_script = os.getenv("JARM_SCRIPT", "jarm.py")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["python3", jarm_script, host, "-p", str(port)],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
for line in result.stdout.splitlines():
|
||||
if len(line.strip()) >= 62:
|
||||
return line.strip().split()[-1]
|
||||
return result.stdout.strip()
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
def check_jarm_against_known(fingerprint):
|
||||
"""Check a JARM fingerprint against known Cobalt Strike signatures."""
|
||||
for jarm_hash, description in KNOWN_CS_JARM.items():
|
||||
if fingerprint.strip() == jarm_hash:
|
||||
return {"match": True, "description": description, "fingerprint": fingerprint}
|
||||
return {"match": False, "fingerprint": fingerprint}
|
||||
|
||||
|
||||
def batch_jarm_scan(targets, port=443):
|
||||
"""Scan multiple targets for JARM fingerprints and check against known CS hashes."""
|
||||
results = []
|
||||
for target in targets:
|
||||
fp = compute_jarm_fingerprint(target, port)
|
||||
match = check_jarm_against_known(fp)
|
||||
match["target"] = target
|
||||
results.append(match)
|
||||
return results
|
||||
|
||||
|
||||
def generate_snort_rules(indicators_list):
|
||||
"""Generate Snort/Suricata rules from extracted profile indicators."""
|
||||
rules = []
|
||||
sid = 1000001
|
||||
for ind in indicators_list:
|
||||
for uri in ind.get("uris", []):
|
||||
rules.append(
|
||||
f'alert http $HOME_NET any -> $EXTERNAL_NET any '
|
||||
f'(msg:"CS Beacon URI {uri}"; '
|
||||
f'content:"{uri}"; http_uri; sid:{sid}; rev:1;)'
|
||||
)
|
||||
sid += 1
|
||||
ua = ind.get("global_options", {}).get("useragent", "")
|
||||
if ua:
|
||||
rules.append(
|
||||
f'alert http $HOME_NET any -> $EXTERNAL_NET any '
|
||||
f'(msg:"CS Beacon User-Agent"; '
|
||||
f'content:"{ua}"; http_header; sid:{sid}; rev:1;)'
|
||||
)
|
||||
sid += 1
|
||||
return rules
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Cobalt Strike Malleable Profile Analyzer")
|
||||
parser.add_argument("--profile", help="Path to a single malleable C2 profile")
|
||||
parser.add_argument("--directory", help="Directory of malleable profiles")
|
||||
parser.add_argument("--jarm-targets", nargs="*", help="Hosts to JARM fingerprint")
|
||||
parser.add_argument("--output", default="cs_analysis_report.json")
|
||||
parser.add_argument("--action", choices=[
|
||||
"parse", "scan_dir", "jarm", "generate_rules", "full_analysis"
|
||||
], default="full_analysis")
|
||||
args = parser.parse_args()
|
||||
|
||||
report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}}
|
||||
|
||||
if args.action in ("parse", "full_analysis") and args.profile:
|
||||
indicators = extract_profile_indicators(args.profile)
|
||||
report["findings"]["profile_indicators"] = indicators
|
||||
print(f"[+] Parsed: {args.profile} ({len(indicators.get('uris', []))} URIs)")
|
||||
|
||||
if args.action in ("scan_dir", "full_analysis") and args.directory:
|
||||
results = scan_directory_profiles(args.directory)
|
||||
report["findings"]["directory_scan"] = results
|
||||
print(f"[+] Scanned {len(results)} profiles in {args.directory}")
|
||||
|
||||
if args.action in ("jarm", "full_analysis") and args.jarm_targets:
|
||||
jarm_results = batch_jarm_scan(args.jarm_targets)
|
||||
report["findings"]["jarm_scan"] = jarm_results
|
||||
matches = [r for r in jarm_results if r.get("match")]
|
||||
print(f"[+] JARM: {len(jarm_results)} scanned, {len(matches)} CS matches")
|
||||
|
||||
if args.action in ("generate_rules", "full_analysis"):
|
||||
profiles = report["findings"].get("directory_scan", [])
|
||||
if not profiles and args.profile:
|
||||
profiles = [report["findings"].get("profile_indicators", {})]
|
||||
rules = generate_snort_rules(profiles)
|
||||
report["findings"]["snort_rules"] = rules
|
||||
print(f"[+] Generated {len(rules)} Snort rules")
|
||||
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2, default=str)
|
||||
print(f"[+] Report saved to {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,112 @@
|
||||
# API Reference: C2 Communication Analysis Tools
|
||||
|
||||
## Scapy - Packet Analysis Library (Python)
|
||||
|
||||
### Reading PCAPs
|
||||
```python
|
||||
from scapy.all import rdpcap, IP, TCP, UDP, DNS, DNSQR
|
||||
packets = rdpcap("capture.pcap")
|
||||
```
|
||||
|
||||
### Filtering Packets
|
||||
```python
|
||||
# TCP SYN packets (connection initiation)
|
||||
syn_pkts = [p for p in packets if TCP in p and (p[TCP].flags & 0x02)]
|
||||
|
||||
# DNS queries
|
||||
dns_pkts = [p for p in packets if DNS in p and p[DNS].qr == 0]
|
||||
|
||||
# Access fields
|
||||
pkt[IP].src # Source IP
|
||||
pkt[IP].dst # Destination IP
|
||||
pkt[TCP].sport # Source port
|
||||
pkt[TCP].dport # Destination port
|
||||
pkt[TCP].flags # TCP flags (0x02 = SYN)
|
||||
float(pkt.time) # Packet timestamp
|
||||
```
|
||||
|
||||
## dpkt - Packet Parsing Library (Python)
|
||||
|
||||
### Reading PCAPs
|
||||
```python
|
||||
import dpkt
|
||||
with open("capture.pcap", "rb") as f:
|
||||
pcap = dpkt.pcap.Reader(f)
|
||||
for timestamp, buf in pcap:
|
||||
eth = dpkt.ethernet.Ethernet(buf)
|
||||
ip = eth.data
|
||||
tcp = ip.data
|
||||
```
|
||||
|
||||
### HTTP Request Parsing
|
||||
```python
|
||||
http = dpkt.http.Request(tcp.data)
|
||||
http.method # GET, POST
|
||||
http.uri # /path
|
||||
http.headers # dict of headers
|
||||
http.body # POST body
|
||||
```
|
||||
|
||||
## tshark - CLI Wireshark
|
||||
|
||||
### Beacon Analysis
|
||||
```bash
|
||||
tshark -r capture.pcap -T fields -e ip.dst -e tcp.dstport -e frame.time_epoch \
|
||||
-Y "tcp.flags.syn==1" > syn_times.csv
|
||||
```
|
||||
|
||||
### HTTP Extraction
|
||||
```bash
|
||||
tshark -r capture.pcap -Y "http.request" -T fields \
|
||||
-e http.request.method -e http.host -e http.request.uri -e http.user_agent
|
||||
```
|
||||
|
||||
### DNS Extraction
|
||||
```bash
|
||||
tshark -r capture.pcap -Y "dns.qr==0" -T fields \
|
||||
-e dns.qry.name -e dns.qry.type -e ip.src
|
||||
```
|
||||
|
||||
### JA3 TLS Fingerprinting
|
||||
```bash
|
||||
tshark -r capture.pcap -Y "tls.handshake.type==1" -T fields \
|
||||
-e ip.src -e tls.handshake.ja3
|
||||
```
|
||||
|
||||
## CobaltStrikeParser - Beacon Config Extraction
|
||||
|
||||
### Usage
|
||||
```python
|
||||
from cobalt_strike_parser import BeaconConfig
|
||||
config = BeaconConfig.from_file("beacon.bin")
|
||||
for key, value in config.items():
|
||||
print(f"{key}: {value}")
|
||||
```
|
||||
|
||||
### Key Config Fields
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `BeaconType` | HTTP, HTTPS, DNS, SMB |
|
||||
| `C2Server` | Primary C2 URL |
|
||||
| `SleepTime` | Beacon interval (ms) |
|
||||
| `Jitter` | Jitter percentage |
|
||||
| `UserAgent` | HTTP User-Agent string |
|
||||
| `Watermark` | License watermark ID |
|
||||
|
||||
## Suricata - Network IDS Rules
|
||||
|
||||
### Rule Syntax
|
||||
```
|
||||
alert <proto> <src> <port> -> <dst> <port> (msg:""; <options>; sid:N; rev:N;)
|
||||
```
|
||||
|
||||
### Key Keywords
|
||||
| Keyword | Purpose |
|
||||
|---------|---------|
|
||||
| `http.method` | Match HTTP method |
|
||||
| `http.uri` | Match request URI |
|
||||
| `http.header` | Match header content |
|
||||
| `ja3.hash` | Match JA3 TLS fingerprint |
|
||||
| `dns.query` | Match DNS query name |
|
||||
| `tls.cert_subject` | Match TLS certificate CN |
|
||||
| `threshold` | Rate-based detection |
|
||||
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
"""C2 communication analysis agent for beacon detection and protocol decoding."""
|
||||
|
||||
import statistics
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
try:
|
||||
from scapy.all import rdpcap, IP, TCP, UDP, DNS, DNSQR, Raw
|
||||
HAS_SCAPY = True
|
||||
except ImportError:
|
||||
HAS_SCAPY = False
|
||||
|
||||
try:
|
||||
import dpkt
|
||||
HAS_DPKT = True
|
||||
except ImportError:
|
||||
HAS_DPKT = False
|
||||
|
||||
|
||||
def detect_beacons(pcap_path, min_connections=5, max_jitter_pct=25.0):
|
||||
"""Analyze PCAP for periodic beacon patterns using TCP SYN timing."""
|
||||
if not HAS_SCAPY:
|
||||
print("[ERROR] scapy not installed: pip install scapy")
|
||||
return []
|
||||
packets = rdpcap(pcap_path)
|
||||
connections = defaultdict(list)
|
||||
for pkt in packets:
|
||||
if IP in pkt and TCP in pkt and (pkt[TCP].flags & 0x02):
|
||||
key = f"{pkt[IP].dst}:{pkt[TCP].dport}"
|
||||
connections[key].append(float(pkt.time))
|
||||
beacons = []
|
||||
for dst, times in sorted(connections.items()):
|
||||
if len(times) < min_connections:
|
||||
continue
|
||||
intervals = [times[i + 1] - times[i] for i in range(len(times) - 1)]
|
||||
avg_interval = statistics.mean(intervals)
|
||||
stdev = statistics.stdev(intervals) if len(intervals) > 1 else 0
|
||||
jitter_pct = (stdev / avg_interval * 100) if avg_interval > 0 else 0
|
||||
is_beacon = 5 < avg_interval < 7200 and jitter_pct < max_jitter_pct
|
||||
record = {
|
||||
"destination": dst,
|
||||
"connections": len(times),
|
||||
"duration_seconds": round(times[-1] - times[0], 1),
|
||||
"avg_interval_seconds": round(avg_interval, 1),
|
||||
"stdev_seconds": round(stdev, 1),
|
||||
"jitter_percent": round(jitter_pct, 1),
|
||||
"is_beacon": is_beacon,
|
||||
}
|
||||
if is_beacon:
|
||||
beacons.append(record)
|
||||
return beacons
|
||||
|
||||
|
||||
def extract_http_requests(pcap_path):
|
||||
"""Extract HTTP requests from a PCAP file using dpkt."""
|
||||
if not HAS_DPKT:
|
||||
print("[ERROR] dpkt not installed: pip install dpkt")
|
||||
return []
|
||||
requests = []
|
||||
with open(pcap_path, "rb") as f:
|
||||
pcap = dpkt.pcap.Reader(f)
|
||||
for ts, buf in pcap:
|
||||
try:
|
||||
eth = dpkt.ethernet.Ethernet(buf)
|
||||
if not isinstance(eth.data, dpkt.ip.IP):
|
||||
continue
|
||||
ip = eth.data
|
||||
if not isinstance(ip.data, dpkt.tcp.TCP):
|
||||
continue
|
||||
tcp = ip.data
|
||||
if len(tcp.data) == 0:
|
||||
continue
|
||||
try:
|
||||
http = dpkt.http.Request(tcp.data)
|
||||
decoded_body = None
|
||||
if http.body:
|
||||
try:
|
||||
decoded_body = base64.b64decode(http.body).decode("utf-8", errors="replace")
|
||||
except Exception:
|
||||
decoded_body = http.body[:200]
|
||||
requests.append({
|
||||
"timestamp": ts,
|
||||
"src_ip": ".".join(str(b) for b in ip.src),
|
||||
"dst_ip": ".".join(str(b) for b in ip.dst),
|
||||
"dst_port": tcp.dport,
|
||||
"method": http.method,
|
||||
"uri": http.uri,
|
||||
"host": http.headers.get("host", ""),
|
||||
"user_agent": http.headers.get("user-agent", ""),
|
||||
"body_size": len(http.body) if http.body else 0,
|
||||
"decoded_body_preview": decoded_body,
|
||||
})
|
||||
except (dpkt.dpkt.NeedData, dpkt.dpkt.UnpackError):
|
||||
pass
|
||||
except Exception:
|
||||
continue
|
||||
return requests
|
||||
|
||||
|
||||
def extract_dns_queries(pcap_path):
|
||||
"""Extract DNS queries from a PCAP for C2 domain identification."""
|
||||
if not HAS_SCAPY:
|
||||
return []
|
||||
packets = rdpcap(pcap_path)
|
||||
queries = []
|
||||
for pkt in packets:
|
||||
if DNS in pkt and pkt[DNS].qr == 0 and DNSQR in pkt:
|
||||
qname = pkt[DNSQR].qname.decode("utf-8", errors="replace").rstrip(".")
|
||||
queries.append({
|
||||
"src_ip": pkt[IP].src if IP in pkt else "?",
|
||||
"query": qname,
|
||||
"type": pkt[DNSQR].qtype,
|
||||
})
|
||||
return queries
|
||||
|
||||
|
||||
def identify_c2_framework(http_requests):
|
||||
"""Match HTTP request patterns against known C2 framework signatures."""
|
||||
cs_uris = ["/pixel", "/submit.php", "/__utm.gif", "/ca", "/dpixel",
|
||||
"/push", "/visit.js", "/tab_icon"]
|
||||
framework_hits = []
|
||||
for req in http_requests:
|
||||
uri = req.get("uri", "")
|
||||
ua = req.get("user_agent", "")
|
||||
for cs_uri in cs_uris:
|
||||
if cs_uri in uri:
|
||||
framework_hits.append({
|
||||
"framework": "Cobalt Strike",
|
||||
"indicator": f"URI pattern: {cs_uri}",
|
||||
"request": req,
|
||||
})
|
||||
break
|
||||
if "MeterSSL" in ua or len(uri) == 5 and uri.startswith("/"):
|
||||
framework_hits.append({
|
||||
"framework": "Metasploit/Meterpreter",
|
||||
"indicator": f"URI/UA pattern: {uri} / {ua[:50]}",
|
||||
"request": req,
|
||||
})
|
||||
return framework_hits
|
||||
|
||||
|
||||
def generate_suricata_rules(beacons, http_requests):
|
||||
"""Generate Suricata IDS rules from observed C2 patterns."""
|
||||
rules = []
|
||||
sid = 9000100
|
||||
for beacon in beacons:
|
||||
dst_ip, dst_port = beacon["destination"].rsplit(":", 1)
|
||||
rules.append(
|
||||
f'alert tcp $HOME_NET any -> {dst_ip} {dst_port} ('
|
||||
f'msg:"MALWARE Detected C2 Beacon to {dst_ip}:{dst_port}"; '
|
||||
f'flow:established,to_server; '
|
||||
f'threshold:type threshold, track by_src, count 5, seconds 600; '
|
||||
f'sid:{sid}; rev:1;)'
|
||||
)
|
||||
sid += 1
|
||||
for req in http_requests[:5]:
|
||||
if req.get("uri"):
|
||||
uri = req["uri"]
|
||||
rules.append(
|
||||
f'alert http $HOME_NET any -> $EXTERNAL_NET any ('
|
||||
f'msg:"MALWARE Suspected C2 HTTP Request {uri}"; '
|
||||
f'flow:established,to_server; '
|
||||
f'http.method; content:"{req["method"]}"; '
|
||||
f'http.uri; content:"{uri}"; '
|
||||
f'sid:{sid}; rev:1;)'
|
||||
)
|
||||
sid += 1
|
||||
return rules
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("C2 Communication Analysis Agent")
|
||||
print("Beacon detection, protocol decoding, signature generation")
|
||||
print("=" * 60)
|
||||
|
||||
pcap_file = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
|
||||
if pcap_file and os.path.exists(pcap_file):
|
||||
print(f"\n[*] Analyzing PCAP: {pcap_file}")
|
||||
|
||||
print("\n--- Beacon Detection ---")
|
||||
beacons = detect_beacons(pcap_file)
|
||||
for b in beacons:
|
||||
print(f"[!] BEACON: {b['destination']} "
|
||||
f"interval={b['avg_interval_seconds']}s "
|
||||
f"jitter={b['jitter_percent']}% "
|
||||
f"sessions={b['connections']}")
|
||||
|
||||
print("\n--- HTTP Requests ---")
|
||||
http_reqs = extract_http_requests(pcap_file)
|
||||
for r in http_reqs[:10]:
|
||||
print(f" {r['method']} {r['host']}{r['uri']}")
|
||||
|
||||
print("\n--- DNS Queries ---")
|
||||
dns_qs = extract_dns_queries(pcap_file)
|
||||
for q in dns_qs[:10]:
|
||||
print(f" {q['src_ip']} -> {q['query']}")
|
||||
|
||||
print("\n--- C2 Framework Identification ---")
|
||||
hits = identify_c2_framework(http_reqs)
|
||||
for h in hits:
|
||||
print(f"[!] {h['framework']}: {h['indicator']}")
|
||||
|
||||
print("\n--- Suricata Rules ---")
|
||||
rules = generate_suricata_rules(beacons, http_reqs)
|
||||
for r in rules:
|
||||
print(r)
|
||||
else:
|
||||
print("\n[DEMO] Usage: python agent.py <capture.pcap>")
|
||||
print("[*] Provide a PCAP file to analyze for C2 communication patterns.")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,96 @@
|
||||
# API Reference: Cyber Kill Chain Analysis Tools
|
||||
|
||||
## Lockheed Martin Cyber Kill Chain Phases
|
||||
|
||||
| Phase | Name | MITRE ATT&CK Tactic |
|
||||
|-------|------|---------------------|
|
||||
| 1 | Reconnaissance | TA0043 Reconnaissance |
|
||||
| 2 | Weaponization | TA0042 Resource Development |
|
||||
| 3 | Delivery | TA0001 Initial Access |
|
||||
| 4 | Exploitation | TA0002 Execution |
|
||||
| 5 | Installation | TA0003 Persistence, TA0004 Privilege Escalation |
|
||||
| 6 | Command & Control | TA0011 Command and Control |
|
||||
| 7 | Actions on Objectives | TA0010 Exfiltration, TA0040 Impact |
|
||||
|
||||
## Courses of Action (COA) Matrix
|
||||
|
||||
| COA | Description |
|
||||
|-----|-------------|
|
||||
| Detect | Alert on adversary activity |
|
||||
| Deny | Prevent phase completion |
|
||||
| Disrupt | Interrupt adversary mid-phase |
|
||||
| Degrade | Reduce adversary effectiveness |
|
||||
| Deceive | Expose activity via deception |
|
||||
| Destroy | Neutralize adversary infrastructure |
|
||||
|
||||
## MITRE ATT&CK Navigator
|
||||
|
||||
### JSON Layer Format
|
||||
```json
|
||||
{
|
||||
"name": "Kill Chain Coverage",
|
||||
"versions": {"navigator": "4.8", "layer": "4.4", "attack": "13"},
|
||||
"domain": "enterprise-attack",
|
||||
"techniques": [
|
||||
{"techniqueID": "T1566", "color": "#ff6666", "comment": "Phase 3: Delivery"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### CLI Usage
|
||||
```bash
|
||||
# Export layer via ATT&CK Navigator API
|
||||
curl -X POST https://mitre-attack.github.io/attack-navigator/api/layers \
|
||||
-d @layer.json -o coverage_map.svg
|
||||
```
|
||||
|
||||
## Splunk - Kill Chain Phase Queries
|
||||
|
||||
### Phase 3 Detection (Delivery)
|
||||
```spl
|
||||
index=email sourcetype=exchange action=delivered
|
||||
| eval has_macro=if(match(attachment, "\.(docm|xlsm|pptm)$"), 1, 0)
|
||||
| where has_macro=1
|
||||
| stats count by sender, subject, attachment
|
||||
```
|
||||
|
||||
### Phase 6 Detection (C2)
|
||||
```spl
|
||||
index=proxy OR index=firewall
|
||||
| stats count AS connections, dc(dest) AS unique_dests by src_ip
|
||||
| where connections > 100 AND unique_dests < 3
|
||||
| sort - connections
|
||||
```
|
||||
|
||||
## Elastic Security EQL
|
||||
|
||||
### Multi-Phase Detection
|
||||
```eql
|
||||
sequence by host.name with maxspan=1h
|
||||
[process where event.action == "start" and process.name == "WINWORD.EXE"]
|
||||
[process where event.action == "start" and process.parent.name == "WINWORD.EXE"]
|
||||
[network where destination.port == 443 and not destination.ip in ("known_good")]
|
||||
```
|
||||
|
||||
## MISP - Kill Chain Tagging
|
||||
|
||||
### Galaxy Cluster Tags
|
||||
```
|
||||
misp-galaxy:kill-chain="reconnaissance"
|
||||
misp-galaxy:kill-chain="delivery"
|
||||
misp-galaxy:kill-chain="exploitation"
|
||||
misp-galaxy:kill-chain="installation"
|
||||
misp-galaxy:kill-chain="command-and-control"
|
||||
misp-galaxy:kill-chain="actions-on-objectives"
|
||||
```
|
||||
|
||||
### PyMISP Event Tagging
|
||||
```python
|
||||
from pymisp import PyMISP, MISPEvent
|
||||
|
||||
misp = PyMISP("https://misp.example.com", "API_KEY")
|
||||
event = MISPEvent()
|
||||
event.add_tag("kill-chain:delivery")
|
||||
event.add_tag("mitre-attack-pattern:T1566 - Phishing")
|
||||
misp.update_event(event)
|
||||
```
|
||||
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cyber Kill Chain analysis agent for mapping incidents to Lockheed Martin kill chain phases."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
|
||||
KILL_CHAIN_PHASES = {
|
||||
1: {
|
||||
"name": "Reconnaissance",
|
||||
"description": "Adversary gathers target information",
|
||||
"indicators": [
|
||||
"DNS queries from adversary IP",
|
||||
"LinkedIn/social media scraping",
|
||||
"Shodan/Censys scans of infrastructure",
|
||||
"Job posting analysis for technology stack",
|
||||
"WHOIS lookups on organization domains",
|
||||
],
|
||||
"mitre_tactics": ["TA0043 - Reconnaissance"],
|
||||
"coas": {
|
||||
"detect": "Monitor for anomalous DNS lookups and port scans from single sources",
|
||||
"deny": "Limit public-facing information, restrict DNS zone transfers",
|
||||
"disrupt": "Block scanning IPs at perimeter firewall",
|
||||
"degrade": "Return honeypot responses to recon probes",
|
||||
"deceive": "Deploy decoy infrastructure and fake employee profiles",
|
||||
},
|
||||
},
|
||||
2: {
|
||||
"name": "Weaponization",
|
||||
"description": "Adversary creates attack tool (malware + exploit)",
|
||||
"indicators": [
|
||||
"Malware compilation timestamps",
|
||||
"Exploit document metadata",
|
||||
"Builder tool artifacts in samples",
|
||||
"Reused infrastructure from previous campaigns",
|
||||
],
|
||||
"mitre_tactics": ["TA0042 - Resource Development"],
|
||||
"coas": {
|
||||
"detect": "Threat intelligence on adversary tooling and TTPs",
|
||||
"deny": "Patch vulnerabilities targeted by known exploit kits",
|
||||
"disrupt": "N/A (occurs outside defender visibility)",
|
||||
"degrade": "Application hardening reduces exploit reliability",
|
||||
"deceive": "Share deceptive vulnerability information",
|
||||
},
|
||||
},
|
||||
3: {
|
||||
"name": "Delivery",
|
||||
"description": "Adversary transmits weapon to target",
|
||||
"indicators": [
|
||||
"Phishing emails with malicious attachments",
|
||||
"Drive-by download URLs",
|
||||
"USB device insertion events",
|
||||
"Supply chain compromise artifacts",
|
||||
"Watering hole website modifications",
|
||||
],
|
||||
"mitre_tactics": ["TA0001 - Initial Access"],
|
||||
"coas": {
|
||||
"detect": "Email security gateway alerts, proxy URL filtering alerts",
|
||||
"deny": "Block malicious attachments, URL filtering, USB device control",
|
||||
"disrupt": "Quarantine suspicious emails before delivery",
|
||||
"degrade": "Sandbox detonation of attachments delays delivery",
|
||||
"deceive": "Canary documents in email attachments",
|
||||
},
|
||||
},
|
||||
4: {
|
||||
"name": "Exploitation",
|
||||
"description": "Adversary exploits vulnerability to execute code",
|
||||
"indicators": [
|
||||
"CVE exploitation in application logs",
|
||||
"Memory corruption crash dumps",
|
||||
"Shellcode execution artifacts",
|
||||
"Exploit kit landing page access",
|
||||
],
|
||||
"mitre_tactics": ["TA0002 - Execution"],
|
||||
"coas": {
|
||||
"detect": "EDR behavioral detection, exploit guard alerts",
|
||||
"deny": "Patch management, application whitelisting",
|
||||
"disrupt": "ASLR, DEP, CFG memory protections",
|
||||
"degrade": "Sandboxed application execution (Protected View)",
|
||||
"deceive": "Honeypot applications with fake vulnerabilities",
|
||||
},
|
||||
},
|
||||
5: {
|
||||
"name": "Installation",
|
||||
"description": "Adversary establishes persistence on target",
|
||||
"indicators": [
|
||||
"New scheduled tasks or services",
|
||||
"Registry Run key modifications",
|
||||
"Web shell deployment",
|
||||
"Startup folder additions",
|
||||
"DLL search-order hijacking",
|
||||
],
|
||||
"mitre_tactics": ["TA0003 - Persistence", "TA0004 - Privilege Escalation"],
|
||||
"coas": {
|
||||
"detect": "Sysmon EventID 11/12/13, EDR persistence monitoring",
|
||||
"deny": "Application whitelisting, UAC enforcement",
|
||||
"disrupt": "Real-time file integrity monitoring alerts",
|
||||
"degrade": "Restrict write access to system directories",
|
||||
"deceive": "Canary registry keys and file system canaries",
|
||||
},
|
||||
},
|
||||
6: {
|
||||
"name": "Command & Control",
|
||||
"description": "Adversary communicates with compromised system",
|
||||
"indicators": [
|
||||
"Beaconing traffic at regular intervals",
|
||||
"DNS tunneling (high entropy subdomain queries)",
|
||||
"HTTPS to newly registered domains",
|
||||
"Known C2 framework signatures",
|
||||
],
|
||||
"mitre_tactics": ["TA0011 - Command and Control"],
|
||||
"coas": {
|
||||
"detect": "Network beacon analysis, JA3 fingerprinting, DNS monitoring",
|
||||
"deny": "DNS sinkholing, firewall egress filtering",
|
||||
"disrupt": "TLS inspection to identify C2 in encrypted traffic",
|
||||
"degrade": "Rate-limit suspicious outbound connections",
|
||||
"deceive": "C2 interception and response manipulation",
|
||||
},
|
||||
},
|
||||
7: {
|
||||
"name": "Actions on Objectives",
|
||||
"description": "Adversary achieves mission goals",
|
||||
"indicators": [
|
||||
"Data staging and exfiltration",
|
||||
"Lateral movement to additional systems",
|
||||
"Ransomware encryption activity",
|
||||
"Destructive operations (wiper malware)",
|
||||
"Credential dumping (LSASS access)",
|
||||
],
|
||||
"mitre_tactics": ["TA0010 - Exfiltration", "TA0040 - Impact"],
|
||||
"coas": {
|
||||
"detect": "DLP alerts, anomalous data transfers, UEBA",
|
||||
"deny": "Network segmentation, data classification controls",
|
||||
"disrupt": "Isolate compromised systems, kill C2 connections",
|
||||
"degrade": "Encrypt sensitive data at rest (attacker gets ciphertext)",
|
||||
"deceive": "Canary files and honeytoken credentials",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def map_event_to_phase(event_description):
|
||||
"""Map an incident event description to the most likely kill chain phase."""
|
||||
event_lower = event_description.lower()
|
||||
keyword_phase_map = {
|
||||
1: ["recon", "scan", "enumerat", "shodan", "whois", "dns lookup"],
|
||||
2: ["weaponiz", "builder", "compile", "payload creat"],
|
||||
3: ["phish", "email", "deliver", "download", "usb", "attachment", "watering hole"],
|
||||
4: ["exploit", "cve-", "buffer overflow", "shellcode", "rce"],
|
||||
5: ["persist", "scheduled task", "registry", "run key", "service install",
|
||||
"web shell", "backdoor", "startup"],
|
||||
6: ["beacon", "c2", "c&c", "command and control", "callback", "dns tunnel"],
|
||||
7: ["exfiltrat", "lateral", "ransomware", "encrypt", "data stag", "credential dump",
|
||||
"mimikatz", "wiper"],
|
||||
}
|
||||
scores = {phase: 0 for phase in range(1, 8)}
|
||||
for phase, keywords in keyword_phase_map.items():
|
||||
for kw in keywords:
|
||||
if kw in event_lower:
|
||||
scores[phase] += 1
|
||||
best_phase = max(scores, key=scores.get)
|
||||
if scores[best_phase] == 0:
|
||||
return None
|
||||
return best_phase
|
||||
|
||||
|
||||
def analyze_incident(events):
|
||||
"""Analyze a list of incident events and map to kill chain phases."""
|
||||
analysis = {phase: {"events": [], "detected": False, "completed": False}
|
||||
for phase in range(1, 8)}
|
||||
for event in events:
|
||||
phase = map_event_to_phase(event.get("description", ""))
|
||||
if phase:
|
||||
analysis[phase]["events"].append(event)
|
||||
analysis[phase]["completed"] = True
|
||||
if event.get("detected", False):
|
||||
analysis[phase]["detected"] = True
|
||||
return analysis
|
||||
|
||||
|
||||
def generate_report(analysis):
|
||||
"""Generate a kill chain analysis report."""
|
||||
report_lines = [
|
||||
"CYBER KILL CHAIN ANALYSIS REPORT",
|
||||
"=" * 50,
|
||||
f"Generated: {datetime.datetime.utcnow().isoformat()}Z",
|
||||
"",
|
||||
]
|
||||
deepest_phase = 0
|
||||
detection_phase = None
|
||||
for phase_num in range(1, 8):
|
||||
phase_data = analysis[phase_num]
|
||||
phase_info = KILL_CHAIN_PHASES[phase_num]
|
||||
if phase_data["completed"]:
|
||||
deepest_phase = phase_num
|
||||
if phase_data["detected"] and detection_phase is None:
|
||||
detection_phase = phase_num
|
||||
status = "COMPLETED" if phase_data["completed"] else "NOT REACHED"
|
||||
if phase_data["detected"]:
|
||||
status += " (DETECTED)"
|
||||
report_lines.append(f"Phase {phase_num}: {phase_info['name']} -> {status}")
|
||||
for evt in phase_data["events"]:
|
||||
report_lines.append(f" - {evt.get('description', 'N/A')}")
|
||||
report_lines.extend([
|
||||
"",
|
||||
f"Deepest phase reached: {deepest_phase} ({KILL_CHAIN_PHASES.get(deepest_phase, {}).get('name', 'N/A')})",
|
||||
f"First detection at phase: {detection_phase or 'None'}",
|
||||
"",
|
||||
"RECOMMENDED COURSES OF ACTION:",
|
||||
])
|
||||
for phase_num in range(1, deepest_phase + 1):
|
||||
phase_info = KILL_CHAIN_PHASES[phase_num]
|
||||
report_lines.append(f"\n Phase {phase_num} - {phase_info['name']}:")
|
||||
for coa_type, coa_desc in phase_info["coas"].items():
|
||||
report_lines.append(f" {coa_type.upper()}: {coa_desc}")
|
||||
return "\n".join(report_lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Cyber Kill Chain Analysis Agent")
|
||||
print("Lockheed Martin framework mapping with MITRE ATT&CK integration")
|
||||
print("=" * 60)
|
||||
|
||||
# Demo incident events
|
||||
demo_events = [
|
||||
{"description": "Shodan scans detected from 203.0.113.50 targeting web servers",
|
||||
"timestamp": "2025-09-10T08:00:00Z", "detected": False},
|
||||
{"description": "Phishing email with malicious .docm attachment delivered to 5 users",
|
||||
"timestamp": "2025-09-11T09:15:00Z", "detected": False},
|
||||
{"description": "CVE-2023-23397 exploitation detected in Outlook process crash",
|
||||
"timestamp": "2025-09-11T09:20:00Z", "detected": False},
|
||||
{"description": "Scheduled task created for persistence by malware dropper",
|
||||
"timestamp": "2025-09-11T09:25:00Z", "detected": True},
|
||||
{"description": "C2 beacon detected to 185.220.101.42 on port 443",
|
||||
"timestamp": "2025-09-11T09:30:00Z", "detected": True},
|
||||
]
|
||||
|
||||
print("\n[*] Analyzing demo incident events...")
|
||||
analysis = analyze_incident(demo_events)
|
||||
report = generate_report(analysis)
|
||||
print(f"\n{report}")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,118 @@
|
||||
# API Reference: Autopsy and The Sleuth Kit (TSK)
|
||||
|
||||
## mmls - Partition Layout
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
mmls <image_file>
|
||||
mmls -t dos <image_file> # Force DOS partition table
|
||||
mmls -t gpt <image_file> # Force GPT partition table
|
||||
```
|
||||
|
||||
### Output Format
|
||||
```
|
||||
DOS Partition Table
|
||||
Offset Sector: 0
|
||||
Slot Start End Length Description
|
||||
00: 00:00 0000002048 0001026047 0001024000 NTFS (0x07)
|
||||
```
|
||||
|
||||
## fls - File Listing
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
fls -o <offset> <image> # List root directory
|
||||
fls -r -o <offset> <image> # Recursive listing
|
||||
fls -rd -o <offset> <image> # Deleted files only, recursive
|
||||
fls -m "/" -r -o <offset> <image> # Bodyfile format for mactime
|
||||
```
|
||||
|
||||
### Flags
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-r` | Recursive listing |
|
||||
| `-d` | Deleted entries only |
|
||||
| `-D` | Directories only |
|
||||
| `-m "/"` | Output in bodyfile format with mount point |
|
||||
| `-o` | Partition sector offset |
|
||||
|
||||
## icat - File Extraction by Inode
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
icat -o <offset> <image> <inode> > recovered_file
|
||||
icat -r -o <offset> <image> <inode> > file # Recover slack space
|
||||
```
|
||||
|
||||
## istat - File Metadata
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
istat -o <offset> <image> <inode>
|
||||
```
|
||||
|
||||
### Output Includes
|
||||
- MFT entry number and sequence
|
||||
- File creation, modification, access, MFT change timestamps
|
||||
- File size and data run locations
|
||||
- Attribute list (NTFS: $STANDARD_INFORMATION, $FILE_NAME, $DATA)
|
||||
|
||||
## mactime - Timeline Generation
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
mactime -b <bodyfile> -d > timeline.csv
|
||||
mactime -b <bodyfile> -d 2024-01-15..2024-01-20 > filtered.csv
|
||||
mactime -b <bodyfile> -z UTC -d > timeline_utc.csv
|
||||
```
|
||||
|
||||
### Output Columns
|
||||
```
|
||||
Date,Size,Type,Mode,UID,GID,Meta,File Name
|
||||
```
|
||||
|
||||
## img_stat - Image Information
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
img_stat <image_file>
|
||||
```
|
||||
|
||||
## sigfind - File Signature Search
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
sigfind -o <offset> <image> <hex_signature>
|
||||
sigfind -o 2048 evidence.dd 25504446 # Find %PDF headers
|
||||
sigfind -o 2048 evidence.dd 504B0304 # Find ZIP/DOCX headers
|
||||
```
|
||||
|
||||
### Common Signatures
|
||||
| Hex | File Type |
|
||||
|-----|-----------|
|
||||
| `FFD8FF` | JPEG |
|
||||
| `89504E47` | PNG |
|
||||
| `25504446` | PDF |
|
||||
| `504B0304` | ZIP/DOCX/XLSX |
|
||||
| `D0CF11E0` | OLE (DOC/XLS) |
|
||||
|
||||
## srch_strings - Keyword Search
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
srch_strings -a -o <offset> <image> | grep -i "keyword"
|
||||
srch_strings -t d <image> # Print offset in decimal
|
||||
```
|
||||
|
||||
## Autopsy GUI Ingest Modules
|
||||
|
||||
| Module | Function |
|
||||
|--------|----------|
|
||||
| Recent Activity | Browser history, downloads, cookies |
|
||||
| Hash Lookup | NSRL and known-bad hash matching |
|
||||
| File Type Identification | Signature-based file type detection |
|
||||
| Keyword Search | Full-text content indexing |
|
||||
| Email Parser | PST/MBOX/EML extraction |
|
||||
| Extension Mismatch | Wrong file extension detection |
|
||||
| Embedded File Extractor | ZIP, Office, PDF extraction |
|
||||
| Encryption Detection | Encrypted container identification |
|
||||
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Forensic disk image analysis agent using The Sleuth Kit (TSK) command-line tools."""
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import csv
|
||||
import datetime
|
||||
|
||||
|
||||
def run_cmd(cmd):
|
||||
"""Execute a shell command and return output."""
|
||||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
||||
return result.stdout.strip(), result.stderr.strip(), result.returncode
|
||||
|
||||
|
||||
def get_image_info(image_path):
|
||||
"""Retrieve disk image metadata using img_stat."""
|
||||
stdout, _, rc = run_cmd(f"img_stat {image_path}")
|
||||
if rc == 0:
|
||||
info = {}
|
||||
for line in stdout.splitlines():
|
||||
if ":" in line:
|
||||
key, _, val = line.partition(":")
|
||||
info[key.strip()] = val.strip()
|
||||
return info
|
||||
return None
|
||||
|
||||
|
||||
def list_partitions(image_path):
|
||||
"""List partition layout using mmls."""
|
||||
stdout, _, rc = run_cmd(f"mmls {image_path}")
|
||||
partitions = []
|
||||
if rc == 0:
|
||||
for line in stdout.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) >= 6 and parts[2].isdigit():
|
||||
partitions.append({
|
||||
"slot": parts[0].rstrip(":"),
|
||||
"start": int(parts[2]),
|
||||
"end": int(parts[3]),
|
||||
"length": int(parts[4]),
|
||||
"description": " ".join(parts[5:]),
|
||||
})
|
||||
return partitions
|
||||
|
||||
|
||||
def list_files(image_path, offset, path="/", recursive=False):
|
||||
"""List files in a partition using fls."""
|
||||
flags = "-r" if recursive else ""
|
||||
cmd = f"fls {flags} -o {offset} {image_path}"
|
||||
if path != "/":
|
||||
cmd += f" -D {path}"
|
||||
stdout, _, rc = run_cmd(cmd)
|
||||
files = []
|
||||
if rc == 0:
|
||||
for line in stdout.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split("\t", 1)
|
||||
if len(parts) == 2:
|
||||
meta = parts[0].strip()
|
||||
name = parts[1].strip()
|
||||
deleted = meta.startswith("*")
|
||||
file_type = "d" if "d/" in meta else "r"
|
||||
inode = ""
|
||||
for token in meta.split():
|
||||
if "-" in token and token.replace("-", "").isdigit():
|
||||
inode = token
|
||||
break
|
||||
files.append({
|
||||
"name": name,
|
||||
"inode": inode,
|
||||
"type": "directory" if file_type == "d" else "file",
|
||||
"deleted": deleted,
|
||||
})
|
||||
return files
|
||||
|
||||
|
||||
def list_deleted_files(image_path, offset):
|
||||
"""List only deleted files using fls -rd."""
|
||||
stdout, _, rc = run_cmd(f"fls -rd -o {offset} {image_path}")
|
||||
deleted = []
|
||||
if rc == 0:
|
||||
for line in stdout.splitlines():
|
||||
line = line.strip()
|
||||
if line:
|
||||
deleted.append(line)
|
||||
return deleted
|
||||
|
||||
|
||||
def recover_file(image_path, offset, inode, output_path):
|
||||
"""Recover a file by inode using icat."""
|
||||
cmd = f"icat -o {offset} {image_path} {inode} > {output_path}"
|
||||
_, _, rc = run_cmd(cmd)
|
||||
return rc == 0
|
||||
|
||||
|
||||
def get_file_metadata(image_path, offset, inode):
|
||||
"""Get detailed file metadata using istat."""
|
||||
stdout, _, rc = run_cmd(f"istat -o {offset} {image_path} {inode}")
|
||||
return stdout if rc == 0 else None
|
||||
|
||||
|
||||
def create_bodyfile(image_path, offset, output_path):
|
||||
"""Generate a TSK bodyfile for timeline creation."""
|
||||
cmd = f'fls -r -m "/" -o {offset} {image_path} > {output_path}'
|
||||
_, _, rc = run_cmd(cmd)
|
||||
return rc == 0
|
||||
|
||||
|
||||
def generate_timeline(bodyfile_path, output_csv, start_date=None, end_date=None):
|
||||
"""Generate a timeline from a bodyfile using mactime."""
|
||||
cmd = f"mactime -b {bodyfile_path} -d"
|
||||
if start_date and end_date:
|
||||
cmd += f" {start_date}..{end_date}"
|
||||
cmd += f" > {output_csv}"
|
||||
_, _, rc = run_cmd(cmd)
|
||||
return rc == 0
|
||||
|
||||
|
||||
def search_keywords(image_path, offset, keyword):
|
||||
"""Search for keyword strings in the disk image."""
|
||||
cmd = f'srch_strings -a -o {offset} {image_path} | grep -i "{keyword}"'
|
||||
stdout, _, rc = run_cmd(cmd)
|
||||
return stdout.splitlines() if rc == 0 else []
|
||||
|
||||
|
||||
def find_file_signature(image_path, offset, hex_signature):
|
||||
"""Find file signatures at the sector level using sigfind."""
|
||||
stdout, _, rc = run_cmd(f"sigfind -o {offset} {image_path} {hex_signature}")
|
||||
return stdout if rc == 0 else None
|
||||
|
||||
|
||||
def analyze_image(image_path, case_dir):
|
||||
"""Run a full automated analysis workflow on a disk image."""
|
||||
os.makedirs(case_dir, exist_ok=True)
|
||||
results = {"image": image_path, "timestamp": datetime.datetime.utcnow().isoformat()}
|
||||
|
||||
print(f"[*] Image info...")
|
||||
results["image_info"] = get_image_info(image_path)
|
||||
|
||||
print(f"[*] Partition layout...")
|
||||
partitions = list_partitions(image_path)
|
||||
results["partitions"] = partitions
|
||||
|
||||
for part in partitions:
|
||||
if "NTFS" in part.get("description", "") or "Linux" in part.get("description", ""):
|
||||
offset = part["start"]
|
||||
print(f"[*] Listing files at offset {offset} ({part['description']})...")
|
||||
files = list_files(image_path, offset, recursive=True)
|
||||
results[f"files_offset_{offset}"] = {
|
||||
"total": len(files),
|
||||
"deleted": sum(1 for f in files if f["deleted"]),
|
||||
}
|
||||
print(f" Total: {len(files)}, Deleted: {results[f'files_offset_{offset}']['deleted']}")
|
||||
|
||||
print(f"[*] Creating bodyfile for timeline...")
|
||||
bf_path = os.path.join(case_dir, f"bodyfile_{offset}.txt")
|
||||
create_bodyfile(image_path, offset, bf_path)
|
||||
|
||||
tl_path = os.path.join(case_dir, f"timeline_{offset}.csv")
|
||||
generate_timeline(bf_path, tl_path)
|
||||
|
||||
report_path = os.path.join(case_dir, "analysis_summary.json")
|
||||
with open(report_path, "w") as f:
|
||||
json.dump(results, f, indent=2, default=str)
|
||||
print(f"[*] Summary saved to {report_path}")
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Disk Image Forensic Analysis Agent")
|
||||
print("Tools: The Sleuth Kit (fls, icat, mmls, mactime)")
|
||||
print("=" * 60)
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
image = sys.argv[1]
|
||||
case = sys.argv[2] if len(sys.argv) > 2 else "/tmp/autopsy_case"
|
||||
if os.path.exists(image):
|
||||
analyze_image(image, case)
|
||||
else:
|
||||
print(f"[ERROR] Image not found: {image}")
|
||||
else:
|
||||
print("\n[DEMO] Usage: python agent.py <disk_image.dd> [case_directory]")
|
||||
print("[*] Supported operations:")
|
||||
print(" - Partition enumeration (mmls)")
|
||||
print(" - File listing with deleted file recovery (fls, icat)")
|
||||
print(" - Timeline generation (mactime)")
|
||||
print(" - Keyword searching (srch_strings)")
|
||||
print(" - File signature detection (sigfind)")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,112 @@
|
||||
# API Reference: DNS Exfiltration Detection Tools
|
||||
|
||||
## Shannon Entropy Calculation
|
||||
|
||||
### Python Implementation
|
||||
```python
|
||||
import math
|
||||
from collections import Counter
|
||||
|
||||
def shannon_entropy(text):
|
||||
counter = Counter(text.lower())
|
||||
length = len(text)
|
||||
return -sum((c/length) * math.log2(c/length) for c in counter.values())
|
||||
```
|
||||
|
||||
### Threshold Values
|
||||
| Entropy | Classification |
|
||||
|---------|---------------|
|
||||
| < 2.5 | Normal domain (e.g., "google") |
|
||||
| 2.5 - 3.5 | Borderline (monitor) |
|
||||
| > 3.5 | Suspicious (likely DGA/tunneling) |
|
||||
| > 4.0 | High confidence malicious |
|
||||
|
||||
## Splunk DNS Queries
|
||||
|
||||
### Tunneling Detection
|
||||
```spl
|
||||
index=dns sourcetype="stream:dns"
|
||||
| eval subdomain_len=len(mvindex(split(query,"."),0))
|
||||
| where subdomain_len > 50
|
||||
| stats count by registered_domain, src_ip
|
||||
```
|
||||
|
||||
### DGA Detection
|
||||
```spl
|
||||
index=dns
|
||||
| eval sld=mvindex(split(query,"."), -2)
|
||||
| where len(sld) > 12
|
||||
| stats count, dc(query) AS unique by src_ip
|
||||
```
|
||||
|
||||
### Volume Anomaly
|
||||
```spl
|
||||
index=dns earliest=-24h
|
||||
| bin _time span=1h
|
||||
| stats count AS queries by src_ip, _time
|
||||
| eventstats avg(queries) AS avg_q, stdev(queries) AS stdev_q by src_ip
|
||||
| eval z_score=(queries - avg_q) / stdev_q
|
||||
| where z_score > 3
|
||||
```
|
||||
|
||||
### TXT Record Abuse
|
||||
```spl
|
||||
index=dns query_type="TXT"
|
||||
| stats count AS txt_queries by src_ip
|
||||
| where txt_queries > 100
|
||||
```
|
||||
|
||||
## Zeek DNS Log Format
|
||||
|
||||
### Log Fields (dns.log)
|
||||
| Column | Field | Description |
|
||||
|--------|-------|-------------|
|
||||
| 0 | ts | Timestamp |
|
||||
| 2 | id.orig_h | Source IP |
|
||||
| 4 | id.resp_h | DNS server IP |
|
||||
| 9 | query | Query domain name |
|
||||
| 13 | qtype_name | Query type (A, TXT, CNAME) |
|
||||
| 15 | rcode_name | Response code |
|
||||
| 21 | answers | Response answers |
|
||||
|
||||
### Zeek CLI Analysis
|
||||
```bash
|
||||
cat dns.log | zeek-cut query qtype_name id.orig_h | sort | uniq -c | sort -rn
|
||||
```
|
||||
|
||||
## DNS Tunneling Tools (Detection Signatures)
|
||||
|
||||
| Tool | DNS Pattern |
|
||||
|------|-------------|
|
||||
| iodine | `*.pirate.sea` (TXT/NULL records) |
|
||||
| dnscat2 | `*.dnscat.` prefix in queries |
|
||||
| dns2tcp | `*.dns2tcp.` pattern |
|
||||
| Cobalt Strike DNS | Periodic TXT queries with encoded payloads |
|
||||
|
||||
## Passive DNS Lookup APIs
|
||||
|
||||
### Farsight DNSDB
|
||||
```bash
|
||||
curl -H "X-API-Key: $KEY" \
|
||||
"https://api.dnsdb.info/dnsdb/v2/lookup/rrset/name/evil.com/A"
|
||||
```
|
||||
|
||||
### VirusTotal Domain Resolutions
|
||||
```bash
|
||||
curl -H "x-apikey: $KEY" \
|
||||
"https://www.virustotal.com/api/v3/domains/evil.com/resolutions"
|
||||
```
|
||||
|
||||
## Cisco Umbrella (OpenDNS) Investigate API
|
||||
|
||||
### Domain Categorization
|
||||
```bash
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
"https://investigate.api.umbrella.com/domains/categorization/evil.com"
|
||||
```
|
||||
|
||||
### Security Information
|
||||
```bash
|
||||
curl -H "Authorization: Bearer $TOKEN" \
|
||||
"https://investigate.api.umbrella.com/security/name/evil.com"
|
||||
```
|
||||
@@ -0,0 +1,234 @@
|
||||
#!/usr/bin/env python3
|
||||
"""DNS exfiltration detection agent using entropy analysis and query pattern detection."""
|
||||
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import csv
|
||||
import datetime
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
|
||||
def shannon_entropy(text):
|
||||
"""Calculate Shannon entropy of a string."""
|
||||
if not text:
|
||||
return 0.0
|
||||
counter = Counter(text.lower())
|
||||
length = len(text)
|
||||
entropy = -sum(
|
||||
(count / length) * math.log2(count / length)
|
||||
for count in counter.values()
|
||||
)
|
||||
return round(entropy, 4)
|
||||
|
||||
|
||||
def extract_subdomain(fqdn):
|
||||
"""Extract the subdomain portion from a fully qualified domain name."""
|
||||
parts = fqdn.rstrip(".").split(".")
|
||||
if len(parts) > 2:
|
||||
return ".".join(parts[:-2])
|
||||
return ""
|
||||
|
||||
|
||||
def extract_registered_domain(fqdn):
|
||||
"""Extract the registered domain (SLD + TLD) from an FQDN."""
|
||||
parts = fqdn.rstrip(".").split(".")
|
||||
if len(parts) >= 2:
|
||||
return ".".join(parts[-2:])
|
||||
return fqdn
|
||||
|
||||
|
||||
def detect_tunneling(dns_records, subdomain_len_threshold=50, min_queries=20):
|
||||
"""Detect DNS tunneling based on subdomain length anomalies."""
|
||||
domain_stats = defaultdict(lambda: {"queries": 0, "unique_queries": set(),
|
||||
"subdomain_lengths": [], "sources": set()})
|
||||
for record in dns_records:
|
||||
query = record.get("query", "")
|
||||
src = record.get("src_ip", "unknown")
|
||||
subdomain = extract_subdomain(query)
|
||||
reg_domain = extract_registered_domain(query)
|
||||
if len(subdomain) > subdomain_len_threshold:
|
||||
stats = domain_stats[reg_domain]
|
||||
stats["queries"] += 1
|
||||
stats["unique_queries"].add(query)
|
||||
stats["subdomain_lengths"].append(len(subdomain))
|
||||
stats["sources"].add(src)
|
||||
alerts = []
|
||||
for domain, stats in domain_stats.items():
|
||||
if stats["queries"] >= min_queries:
|
||||
avg_len = sum(stats["subdomain_lengths"]) / len(stats["subdomain_lengths"])
|
||||
max_len = max(stats["subdomain_lengths"])
|
||||
alerts.append({
|
||||
"domain": domain,
|
||||
"queries": stats["queries"],
|
||||
"unique_queries": len(stats["unique_queries"]),
|
||||
"avg_subdomain_length": round(avg_len, 1),
|
||||
"max_subdomain_length": max_len,
|
||||
"sources": list(stats["sources"]),
|
||||
"verdict": "CRITICAL - Likely DNS tunneling",
|
||||
})
|
||||
return sorted(alerts, key=lambda x: x["avg_subdomain_length"], reverse=True)
|
||||
|
||||
|
||||
def detect_dga(dns_records, entropy_threshold=3.5, min_sld_length=12):
|
||||
"""Detect Domain Generation Algorithm queries using entropy scoring."""
|
||||
suspicious = defaultdict(lambda: {"count": 0, "sources": set(), "entropies": []})
|
||||
for record in dns_records:
|
||||
query = record.get("query", "").rstrip(".")
|
||||
src = record.get("src_ip", "unknown")
|
||||
parts = query.split(".")
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
sld = parts[-2]
|
||||
if len(sld) < min_sld_length:
|
||||
continue
|
||||
ent = shannon_entropy(sld)
|
||||
if ent > entropy_threshold:
|
||||
suspicious[query]["count"] += 1
|
||||
suspicious[query]["sources"].add(src)
|
||||
suspicious[query]["entropies"].append(ent)
|
||||
alerts = []
|
||||
for domain, data in suspicious.items():
|
||||
avg_entropy = sum(data["entropies"]) / len(data["entropies"])
|
||||
alerts.append({
|
||||
"domain": domain,
|
||||
"queries": data["count"],
|
||||
"avg_entropy": round(avg_entropy, 4),
|
||||
"sources": list(data["sources"]),
|
||||
"verdict": "HIGH - Possible DGA domain",
|
||||
})
|
||||
return sorted(alerts, key=lambda x: x["avg_entropy"], reverse=True)
|
||||
|
||||
|
||||
def detect_volume_anomaly(dns_records, z_score_threshold=3.0):
|
||||
"""Detect hosts with anomalously high DNS query volumes."""
|
||||
host_counts = defaultdict(int)
|
||||
for record in dns_records:
|
||||
src = record.get("src_ip", "unknown")
|
||||
host_counts[src] += 1
|
||||
if not host_counts:
|
||||
return []
|
||||
values = list(host_counts.values())
|
||||
mean_q = sum(values) / len(values)
|
||||
if len(values) < 2:
|
||||
return []
|
||||
variance = sum((x - mean_q) ** 2 for x in values) / (len(values) - 1)
|
||||
stdev_q = variance ** 0.5
|
||||
if stdev_q == 0:
|
||||
return []
|
||||
anomalies = []
|
||||
for host, count in host_counts.items():
|
||||
z = (count - mean_q) / stdev_q
|
||||
if z > z_score_threshold:
|
||||
anomalies.append({
|
||||
"src_ip": host,
|
||||
"queries": count,
|
||||
"z_score": round(z, 2),
|
||||
"mean": round(mean_q, 1),
|
||||
"verdict": "HIGH - Anomalous query volume",
|
||||
})
|
||||
return sorted(anomalies, key=lambda x: x["z_score"], reverse=True)
|
||||
|
||||
|
||||
def detect_txt_abuse(dns_records, threshold=100):
|
||||
"""Detect excessive TXT record queries (common tunneling method)."""
|
||||
txt_counts = defaultdict(lambda: {"count": 0, "unique_domains": set()})
|
||||
for record in dns_records:
|
||||
qtype = str(record.get("query_type", "")).upper()
|
||||
if qtype in ("TXT", "16"):
|
||||
src = record.get("src_ip", "unknown")
|
||||
txt_counts[src]["count"] += 1
|
||||
txt_counts[src]["unique_domains"].add(record.get("query", ""))
|
||||
alerts = []
|
||||
for src, data in txt_counts.items():
|
||||
if data["count"] > threshold:
|
||||
level = "CRITICAL" if data["count"] > 1000 else "HIGH" if data["count"] > 500 else "MEDIUM"
|
||||
alerts.append({
|
||||
"src_ip": src,
|
||||
"txt_queries": data["count"],
|
||||
"unique_domains": len(data["unique_domains"]),
|
||||
"verdict": f"{level} - Possible DNS tunneling via TXT records",
|
||||
})
|
||||
return sorted(alerts, key=lambda x: x["txt_queries"], reverse=True)
|
||||
|
||||
|
||||
def estimate_exfil_volume(dns_records, target_domain):
|
||||
"""Estimate data volume encoded in DNS queries to a specific domain."""
|
||||
total_encoded_bytes = 0
|
||||
query_count = 0
|
||||
for record in dns_records:
|
||||
query = record.get("query", "")
|
||||
if target_domain in query:
|
||||
subdomain = extract_subdomain(query)
|
||||
total_encoded_bytes += len(subdomain)
|
||||
query_count += 1
|
||||
decoded_bytes = int(total_encoded_bytes * 0.75) # Base64 decode factor
|
||||
return {
|
||||
"target_domain": target_domain,
|
||||
"total_queries": query_count,
|
||||
"encoded_bytes": total_encoded_bytes,
|
||||
"estimated_decoded_bytes": decoded_bytes,
|
||||
"estimated_kb": round(decoded_bytes / 1024, 1),
|
||||
"estimated_mb": round(decoded_bytes / (1024 * 1024), 3),
|
||||
}
|
||||
|
||||
|
||||
def parse_zeek_dns_log(log_path):
|
||||
"""Parse a Zeek dns.log file into structured records."""
|
||||
records = []
|
||||
with open(log_path, "r") as f:
|
||||
for line in f:
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
parts = line.strip().split("\t")
|
||||
if len(parts) >= 10:
|
||||
records.append({
|
||||
"timestamp": parts[0],
|
||||
"src_ip": parts[2],
|
||||
"src_port": parts[3],
|
||||
"dst_ip": parts[4],
|
||||
"query": parts[9] if len(parts) > 9 else "",
|
||||
"query_type": parts[13] if len(parts) > 13 else "",
|
||||
})
|
||||
return records
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("DNS Exfiltration Detection Agent")
|
||||
print("Tunneling, DGA, volume anomaly, and TXT abuse detection")
|
||||
print("=" * 60)
|
||||
|
||||
# Demo with synthetic DNS records
|
||||
demo_records = [
|
||||
{"query": f"{'a' * 60}.evil-tunnel.com", "src_ip": "192.168.1.105",
|
||||
"query_type": "TXT"} for _ in range(50)
|
||||
] + [
|
||||
{"query": "x8kj2m9p4qw7nz3.xyz", "src_ip": "192.168.1.110",
|
||||
"query_type": "A"} for _ in range(5)
|
||||
] + [
|
||||
{"query": "google.com", "src_ip": "192.168.1.50", "query_type": "A"}
|
||||
for _ in range(10)
|
||||
]
|
||||
|
||||
print("\n--- DNS Tunneling Detection ---")
|
||||
tunneling = detect_tunneling(demo_records, subdomain_len_threshold=30, min_queries=10)
|
||||
for t in tunneling:
|
||||
print(f"[!] {t['domain']}: {t['queries']} queries, "
|
||||
f"avg subdomain len={t['avg_subdomain_length']}")
|
||||
|
||||
print("\n--- DGA Detection ---")
|
||||
dga = detect_dga(demo_records, entropy_threshold=3.0, min_sld_length=10)
|
||||
for d in dga[:5]:
|
||||
print(f"[!] {d['domain']}: entropy={d['avg_entropy']}")
|
||||
|
||||
print("\n--- TXT Record Abuse ---")
|
||||
txt = detect_txt_abuse(demo_records, threshold=10)
|
||||
for t in txt:
|
||||
print(f"[!] {t['src_ip']}: {t['txt_queries']} TXT queries")
|
||||
|
||||
print("\n--- Entropy Examples ---")
|
||||
examples = ["google", "x8kj2m9p4qw7n", "aGVsbG8gd29ybGQ"]
|
||||
for ex in examples:
|
||||
print(f" '{ex}' -> entropy={shannon_entropy(ex)}")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,116 @@
|
||||
# API Reference: Docker Container Forensics Tools
|
||||
|
||||
## docker inspect - Container Details
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
docker inspect <container_id>
|
||||
docker inspect --format '{{.HostConfig.Privileged}}' <container_id>
|
||||
docker inspect --format '{{json .Mounts}}' <container_id> | jq
|
||||
docker inspect --format '{{.GraphDriver.Data.MergedDir}}' <container_id>
|
||||
```
|
||||
|
||||
### Key JSON Paths
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `.HostConfig.Privileged` | Privileged mode status |
|
||||
| `.HostConfig.CapAdd` | Added capabilities |
|
||||
| `.HostConfig.PidMode` | PID namespace mode |
|
||||
| `.HostConfig.NetworkMode` | Network namespace mode |
|
||||
| `.Mounts` | Volume mount configuration |
|
||||
| `.Config.User` | Container user |
|
||||
| `.Config.Env` | Environment variables |
|
||||
| `.Config.Image` | Source image name |
|
||||
| `.State.StartedAt` | Container start time |
|
||||
|
||||
## docker diff - Filesystem Changes
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
docker diff <container_id>
|
||||
```
|
||||
|
||||
### Output Codes
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| `A` | File or directory was added |
|
||||
| `C` | File or directory was changed |
|
||||
| `D` | File or directory was deleted |
|
||||
|
||||
## docker export - Container Filesystem Export
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
docker export <container_id> > container_fs.tar
|
||||
docker export <container_id> | gzip > container_fs.tar.gz
|
||||
```
|
||||
|
||||
## docker commit / docker save - Image Preservation
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
docker commit <container_id> forensic-evidence:case001
|
||||
docker save forensic-evidence:case001 > evidence_image.tar
|
||||
```
|
||||
|
||||
## docker logs - Container Log Retrieval
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
docker logs --timestamps <container_id>
|
||||
docker logs --since 2024-01-15 <container_id>
|
||||
docker logs --tail 1000 <container_id>
|
||||
docker logs -f <container_id> # Follow (live)
|
||||
```
|
||||
|
||||
## dive - Image Layer Analysis
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
dive <image_name> # Interactive mode
|
||||
dive <image_name> --ci # CI mode (non-interactive)
|
||||
dive <image_name> --ci --json out.json # JSON output
|
||||
```
|
||||
|
||||
### Output Includes
|
||||
- Layer-by-layer filesystem changes
|
||||
- Image efficiency score
|
||||
- Wasted space analysis
|
||||
|
||||
## container-diff - Image Comparison
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
container-diff diff daemon://nginx:latest daemon://suspect:latest \
|
||||
--type=file --type=apt --type=history --json
|
||||
```
|
||||
|
||||
### Diff Types
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `file` | File system differences |
|
||||
| `apt` | APT package differences |
|
||||
| `pip` | Python package differences |
|
||||
| `history` | Docker build history differences |
|
||||
|
||||
## Trivy - Vulnerability Scanning
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
trivy image <image_name>
|
||||
trivy image --format json <image_name>
|
||||
trivy image --scanners vuln,secret <image_name>
|
||||
trivy fs /path/to/exported/container/
|
||||
```
|
||||
|
||||
### Severity Levels
|
||||
`CRITICAL` | `HIGH` | `MEDIUM` | `LOW` | `UNKNOWN`
|
||||
|
||||
## docker-explorer - Offline Forensics
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
de.py -r /var/lib/docker list
|
||||
de.py -r /var/lib/docker mount <container_id> /mnt/forensic
|
||||
de.py -r /var/lib/docker history <container_id>
|
||||
```
|
||||
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Docker container forensics agent for investigating compromised containers."""
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import hashlib
|
||||
import datetime
|
||||
|
||||
|
||||
def run_cmd(cmd):
|
||||
"""Execute a shell command and return output."""
|
||||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
||||
return result.stdout.strip(), result.stderr.strip(), result.returncode
|
||||
|
||||
|
||||
def list_containers(all_containers=True):
|
||||
"""List Docker containers with detailed information."""
|
||||
flags = "-a" if all_containers else ""
|
||||
cmd = f"docker ps {flags} --no-trunc --format '{{{{json .}}}}'"
|
||||
stdout, _, rc = run_cmd(cmd)
|
||||
containers = []
|
||||
if rc == 0 and stdout:
|
||||
for line in stdout.splitlines():
|
||||
try:
|
||||
containers.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return containers
|
||||
|
||||
|
||||
def inspect_container(container_id):
|
||||
"""Get detailed container inspection data."""
|
||||
stdout, _, rc = run_cmd(f"docker inspect {container_id}")
|
||||
if rc == 0 and stdout:
|
||||
return json.loads(stdout)
|
||||
return None
|
||||
|
||||
|
||||
def analyze_security_config(inspect_data):
|
||||
"""Analyze container security configuration for misconfigurations."""
|
||||
if isinstance(inspect_data, list):
|
||||
inspect_data = inspect_data[0]
|
||||
findings = []
|
||||
host_config = inspect_data.get("HostConfig", {})
|
||||
config = inspect_data.get("Config", {})
|
||||
|
||||
if host_config.get("Privileged"):
|
||||
findings.append({"severity": "CRITICAL", "finding": "Container running in PRIVILEGED mode"})
|
||||
|
||||
cap_add = host_config.get("CapAdd") or []
|
||||
dangerous_caps = ["SYS_ADMIN", "SYS_PTRACE", "NET_ADMIN", "SYS_MODULE",
|
||||
"DAC_OVERRIDE", "NET_RAW"]
|
||||
for cap in cap_add:
|
||||
if cap in dangerous_caps:
|
||||
findings.append({"severity": "HIGH", "finding": f"Dangerous capability added: {cap}"})
|
||||
|
||||
if host_config.get("PidMode") == "host":
|
||||
findings.append({"severity": "HIGH", "finding": "Shares host PID namespace"})
|
||||
|
||||
if host_config.get("NetworkMode") == "host":
|
||||
findings.append({"severity": "HIGH", "finding": "Shares host network namespace"})
|
||||
|
||||
mounts = inspect_data.get("Mounts", [])
|
||||
sensitive_paths = ["/", "/etc", "/var", "/root", "/home", "/var/run/docker.sock"]
|
||||
for mount in mounts:
|
||||
src = mount.get("Source", "")
|
||||
rw = mount.get("RW", False)
|
||||
if src in sensitive_paths and rw:
|
||||
findings.append({
|
||||
"severity": "CRITICAL",
|
||||
"finding": f"Sensitive host path mounted RW: {src} -> {mount.get('Destination')}"
|
||||
})
|
||||
if "docker.sock" in src:
|
||||
findings.append({
|
||||
"severity": "CRITICAL",
|
||||
"finding": "Docker socket mounted (container can control Docker daemon)"
|
||||
})
|
||||
|
||||
user = config.get("User", "")
|
||||
if not user or user == "root":
|
||||
findings.append({"severity": "MEDIUM", "finding": "Running as root user"})
|
||||
|
||||
env_vars = config.get("Env", [])
|
||||
secret_keywords = ["PASSWORD", "SECRET", "KEY", "TOKEN", "CREDENTIAL", "API_KEY"]
|
||||
for env in env_vars:
|
||||
key = env.split("=")[0]
|
||||
if any(s in key.upper() for s in secret_keywords):
|
||||
findings.append({"severity": "HIGH", "finding": f"Sensitive env var exposed: {key}"})
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def get_filesystem_changes(container_id):
|
||||
"""Get filesystem changes between container and its image."""
|
||||
stdout, _, rc = run_cmd(f"docker diff {container_id}")
|
||||
changes = {"added": [], "changed": [], "deleted": []}
|
||||
if rc == 0 and stdout:
|
||||
for line in stdout.splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("A "):
|
||||
changes["added"].append(line[2:])
|
||||
elif line.startswith("C "):
|
||||
changes["changed"].append(line[2:])
|
||||
elif line.startswith("D "):
|
||||
changes["deleted"].append(line[2:])
|
||||
return changes
|
||||
|
||||
|
||||
def detect_suspicious_files(changes):
|
||||
"""Analyze filesystem changes for indicators of compromise."""
|
||||
suspicious_patterns = [
|
||||
"/tmp/", "/dev/shm/", "/root/", ".sh", ".py", ".elf",
|
||||
"reverse", "shell", "backdoor", "miner", "xmr", "nc ",
|
||||
".php", "webshell", "c2", "beacon",
|
||||
]
|
||||
suspicious_changes = ["/etc/passwd", "/etc/shadow", "/etc/crontab",
|
||||
"/etc/ssh", ".bashrc", "/etc/sudoers", "authorized_keys"]
|
||||
|
||||
findings = []
|
||||
for f in changes["added"]:
|
||||
for pattern in suspicious_patterns:
|
||||
if pattern in f.lower():
|
||||
findings.append({"type": "ADDED", "path": f, "reason": f"Matches pattern: {pattern}"})
|
||||
break
|
||||
for f in changes["changed"]:
|
||||
for pattern in suspicious_changes:
|
||||
if pattern in f.lower():
|
||||
findings.append({"type": "CHANGED", "path": f, "reason": f"Critical file modified"})
|
||||
break
|
||||
return findings
|
||||
|
||||
|
||||
def export_container(container_id, output_path):
|
||||
"""Export container filesystem as a tarball for offline analysis."""
|
||||
cmd = f"docker export {container_id} > {output_path}"
|
||||
_, _, rc = run_cmd(cmd)
|
||||
if rc == 0 and os.path.exists(output_path):
|
||||
sha256 = hashlib.sha256()
|
||||
with open(output_path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(65536), b""):
|
||||
sha256.update(chunk)
|
||||
return True, sha256.hexdigest()
|
||||
return False, None
|
||||
|
||||
|
||||
def get_container_logs(container_id, tail=500):
|
||||
"""Retrieve container logs with timestamps."""
|
||||
stdout, stderr, rc = run_cmd(f"docker logs --timestamps --tail {tail} {container_id}")
|
||||
return stdout + "\n" + stderr if rc == 0 else None
|
||||
|
||||
|
||||
def scan_image_vulnerabilities(image_name):
|
||||
"""Run Trivy vulnerability scan on a container image."""
|
||||
cmd = f"trivy image --format json {image_name}"
|
||||
stdout, _, rc = run_cmd(cmd)
|
||||
if rc == 0 and stdout:
|
||||
try:
|
||||
return json.loads(stdout)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def generate_report(container_id, inspect_data, security_findings,
|
||||
fs_changes, suspicious_files):
|
||||
"""Generate a forensic analysis report."""
|
||||
container_name = "unknown"
|
||||
image = "unknown"
|
||||
if inspect_data:
|
||||
data = inspect_data[0] if isinstance(inspect_data, list) else inspect_data
|
||||
container_name = data.get("Name", "").lstrip("/")
|
||||
image = data.get("Config", {}).get("Image", "unknown")
|
||||
|
||||
report = {
|
||||
"report_type": "Docker Container Forensics",
|
||||
"timestamp": datetime.datetime.utcnow().isoformat() + "Z",
|
||||
"container_id": container_id,
|
||||
"container_name": container_name,
|
||||
"image": image,
|
||||
"security_findings": security_findings,
|
||||
"filesystem_changes": {
|
||||
"added": len(fs_changes["added"]),
|
||||
"changed": len(fs_changes["changed"]),
|
||||
"deleted": len(fs_changes["deleted"]),
|
||||
},
|
||||
"suspicious_files": suspicious_files,
|
||||
}
|
||||
return report
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Docker Container Forensics Agent")
|
||||
print("Security analysis, filesystem diffing, evidence collection")
|
||||
print("=" * 60)
|
||||
|
||||
container_id = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
|
||||
if container_id:
|
||||
print(f"\n[*] Analyzing container: {container_id}")
|
||||
|
||||
inspect_data = inspect_container(container_id)
|
||||
if not inspect_data:
|
||||
print("[ERROR] Failed to inspect container. Is Docker running?")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n--- Security Configuration Analysis ---")
|
||||
findings = analyze_security_config(inspect_data)
|
||||
for f in findings:
|
||||
print(f"[{f['severity']}] {f['finding']}")
|
||||
|
||||
print("\n--- Filesystem Changes ---")
|
||||
changes = get_filesystem_changes(container_id)
|
||||
print(f" Added: {len(changes['added'])}, Changed: {len(changes['changed'])}, "
|
||||
f"Deleted: {len(changes['deleted'])}")
|
||||
|
||||
print("\n--- Suspicious Files ---")
|
||||
suspicious = detect_suspicious_files(changes)
|
||||
for s in suspicious:
|
||||
print(f"[!] {s['type']}: {s['path']} ({s['reason']})")
|
||||
|
||||
report = generate_report(container_id, inspect_data, findings, changes, suspicious)
|
||||
print(f"\n[*] Report:\n{json.dumps(report, indent=2)}")
|
||||
else:
|
||||
print("\n[*] Listing all containers...")
|
||||
containers = list_containers()
|
||||
for c in containers:
|
||||
print(f" {c.get('ID', '?')[:12]} {c.get('Names', '?')} {c.get('Status', '?')}")
|
||||
print(f"\n[DEMO] Usage: python agent.py <container_id>")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
# API Reference: Email Header Analysis Tools
|
||||
|
||||
## Python email Module
|
||||
|
||||
### Parsing EML Files
|
||||
```python
|
||||
import email
|
||||
from email import policy
|
||||
|
||||
with open("phishing.eml", "r") as f:
|
||||
msg = email.message_from_file(f, policy=policy.default)
|
||||
|
||||
msg["From"] # From header
|
||||
msg["To"] # To header
|
||||
msg["Subject"] # Subject line
|
||||
msg["Message-ID"] # Unique message identifier
|
||||
msg["Reply-To"] # Reply-To address
|
||||
msg["Return-Path"] # Envelope sender
|
||||
msg.get_all("Received") # All Received headers (list)
|
||||
msg.get_all("Authentication-Results") # Auth results
|
||||
```
|
||||
|
||||
### Body and Attachment Extraction
|
||||
```python
|
||||
body = msg.get_body(preferencelist=("html", "plain"))
|
||||
content = body.get_content()
|
||||
|
||||
for part in msg.walk():
|
||||
if part.get_content_disposition() == "attachment":
|
||||
filename = part.get_filename()
|
||||
data = part.get_payload(decode=True)
|
||||
```
|
||||
|
||||
## dig - DNS Record Lookup
|
||||
|
||||
### SPF Record
|
||||
```bash
|
||||
dig TXT example.com +short
|
||||
# Output: "v=spf1 include:_spf.google.com ~all"
|
||||
```
|
||||
|
||||
### DKIM Record
|
||||
```bash
|
||||
dig TXT selector1._domainkey.example.com +short
|
||||
```
|
||||
|
||||
### DMARC Record
|
||||
```bash
|
||||
dig TXT _dmarc.example.com +short
|
||||
# Output: "v=DMARC1; p=reject; rua=mailto:dmarc@example.com"
|
||||
```
|
||||
|
||||
## pyspf - SPF Validation (Python)
|
||||
|
||||
### Syntax
|
||||
```python
|
||||
import spf
|
||||
result, explanation = spf.check2(
|
||||
i="203.0.113.45", # Sending IP
|
||||
s="sender@example.com", # Envelope sender
|
||||
h="mail.example.com" # HELO hostname
|
||||
)
|
||||
# Results: pass, fail, softfail, neutral, none, temperror, permerror
|
||||
```
|
||||
|
||||
## dkimpy - DKIM Verification (Python)
|
||||
|
||||
### Syntax
|
||||
```python
|
||||
import dkim
|
||||
with open("email.eml", "rb") as f:
|
||||
message = f.read()
|
||||
result = dkim.verify(message)
|
||||
# Returns True/False
|
||||
```
|
||||
|
||||
## AbuseIPDB - IP Reputation
|
||||
|
||||
### API Endpoint
|
||||
```bash
|
||||
curl -G "https://api.abuseipdb.com/api/v2/check" \
|
||||
-H "Key: YOUR_API_KEY" \
|
||||
-H "Accept: application/json" \
|
||||
-d "ipAddress=203.0.113.45" -d "maxAgeInDays=90"
|
||||
```
|
||||
|
||||
### Response Fields
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `abuseConfidenceScore` | 0-100 confidence of abuse |
|
||||
| `totalReports` | Number of abuse reports |
|
||||
| `countryCode` | Source country |
|
||||
| `isp` | Internet service provider |
|
||||
|
||||
## VirusTotal - Domain/URL Reputation
|
||||
|
||||
### Domain Lookup
|
||||
```bash
|
||||
curl -H "x-apikey: YOUR_KEY" \
|
||||
"https://www.virustotal.com/api/v3/domains/suspicious.com"
|
||||
```
|
||||
|
||||
### URL Scan
|
||||
```bash
|
||||
curl -X POST "https://www.virustotal.com/api/v3/urls" \
|
||||
-H "x-apikey: YOUR_KEY" \
|
||||
-d "url=http://suspicious-url.com/login"
|
||||
```
|
||||
|
||||
## whois - Domain Registration
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
whois suspicious-domain.com
|
||||
```
|
||||
|
||||
### Key Fields
|
||||
- `Registrar` - Domain registrar
|
||||
- `Creation Date` - When domain was registered
|
||||
- `Registrant` - Domain owner info
|
||||
- `Name Server` - Authoritative DNS servers
|
||||
@@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Email header analysis agent for phishing investigation and sender verification."""
|
||||
|
||||
import email
|
||||
import email.utils
|
||||
import re
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import json
|
||||
from email import policy
|
||||
|
||||
|
||||
def parse_email_file(eml_path):
|
||||
"""Parse an EML file and extract key header fields."""
|
||||
with open(eml_path, "r", errors="replace") as f:
|
||||
msg = email.message_from_file(f, policy=policy.default)
|
||||
headers = {
|
||||
"from": str(msg["From"] or ""),
|
||||
"to": str(msg["To"] or ""),
|
||||
"subject": str(msg["Subject"] or ""),
|
||||
"date": str(msg["Date"] or ""),
|
||||
"message_id": str(msg["Message-ID"] or ""),
|
||||
"reply_to": str(msg["Reply-To"] or ""),
|
||||
"return_path": str(msg["Return-Path"] or ""),
|
||||
"x_mailer": str(msg["X-Mailer"] or ""),
|
||||
"x_originating_ip": str(msg["X-Originating-IP"] or ""),
|
||||
}
|
||||
return msg, headers
|
||||
|
||||
|
||||
def extract_received_chain(msg):
|
||||
"""Extract and parse the Received header chain (bottom-up = chronological)."""
|
||||
received_headers = msg.get_all("Received") or []
|
||||
hops = []
|
||||
ip_pattern = re.compile(r"\[?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]?")
|
||||
for i, header in enumerate(reversed(received_headers)):
|
||||
ips = ip_pattern.findall(header)
|
||||
hops.append({
|
||||
"hop": i + 1,
|
||||
"header": header.strip()[:200],
|
||||
"ips": ips,
|
||||
})
|
||||
return hops
|
||||
|
||||
|
||||
def extract_authentication_results(msg):
|
||||
"""Extract SPF, DKIM, and DMARC results from Authentication-Results headers."""
|
||||
auth_results = msg.get_all("Authentication-Results") or []
|
||||
received_spf = str(msg.get("Received-SPF", ""))
|
||||
dkim_sig = str(msg.get("DKIM-Signature", ""))
|
||||
results = {
|
||||
"spf": "unknown",
|
||||
"dkim": "unknown",
|
||||
"dmarc": "unknown",
|
||||
"raw_authentication_results": [],
|
||||
"received_spf": received_spf,
|
||||
"has_dkim_signature": bool(dkim_sig),
|
||||
}
|
||||
for ar in auth_results:
|
||||
results["raw_authentication_results"].append(ar.strip())
|
||||
ar_lower = ar.lower()
|
||||
if "spf=" in ar_lower:
|
||||
spf_match = re.search(r"spf=(\w+)", ar_lower)
|
||||
if spf_match:
|
||||
results["spf"] = spf_match.group(1)
|
||||
if "dkim=" in ar_lower:
|
||||
dkim_match = re.search(r"dkim=(\w+)", ar_lower)
|
||||
if dkim_match:
|
||||
results["dkim"] = dkim_match.group(1)
|
||||
if "dmarc=" in ar_lower:
|
||||
dmarc_match = re.search(r"dmarc=(\w+)", ar_lower)
|
||||
if dmarc_match:
|
||||
results["dmarc"] = dmarc_match.group(1)
|
||||
return results
|
||||
|
||||
|
||||
def check_from_replyto_mismatch(headers):
|
||||
"""Detect mismatch between From and Reply-To addresses."""
|
||||
from_addr = email.utils.parseaddr(headers["from"])[1].lower()
|
||||
reply_to = headers["reply_to"]
|
||||
if reply_to:
|
||||
reply_addr = email.utils.parseaddr(reply_to)[1].lower()
|
||||
if reply_addr and from_addr != reply_addr:
|
||||
return True, from_addr, reply_addr
|
||||
return False, from_addr, None
|
||||
|
||||
|
||||
def extract_urls(msg):
|
||||
"""Extract all URLs from the email body."""
|
||||
body = msg.get_body(preferencelist=("html", "plain"))
|
||||
urls = []
|
||||
if body:
|
||||
content = body.get_content()
|
||||
urls = list(set(re.findall(r"https?://[^\s<>\"']+", content)))
|
||||
return urls
|
||||
|
||||
|
||||
def detect_url_mismatch(msg):
|
||||
"""Detect hyperlinks where display text differs from actual href."""
|
||||
body = msg.get_body(preferencelist=("html",))
|
||||
mismatches = []
|
||||
if body:
|
||||
content = body.get_content()
|
||||
href_pattern = re.findall(
|
||||
r'<a[^>]*href=["\']([^"\']+)["\'][^>]*>(.*?)</a>', content, re.DOTALL
|
||||
)
|
||||
for href, text in href_pattern:
|
||||
display_urls = re.findall(r"https?://[^\s<]+", text)
|
||||
if display_urls:
|
||||
for display_url in display_urls:
|
||||
if display_url.rstrip("/") != href.rstrip("/"):
|
||||
mismatches.append({
|
||||
"display_url": display_url,
|
||||
"actual_url": href,
|
||||
})
|
||||
return mismatches
|
||||
|
||||
|
||||
def extract_attachments(msg, output_dir=None):
|
||||
"""Extract and hash all email attachments."""
|
||||
attachments = []
|
||||
for part in msg.walk():
|
||||
if part.get_content_disposition() == "attachment":
|
||||
filename = part.get_filename() or "unnamed_attachment"
|
||||
content = part.get_payload(decode=True)
|
||||
if content:
|
||||
sha256 = hashlib.sha256(content).hexdigest()
|
||||
md5 = hashlib.md5(content).hexdigest()
|
||||
att_info = {
|
||||
"filename": filename,
|
||||
"size": len(content),
|
||||
"sha256": sha256,
|
||||
"md5": md5,
|
||||
"content_type": part.get_content_type(),
|
||||
}
|
||||
if output_dir:
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(content)
|
||||
att_info["saved_to"] = filepath
|
||||
attachments.append(att_info)
|
||||
return attachments
|
||||
|
||||
|
||||
def dns_lookup(domain, record_type="TXT"):
|
||||
"""Perform DNS lookup for SPF/DKIM/DMARC records."""
|
||||
cmd = f"dig {record_type} {domain} +short"
|
||||
stdout, _, rc = subprocess.run(cmd, shell=True, capture_output=True, text=True,
|
||||
timeout=10).stdout, "", 0
|
||||
return stdout.strip() if stdout else ""
|
||||
|
||||
|
||||
def check_domain_spf(domain):
|
||||
"""Look up the SPF record for a domain."""
|
||||
return dns_lookup(domain, "TXT")
|
||||
|
||||
|
||||
def check_domain_dmarc(domain):
|
||||
"""Look up the DMARC record for a domain."""
|
||||
return dns_lookup(f"_dmarc.{domain}", "TXT")
|
||||
|
||||
|
||||
def generate_phishing_indicators(headers, auth, hops, url_mismatches, attachments):
|
||||
"""Compile a list of phishing indicators from the analysis."""
|
||||
indicators = []
|
||||
mismatch, from_addr, reply_addr = check_from_replyto_mismatch(headers)
|
||||
if mismatch:
|
||||
indicators.append(f"From/Reply-To mismatch: {from_addr} vs {reply_addr}")
|
||||
if auth["spf"] in ("fail", "softfail"):
|
||||
indicators.append(f"SPF {auth['spf']}")
|
||||
if auth["dkim"] == "fail" or not auth["has_dkim_signature"]:
|
||||
indicators.append("DKIM failed or missing")
|
||||
if auth["dmarc"] in ("fail", "none"):
|
||||
indicators.append(f"DMARC {auth['dmarc']}")
|
||||
if url_mismatches:
|
||||
indicators.append(f"{len(url_mismatches)} URL display/href mismatches detected")
|
||||
for att in attachments:
|
||||
if any(att["filename"].endswith(ext) for ext in [".exe", ".scr", ".vbs", ".js",
|
||||
".docm", ".xlsm", ".bat", ".ps1", ".hta"]):
|
||||
indicators.append(f"Suspicious attachment: {att['filename']}")
|
||||
return indicators
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Email Header Phishing Analysis Agent")
|
||||
print("SPF/DKIM/DMARC validation, URL analysis, attachment extraction")
|
||||
print("=" * 60)
|
||||
|
||||
eml_file = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
|
||||
if eml_file and os.path.exists(eml_file):
|
||||
print(f"\n[*] Analyzing: {eml_file}")
|
||||
msg, headers = parse_email_file(eml_file)
|
||||
print(f" From: {headers['from']}")
|
||||
print(f" To: {headers['to']}")
|
||||
print(f" Subject: {headers['subject']}")
|
||||
print(f" Date: {headers['date']}")
|
||||
|
||||
hops = extract_received_chain(msg)
|
||||
print(f"\n[*] Delivery path: {len(hops)} hops")
|
||||
for hop in hops:
|
||||
print(f" Hop {hop['hop']}: IPs={hop['ips']}")
|
||||
|
||||
auth = extract_authentication_results(msg)
|
||||
print(f"\n[*] Authentication: SPF={auth['spf']} DKIM={auth['dkim']} DMARC={auth['dmarc']}")
|
||||
|
||||
urls = extract_urls(msg)
|
||||
print(f"\n[*] URLs found: {len(urls)}")
|
||||
url_mismatches = detect_url_mismatch(msg)
|
||||
for m in url_mismatches:
|
||||
print(f" [!] MISMATCH: Display='{m['display_url']}' Actual='{m['actual_url']}'")
|
||||
|
||||
attachments = extract_attachments(msg)
|
||||
print(f"\n[*] Attachments: {len(attachments)}")
|
||||
for att in attachments:
|
||||
print(f" {att['filename']} ({att['size']} bytes) SHA256={att['sha256'][:16]}...")
|
||||
|
||||
indicators = generate_phishing_indicators(headers, auth, hops, url_mismatches, attachments)
|
||||
if indicators:
|
||||
print(f"\n[!] PHISHING INDICATORS:")
|
||||
for ind in indicators:
|
||||
print(f" - {ind}")
|
||||
else:
|
||||
print(f"\n[DEMO] Usage: python agent.py <email.eml>")
|
||||
print("[*] Provide an EML file for phishing analysis.")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,90 @@
|
||||
# API Reference: Go Malware Analysis with Ghidra
|
||||
|
||||
## Ghidra Go Analysis Setup
|
||||
|
||||
### GoResolver Script (Volexity)
|
||||
```bash
|
||||
# Install GoResolver for stripped Go binary function recovery
|
||||
git clone https://github.com/volexity/GoResolver
|
||||
# Run against Ghidra project
|
||||
analyzeHeadless /ghidra_projects MyProject -process go_malware.exe \
|
||||
-postScript GoResolver.java
|
||||
```
|
||||
|
||||
### Ghidra Built-in Go Support (10.3+)
|
||||
```
|
||||
File > Import > Select Go binary
|
||||
Analysis > Auto Analyze (includes GolangAnalyzer)
|
||||
Window > Function Tags > Filter "go."
|
||||
```
|
||||
|
||||
## Go Binary Characteristics
|
||||
|
||||
### Build Info Magic
|
||||
```
|
||||
Offset in .go.buildinfo section: "\xff Go buildinf:"
|
||||
```
|
||||
|
||||
### gopclntab Magic Bytes
|
||||
| Go Version | Magic |
|
||||
|------------|-------|
|
||||
| 1.2-1.15 | `FB FF FF FF 00 00` |
|
||||
| 1.16-1.17 | `FA FF FF FF 00 00` |
|
||||
| 1.18-1.19 | `F0 FF FF FF 00 00` |
|
||||
| 1.20+ | `F1 FF FF FF 00 00` |
|
||||
|
||||
### String Format
|
||||
Go strings are length-prefixed (not null-terminated):
|
||||
```
|
||||
struct GoString {
|
||||
char *ptr; // pointer to string data
|
||||
int64 length; // string length
|
||||
};
|
||||
```
|
||||
|
||||
## Go-Specific Ghidra Scripts
|
||||
|
||||
### GoReSym (Mandiant)
|
||||
```bash
|
||||
GoReSym -t -d -p /path/to/binary
|
||||
# -t: Recover type information
|
||||
# -d: Dump function metadata
|
||||
# -p: Print package listing
|
||||
```
|
||||
|
||||
### redress (Go Reverse Engineering)
|
||||
```bash
|
||||
redress -src binary.exe # Reconstruct source tree
|
||||
redress -pkg binary.exe # List packages
|
||||
redress -type binary.exe # Type information
|
||||
redress -string binary.exe # Go string extraction
|
||||
redress -interface binary.exe # Interface types
|
||||
```
|
||||
|
||||
## Go Obfuscation Tools
|
||||
|
||||
| Tool | Technique | Detection |
|
||||
|------|-----------|-----------|
|
||||
| garble | Function name hashing, literal obfuscation | Hash-like symbols, missing debug info |
|
||||
| gobfuscate | Package/function renaming | Random 8-char names |
|
||||
| go-strip | Symbol table removal | Missing gopclntab entries |
|
||||
|
||||
## Common Go Malware Families
|
||||
|
||||
| Family | Type | Notable Packages |
|
||||
|--------|------|-----------------|
|
||||
| Sliver | C2 implant | protobuf, grpc, mtls |
|
||||
| Merlin | C2 agent | http2, jose, websocket |
|
||||
| Sunlogin/Cobalt | RAT | screenshot, clipboard, keylog |
|
||||
| BianLian | Ransomware | crypto/aes, filepath.Walk |
|
||||
| Royal | Ransomware | goroutine-based parallel encryption |
|
||||
|
||||
## Key Ghidra Analysis Steps
|
||||
```
|
||||
1. Search > For Strings > "go1." (version identification)
|
||||
2. Search > For Bytes > FB FF FF FF (gopclntab)
|
||||
3. Symbol Table > Filter "main." (entry points)
|
||||
4. Navigation > Go To "runtime.main" (program start)
|
||||
5. Decompiler > Check goroutine spawns (runtime.newproc)
|
||||
6. Data Types > Apply GoString struct to string references
|
||||
```
|
||||
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Go malware analysis agent for Ghidra-assisted reverse engineering.
|
||||
|
||||
Analyzes Go binaries to extract function names, strings, build metadata,
|
||||
package information, and detects common Go malware characteristics.
|
||||
"""
|
||||
|
||||
import struct
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import hashlib
|
||||
import re
|
||||
import math
|
||||
from collections import Counter
|
||||
|
||||
|
||||
def compute_hash(filepath):
|
||||
"""Compute SHA-256 hash of file."""
|
||||
sha256 = hashlib.sha256()
|
||||
with open(filepath, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(65536), b""):
|
||||
sha256.update(chunk)
|
||||
return sha256.hexdigest()
|
||||
|
||||
|
||||
def shannon_entropy(data):
|
||||
"""Calculate Shannon entropy."""
|
||||
if not data:
|
||||
return 0.0
|
||||
freq = Counter(data)
|
||||
length = len(data)
|
||||
return -sum((c / length) * math.log2(c / length) for c in freq.values())
|
||||
|
||||
|
||||
def detect_go_binary(filepath):
|
||||
"""Detect if a binary is compiled with Go and extract version info."""
|
||||
with open(filepath, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
indicators = {
|
||||
"is_go_binary": False,
|
||||
"go_version": None,
|
||||
"go_buildinfo": False,
|
||||
"gopclntab_found": False,
|
||||
}
|
||||
|
||||
# Go build info magic
|
||||
buildinfo_magic = b"\xff Go buildinf:"
|
||||
offset = data.find(buildinfo_magic)
|
||||
if offset != -1:
|
||||
indicators["is_go_binary"] = True
|
||||
indicators["go_buildinfo"] = True
|
||||
|
||||
# Go version string
|
||||
version_pattern = rb"go(\d+\.\d+(?:\.\d+)?)"
|
||||
matches = re.findall(version_pattern, data)
|
||||
if matches:
|
||||
indicators["is_go_binary"] = True
|
||||
versions = sorted(set(m.decode() for m in matches))
|
||||
indicators["go_version"] = versions[-1] if versions else None
|
||||
|
||||
# gopclntab (Go PC line table) magic bytes
|
||||
gopclntab_magics = [
|
||||
b"\xfb\xff\xff\xff\x00\x00", # Go 1.2-1.15
|
||||
b"\xfa\xff\xff\xff\x00\x00", # Go 1.16-1.17
|
||||
b"\xf0\xff\xff\xff\x00\x00", # Go 1.18+
|
||||
b"\xf1\xff\xff\xff\x00\x00", # Go 1.20+
|
||||
]
|
||||
for magic in gopclntab_magics:
|
||||
if magic in data:
|
||||
indicators["gopclntab_found"] = True
|
||||
indicators["is_go_binary"] = True
|
||||
break
|
||||
|
||||
# Runtime strings
|
||||
go_strings = [b"runtime.main", b"runtime.goexit", b"runtime.gopanic",
|
||||
b"runtime.newproc", b"GOROOT", b"GOPATH"]
|
||||
found_runtime = sum(1 for s in go_strings if s in data)
|
||||
if found_runtime >= 2:
|
||||
indicators["is_go_binary"] = True
|
||||
indicators["runtime_strings_found"] = found_runtime
|
||||
|
||||
return indicators
|
||||
|
||||
|
||||
def extract_go_strings(filepath, min_length=6):
|
||||
"""Extract Go-style strings (length-prefixed, not null-terminated)."""
|
||||
with open(filepath, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
# Standard ASCII string extraction
|
||||
ascii_pattern = re.compile(rb"[\x20-\x7e]{%d,}" % min_length)
|
||||
strings = [m.group().decode("ascii", errors="replace") for m in ascii_pattern.finditer(data)]
|
||||
return strings
|
||||
|
||||
|
||||
def extract_go_packages(strings_list):
|
||||
"""Identify Go packages from extracted strings."""
|
||||
packages = set()
|
||||
pkg_pattern = re.compile(r"^([a-zA-Z0-9_]+(?:/[a-zA-Z0-9_.-]+)+)\.")
|
||||
for s in strings_list:
|
||||
match = pkg_pattern.match(s)
|
||||
if match:
|
||||
packages.add(match.group(1))
|
||||
# Also look for known Go import paths
|
||||
for s in strings_list:
|
||||
if s.startswith("github.com/") or s.startswith("golang.org/"):
|
||||
parts = s.split("/")
|
||||
if len(parts) >= 3:
|
||||
packages.add("/".join(parts[:3]))
|
||||
return sorted(packages)
|
||||
|
||||
|
||||
SUSPICIOUS_GO_PACKAGES = {
|
||||
"github.com/kbinani/screenshot": "Screen capture capability",
|
||||
"github.com/atotto/clipboard": "Clipboard access",
|
||||
"github.com/go-vgo/robotgo": "Desktop automation / keylogging",
|
||||
"github.com/miekg/dns": "Custom DNS resolution (C2/tunneling)",
|
||||
"golang.org/x/crypto/ssh": "SSH client (lateral movement)",
|
||||
"github.com/shirou/gopsutil": "System enumeration",
|
||||
"github.com/mitchellh/go-ps": "Process listing",
|
||||
"github.com/gobuffalo/packr": "Binary resource embedding",
|
||||
"github.com/Ne0nd0g/merlin": "Merlin C2 agent",
|
||||
"github.com/BishopFox/sliver": "Sliver C2 framework",
|
||||
"github.com/traefik/yaegi": "Go interpreter (dynamic execution)",
|
||||
}
|
||||
|
||||
|
||||
def detect_suspicious_packages(packages):
|
||||
"""Flag suspicious Go packages commonly used in malware."""
|
||||
findings = []
|
||||
for pkg in packages:
|
||||
for sus_pkg, description in SUSPICIOUS_GO_PACKAGES.items():
|
||||
if sus_pkg in pkg:
|
||||
findings.append({"package": pkg, "concern": description})
|
||||
return findings
|
||||
|
||||
|
||||
def analyze_sections(filepath):
|
||||
"""Analyze PE/ELF sections for Go binary characteristics."""
|
||||
with open(filepath, "rb") as f:
|
||||
magic = f.read(4)
|
||||
f.seek(0)
|
||||
data = f.read()
|
||||
|
||||
sections = []
|
||||
if magic[:2] == b"MZ": # PE
|
||||
try:
|
||||
import pefile
|
||||
pe = pefile.PE(data=data)
|
||||
for section in pe.sections:
|
||||
name = section.Name.rstrip(b"\x00").decode("ascii", errors="replace")
|
||||
entropy = section.get_entropy()
|
||||
sections.append({
|
||||
"name": name, "virtual_size": section.Misc_VirtualSize,
|
||||
"raw_size": section.SizeOfRawData, "entropy": round(entropy, 3),
|
||||
})
|
||||
pe.close()
|
||||
except ImportError:
|
||||
sections.append({"note": "pefile not installed"})
|
||||
elif magic[:4] == b"\x7fELF":
|
||||
try:
|
||||
from elftools.elf.elffile import ELFFile
|
||||
from io import BytesIO
|
||||
elf = ELFFile(BytesIO(data))
|
||||
for section in elf.iter_sections():
|
||||
sec_data = section.data() if section.header.sh_size > 0 else b""
|
||||
entropy = shannon_entropy(sec_data) if sec_data else 0
|
||||
sections.append({
|
||||
"name": section.name, "size": section.header.sh_size,
|
||||
"entropy": round(entropy, 3), "type": section.header.sh_type,
|
||||
})
|
||||
except ImportError:
|
||||
sections.append({"note": "pyelftools not installed"})
|
||||
return sections
|
||||
|
||||
|
||||
def detect_obfuscation(go_info, strings_list):
|
||||
"""Detect Go binary obfuscation (garble, gobfuscate)."""
|
||||
indicators = {"obfuscated": False, "techniques": []}
|
||||
|
||||
# Garble replaces function names with hashes
|
||||
hash_names = sum(1 for s in strings_list if re.match(r"^[a-f0-9]{16,}$", s))
|
||||
if hash_names > 20:
|
||||
indicators["obfuscated"] = True
|
||||
indicators["techniques"].append("Possible garble obfuscation (hash-like function names)")
|
||||
|
||||
# Missing gopclntab suggests stripping
|
||||
if not go_info.get("gopclntab_found"):
|
||||
indicators["techniques"].append("gopclntab not found - may be stripped or modified")
|
||||
|
||||
# Low runtime string count
|
||||
if go_info.get("runtime_strings_found", 0) < 2:
|
||||
indicators["obfuscated"] = True
|
||||
indicators["techniques"].append("Low Go runtime string count - possible obfuscation")
|
||||
|
||||
return indicators
|
||||
|
||||
|
||||
def generate_report(filepath):
|
||||
"""Generate comprehensive Go malware analysis report."""
|
||||
report = {
|
||||
"file": filepath,
|
||||
"sha256": compute_hash(filepath),
|
||||
"size": os.path.getsize(filepath),
|
||||
}
|
||||
|
||||
go_info = detect_go_binary(filepath)
|
||||
report["go_detection"] = go_info
|
||||
|
||||
if not go_info["is_go_binary"]:
|
||||
report["conclusion"] = "Not identified as a Go binary"
|
||||
return report
|
||||
|
||||
strings_list = extract_go_strings(filepath)
|
||||
report["total_strings"] = len(strings_list)
|
||||
|
||||
packages = extract_go_packages(strings_list)
|
||||
report["packages"] = packages[:50]
|
||||
|
||||
suspicious = detect_suspicious_packages(packages)
|
||||
report["suspicious_packages"] = suspicious
|
||||
|
||||
sections = analyze_sections(filepath)
|
||||
report["sections"] = sections
|
||||
|
||||
obfuscation = detect_obfuscation(go_info, strings_list)
|
||||
report["obfuscation"] = obfuscation
|
||||
|
||||
return report
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Go Malware Analysis Agent (Ghidra-assisted)")
|
||||
print("Go binary detection, package extraction, obfuscation detection")
|
||||
print("=" * 60)
|
||||
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
|
||||
if not target or not os.path.exists(target):
|
||||
print("\n[DEMO] Usage: python agent.py <go_binary>")
|
||||
sys.exit(0)
|
||||
|
||||
report = generate_report(target)
|
||||
go = report.get("go_detection", {})
|
||||
print(f"\n[*] File: {target}")
|
||||
print(f"[*] SHA-256: {report['sha256']}")
|
||||
print(f"[*] Go binary: {go.get('is_go_binary', False)}")
|
||||
print(f"[*] Go version: {go.get('go_version', 'unknown')}")
|
||||
print(f"[*] Strings: {report.get('total_strings', 0)}")
|
||||
|
||||
print("\n--- Packages ---")
|
||||
for pkg in report.get("packages", [])[:15]:
|
||||
print(f" {pkg}")
|
||||
|
||||
print("\n--- Suspicious Packages ---")
|
||||
for s in report.get("suspicious_packages", []):
|
||||
print(f" [!] {s['package']}: {s['concern']}")
|
||||
|
||||
print("\n--- Obfuscation ---")
|
||||
obf = report.get("obfuscation", {})
|
||||
print(f" Obfuscated: {obf.get('obfuscated', False)}")
|
||||
for t in obf.get("techniques", []):
|
||||
print(f" {t}")
|
||||
|
||||
print(f"\n{json.dumps(report, indent=2, default=str)}")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,120 @@
|
||||
# API Reference: IOC Enrichment Tools
|
||||
|
||||
## VirusTotal API v3
|
||||
|
||||
### File Hash Lookup
|
||||
```bash
|
||||
curl -H "x-apikey: $VT_KEY" \
|
||||
"https://www.virustotal.com/api/v3/files/<sha256>"
|
||||
```
|
||||
|
||||
### Domain Lookup
|
||||
```bash
|
||||
curl -H "x-apikey: $VT_KEY" \
|
||||
"https://www.virustotal.com/api/v3/domains/<domain>"
|
||||
```
|
||||
|
||||
### IP Lookup
|
||||
```bash
|
||||
curl -H "x-apikey: $VT_KEY" \
|
||||
"https://www.virustotal.com/api/v3/ip_addresses/<ip>"
|
||||
```
|
||||
|
||||
### Key Response Fields
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `last_analysis_stats.malicious` | Number of AV engines detecting as malicious |
|
||||
| `last_analysis_stats.undetected` | AV engines finding clean |
|
||||
| `reputation` | Community reputation score |
|
||||
| `popular_threat_classification` | Threat label consensus |
|
||||
|
||||
### Python (vt-py)
|
||||
```python
|
||||
import vt
|
||||
client = vt.Client("API_KEY")
|
||||
file_obj = client.get_object(f"/files/{sha256}")
|
||||
stats = file_obj.last_analysis_stats
|
||||
client.close()
|
||||
```
|
||||
|
||||
## AbuseIPDB API v2
|
||||
|
||||
### Check IP
|
||||
```bash
|
||||
curl -G "https://api.abuseipdb.com/api/v2/check" \
|
||||
-H "Key: $ABUSE_KEY" -H "Accept: application/json" \
|
||||
-d "ipAddress=1.2.3.4" -d "maxAgeInDays=90"
|
||||
```
|
||||
|
||||
### Response Fields
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `abuseConfidenceScore` | 0-100 abuse confidence |
|
||||
| `totalReports` | Report count in timeframe |
|
||||
| `countryCode` | Source country |
|
||||
| `isp` | Internet service provider |
|
||||
| `isTor` | Tor exit node flag |
|
||||
|
||||
## MalwareBazaar API (abuse.ch)
|
||||
|
||||
### Hash Lookup
|
||||
```bash
|
||||
curl -X POST "https://mb-api.abuse.ch/api/v1/" \
|
||||
-d "query=get_info" -d "hash=<sha256>"
|
||||
```
|
||||
|
||||
### Response Fields
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `signature` | Malware family name |
|
||||
| `tags` | Associated tags |
|
||||
| `file_type` | File type identification |
|
||||
| `first_seen` | First submission date |
|
||||
| `reporter` | Submitting analyst |
|
||||
|
||||
## URLScan.io API
|
||||
|
||||
### Submit URL for Scan
|
||||
```bash
|
||||
curl -X POST "https://urlscan.io/api/v1/scan/" \
|
||||
-H "API-Key: $KEY" -H "Content-Type: application/json" \
|
||||
-d '{"url": "http://suspicious.com", "visibility": "private"}'
|
||||
```
|
||||
|
||||
### Retrieve Results
|
||||
```bash
|
||||
curl "https://urlscan.io/api/v1/result/<uuid>/"
|
||||
```
|
||||
|
||||
## Shodan API
|
||||
|
||||
### IP Lookup
|
||||
```bash
|
||||
curl "https://api.shodan.io/shodan/host/<ip>?key=$SHODAN_KEY"
|
||||
```
|
||||
|
||||
### Response Fields
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `ports` | Open ports list |
|
||||
| `os` | Operating system |
|
||||
| `org` | Organization |
|
||||
| `asn` | Autonomous system number |
|
||||
| `hostnames` | Associated hostnames |
|
||||
|
||||
## IOC Confidence Scoring Framework
|
||||
|
||||
| Score | Disposition | Criteria |
|
||||
|-------|-------------|----------|
|
||||
| >= 70 | BLOCK | 15+ VT detections, AbuseIPDB >= 70%, or MalwareBazaar match |
|
||||
| 40-69 | MONITOR | 5-14 VT detections, moderate abuse score |
|
||||
| < 40 | INVESTIGATE | Low detection, no campaign attribution |
|
||||
|
||||
## Defanging Convention
|
||||
|
||||
| Original | Defanged |
|
||||
|----------|----------|
|
||||
| `http://` | `hxxp://` |
|
||||
| `https://` | `hxxps://` |
|
||||
| `.com` | `[.]com` |
|
||||
| `evil.com` | `evil[.]com` |
|
||||
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env python3
|
||||
"""IOC analysis and enrichment agent using VirusTotal, AbuseIPDB, and MalwareBazaar APIs."""
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import hashlib
|
||||
import datetime
|
||||
|
||||
try:
|
||||
import requests
|
||||
HAS_REQUESTS = True
|
||||
except ImportError:
|
||||
HAS_REQUESTS = False
|
||||
|
||||
|
||||
def classify_ioc(value):
|
||||
"""Classify an IOC by type: ipv4, domain, url, sha256, sha1, md5, email."""
|
||||
value = value.strip()
|
||||
if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", value):
|
||||
return "ipv4"
|
||||
if re.match(r"^[a-fA-F0-9]{64}$", value):
|
||||
return "sha256"
|
||||
if re.match(r"^[a-fA-F0-9]{40}$", value):
|
||||
return "sha1"
|
||||
if re.match(r"^[a-fA-F0-9]{32}$", value):
|
||||
return "md5"
|
||||
if re.match(r"^https?://", value):
|
||||
return "url"
|
||||
if re.match(r"^[^@]+@[^@]+\.[^@]+$", value):
|
||||
return "email"
|
||||
if re.match(r"^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", value):
|
||||
return "domain"
|
||||
return "unknown"
|
||||
|
||||
|
||||
def defang_ioc(value):
|
||||
"""Defang an IOC for safe documentation."""
|
||||
value = value.replace("http://", "hxxp://")
|
||||
value = value.replace("https://", "hxxps://")
|
||||
value = re.sub(r"\.(?=\w)", "[.]", value)
|
||||
return value
|
||||
|
||||
|
||||
def refang_ioc(value):
|
||||
"""Refang a defanged IOC for querying APIs."""
|
||||
value = value.replace("hxxp://", "http://")
|
||||
value = value.replace("hxxps://", "https://")
|
||||
value = value.replace("[.]", ".")
|
||||
value = value.replace("[://]", "://")
|
||||
return value
|
||||
|
||||
|
||||
def is_private_ip(ip):
|
||||
"""Check if an IP is RFC 1918 private."""
|
||||
octets = [int(o) for o in ip.split(".")]
|
||||
if octets[0] == 10:
|
||||
return True
|
||||
if octets[0] == 172 and 16 <= octets[1] <= 31:
|
||||
return True
|
||||
if octets[0] == 192 and octets[1] == 168:
|
||||
return True
|
||||
if octets[0] == 127:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def query_virustotal_hash(sha256, api_key):
|
||||
"""Query VirusTotal for a file hash."""
|
||||
url = f"https://www.virustotal.com/api/v3/files/{sha256}"
|
||||
resp = requests.get(url, headers={"x-apikey": api_key})
|
||||
if resp.status_code == 200:
|
||||
data = resp.json().get("data", {}).get("attributes", {})
|
||||
stats = data.get("last_analysis_stats", {})
|
||||
return {
|
||||
"sha256": sha256,
|
||||
"malicious": stats.get("malicious", 0),
|
||||
"total": sum(stats.values()),
|
||||
"type_description": data.get("type_description", ""),
|
||||
"popular_threat_name": data.get("popular_threat_classification", {}).get(
|
||||
"suggested_threat_label", ""),
|
||||
"tags": data.get("tags", []),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def query_virustotal_domain(domain, api_key):
|
||||
"""Query VirusTotal for domain reputation."""
|
||||
url = f"https://www.virustotal.com/api/v3/domains/{domain}"
|
||||
resp = requests.get(url, headers={"x-apikey": api_key})
|
||||
if resp.status_code == 200:
|
||||
data = resp.json().get("data", {}).get("attributes", {})
|
||||
stats = data.get("last_analysis_stats", {})
|
||||
return {
|
||||
"domain": domain,
|
||||
"malicious": stats.get("malicious", 0),
|
||||
"suspicious": stats.get("suspicious", 0),
|
||||
"reputation": data.get("reputation", 0),
|
||||
"registrar": data.get("registrar", ""),
|
||||
"creation_date": data.get("creation_date", ""),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def query_abuseipdb(ip, api_key, max_age_days=90):
|
||||
"""Query AbuseIPDB for IP reputation."""
|
||||
url = "https://api.abuseipdb.com/api/v2/check"
|
||||
resp = requests.get(url, headers={"Key": api_key, "Accept": "application/json"},
|
||||
params={"ipAddress": ip, "maxAgeInDays": max_age_days})
|
||||
if resp.status_code == 200:
|
||||
data = resp.json().get("data", {})
|
||||
return {
|
||||
"ip": ip,
|
||||
"abuse_confidence": data.get("abuseConfidenceScore", 0),
|
||||
"total_reports": data.get("totalReports", 0),
|
||||
"country": data.get("countryCode", ""),
|
||||
"isp": data.get("isp", ""),
|
||||
"domain": data.get("domain", ""),
|
||||
"is_tor": data.get("isTor", False),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def query_malwarebazaar(sha256):
|
||||
"""Query MalwareBazaar for file hash information."""
|
||||
url = "https://mb-api.abuse.ch/api/v1/"
|
||||
resp = requests.post(url, data={"query": "get_info", "hash": sha256})
|
||||
if resp.status_code == 200:
|
||||
result = resp.json()
|
||||
if result.get("query_status") == "ok" and result.get("data"):
|
||||
entry = result["data"][0]
|
||||
return {
|
||||
"sha256": sha256,
|
||||
"signature": entry.get("signature", ""),
|
||||
"tags": entry.get("tags", []),
|
||||
"file_type": entry.get("file_type", ""),
|
||||
"reporter": entry.get("reporter", ""),
|
||||
"first_seen": entry.get("first_seen", ""),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def score_ioc(vt_result=None, abuse_result=None, mb_result=None):
|
||||
"""Assign a confidence score and disposition to an IOC."""
|
||||
score = 0
|
||||
reasons = []
|
||||
if vt_result:
|
||||
malicious = vt_result.get("malicious", 0)
|
||||
if malicious >= 15:
|
||||
score += 40
|
||||
reasons.append(f"VT: {malicious} detections (high)")
|
||||
elif malicious >= 5:
|
||||
score += 20
|
||||
reasons.append(f"VT: {malicious} detections (moderate)")
|
||||
elif malicious > 0:
|
||||
score += 5
|
||||
reasons.append(f"VT: {malicious} detections (low)")
|
||||
if abuse_result:
|
||||
abuse_score = abuse_result.get("abuse_confidence", 0)
|
||||
if abuse_score >= 70:
|
||||
score += 30
|
||||
reasons.append(f"AbuseIPDB: {abuse_score}% confidence")
|
||||
elif abuse_score >= 30:
|
||||
score += 15
|
||||
reasons.append(f"AbuseIPDB: {abuse_score}% confidence")
|
||||
if mb_result:
|
||||
score += 30
|
||||
reasons.append(f"MalwareBazaar: {mb_result.get('signature', 'known malware')}")
|
||||
|
||||
if score >= 70:
|
||||
disposition = "BLOCK"
|
||||
elif score >= 40:
|
||||
disposition = "MONITOR"
|
||||
else:
|
||||
disposition = "INVESTIGATE"
|
||||
|
||||
return {"score": score, "disposition": disposition, "reasons": reasons}
|
||||
|
||||
|
||||
def enrich_ioc(value, vt_key=None, abuse_key=None):
|
||||
"""Enrich a single IOC with multi-source intelligence."""
|
||||
ioc_type = classify_ioc(value)
|
||||
result = {
|
||||
"ioc": value,
|
||||
"type": ioc_type,
|
||||
"defanged": defang_ioc(value),
|
||||
"enrichment": {},
|
||||
"timestamp": datetime.datetime.utcnow().isoformat() + "Z",
|
||||
}
|
||||
if not HAS_REQUESTS:
|
||||
result["error"] = "requests library not installed"
|
||||
return result
|
||||
if ioc_type == "ipv4" and is_private_ip(value):
|
||||
result["note"] = "RFC 1918 private IP - skipping external enrichment"
|
||||
return result
|
||||
if ioc_type in ("sha256", "sha1", "md5") and vt_key:
|
||||
result["enrichment"]["virustotal"] = query_virustotal_hash(value, vt_key)
|
||||
result["enrichment"]["malwarebazaar"] = query_malwarebazaar(value)
|
||||
elif ioc_type == "ipv4":
|
||||
if abuse_key:
|
||||
result["enrichment"]["abuseipdb"] = query_abuseipdb(value, abuse_key)
|
||||
if vt_key:
|
||||
result["enrichment"]["virustotal"] = query_virustotal_domain(value, vt_key)
|
||||
elif ioc_type == "domain" and vt_key:
|
||||
result["enrichment"]["virustotal"] = query_virustotal_domain(value, vt_key)
|
||||
|
||||
scoring = score_ioc(
|
||||
result["enrichment"].get("virustotal"),
|
||||
result["enrichment"].get("abuseipdb"),
|
||||
result["enrichment"].get("malwarebazaar"),
|
||||
)
|
||||
result["score"] = scoring["score"]
|
||||
result["disposition"] = scoring["disposition"]
|
||||
result["reasons"] = scoring["reasons"]
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("IOC Analysis & Enrichment Agent")
|
||||
print("VirusTotal, AbuseIPDB, MalwareBazaar integration")
|
||||
print("=" * 60)
|
||||
|
||||
demo_iocs = [
|
||||
"185.220.101.42",
|
||||
"evil-domain.com",
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"http://malicious-site.com/payload.exe",
|
||||
"192.168.1.100",
|
||||
]
|
||||
|
||||
print("\n--- IOC Classification & Defanging ---")
|
||||
for ioc in demo_iocs:
|
||||
ioc_type = classify_ioc(ioc)
|
||||
defanged = defang_ioc(ioc)
|
||||
private = " (private)" if ioc_type == "ipv4" and is_private_ip(ioc) else ""
|
||||
print(f" {ioc_type:8s} | {defanged}{private}")
|
||||
|
||||
vt_key = os.environ.get("VT_API_KEY")
|
||||
abuse_key = os.environ.get("ABUSEIPDB_API_KEY")
|
||||
|
||||
if vt_key or abuse_key:
|
||||
print("\n--- Enrichment (live API queries) ---")
|
||||
for ioc in demo_iocs:
|
||||
result = enrich_ioc(ioc, vt_key, abuse_key)
|
||||
print(f"\n {result['ioc']} ({result['type']})")
|
||||
print(f" Disposition: {result.get('disposition', 'N/A')} "
|
||||
f"(score: {result.get('score', 0)})")
|
||||
for reason in result.get("reasons", []):
|
||||
print(f" - {reason}")
|
||||
else:
|
||||
print("\n[*] Set VT_API_KEY and/or ABUSEIPDB_API_KEY environment variables for live enrichment.")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,105 @@
|
||||
# API Reference: iOS App Security with Objection
|
||||
|
||||
## Objection CLI
|
||||
|
||||
### Launch
|
||||
```bash
|
||||
objection -g com.example.app explore # Attach to running app
|
||||
objection -g com.example.app explore -s "command" # Run startup command
|
||||
objection patchipa --source app.ipa # Patch IPA with Frida gadget
|
||||
```
|
||||
|
||||
### Keychain & Data Storage
|
||||
```bash
|
||||
ios keychain dump # Dump keychain items
|
||||
ios keychain dump --json # JSON output
|
||||
ios cookies get # List HTTP cookies
|
||||
ios nsuserdefaults get # Read NSUserDefaults
|
||||
ios plist cat Info.plist # Read plist file
|
||||
```
|
||||
|
||||
### SSL Pinning
|
||||
```bash
|
||||
ios sslpinning disable # Bypass SSL pinning
|
||||
ios sslpinning disable --quiet # Quiet mode
|
||||
```
|
||||
|
||||
### Jailbreak Detection
|
||||
```bash
|
||||
ios jailbreak disable # Bypass jailbreak detection
|
||||
ios jailbreak simulate # Simulate jailbroken device
|
||||
```
|
||||
|
||||
### Hooking
|
||||
```bash
|
||||
ios hooking list classes # List all classes
|
||||
ios hooking list classes --include Auth # Filter classes
|
||||
ios hooking list class_methods ClassName # List methods
|
||||
ios hooking watch method "-[Class method]" # Watch method calls
|
||||
ios hooking set return_value "-[Class isJB]" false # Override return
|
||||
```
|
||||
|
||||
### Filesystem
|
||||
```bash
|
||||
ls / # List app sandbox root
|
||||
ls /Documents # List Documents directory
|
||||
file download /path/to/file local.out # Download file
|
||||
file upload local.file /remote/path # Upload file
|
||||
```
|
||||
|
||||
### Memory
|
||||
```bash
|
||||
memory dump all dump.bin # Dump all memory
|
||||
memory search "password" # Search memory for string
|
||||
memory list modules # List loaded modules
|
||||
memory list exports libModule.dylib # List module exports
|
||||
```
|
||||
|
||||
## Frida CLI
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
frida -U -n AppName # Attach by name
|
||||
frida -U -f com.app.id # Spawn and attach
|
||||
frida -U -n AppName -l script.js # Load script
|
||||
frida-ps -U # List running processes
|
||||
frida-ls-devices # List connected devices
|
||||
```
|
||||
|
||||
### Common Frida Scripts
|
||||
```javascript
|
||||
// Hook method and log arguments
|
||||
ObjC.choose(ObjC.classes.ClassName, {
|
||||
onMatch: function(instance) {
|
||||
Interceptor.attach(instance['- methodName:'].implementation, {
|
||||
onEnter: function(args) {
|
||||
console.log('arg1:', ObjC.Object(args[2]));
|
||||
}
|
||||
});
|
||||
}, onComplete: function() {}
|
||||
});
|
||||
```
|
||||
|
||||
## OWASP Mobile Top 10 (2024)
|
||||
|
||||
| ID | Category | Objection Check |
|
||||
|----|----------|-----------------|
|
||||
| M1 | Improper Credential Usage | `ios keychain dump` |
|
||||
| M2 | Inadequate Supply Chain Security | Binary analysis |
|
||||
| M3 | Insecure Authentication | Hook auth classes |
|
||||
| M4 | Insufficient Input/Output Validation | Hook input methods |
|
||||
| M5 | Insecure Communication | `ios sslpinning disable` |
|
||||
| M6 | Inadequate Privacy Controls | `ios nsuserdefaults get` |
|
||||
| M7 | Insufficient Binary Protections | Check PIE, ARC, stack canary |
|
||||
| M8 | Security Misconfiguration | `ios plist cat Info.plist` |
|
||||
| M9 | Insecure Data Storage | Filesystem + keychain review |
|
||||
| M10 | Insufficient Cryptography | Hook crypto classes |
|
||||
|
||||
## iOS App Sandbox Paths
|
||||
| Path | Contents |
|
||||
|------|----------|
|
||||
| `/Documents` | User-generated data |
|
||||
| `/Library/Caches` | Cached data |
|
||||
| `/Library/Preferences` | Plist settings |
|
||||
| `/tmp` | Temporary files |
|
||||
| `/Library/Cookies` | Cookie storage |
|
||||
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""iOS app security analysis agent using Objection/Frida concepts.
|
||||
|
||||
Performs runtime security assessment of iOS apps including SSL pinning bypass,
|
||||
keychain dumping, filesystem inspection, and jailbreak detection bypass.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
|
||||
|
||||
def run_objection(command, app_id=None, timeout=30):
|
||||
"""Execute an Objection command against a target app."""
|
||||
cmd = ["objection"]
|
||||
if app_id:
|
||||
cmd.extend(["-g", app_id])
|
||||
cmd.extend(["explore", "-c", command])
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
||||
return result.stdout, result.returncode
|
||||
except FileNotFoundError:
|
||||
return "objection not installed (pip install objection)", 1
|
||||
except subprocess.TimeoutExpired:
|
||||
return "Command timed out", 1
|
||||
|
||||
|
||||
def run_frida(script_code, app_id, timeout=30):
|
||||
"""Execute a Frida script against target app."""
|
||||
cmd = ["frida", "-U", "-n", app_id, "-e", script_code]
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
||||
return result.stdout, result.returncode
|
||||
except FileNotFoundError:
|
||||
return "frida not installed (pip install frida-tools)", 1
|
||||
except subprocess.TimeoutExpired:
|
||||
return "Command timed out", 1
|
||||
|
||||
|
||||
def dump_keychain(app_id):
|
||||
"""Dump keychain items accessible by the application."""
|
||||
return run_objection("ios keychain dump", app_id)
|
||||
|
||||
|
||||
def dump_cookies(app_id):
|
||||
"""Dump HTTP cookies stored by the application."""
|
||||
return run_objection("ios cookies get", app_id)
|
||||
|
||||
|
||||
def list_classes(app_id, filter_str=None):
|
||||
"""List Objective-C classes loaded in the app."""
|
||||
cmd = "ios hooking list classes"
|
||||
if filter_str:
|
||||
cmd += f" --include {filter_str}"
|
||||
return run_objection(cmd, app_id)
|
||||
|
||||
|
||||
def check_ssl_pinning(app_id):
|
||||
"""Check and bypass SSL certificate pinning."""
|
||||
return run_objection("ios sslpinning disable", app_id)
|
||||
|
||||
|
||||
def check_jailbreak_detection(app_id):
|
||||
"""Check for and bypass jailbreak detection."""
|
||||
return run_objection("ios jailbreak disable", app_id)
|
||||
|
||||
|
||||
def inspect_filesystem(app_id, path="/"):
|
||||
"""Inspect the application's filesystem sandbox."""
|
||||
return run_objection(f"ls {path}", app_id)
|
||||
|
||||
|
||||
def dump_plist(app_id):
|
||||
"""Dump application plist configuration files."""
|
||||
return run_objection("ios plist cat Info.plist", app_id)
|
||||
|
||||
|
||||
def check_pasteboard(app_id):
|
||||
"""Check pasteboard/clipboard for sensitive data."""
|
||||
return run_objection("ios pasteboard monitor", app_id)
|
||||
|
||||
|
||||
def search_binary_strings(app_id, pattern):
|
||||
"""Search for strings in the app binary."""
|
||||
return run_objection(f"memory search '{pattern}'", app_id)
|
||||
|
||||
|
||||
OWASP_MOBILE_CHECKS = {
|
||||
"M1_Improper_Platform_Usage": {
|
||||
"checks": ["ios keychain dump", "ios plist cat Info.plist"],
|
||||
"description": "Check for misuse of platform security features",
|
||||
},
|
||||
"M2_Insecure_Data_Storage": {
|
||||
"checks": ["ios keychain dump", "ios cookies get", "ios nsuserdefaults get"],
|
||||
"description": "Check for sensitive data in insecure storage",
|
||||
},
|
||||
"M3_Insecure_Communication": {
|
||||
"checks": ["ios sslpinning disable"],
|
||||
"description": "Test SSL/TLS implementation and certificate pinning",
|
||||
},
|
||||
"M4_Insecure_Authentication": {
|
||||
"checks": ["ios hooking list classes --include Auth",
|
||||
"ios hooking list classes --include Login"],
|
||||
"description": "Analyze authentication mechanisms",
|
||||
},
|
||||
"M5_Insufficient_Cryptography": {
|
||||
"checks": ["ios hooking list classes --include Crypto",
|
||||
"ios hooking list classes --include AES"],
|
||||
"description": "Review cryptographic implementations",
|
||||
},
|
||||
"M8_Code_Tampering": {
|
||||
"checks": ["ios jailbreak disable"],
|
||||
"description": "Test runtime integrity and jailbreak detection",
|
||||
},
|
||||
"M9_Reverse_Engineering": {
|
||||
"checks": ["ios hooking list classes"],
|
||||
"description": "Assess reverse engineering protections",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def run_owasp_assessment(app_id):
|
||||
"""Run OWASP Mobile Top 10 security checks."""
|
||||
results = {}
|
||||
for category, config in OWASP_MOBILE_CHECKS.items():
|
||||
category_results = {"description": config["description"], "findings": []}
|
||||
for check in config["checks"]:
|
||||
output, rc = run_objection(check, app_id)
|
||||
category_results["findings"].append({
|
||||
"command": check,
|
||||
"status": "success" if rc == 0 else "failed",
|
||||
"output_preview": output[:200] if output else "",
|
||||
})
|
||||
results[category] = category_results
|
||||
return results
|
||||
|
||||
|
||||
FRIDA_SCRIPTS = {
|
||||
"ssl_pinning_bypass": """
|
||||
ObjC.choose(ObjC.classes.NSURLSessionConfiguration, {
|
||||
onMatch: function(instance) {
|
||||
instance['- setTLSMinimumSupportedProtocol:'](0);
|
||||
}, onComplete: function() {}
|
||||
});
|
||||
""",
|
||||
"jailbreak_bypass": """
|
||||
var paths = ['/Applications/Cydia.app', '/usr/sbin/sshd', '/etc/apt'];
|
||||
Interceptor.attach(ObjC.classes.NSFileManager['- fileExistsAtPath:'].implementation, {
|
||||
onEnter: function(args) { this.path = ObjC.Object(args[2]).toString(); },
|
||||
onLeave: function(retval) {
|
||||
if (paths.some(p => this.path.includes(p))) retval.replace(0);
|
||||
}
|
||||
});
|
||||
""",
|
||||
"keychain_dump": """
|
||||
var kSecClass = ObjC.classes.__NSDictionary.dictionaryWithObject_forKey_(
|
||||
ObjC.classes.__NSCFConstantString.alloc().initWithUTF8String_('genp'),
|
||||
ObjC.classes.__NSCFConstantString.alloc().initWithUTF8String_('class')
|
||||
);
|
||||
console.log('Keychain query prepared');
|
||||
""",
|
||||
}
|
||||
|
||||
|
||||
def generate_report(app_id, assessment_results):
|
||||
"""Generate iOS security assessment report."""
|
||||
findings_count = sum(
|
||||
len(cat["findings"]) for cat in assessment_results.values()
|
||||
)
|
||||
return {
|
||||
"app_identifier": app_id,
|
||||
"assessment_framework": "OWASP Mobile Top 10",
|
||||
"categories_tested": len(assessment_results),
|
||||
"total_checks": findings_count,
|
||||
"results": assessment_results,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("iOS App Security Analysis Agent (Objection/Frida)")
|
||||
print("Runtime analysis, SSL bypass, keychain dump, OWASP checks")
|
||||
print("=" * 60)
|
||||
|
||||
app_id = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
|
||||
if not app_id:
|
||||
print("\n[DEMO] Usage: python agent.py <app_bundle_id>")
|
||||
print(" e.g. python agent.py com.example.app")
|
||||
print("\nAvailable checks:")
|
||||
for category, config in OWASP_MOBILE_CHECKS.items():
|
||||
print(f" {category}: {config['description']}")
|
||||
print("\nFrida scripts available:")
|
||||
for name in FRIDA_SCRIPTS:
|
||||
print(f" {name}")
|
||||
sys.exit(0)
|
||||
|
||||
print(f"\n[*] Target: {app_id}")
|
||||
print("[*] Running OWASP Mobile Top 10 assessment...")
|
||||
|
||||
results = run_owasp_assessment(app_id)
|
||||
report = generate_report(app_id, results)
|
||||
|
||||
for category, data in results.items():
|
||||
status_counts = {"success": 0, "failed": 0}
|
||||
for f in data["findings"]:
|
||||
status_counts[f["status"]] += 1
|
||||
print(f"\n [{category}] {data['description']}")
|
||||
print(f" Checks: {status_counts['success']} passed, {status_counts['failed']} failed")
|
||||
|
||||
print(f"\n{json.dumps(report, indent=2, default=str)}")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: analyzing-kubernetes-audit-logs
|
||||
description: >
|
||||
Parses Kubernetes API server audit logs (JSON lines) to detect exec-into-pod, secret
|
||||
access, RBAC modifications, privileged pod creation, and anonymous API access. Builds
|
||||
threat detection rules from audit event patterns. Use when investigating Kubernetes
|
||||
cluster compromise or building k8s-specific SIEM detection rules.
|
||||
---
|
||||
|
||||
# Analyzing Kubernetes Audit Logs
|
||||
|
||||
## Instructions
|
||||
|
||||
Parse Kubernetes audit log files (JSON lines format) to detect security-relevant
|
||||
events including unauthorized access, privilege escalation, and data exfiltration.
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
with open("/var/log/kubernetes/audit.log") as f:
|
||||
for line in f:
|
||||
event = json.loads(line)
|
||||
verb = event.get("verb")
|
||||
resource = event.get("objectRef", {}).get("resource")
|
||||
user = event.get("user", {}).get("username")
|
||||
if verb == "create" and resource == "pods/exec":
|
||||
print(f"Pod exec by {user}")
|
||||
```
|
||||
|
||||
Key events to detect:
|
||||
1. pods/exec and pods/attach (shell into containers)
|
||||
2. secrets access (get/list/watch)
|
||||
3. clusterrolebindings creation (RBAC escalation)
|
||||
4. Privileged pod creation
|
||||
5. Anonymous or system:unauthenticated access
|
||||
|
||||
## Examples
|
||||
|
||||
```python
|
||||
# Detect secret enumeration
|
||||
if verb in ("get", "list") and resource == "secrets":
|
||||
print(f"Secret access: {user} -> {event['objectRef'].get('name')}")
|
||||
```
|
||||
@@ -0,0 +1,57 @@
|
||||
# API Reference: Analyzing Kubernetes Audit Logs
|
||||
|
||||
## Audit Log Format (JSON Lines)
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "Event",
|
||||
"apiVersion": "audit.k8s.io/v1",
|
||||
"level": "RequestResponse",
|
||||
"verb": "create",
|
||||
"user": {"username": "admin", "groups": ["system:masters"]},
|
||||
"sourceIPs": ["10.0.0.5"],
|
||||
"objectRef": {
|
||||
"resource": "pods",
|
||||
"subresource": "exec",
|
||||
"namespace": "default",
|
||||
"name": "web-pod"
|
||||
},
|
||||
"responseStatus": {"code": 200},
|
||||
"requestReceivedTimestamp": "2025-03-15T14:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Security-Critical Audit Events
|
||||
|
||||
| Event | objectRef | Severity |
|
||||
|-------|-----------|----------|
|
||||
| Pod exec | `resource: pods, subresource: exec` | HIGH |
|
||||
| Secret access | `resource: secrets, verb: get/list` | HIGH |
|
||||
| RBAC change | `resource: clusterrolebindings` | CRITICAL |
|
||||
| Privileged pod | `requestObject.spec.containers[].securityContext.privileged` | CRITICAL |
|
||||
| Anonymous access | `user.username: system:anonymous` | CRITICAL |
|
||||
|
||||
## Audit Policy Levels
|
||||
|
||||
| Level | Captures |
|
||||
|-------|----------|
|
||||
| None | No logging |
|
||||
| Metadata | Timestamp, user, verb, resource |
|
||||
| Request | Metadata + request body |
|
||||
| RequestResponse | Request + response body |
|
||||
|
||||
## Python Parsing
|
||||
|
||||
```python
|
||||
import json
|
||||
with open("audit.log") as f:
|
||||
for line in f:
|
||||
event = json.loads(line)
|
||||
print(event["verb"], event["objectRef"]["resource"])
|
||||
```
|
||||
|
||||
### References
|
||||
|
||||
- K8s Auditing: https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/
|
||||
- Audit policy: https://kubernetes.io/docs/reference/config-api/apiserver-audit.v1/
|
||||
- Datadog k8s audit: https://www.datadoghq.com/blog/monitor-kubernetes-audit-logs/
|
||||
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Agent for analyzing Kubernetes audit logs for security threats."""
|
||||
|
||||
import os
|
||||
import json
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def parse_audit_log(log_path):
|
||||
"""Parse Kubernetes audit log file (JSON lines format)."""
|
||||
events = []
|
||||
with open(log_path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
events.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return events
|
||||
|
||||
|
||||
def detect_pod_exec(events):
|
||||
"""Detect kubectl exec and attach events (shell access to pods)."""
|
||||
findings = []
|
||||
for event in events:
|
||||
obj_ref = event.get("objectRef", {})
|
||||
subresource = obj_ref.get("subresource", "")
|
||||
if subresource in ("exec", "attach"):
|
||||
findings.append({
|
||||
"timestamp": event.get("requestReceivedTimestamp", ""),
|
||||
"user": event.get("user", {}).get("username", ""),
|
||||
"groups": event.get("user", {}).get("groups", []),
|
||||
"verb": event.get("verb", ""),
|
||||
"namespace": obj_ref.get("namespace", ""),
|
||||
"pod": obj_ref.get("name", ""),
|
||||
"subresource": subresource,
|
||||
"source_ip": event.get("sourceIPs", [""])[0],
|
||||
"severity": "HIGH",
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def detect_secret_access(events):
|
||||
"""Detect access to Kubernetes secrets."""
|
||||
findings = []
|
||||
for event in events:
|
||||
obj_ref = event.get("objectRef", {})
|
||||
if obj_ref.get("resource") != "secrets":
|
||||
continue
|
||||
verb = event.get("verb", "")
|
||||
if verb not in ("get", "list", "watch", "create", "update", "delete"):
|
||||
continue
|
||||
findings.append({
|
||||
"timestamp": event.get("requestReceivedTimestamp", ""),
|
||||
"user": event.get("user", {}).get("username", ""),
|
||||
"verb": verb,
|
||||
"namespace": obj_ref.get("namespace", ""),
|
||||
"secret_name": obj_ref.get("name", ""),
|
||||
"source_ip": event.get("sourceIPs", [""])[0],
|
||||
"severity": "HIGH" if verb in ("list", "delete") else "MEDIUM",
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def detect_rbac_changes(events):
|
||||
"""Detect RBAC role and binding modifications."""
|
||||
rbac_resources = {"clusterroles", "clusterrolebindings", "roles", "rolebindings"}
|
||||
findings = []
|
||||
for event in events:
|
||||
obj_ref = event.get("objectRef", {})
|
||||
resource = obj_ref.get("resource", "")
|
||||
verb = event.get("verb", "")
|
||||
if resource in rbac_resources and verb in ("create", "update", "patch", "delete"):
|
||||
findings.append({
|
||||
"timestamp": event.get("requestReceivedTimestamp", ""),
|
||||
"user": event.get("user", {}).get("username", ""),
|
||||
"verb": verb,
|
||||
"resource": resource,
|
||||
"name": obj_ref.get("name", ""),
|
||||
"namespace": obj_ref.get("namespace", ""),
|
||||
"source_ip": event.get("sourceIPs", [""])[0],
|
||||
"severity": "CRITICAL" if "cluster" in resource else "HIGH",
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def detect_privileged_pods(events):
|
||||
"""Detect creation of privileged pods."""
|
||||
findings = []
|
||||
for event in events:
|
||||
if event.get("verb") != "create":
|
||||
continue
|
||||
obj_ref = event.get("objectRef", {})
|
||||
if obj_ref.get("resource") != "pods":
|
||||
continue
|
||||
request_obj = event.get("requestObject", {})
|
||||
spec = request_obj.get("spec", {})
|
||||
containers = spec.get("containers", [])
|
||||
for container in containers:
|
||||
sc = container.get("securityContext", {})
|
||||
if sc.get("privileged"):
|
||||
findings.append({
|
||||
"timestamp": event.get("requestReceivedTimestamp", ""),
|
||||
"user": event.get("user", {}).get("username", ""),
|
||||
"namespace": obj_ref.get("namespace", ""),
|
||||
"pod": obj_ref.get("name", ""),
|
||||
"container": container.get("name", ""),
|
||||
"severity": "CRITICAL",
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def detect_anonymous_access(events):
|
||||
"""Detect API access by anonymous or unauthenticated users."""
|
||||
findings = []
|
||||
anon_users = {"system:anonymous", "system:unauthenticated"}
|
||||
for event in events:
|
||||
user = event.get("user", {}).get("username", "")
|
||||
groups = event.get("user", {}).get("groups", [])
|
||||
if user in anon_users or "system:unauthenticated" in groups:
|
||||
status_code = event.get("responseStatus", {}).get("code", 0)
|
||||
if status_code < 400:
|
||||
findings.append({
|
||||
"timestamp": event.get("requestReceivedTimestamp", ""),
|
||||
"user": user,
|
||||
"verb": event.get("verb", ""),
|
||||
"resource": event.get("objectRef", {}).get("resource", ""),
|
||||
"source_ip": event.get("sourceIPs", [""])[0],
|
||||
"status_code": status_code,
|
||||
"severity": "CRITICAL",
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def detect_forbidden_surge(events, threshold=20):
|
||||
"""Detect 403 surges indicating enumeration or brute force."""
|
||||
user_forbidden = defaultdict(int)
|
||||
for event in events:
|
||||
if event.get("responseStatus", {}).get("code") == 403:
|
||||
user = event.get("user", {}).get("username", "")
|
||||
user_forbidden[user] += 1
|
||||
surges = []
|
||||
for user, count in user_forbidden.items():
|
||||
if count >= threshold:
|
||||
surges.append({"user": user, "forbidden_count": count, "severity": "MEDIUM"})
|
||||
return sorted(surges, key=lambda x: x["forbidden_count"], reverse=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Kubernetes Audit Log Analyzer")
|
||||
parser.add_argument("--audit-log", required=True, help="Path to audit log file")
|
||||
parser.add_argument("--output", default="k8s_audit_report.json")
|
||||
parser.add_argument("--action", choices=[
|
||||
"exec", "secrets", "rbac", "privileged", "anonymous", "full_analysis"
|
||||
], default="full_analysis")
|
||||
args = parser.parse_args()
|
||||
|
||||
events = parse_audit_log(args.audit_log)
|
||||
report = {"log_file": args.audit_log, "total_events": len(events),
|
||||
"generated_at": datetime.utcnow().isoformat(), "findings": {}}
|
||||
print(f"[+] Parsed {len(events)} audit events")
|
||||
|
||||
if args.action in ("exec", "full_analysis"):
|
||||
findings = detect_pod_exec(events)
|
||||
report["findings"]["pod_exec"] = findings
|
||||
print(f"[+] Pod exec/attach events: {len(findings)}")
|
||||
|
||||
if args.action in ("secrets", "full_analysis"):
|
||||
findings = detect_secret_access(events)
|
||||
report["findings"]["secret_access"] = findings
|
||||
print(f"[+] Secret access events: {len(findings)}")
|
||||
|
||||
if args.action in ("rbac", "full_analysis"):
|
||||
findings = detect_rbac_changes(events)
|
||||
report["findings"]["rbac_changes"] = findings
|
||||
print(f"[+] RBAC changes: {len(findings)}")
|
||||
|
||||
if args.action in ("privileged", "full_analysis"):
|
||||
findings = detect_privileged_pods(events)
|
||||
report["findings"]["privileged_pods"] = findings
|
||||
print(f"[+] Privileged pod creation: {len(findings)}")
|
||||
|
||||
if args.action in ("anonymous", "full_analysis"):
|
||||
findings = detect_anonymous_access(events)
|
||||
report["findings"]["anonymous_access"] = findings
|
||||
print(f"[+] Anonymous access events: {len(findings)}")
|
||||
|
||||
forbidden = detect_forbidden_surge(events)
|
||||
report["findings"]["forbidden_surges"] = forbidden
|
||||
print(f"[+] 403 surges: {len(forbidden)}")
|
||||
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2, default=str)
|
||||
print(f"[+] Report saved to {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,119 @@
|
||||
# API Reference: Linux ELF Malware Analysis Tools
|
||||
|
||||
## readelf - ELF Binary Inspection
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
readelf -h <binary> # ELF header
|
||||
readelf -S <binary> # Section headers
|
||||
readelf -l <binary> # Program headers (segments)
|
||||
readelf -s <binary> # Symbol table
|
||||
readelf -d <binary> # Dynamic section
|
||||
readelf -r <binary> # Relocation entries
|
||||
readelf -n <binary> # Notes section
|
||||
```
|
||||
|
||||
### Key ELF Header Fields
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `Class` | 32-bit or 64-bit |
|
||||
| `Machine` | Architecture (x86-64, ARM, MIPS) |
|
||||
| `Type` | EXEC (executable), DYN (shared object) |
|
||||
| `Entry point` | Code execution start address |
|
||||
|
||||
## pyelftools - Python ELF Parsing
|
||||
|
||||
### Usage
|
||||
```python
|
||||
from elftools.elf.elffile import ELFFile
|
||||
|
||||
with open("binary", "rb") as f:
|
||||
elf = ELFFile(f)
|
||||
elf.elfclass # 32 or 64
|
||||
elf.little_endian # True/False
|
||||
elf.header.e_machine # Architecture
|
||||
elf.header.e_entry # Entry point
|
||||
elf.num_sections() # Section count
|
||||
elf.get_section_by_name(".symtab") # Symbol table
|
||||
```
|
||||
|
||||
## strings - String Extraction
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
strings <binary> # ASCII strings (default min 4)
|
||||
strings -n 8 <binary> # Minimum 8 characters
|
||||
strings -e l <binary> # 16-bit little-endian (Unicode)
|
||||
strings -t x <binary> # Print offset in hex
|
||||
```
|
||||
|
||||
## strace - System Call Tracing
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
strace -f ./binary # Follow forks
|
||||
strace -e trace=network ./binary # Network calls only
|
||||
strace -e trace=file ./binary # File operations only
|
||||
strace -e trace=process ./binary # Process operations
|
||||
strace -o output.txt ./binary # Log to file
|
||||
strace -c ./binary # Summary statistics
|
||||
```
|
||||
|
||||
### Key System Calls
|
||||
| Call | Category |
|
||||
|------|----------|
|
||||
| `socket`, `connect`, `bind` | Network |
|
||||
| `fork`, `execve`, `clone` | Process |
|
||||
| `open`, `read`, `write`, `unlink` | File I/O |
|
||||
| `ptrace` | Anti-debug/injection |
|
||||
|
||||
## ltrace - Library Call Tracing
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
ltrace -f ./binary # Follow child processes
|
||||
ltrace -e malloc+free ./binary # Specific functions
|
||||
ltrace -o output.txt ./binary # Log to file
|
||||
```
|
||||
|
||||
## GDB - GNU Debugger
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
gdb ./binary
|
||||
(gdb) break main
|
||||
(gdb) break *0x400580 # Break at address
|
||||
(gdb) run
|
||||
(gdb) info registers
|
||||
(gdb) x/20s $rdi # Examine string at RDI
|
||||
(gdb) x/10i $rip # Disassemble at RIP
|
||||
(gdb) bt # Backtrace
|
||||
```
|
||||
|
||||
## UPX - Packer Detection/Unpacking
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
upx -t <binary> # Test if packed
|
||||
upx -d <binary> # Decompress/unpack
|
||||
upx -l <binary> # List compression details
|
||||
```
|
||||
|
||||
## objdump - Disassembly
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
objdump -d <binary> # Disassemble .text
|
||||
objdump -D <binary> # Disassemble all sections
|
||||
objdump -M intel -d <binary> # Intel syntax
|
||||
objdump -t <binary> # Symbol table
|
||||
```
|
||||
|
||||
## nm - Symbol Listing
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
nm <binary> # List symbols
|
||||
nm -D <binary> # Dynamic symbols only
|
||||
nm -u <binary> # Undefined (imported) symbols
|
||||
```
|
||||
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Linux ELF malware static analysis agent using pyelftools and binary inspection."""
|
||||
|
||||
import hashlib
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import struct
|
||||
from collections import Counter
|
||||
|
||||
try:
|
||||
from elftools.elf.elffile import ELFFile
|
||||
from elftools.elf.sections import SymbolTableSection
|
||||
HAS_ELFTOOLS = True
|
||||
except ImportError:
|
||||
HAS_ELFTOOLS = False
|
||||
|
||||
|
||||
def compute_hashes(filepath):
|
||||
"""Compute MD5, SHA1, and SHA256 hashes of a file."""
|
||||
md5 = hashlib.md5()
|
||||
sha1 = hashlib.sha1()
|
||||
sha256 = hashlib.sha256()
|
||||
with open(filepath, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(65536), b""):
|
||||
md5.update(chunk)
|
||||
sha1.update(chunk)
|
||||
sha256.update(chunk)
|
||||
return {"md5": md5.hexdigest(), "sha1": sha1.hexdigest(), "sha256": sha256.hexdigest()}
|
||||
|
||||
|
||||
def calculate_entropy(data):
|
||||
"""Calculate Shannon entropy of binary data."""
|
||||
if not data:
|
||||
return 0.0
|
||||
counter = Counter(data)
|
||||
length = len(data)
|
||||
return -sum((c / length) * math.log2(c / length) for c in counter.values())
|
||||
|
||||
|
||||
def analyze_elf_header(filepath):
|
||||
"""Parse ELF header and extract key properties."""
|
||||
if not HAS_ELFTOOLS:
|
||||
return {"error": "pyelftools not installed: pip install pyelftools"}
|
||||
with open(filepath, "rb") as f:
|
||||
elf = ELFFile(f)
|
||||
symtab = elf.get_section_by_name(".symtab")
|
||||
info = {
|
||||
"class": f"{elf.elfclass}-bit",
|
||||
"endian": "Little" if elf.little_endian else "Big",
|
||||
"machine": elf.header.e_machine,
|
||||
"type": elf.header.e_type,
|
||||
"entry_point": f"0x{elf.header.e_entry:X}",
|
||||
"stripped": symtab is None,
|
||||
"num_sections": elf.num_sections(),
|
||||
"num_segments": elf.num_segments(),
|
||||
}
|
||||
return info
|
||||
|
||||
|
||||
def analyze_sections(filepath):
|
||||
"""Analyze ELF sections for entropy and suspicious characteristics."""
|
||||
if not HAS_ELFTOOLS:
|
||||
return []
|
||||
sections = []
|
||||
with open(filepath, "rb") as f:
|
||||
elf = ELFFile(f)
|
||||
for section in elf.iter_sections():
|
||||
data = section.data()
|
||||
if len(data) == 0:
|
||||
continue
|
||||
entropy = calculate_entropy(data)
|
||||
sections.append({
|
||||
"name": section.name,
|
||||
"type": section["sh_type"],
|
||||
"size": len(data),
|
||||
"entropy": round(entropy, 4),
|
||||
"high_entropy": entropy > 7.0,
|
||||
"flags": section["sh_flags"],
|
||||
})
|
||||
return sections
|
||||
|
||||
|
||||
def extract_strings(filepath, min_length=6):
|
||||
"""Extract ASCII strings from the binary and categorize by type."""
|
||||
stdout, _, rc = subprocess.run(
|
||||
f"strings -n {min_length} {filepath}", shell=True,
|
||||
capture_output=True, text=True
|
||||
).stdout, "", 0
|
||||
if not stdout:
|
||||
return {}
|
||||
all_strings = stdout.strip().splitlines()
|
||||
categorized = {
|
||||
"urls": [], "ips": [], "domains": [], "shell_commands": [],
|
||||
"crypto_mining": [], "persistence": [], "ssh_related": [],
|
||||
"total": len(all_strings),
|
||||
}
|
||||
for s in all_strings:
|
||||
s_lower = s.lower()
|
||||
if any(proto in s_lower for proto in ["http://", "https://", "ftp://"]):
|
||||
categorized["urls"].append(s)
|
||||
if any(p in s_lower for p in ["stratum", "xmr", "monero", "pool.", "mining"]):
|
||||
categorized["crypto_mining"].append(s)
|
||||
if any(p in s_lower for p in ["crontab", "systemd", "init.d", "rc.local",
|
||||
"ld.so.preload", "systemctl"]):
|
||||
categorized["persistence"].append(s)
|
||||
if any(p in s_lower for p in ["ssh", "authorized_keys", "id_rsa", "shadow", "passwd"]):
|
||||
categorized["ssh_related"].append(s)
|
||||
if any(p in s_lower for p in ["bash", "wget", "curl", "chmod", "/tmp/", "/dev/"]):
|
||||
categorized["shell_commands"].append(s)
|
||||
import re
|
||||
if re.match(r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", s):
|
||||
categorized["ips"].append(s)
|
||||
if re.match(r"[a-zA-Z0-9.-]+\.(com|net|org|io|ru|cn|xyz)", s):
|
||||
categorized["domains"].append(s)
|
||||
return categorized
|
||||
|
||||
|
||||
def check_packing(filepath):
|
||||
"""Check if the binary is packed with UPX or other packers."""
|
||||
with open(filepath, "rb") as f:
|
||||
data = f.read(4096)
|
||||
indicators = []
|
||||
if b"UPX!" in data:
|
||||
indicators.append("UPX packer detected (UPX! magic)")
|
||||
if b"UPX0" in data or b"UPX1" in data:
|
||||
indicators.append("UPX section names found")
|
||||
stdout, _, _ = subprocess.run(f"upx -t {filepath} 2>&1", shell=True,
|
||||
capture_output=True, text=True).stdout, "", 0
|
||||
if stdout and "packed" in stdout.lower():
|
||||
indicators.append("UPX verification confirms packing")
|
||||
return indicators
|
||||
|
||||
|
||||
def analyze_dynamic_linking(filepath):
|
||||
"""Analyze dynamic linking information and imported functions."""
|
||||
stdout, _, rc = subprocess.run(f"readelf -d {filepath}", shell=True,
|
||||
capture_output=True, text=True).stdout, "", 0
|
||||
dynamic_info = {"libraries": [], "rpath": None}
|
||||
if stdout:
|
||||
for line in stdout.splitlines():
|
||||
if "NEEDED" in line:
|
||||
lib = line.split("[")[-1].rstrip("]") if "[" in line else ""
|
||||
dynamic_info["libraries"].append(lib)
|
||||
if "RPATH" in line or "RUNPATH" in line:
|
||||
dynamic_info["rpath"] = line.split("[")[-1].rstrip("]")
|
||||
|
||||
stdout2, _, _ = subprocess.run(
|
||||
f"readelf -r {filepath} | grep -E 'socket|connect|exec|fork|open|write|bind|listen|send|recv'",
|
||||
shell=True, capture_output=True, text=True
|
||||
).stdout, "", 0
|
||||
dynamic_info["suspicious_imports"] = [
|
||||
line.strip() for line in (stdout2 or "").splitlines() if line.strip()
|
||||
]
|
||||
return dynamic_info
|
||||
|
||||
|
||||
def detect_malware_type(strings_data):
|
||||
"""Classify malware type based on extracted strings."""
|
||||
classifications = []
|
||||
if strings_data.get("crypto_mining"):
|
||||
classifications.append("Cryptominer")
|
||||
if any("flood" in s.lower() or "ddos" in s.lower()
|
||||
for s in strings_data.get("shell_commands", [])):
|
||||
classifications.append("DDoS Botnet")
|
||||
if strings_data.get("ssh_related") and strings_data.get("persistence"):
|
||||
classifications.append("Backdoor/Trojan")
|
||||
if any("insmod" in s or "modprobe" in s or "init_module" in s
|
||||
for s in strings_data.get("shell_commands", [])):
|
||||
classifications.append("Rootkit")
|
||||
if any("ransom" in s.lower() or "encrypt" in s.lower() or "bitcoin" in s.lower()
|
||||
for cat in strings_data.values() if isinstance(cat, list) for s in cat):
|
||||
classifications.append("Ransomware")
|
||||
return classifications or ["Unknown"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Linux ELF Malware Analysis Agent")
|
||||
print("Static analysis with pyelftools, strings, readelf")
|
||||
print("=" * 60)
|
||||
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
|
||||
if target and os.path.exists(target):
|
||||
print(f"\n[*] Analyzing: {target}")
|
||||
print(f"[*] Size: {os.path.getsize(target)} bytes")
|
||||
|
||||
hashes = compute_hashes(target)
|
||||
print(f"[*] MD5: {hashes['md5']}")
|
||||
print(f"[*] SHA256: {hashes['sha256']}")
|
||||
|
||||
elf_info = analyze_elf_header(target)
|
||||
print(f"\n--- ELF Header ---")
|
||||
for k, v in elf_info.items():
|
||||
print(f" {k}: {v}")
|
||||
|
||||
packing = check_packing(target)
|
||||
if packing:
|
||||
for p in packing:
|
||||
print(f"[!] {p}")
|
||||
|
||||
sections = analyze_sections(target)
|
||||
high_ent = [s for s in sections if s.get("high_entropy")]
|
||||
if high_ent:
|
||||
print(f"\n[!] High entropy sections (possible packing/encryption):")
|
||||
for s in high_ent:
|
||||
print(f" {s['name']}: entropy={s['entropy']}, size={s['size']}")
|
||||
|
||||
strings_data = extract_strings(target)
|
||||
print(f"\n--- Strings Analysis ({strings_data.get('total', 0)} total) ---")
|
||||
for category in ["urls", "ips", "domains", "crypto_mining", "persistence", "ssh_related"]:
|
||||
items = strings_data.get(category, [])
|
||||
if items:
|
||||
print(f" {category}: {len(items)}")
|
||||
for item in items[:5]:
|
||||
print(f" - {item}")
|
||||
|
||||
classification = detect_malware_type(strings_data)
|
||||
print(f"\n[*] Classification: {', '.join(classification)}")
|
||||
else:
|
||||
print(f"\n[DEMO] Usage: python agent.py <elf_binary>")
|
||||
print("[*] Provide a Linux ELF binary for analysis.")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,114 @@
|
||||
# API Reference: Linux Forensic Artifact Analysis Tools
|
||||
|
||||
## Key Artifact Locations
|
||||
|
||||
| Artifact | Path | Description |
|
||||
|----------|------|-------------|
|
||||
| Auth logs | `/var/log/auth.log` (Debian) `/var/log/secure` (RHEL) | Authentication events |
|
||||
| Login history | `/var/log/wtmp` | Successful logins (binary, use `last`) |
|
||||
| Failed logins | `/var/log/btmp` | Failed logins (binary, use `lastb`) |
|
||||
| Bash history | `~/.bash_history` | Command history per user |
|
||||
| SSH keys | `~/.ssh/authorized_keys` | Authorized public keys |
|
||||
| Crontab | `/etc/crontab`, `/var/spool/cron/crontabs/` | Scheduled tasks |
|
||||
| Systemd services | `/etc/systemd/system/` | Service definitions |
|
||||
| LD_PRELOAD | `/etc/ld.so.preload` | Shared library preloading |
|
||||
| SUID binaries | `find / -perm -4000` | Setuid executables |
|
||||
|
||||
## last / lastb - Login History
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
last -f /var/log/wtmp # Successful logins
|
||||
lastb -f /var/log/btmp # Failed logins
|
||||
last -i -f /var/log/wtmp # Show IP addresses
|
||||
last -s 2024-01-15 -t 2024-01-20 # Date range filter
|
||||
```
|
||||
|
||||
### Output Format
|
||||
```
|
||||
user pts/0 192.168.1.50 Mon Jan 15 09:00 still logged in
|
||||
```
|
||||
|
||||
## chkrootkit - Rootkit Scanner
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
chkrootkit # Full scan
|
||||
chkrootkit -r /mnt/evidence # Scan mounted evidence
|
||||
chkrootkit -q # Quiet (infected only)
|
||||
```
|
||||
|
||||
## rkhunter - Rootkit Hunter
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
rkhunter --check # Full system check
|
||||
rkhunter --check --rootdir /mnt/ev # Check evidence root
|
||||
rkhunter --list tests # List available tests
|
||||
rkhunter --propupd # Update file properties DB
|
||||
```
|
||||
|
||||
### Check Categories
|
||||
| Check | Description |
|
||||
|-------|-------------|
|
||||
| `rootkits` | Known rootkit signatures |
|
||||
| `trojans` | Trojanized system binaries |
|
||||
| `properties` | File permission anomalies |
|
||||
| `filesystem` | Hidden files and directories |
|
||||
|
||||
## auditd Log Parsing
|
||||
|
||||
### ausearch Syntax
|
||||
```bash
|
||||
ausearch -m execve -ts recent # Recent command execution
|
||||
ausearch -m USER_AUTH -ts today # Authentication events
|
||||
ausearch -k suspicious_activity # Custom audit rule key
|
||||
ausearch -ua 0 -ts today # Root user actions
|
||||
```
|
||||
|
||||
### aureport Syntax
|
||||
```bash
|
||||
aureport --auth # Authentication summary
|
||||
aureport --login # Login summary
|
||||
aureport --file # File access summary
|
||||
aureport --summary # Overall summary
|
||||
```
|
||||
|
||||
## osquery - SQL-based System Queries
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
osqueryi "SELECT * FROM users WHERE uid = 0"
|
||||
osqueryi "SELECT * FROM crontab"
|
||||
osqueryi "SELECT * FROM authorized_keys"
|
||||
osqueryi "SELECT * FROM suid_bin"
|
||||
osqueryi "SELECT * FROM process_open_sockets"
|
||||
```
|
||||
|
||||
### Key Tables
|
||||
| Table | Content |
|
||||
|-------|---------|
|
||||
| `users` | User account information |
|
||||
| `crontab` | Cron job entries |
|
||||
| `authorized_keys` | SSH authorized keys |
|
||||
| `suid_bin` | SUID binaries |
|
||||
| `process_open_sockets` | Network connections by process |
|
||||
| `shell_history` | Command history entries |
|
||||
|
||||
## Plaso / log2timeline - Super Timeline
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
log2timeline.py /cases/timeline.plaso /mnt/evidence
|
||||
psort.py -o l2tcsv /cases/timeline.plaso > timeline.csv
|
||||
psort.py -o l2tcsv /cases/timeline.plaso "date > '2024-01-15'"
|
||||
```
|
||||
|
||||
## AIDE - File Integrity
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
aide --init # Initialize database
|
||||
aide --check # Check for changes
|
||||
aide --compare # Compare databases
|
||||
```
|
||||
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Linux system artifact forensics agent for investigating compromised systems."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import glob
|
||||
import json
|
||||
import re
|
||||
import datetime
|
||||
import subprocess
|
||||
|
||||
|
||||
def run_cmd(cmd):
|
||||
"""Execute a shell command and return output."""
|
||||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
|
||||
return result.stdout.strip(), result.stderr.strip(), result.returncode
|
||||
|
||||
|
||||
def analyze_passwd(passwd_path):
|
||||
"""Analyze /etc/passwd for suspicious accounts."""
|
||||
findings = []
|
||||
with open(passwd_path, "r") as f:
|
||||
for line in f:
|
||||
parts = line.strip().split(":")
|
||||
if len(parts) < 7:
|
||||
continue
|
||||
username, _, uid, gid = parts[0], parts[1], int(parts[2]), int(parts[3])
|
||||
home, shell = parts[5], parts[6]
|
||||
if uid == 0 and username != "root":
|
||||
findings.append({
|
||||
"severity": "CRITICAL",
|
||||
"finding": f"UID 0 account: {username} (shell: {shell})",
|
||||
})
|
||||
login_shells = ["/bin/bash", "/bin/sh", "/bin/zsh", "/usr/bin/zsh"]
|
||||
if uid < 1000 and uid > 0 and shell in login_shells:
|
||||
findings.append({
|
||||
"severity": "WARNING",
|
||||
"finding": f"System account with login shell: {username} (UID:{uid})",
|
||||
})
|
||||
if uid >= 1000 and shell not in ["/bin/false", "/usr/sbin/nologin", "/bin/sync"]:
|
||||
findings.append({
|
||||
"severity": "INFO",
|
||||
"finding": f"Interactive user: {username} (UID:{uid}, Home:{home})",
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def analyze_shadow(shadow_path):
|
||||
"""Analyze /etc/shadow for password hash types and status."""
|
||||
findings = []
|
||||
with open(shadow_path, "r") as f:
|
||||
for line in f:
|
||||
parts = line.strip().split(":")
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
username = parts[0]
|
||||
pwd_hash = parts[1]
|
||||
if pwd_hash and pwd_hash not in ("*", "!", "!!", ""):
|
||||
hash_type = "Unknown"
|
||||
if pwd_hash.startswith("$6$"):
|
||||
hash_type = "SHA-512"
|
||||
elif pwd_hash.startswith("$5$"):
|
||||
hash_type = "SHA-256"
|
||||
elif pwd_hash.startswith("$y$"):
|
||||
hash_type = "yescrypt"
|
||||
elif pwd_hash.startswith("$1$"):
|
||||
hash_type = "MD5 (WEAK)"
|
||||
findings.append({
|
||||
"severity": "WARNING",
|
||||
"finding": f"{username} uses weak MD5 password hash",
|
||||
})
|
||||
findings.append({
|
||||
"severity": "INFO",
|
||||
"finding": f"{username}: {hash_type} hash, last changed day {parts[2]}",
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def analyze_bash_history(history_path, username="unknown"):
|
||||
"""Analyze bash history for suspicious commands."""
|
||||
suspicious_patterns = [
|
||||
"wget", "curl", "nc ", "ncat", "netcat", "python -c", "python3 -c",
|
||||
"perl -e", "base64", "chmod 777", "chmod +s", "/dev/tcp", "/dev/udp",
|
||||
"nmap", "masscan", "hydra", "john", "hashcat", "passwd", "useradd",
|
||||
"iptables -F", "ufw disable", "history -c", "rm -rf", "dd if=",
|
||||
"crontab", "systemctl enable", "ssh-keygen", "scp ", "rsync",
|
||||
"/tmp/", "/dev/shm/", "mkfifo", "socat",
|
||||
]
|
||||
findings = []
|
||||
with open(history_path, "r", errors="ignore") as f:
|
||||
lines = f.readlines()
|
||||
for i, line in enumerate(lines):
|
||||
line_stripped = line.strip()
|
||||
for pattern in suspicious_patterns:
|
||||
if pattern in line_stripped.lower():
|
||||
findings.append({
|
||||
"user": username,
|
||||
"line_number": i + 1,
|
||||
"command": line_stripped[:200],
|
||||
"matched_pattern": pattern,
|
||||
})
|
||||
break
|
||||
return findings
|
||||
|
||||
|
||||
def check_cron_persistence(evidence_root):
|
||||
"""Check cron jobs for persistence mechanisms."""
|
||||
findings = []
|
||||
cron_paths = [
|
||||
os.path.join(evidence_root, "etc/crontab"),
|
||||
*glob.glob(os.path.join(evidence_root, "etc/cron.d/*")),
|
||||
*glob.glob(os.path.join(evidence_root, "var/spool/cron/crontabs/*")),
|
||||
]
|
||||
for cron_path in cron_paths:
|
||||
if os.path.exists(cron_path) and os.path.isfile(cron_path):
|
||||
with open(cron_path, "r", errors="ignore") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#"):
|
||||
suspicious = any(
|
||||
p in line.lower()
|
||||
for p in ["wget", "curl", "/tmp/", "/dev/shm/", "base64",
|
||||
"python", "bash -i", "reverse", "nc ", "ncat"]
|
||||
)
|
||||
if suspicious:
|
||||
findings.append({
|
||||
"severity": "HIGH",
|
||||
"source": cron_path,
|
||||
"entry": line[:200],
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def check_ssh_keys(evidence_root):
|
||||
"""Check for unauthorized SSH authorized_keys."""
|
||||
findings = []
|
||||
key_files = glob.glob(
|
||||
os.path.join(evidence_root, "home/*/.ssh/authorized_keys")
|
||||
) + glob.glob(
|
||||
os.path.join(evidence_root, "root/.ssh/authorized_keys")
|
||||
)
|
||||
for key_file in key_files:
|
||||
if os.path.exists(key_file):
|
||||
with open(key_file, "r") as f:
|
||||
keys = [l.strip() for l in f if l.strip() and not l.startswith("#")]
|
||||
if keys:
|
||||
findings.append({
|
||||
"file": key_file,
|
||||
"key_count": len(keys),
|
||||
"keys": [k[:80] + "..." for k in keys],
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def check_systemd_persistence(evidence_root):
|
||||
"""Check for suspicious systemd service files."""
|
||||
findings = []
|
||||
service_dirs = [
|
||||
os.path.join(evidence_root, "etc/systemd/system"),
|
||||
os.path.join(evidence_root, "usr/lib/systemd/system"),
|
||||
]
|
||||
for svc_dir in service_dirs:
|
||||
if not os.path.exists(svc_dir):
|
||||
continue
|
||||
for svc_file in glob.glob(os.path.join(svc_dir, "*.service")):
|
||||
with open(svc_file, "r", errors="ignore") as f:
|
||||
content = f.read()
|
||||
suspicious = any(
|
||||
p in content.lower()
|
||||
for p in ["/tmp/", "/dev/shm/", "wget", "curl", "reverse",
|
||||
"bash -i", "nc ", "python", "base64"]
|
||||
)
|
||||
if suspicious:
|
||||
findings.append({
|
||||
"severity": "HIGH",
|
||||
"file": svc_file,
|
||||
"preview": content[:300],
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def check_ld_preload(evidence_root):
|
||||
"""Check for LD_PRELOAD rootkit indicators."""
|
||||
findings = []
|
||||
preload_path = os.path.join(evidence_root, "etc/ld.so.preload")
|
||||
if os.path.exists(preload_path):
|
||||
with open(preload_path, "r") as f:
|
||||
content = f.read().strip()
|
||||
if content:
|
||||
findings.append({
|
||||
"severity": "CRITICAL",
|
||||
"finding": f"/etc/ld.so.preload contains: {content}",
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
def find_suid_binaries(evidence_root):
|
||||
"""Find SUID/SGID binaries (potential privilege escalation)."""
|
||||
stdout, _, rc = run_cmd(
|
||||
f"find {evidence_root} -perm -4000 -type f 2>/dev/null"
|
||||
)
|
||||
return stdout.splitlines() if rc == 0 and stdout else []
|
||||
|
||||
|
||||
def find_suspicious_tmp_files(evidence_root):
|
||||
"""Find suspicious files in /tmp and /dev/shm."""
|
||||
findings = []
|
||||
for tmp_dir in ["tmp", "dev/shm"]:
|
||||
full_path = os.path.join(evidence_root, tmp_dir)
|
||||
if os.path.exists(full_path):
|
||||
for root, dirs, files in os.walk(full_path):
|
||||
for fname in files:
|
||||
fpath = os.path.join(root, fname)
|
||||
findings.append(fpath)
|
||||
return findings
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Linux System Artifacts Forensics Agent")
|
||||
print("User accounts, persistence, shell history, rootkit detection")
|
||||
print("=" * 60)
|
||||
|
||||
evidence_root = sys.argv[1] if len(sys.argv) > 1 else "/mnt/evidence"
|
||||
|
||||
if os.path.exists(evidence_root):
|
||||
print(f"\n[*] Examining evidence root: {evidence_root}")
|
||||
|
||||
passwd_path = os.path.join(evidence_root, "etc/passwd")
|
||||
if os.path.exists(passwd_path):
|
||||
print("\n--- User Account Analysis ---")
|
||||
for f in analyze_passwd(passwd_path):
|
||||
print(f" [{f['severity']}] {f['finding']}")
|
||||
|
||||
print("\n--- Cron Persistence ---")
|
||||
cron = check_cron_persistence(evidence_root)
|
||||
for c in cron:
|
||||
print(f" [{c['severity']}] {c['source']}: {c['entry'][:80]}")
|
||||
|
||||
print("\n--- SSH Authorized Keys ---")
|
||||
ssh = check_ssh_keys(evidence_root)
|
||||
for s in ssh:
|
||||
print(f" {s['file']}: {s['key_count']} keys")
|
||||
|
||||
print("\n--- Systemd Persistence ---")
|
||||
systemd = check_systemd_persistence(evidence_root)
|
||||
for s in systemd:
|
||||
print(f" [{s['severity']}] {s['file']}")
|
||||
|
||||
print("\n--- LD_PRELOAD Rootkit Check ---")
|
||||
ld = check_ld_preload(evidence_root)
|
||||
for l in ld:
|
||||
print(f" [{l['severity']}] {l['finding']}")
|
||||
|
||||
print("\n--- Suspicious Temp Files ---")
|
||||
tmp = find_suspicious_tmp_files(evidence_root)
|
||||
for t in tmp[:20]:
|
||||
print(f" {t}")
|
||||
else:
|
||||
print(f"\n[DEMO] Usage: python agent.py <evidence_mount_point>")
|
||||
print("[*] Mount a forensic image and provide the path for analysis.")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,109 @@
|
||||
# API Reference: LNK File and Jump List Forensics
|
||||
|
||||
## LECmd (Eric Zimmerman) - LNK Parser
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
LECmd.exe -f <file.lnk> # Single file
|
||||
LECmd.exe -d <directory> --all # All files in directory
|
||||
LECmd.exe -d <dir> --csv <output_dir> # CSV export
|
||||
LECmd.exe -d <dir> --json <output_dir> # JSON export
|
||||
LECmd.exe -f <file.lnk> -q # Quiet mode
|
||||
LECmd.exe -d <dir> -r # Only removable drives
|
||||
```
|
||||
|
||||
### Output Fields
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| SourceFile | Path to the .lnk file |
|
||||
| TargetCreated | Target file creation timestamp |
|
||||
| TargetModified | Target file modification timestamp |
|
||||
| TargetAccessed | Target file access timestamp |
|
||||
| FileSize | Target file size |
|
||||
| RelativePath | Relative path to target |
|
||||
| WorkingDirectory | Working directory for target |
|
||||
| Arguments | Command-line arguments |
|
||||
| LocalPath | Full local path to target |
|
||||
| VolumeSerialNumber | Volume serial of target drive |
|
||||
| DriveType | Fixed, Removable, Network |
|
||||
| MachineID | NetBIOS name from tracker block |
|
||||
| MacAddress | MAC from distributed tracker |
|
||||
|
||||
## JLECmd (Eric Zimmerman) - Jump List Parser
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
JLECmd.exe -f <jumplist_file> # Single file
|
||||
JLECmd.exe -d <directory> # All jump lists
|
||||
JLECmd.exe -d <dir> --csv <output> # CSV export
|
||||
JLECmd.exe -d <dir> --fd # Full LNK details
|
||||
JLECmd.exe -d <dir> --dumpTo <dir> # Extract embedded LNK files
|
||||
```
|
||||
|
||||
### Jump List Locations
|
||||
```
|
||||
%APPDATA%\Microsoft\Windows\Recent\AutomaticDestinations\
|
||||
%APPDATA%\Microsoft\Windows\Recent\CustomDestinations\
|
||||
```
|
||||
|
||||
## LnkParse3 (Python)
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
pip install LnkParse3
|
||||
```
|
||||
|
||||
### Usage
|
||||
```python
|
||||
import LnkParse3
|
||||
|
||||
with open("shortcut.lnk", "rb") as f:
|
||||
lnk = LnkParse3.lnk_file(f)
|
||||
|
||||
info = lnk.get_json()
|
||||
print(info["data"]["relative_path"])
|
||||
print(info["header"]["creation_time"])
|
||||
print(info["link_info"]["local_base_path"])
|
||||
|
||||
# Extra data blocks
|
||||
extra = info.get("extra", {})
|
||||
tracker = extra.get("DISTRIBUTED_LINK_TRACKER_BLOCK", {})
|
||||
print(tracker.get("machine_id"))
|
||||
print(tracker.get("mac_address"))
|
||||
```
|
||||
|
||||
## Shell Link Binary Format (MS-SHLLINK)
|
||||
|
||||
### Header Structure (76 bytes)
|
||||
| Offset | Size | Field |
|
||||
|--------|------|-------|
|
||||
| 0 | 4 | HeaderSize (0x0000004C) |
|
||||
| 4 | 16 | LinkCLSID |
|
||||
| 20 | 4 | LinkFlags |
|
||||
| 24 | 4 | FileAttributes |
|
||||
| 28 | 8 | CreationTime (FILETIME) |
|
||||
| 36 | 8 | AccessTime (FILETIME) |
|
||||
| 44 | 8 | WriteTime (FILETIME) |
|
||||
| 52 | 4 | FileSize |
|
||||
| 56 | 4 | IconIndex |
|
||||
| 60 | 4 | ShowCommand |
|
||||
|
||||
### Common App IDs (Jump Lists)
|
||||
| App ID | Application |
|
||||
|--------|-------------|
|
||||
| 1b4dd67f29cb1962 | Windows Explorer |
|
||||
| 5d696d521de238c3 | Google Chrome |
|
||||
| ecd21b58c2f65a4f | Firefox |
|
||||
| 1bc392b8e104a00e | Remote Desktop (mstsc) |
|
||||
| b8ab77100df80ab2 | Microsoft Word |
|
||||
| cfb56c56fa0f0478 | PuTTY |
|
||||
| b74736c2bd8cc8a5 | WinSCP |
|
||||
|
||||
## Suspicious LNK Indicators
|
||||
| Pattern | Concern |
|
||||
|---------|---------|
|
||||
| PowerShell in arguments | Script execution via shortcut |
|
||||
| cmd.exe /c in target | Command execution chain |
|
||||
| UNC path to IP | Network-based payload delivery |
|
||||
| Base64 encoded arguments | Obfuscated commands |
|
||||
| mshta/wscript target | Living-off-the-land execution |
|
||||
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Windows LNK file and Jump List artifact analysis agent.
|
||||
|
||||
Parses Windows Shell Link (.lnk) files and Jump List artifacts to extract
|
||||
file access evidence, program execution history, and user activity timelines.
|
||||
Uses LnkParse3 for binary parsing and supports LECmd/JLECmd CSV output analysis.
|
||||
"""
|
||||
|
||||
import struct
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import hashlib
|
||||
import datetime
|
||||
import re
|
||||
import glob as glob_mod
|
||||
|
||||
try:
|
||||
import LnkParse3
|
||||
HAS_LNKPARSE = True
|
||||
except ImportError:
|
||||
HAS_LNKPARSE = False
|
||||
|
||||
|
||||
def compute_hash(filepath):
|
||||
"""Compute SHA-256 hash of file."""
|
||||
sha256 = hashlib.sha256()
|
||||
with open(filepath, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(65536), b""):
|
||||
sha256.update(chunk)
|
||||
return sha256.hexdigest()
|
||||
|
||||
|
||||
def parse_lnk_with_lnkparse3(filepath):
|
||||
"""Parse LNK file using LnkParse3 library."""
|
||||
if not HAS_LNKPARSE:
|
||||
return {"error": "LnkParse3 not installed. pip install LnkParse3"}
|
||||
with open(filepath, "rb") as f:
|
||||
lnk = LnkParse3.lnk_file(f)
|
||||
info = lnk.get_json()
|
||||
result = {
|
||||
"target_path": info.get("data", {}).get("relative_path", ""),
|
||||
"working_dir": info.get("data", {}).get("working_directory", ""),
|
||||
"arguments": info.get("data", {}).get("command_line_arguments", ""),
|
||||
"icon_location": info.get("data", {}).get("icon_location", ""),
|
||||
"description": info.get("data", {}).get("description", ""),
|
||||
}
|
||||
header = info.get("header", {})
|
||||
result["creation_time"] = header.get("creation_time", "")
|
||||
result["access_time"] = header.get("access_time", "")
|
||||
result["write_time"] = header.get("write_time", "")
|
||||
result["file_size"] = header.get("file_size", 0)
|
||||
result["file_flags"] = header.get("file_attributes", "")
|
||||
link_info = info.get("link_info", {})
|
||||
if link_info:
|
||||
result["local_base_path"] = link_info.get("local_base_path", "")
|
||||
result["volume_serial"] = link_info.get("volume_serial_number", "")
|
||||
result["volume_label"] = link_info.get("volume_label", "")
|
||||
result["drive_type"] = link_info.get("drive_type", "")
|
||||
extra = info.get("extra", {})
|
||||
if extra:
|
||||
tracker = extra.get("DISTRIBUTED_LINK_TRACKER_BLOCK", {})
|
||||
if tracker:
|
||||
result["machine_id"] = tracker.get("machine_id", "")
|
||||
result["mac_address"] = tracker.get("mac_address", "")
|
||||
result["droid_volume_id"] = tracker.get("droid_volume_identifier", "")
|
||||
result["droid_file_id"] = tracker.get("droid_file_identifier", "")
|
||||
return result
|
||||
|
||||
|
||||
def parse_lnk_header_raw(filepath):
|
||||
"""Parse LNK file header manually from raw bytes."""
|
||||
with open(filepath, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
if len(data) < 76:
|
||||
return {"error": "File too small for LNK header"}
|
||||
|
||||
# Shell Link Header (76 bytes)
|
||||
header_size = struct.unpack_from("<I", data, 0)[0]
|
||||
if header_size != 0x4C:
|
||||
return {"error": f"Invalid header size: {header_size:#x} (expected 0x4C)"}
|
||||
|
||||
# CLSID check: 00021401-0000-0000-C000-000000000046
|
||||
clsid = data[4:20]
|
||||
expected_clsid = bytes.fromhex("01140200000000c0000000000000046".replace("0", "0"))
|
||||
|
||||
link_flags = struct.unpack_from("<I", data, 20)[0]
|
||||
file_attrs = struct.unpack_from("<I", data, 24)[0]
|
||||
|
||||
creation_time = filetime_to_datetime(struct.unpack_from("<Q", data, 28)[0])
|
||||
access_time = filetime_to_datetime(struct.unpack_from("<Q", data, 36)[0])
|
||||
write_time = filetime_to_datetime(struct.unpack_from("<Q", data, 44)[0])
|
||||
|
||||
file_size = struct.unpack_from("<I", data, 52)[0]
|
||||
icon_index = struct.unpack_from("<I", data, 56)[0]
|
||||
show_command = struct.unpack_from("<I", data, 60)[0]
|
||||
|
||||
result = {
|
||||
"header_size": header_size,
|
||||
"link_flags": f"0x{link_flags:08X}",
|
||||
"file_attributes": f"0x{file_attrs:08X}",
|
||||
"creation_time": creation_time,
|
||||
"access_time": access_time,
|
||||
"write_time": write_time,
|
||||
"target_file_size": file_size,
|
||||
"icon_index": icon_index,
|
||||
"show_command": {1: "Normal", 3: "Maximized", 7: "Minimized"}.get(show_command, str(show_command)),
|
||||
"flags_decoded": decode_link_flags(link_flags),
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def filetime_to_datetime(filetime):
|
||||
"""Convert Windows FILETIME to ISO string."""
|
||||
if filetime == 0:
|
||||
return "N/A"
|
||||
try:
|
||||
epoch = datetime.datetime(1601, 1, 1)
|
||||
delta = datetime.timedelta(microseconds=filetime // 10)
|
||||
return (epoch + delta).isoformat() + "Z"
|
||||
except (OverflowError, OSError):
|
||||
return "Invalid"
|
||||
|
||||
|
||||
def decode_link_flags(flags):
|
||||
"""Decode Shell Link header flags."""
|
||||
flag_names = {
|
||||
0x00000001: "HasLinkTargetIDList",
|
||||
0x00000002: "HasLinkInfo",
|
||||
0x00000004: "HasName",
|
||||
0x00000008: "HasRelativePath",
|
||||
0x00000010: "HasWorkingDir",
|
||||
0x00000020: "HasArguments",
|
||||
0x00000040: "HasIconLocation",
|
||||
0x00000080: "IsUnicode",
|
||||
0x00000100: "ForceNoLinkInfo",
|
||||
0x00000800: "RunInSeparateProcess",
|
||||
0x00001000: "HasDarwinID",
|
||||
0x00002000: "RunAsUser",
|
||||
0x00004000: "HasExpIcon",
|
||||
0x00020000: "HasExpString",
|
||||
0x00040000: "RunInSeparateProcess",
|
||||
0x00080000: "PreferEnvironmentPath",
|
||||
0x00200000: "DisableLinkPathTracking",
|
||||
0x00800000: "EnableTargetMetadata",
|
||||
0x04000000: "AllowLinkToLink",
|
||||
}
|
||||
decoded = []
|
||||
for bit, name in flag_names.items():
|
||||
if flags & bit:
|
||||
decoded.append(name)
|
||||
return decoded
|
||||
|
||||
|
||||
JUMP_LIST_APP_IDS = {
|
||||
"1b4dd67f29cb1962": "Windows Explorer",
|
||||
"5d696d521de238c3": "Google Chrome",
|
||||
"9b9cdc69c1c24e2b": "Notepad",
|
||||
"f01b4d95cf55d32a": "Windows Explorer",
|
||||
"a7bd71699cd38d1c": "Notepad++",
|
||||
"918e0ecb43d17e23": "Notepad (Win10)",
|
||||
"12dc1ea8e34b5a6": "Microsoft Paint",
|
||||
"b8ab77100df80ab2": "Microsoft Word 2019",
|
||||
"a4a5324453625195": "Microsoft Excel 2019",
|
||||
"bc0c37e84e063727": "Microsoft PowerPoint 2019",
|
||||
"9839aec31243a928": "Microsoft Outlook 2019",
|
||||
"fb3b0dbfee58fac8": "Acrobat Reader DC",
|
||||
"ecd21b58c2f65a4f": "Firefox",
|
||||
"1bc392b8e104a00e": "Remote Desktop (mstsc)",
|
||||
"b91050d8b077a4e8": "WinRAR",
|
||||
"290532160612e071": "Windows Media Player",
|
||||
"28c8b86deab549a1": "Internet Explorer",
|
||||
"7e4dca80246863e3": "Control Panel",
|
||||
"e2a593822e01aed3": "Snipping Tool",
|
||||
"b74736c2bd8cc8a5": "WinSCP",
|
||||
"cfb56c56fa0f0478": "PuTTY",
|
||||
}
|
||||
|
||||
|
||||
def scan_jump_lists(jump_list_dir):
|
||||
"""Scan Jump List directory for automatic and custom destinations."""
|
||||
results = []
|
||||
auto_pattern = os.path.join(jump_list_dir, "*.automaticDestinations-ms")
|
||||
custom_pattern = os.path.join(jump_list_dir, "*.customDestinations-ms")
|
||||
|
||||
for jl_file in sorted(glob_mod.glob(auto_pattern) + glob_mod.glob(custom_pattern)):
|
||||
basename = os.path.basename(jl_file)
|
||||
app_id = basename.split(".")[0]
|
||||
jl_type = "automatic" if "automatic" in basename else "custom"
|
||||
app_name = JUMP_LIST_APP_IDS.get(app_id, "Unknown Application")
|
||||
results.append({
|
||||
"file": basename,
|
||||
"app_id": app_id,
|
||||
"app_name": app_name,
|
||||
"type": jl_type,
|
||||
"size": os.path.getsize(jl_file),
|
||||
"modified": datetime.datetime.fromtimestamp(
|
||||
os.path.getmtime(jl_file)).isoformat(),
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
def detect_suspicious_lnk(parsed_lnk):
|
||||
"""Detect suspicious characteristics in LNK files."""
|
||||
findings = []
|
||||
args = parsed_lnk.get("arguments", "")
|
||||
target = parsed_lnk.get("target_path", "") + " " + parsed_lnk.get("local_base_path", "")
|
||||
|
||||
suspicious_patterns = [
|
||||
(r"powershell", "PowerShell execution via LNK"),
|
||||
(r"cmd\.exe\s*/c", "Command prompt execution via LNK"),
|
||||
(r"mshta", "MSHTA execution (HTA payload)"),
|
||||
(r"certutil.*-decode", "CertUtil decode (file download)"),
|
||||
(r"bitsadmin.*transfer", "BitsAdmin file download"),
|
||||
(r"regsvr32.*scrobj", "Regsvr32 COM scriptlet execution"),
|
||||
(r"wscript|cscript", "Script host execution"),
|
||||
(r"\\\\[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\\", "UNC path to IP address"),
|
||||
(r"http[s]?://", "URL in LNK arguments"),
|
||||
(r"-enc\s+[A-Za-z0-9+/=]{20,}", "Base64-encoded PowerShell"),
|
||||
]
|
||||
combined = f"{target} {args}".lower()
|
||||
for pattern, description in suspicious_patterns:
|
||||
if re.search(pattern, combined, re.IGNORECASE):
|
||||
findings.append({"indicator": description, "pattern": pattern})
|
||||
|
||||
if parsed_lnk.get("drive_type") == "DRIVE_REMOTE":
|
||||
findings.append({"indicator": "Target on network drive", "pattern": "DRIVE_REMOTE"})
|
||||
|
||||
return findings
|
||||
|
||||
|
||||
def scan_lnk_directory(directory):
|
||||
"""Scan directory for LNK files and analyze each."""
|
||||
results = []
|
||||
for lnk_file in sorted(glob_mod.glob(os.path.join(directory, "*.lnk"))):
|
||||
parsed = parse_lnk_with_lnkparse3(lnk_file) if HAS_LNKPARSE else parse_lnk_header_raw(lnk_file)
|
||||
suspicious = detect_suspicious_lnk(parsed)
|
||||
results.append({
|
||||
"file": os.path.basename(lnk_file),
|
||||
"sha256": compute_hash(lnk_file),
|
||||
"parsed": parsed,
|
||||
"suspicious": suspicious,
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Windows LNK & Jump List Forensics Agent")
|
||||
print("Shell Link parsing, Jump List analysis, suspicious detection")
|
||||
print("=" * 60)
|
||||
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
|
||||
if not target or not os.path.exists(target):
|
||||
print("\n[DEMO] Usage:")
|
||||
print(" python agent.py <file.lnk> # Analyze single LNK")
|
||||
print(" python agent.py <directory> # Scan directory for LNK/JumpList")
|
||||
print(f"\n LnkParse3 available: {HAS_LNKPARSE}")
|
||||
sys.exit(0)
|
||||
|
||||
if os.path.isfile(target) and target.lower().endswith(".lnk"):
|
||||
print(f"\n[*] Analyzing: {target}")
|
||||
print(f"[*] SHA-256: {compute_hash(target)}")
|
||||
if HAS_LNKPARSE:
|
||||
parsed = parse_lnk_with_lnkparse3(target)
|
||||
else:
|
||||
parsed = parse_lnk_header_raw(target)
|
||||
print("\n--- LNK Properties ---")
|
||||
for k, v in parsed.items():
|
||||
print(f" {k}: {v}")
|
||||
suspicious = detect_suspicious_lnk(parsed)
|
||||
if suspicious:
|
||||
print("\n--- Suspicious Indicators ---")
|
||||
for s in suspicious:
|
||||
print(f" [!] {s['indicator']}")
|
||||
elif os.path.isdir(target):
|
||||
print(f"\n[*] Scanning directory: {target}")
|
||||
lnk_results = scan_lnk_directory(target)
|
||||
print(f"[*] Found {len(lnk_results)} LNK files")
|
||||
for r in lnk_results[:20]:
|
||||
print(f" {r['file']}: {r['parsed'].get('target_path', r['parsed'].get('local_base_path', '?'))}")
|
||||
for s in r.get("suspicious", []):
|
||||
print(f" [!] {s['indicator']}")
|
||||
|
||||
jl_dir = os.path.join(target, "AutomaticDestinations")
|
||||
if not os.path.isdir(jl_dir):
|
||||
jl_dir = target
|
||||
jl_results = scan_jump_lists(jl_dir)
|
||||
if jl_results:
|
||||
print(f"\n--- Jump Lists ({len(jl_results)}) ---")
|
||||
for jl in jl_results:
|
||||
print(f" {jl['app_name']:30s} [{jl['type']}] {jl['app_id']}")
|
||||
|
||||
print(f"\n{json.dumps({'lnk_count': len(lnk_results) if os.path.isdir(target) else 1}, indent=2)}")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,112 @@
|
||||
# API Reference: Office Macro Malware Analysis Tools
|
||||
|
||||
## olevba - VBA Macro Extraction (oletools)
|
||||
|
||||
### CLI Syntax
|
||||
```bash
|
||||
olevba document.docm # Full analysis
|
||||
olevba --decode --deobf document.docm # Decode + deobfuscate
|
||||
olevba --code document.docm # Extract VBA source only
|
||||
olevba --json document.docm # JSON output
|
||||
olevba --reveal document.docm # Reveal hidden content
|
||||
```
|
||||
|
||||
### Output Sections
|
||||
| Section | Content |
|
||||
|---------|---------|
|
||||
| `AutoExec` | Auto-execution triggers (AutoOpen, Document_Open) |
|
||||
| `Suspicious` | Dangerous functions (Shell, WScript, CreateObject) |
|
||||
| `IOC` | Extracted indicators (URLs, IPs, file paths) |
|
||||
| `Hex String` | Decoded hex-encoded strings |
|
||||
|
||||
### Python API
|
||||
```python
|
||||
from oletools.olevba import VBA_Parser
|
||||
vba = VBA_Parser("document.docm")
|
||||
if vba.detect_vba_macros():
|
||||
for (fn, stream, vba_fn, code) in vba.extract_macros():
|
||||
print(code)
|
||||
for (kw_type, keyword, desc) in vba.analyze_macros():
|
||||
print(f"{kw_type}: {keyword}")
|
||||
vba.close()
|
||||
```
|
||||
|
||||
## oleid - Document Capability Identification
|
||||
|
||||
### CLI Syntax
|
||||
```bash
|
||||
oleid document.docm
|
||||
```
|
||||
|
||||
### Indicators
|
||||
| Indicator | Risk Values |
|
||||
|-----------|-------------|
|
||||
| `VBA Macros` | True/False |
|
||||
| `XLM Macros` | True/False |
|
||||
| `External Relationships` | True/False |
|
||||
| `ObjectPool` | True/False |
|
||||
| `Flash` | True/False |
|
||||
|
||||
## oledump.py - OLE Stream Analysis
|
||||
|
||||
### CLI Syntax
|
||||
```bash
|
||||
oledump.py document.docm # List streams
|
||||
oledump.py -s 8 -v document.docm # Extract stream 8
|
||||
oledump.py -p plugin_vba_dco document.docm # VBA decompile
|
||||
oledump.py -p plugin_msg.py document.msg # MSG file parsing
|
||||
```
|
||||
|
||||
### Stream Markers
|
||||
| Marker | Meaning |
|
||||
|--------|---------|
|
||||
| `M` | Contains VBA macros |
|
||||
| `m` | Contains macro attributes |
|
||||
| `O` | Contains OLE objects |
|
||||
|
||||
## XLMDeobfuscator - Excel 4.0 Macros
|
||||
|
||||
### CLI Syntax
|
||||
```bash
|
||||
xlmdeobfuscator -f document.xlsm
|
||||
xlmdeobfuscator -f document.xlsm --output-format json
|
||||
```
|
||||
|
||||
### Dangerous XLM Functions
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `EXEC()` | Execute shell command |
|
||||
| `CALL()` | Call DLL function |
|
||||
| `REGISTER()` | Register DLL function |
|
||||
| `URLDownloadToFileA` | Download file from URL |
|
||||
|
||||
## VBA Auto-Execution Triggers
|
||||
|
||||
| Trigger | Application |
|
||||
|---------|-------------|
|
||||
| `Auto_Open` / `AutoOpen` | Word |
|
||||
| `Document_Open` | Word |
|
||||
| `Workbook_Open` | Excel |
|
||||
| `Auto_Close` | Word |
|
||||
| `AutoExec` | Word |
|
||||
|
||||
## VBA Suspicious Functions
|
||||
|
||||
| Function | Risk |
|
||||
|----------|------|
|
||||
| `Shell()` | Command execution |
|
||||
| `WScript.Shell` | Windows scripting |
|
||||
| `CreateObject()` | COM object instantiation |
|
||||
| `URLDownloadToFile` | File download |
|
||||
| `MSXML2.XMLHTTP` | HTTP requests |
|
||||
| `ADODB.Stream` | Binary file writing |
|
||||
| `CallByName` | Indirect method invocation |
|
||||
| `Environ()` | Environment variable access |
|
||||
|
||||
## ViperMonkey - VBA Emulation
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
vmonkey document.docm
|
||||
vmonkey --iocs document.docm # Extract IOCs only
|
||||
```
|
||||
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Office macro malware analysis agent using oletools for VBA extraction and deobfuscation."""
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import hashlib
|
||||
import subprocess
|
||||
import json
|
||||
import zipfile
|
||||
|
||||
try:
|
||||
from oletools.olevba import VBA_Parser, TYPE_OLE, TYPE_OpenXML
|
||||
from oletools import oleid
|
||||
HAS_OLETOOLS = True
|
||||
except ImportError:
|
||||
HAS_OLETOOLS = False
|
||||
|
||||
|
||||
def compute_hash(filepath):
|
||||
"""Compute SHA-256 hash of a file."""
|
||||
sha256 = hashlib.sha256()
|
||||
with open(filepath, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(65536), b""):
|
||||
sha256.update(chunk)
|
||||
return sha256.hexdigest()
|
||||
|
||||
|
||||
def triage_document(filepath):
|
||||
"""Quick triage using oleid to identify document capabilities."""
|
||||
if not HAS_OLETOOLS:
|
||||
return {"error": "oletools not installed: pip install oletools"}
|
||||
oid = oleid.OleID(filepath)
|
||||
indicators = oid.check()
|
||||
results = {}
|
||||
for indicator in indicators:
|
||||
results[indicator.name] = {
|
||||
"value": str(indicator.value),
|
||||
"risk": indicator.risk,
|
||||
"description": indicator.description,
|
||||
}
|
||||
return results
|
||||
|
||||
|
||||
def extract_vba_macros(filepath):
|
||||
"""Extract VBA macro code from an Office document."""
|
||||
if not HAS_OLETOOLS:
|
||||
return {"error": "oletools not installed"}
|
||||
vba_parser = VBA_Parser(filepath)
|
||||
macros = []
|
||||
if vba_parser.detect_vba_macros():
|
||||
for (filename, stream_path, vba_filename, vba_code) in vba_parser.extract_macros():
|
||||
macros.append({
|
||||
"filename": filename,
|
||||
"stream_path": stream_path,
|
||||
"vba_filename": vba_filename,
|
||||
"code": vba_code,
|
||||
"code_length": len(vba_code),
|
||||
})
|
||||
vba_parser.close()
|
||||
return macros
|
||||
|
||||
|
||||
def analyze_vba_suspicious(filepath):
|
||||
"""Analyze VBA macros for suspicious keywords and patterns."""
|
||||
if not HAS_OLETOOLS:
|
||||
return {"error": "oletools not installed"}
|
||||
vba_parser = VBA_Parser(filepath)
|
||||
analysis = {"auto_exec": [], "suspicious": [], "iocs": [], "hex_strings": []}
|
||||
if vba_parser.detect_vba_macros():
|
||||
results = vba_parser.analyze_macros()
|
||||
for (kw_type, keyword, description) in results:
|
||||
entry = {"type": kw_type, "keyword": keyword, "description": description}
|
||||
if kw_type == "AutoExec":
|
||||
analysis["auto_exec"].append(entry)
|
||||
elif kw_type == "Suspicious":
|
||||
analysis["suspicious"].append(entry)
|
||||
elif kw_type == "IOC":
|
||||
analysis["iocs"].append(entry)
|
||||
elif kw_type == "Hex String":
|
||||
analysis["hex_strings"].append(entry)
|
||||
vba_parser.close()
|
||||
return analysis
|
||||
|
||||
|
||||
def deobfuscate_chr_calls(vba_code):
|
||||
"""Resolve Chr() and ChrW() calls in VBA code."""
|
||||
def resolve_chr(match):
|
||||
try:
|
||||
return chr(int(match.group(1)))
|
||||
except (ValueError, OverflowError):
|
||||
return match.group(0)
|
||||
code = re.sub(r'Chr\$?\((\d+)\)', resolve_chr, vba_code)
|
||||
code = re.sub(r'ChrW\$?\((\d+)\)', resolve_chr, code)
|
||||
return code
|
||||
|
||||
|
||||
def deobfuscate_concatenation(vba_code):
|
||||
"""Remove string concatenation: "abc" & "def" -> "abcdef"."""
|
||||
return re.sub(r'"\s*&\s*"', '', vba_code)
|
||||
|
||||
|
||||
def deobfuscate_strreverse(vba_code):
|
||||
"""Resolve StrReverse() calls."""
|
||||
def resolve_reverse(match):
|
||||
return '"' + match.group(1)[::-1] + '"'
|
||||
return re.sub(r'StrReverse\("([^"]+)"\)', resolve_reverse, vba_code)
|
||||
|
||||
|
||||
def deobfuscate_replace(vba_code):
|
||||
"""Resolve Replace() function calls."""
|
||||
def resolve_replace(match):
|
||||
original = match.group(1)
|
||||
find = match.group(2)
|
||||
replace_with = match.group(3)
|
||||
return '"' + original.replace(find, replace_with) + '"'
|
||||
return re.sub(r'Replace\("([^"]+)",\s*"([^"]+)",\s*"([^"]*)"\)',
|
||||
resolve_replace, vba_code)
|
||||
|
||||
|
||||
def full_deobfuscation(vba_code):
|
||||
"""Apply all deobfuscation techniques to VBA code."""
|
||||
code = deobfuscate_chr_calls(vba_code)
|
||||
code = deobfuscate_concatenation(code)
|
||||
code = deobfuscate_strreverse(code)
|
||||
code = deobfuscate_replace(code)
|
||||
return code
|
||||
|
||||
|
||||
def extract_urls_from_code(code):
|
||||
"""Extract URLs from deobfuscated VBA code."""
|
||||
return list(set(re.findall(r'https?://[^\s"\'<>]+', code)))
|
||||
|
||||
|
||||
def check_dde(filepath):
|
||||
"""Check for DDE (Dynamic Data Exchange) attacks in OOXML documents."""
|
||||
findings = []
|
||||
try:
|
||||
z = zipfile.ZipFile(filepath)
|
||||
for name in z.namelist():
|
||||
if name.endswith(".xml") or name.endswith(".rels"):
|
||||
content = z.read(name).decode("utf-8", errors="ignore")
|
||||
if "DDEAUTO" in content or "DDE " in content:
|
||||
dde_cmds = re.findall(r'DDEAUTO[^"]*"([^"]+)"', content)
|
||||
findings.append({
|
||||
"type": "DDE",
|
||||
"file": name,
|
||||
"commands": dde_cmds,
|
||||
})
|
||||
if "attachedTemplate" in content or "Target=" in content:
|
||||
urls = re.findall(r'Target="(https?://[^"]+)"', content)
|
||||
for url in urls:
|
||||
findings.append({
|
||||
"type": "Remote Template",
|
||||
"file": name,
|
||||
"url": url,
|
||||
})
|
||||
except (zipfile.BadZipFile, KeyError):
|
||||
pass
|
||||
return findings
|
||||
|
||||
|
||||
def check_external_relationships(filepath):
|
||||
"""Check OOXML relationships for external references."""
|
||||
externals = []
|
||||
try:
|
||||
z = zipfile.ZipFile(filepath)
|
||||
for name in z.namelist():
|
||||
if ".rels" in name:
|
||||
content = z.read(name).decode("utf-8", errors="ignore")
|
||||
urls = re.findall(r'Target="(https?://[^"]+)"', content)
|
||||
for url in urls:
|
||||
externals.append({"file": name, "url": url})
|
||||
except (zipfile.BadZipFile, KeyError):
|
||||
pass
|
||||
return externals
|
||||
|
||||
|
||||
def generate_report(filepath, triage, macros, analysis, deobfuscated_urls, dde_findings):
|
||||
"""Generate a comprehensive macro malware analysis report."""
|
||||
report = {
|
||||
"file": filepath,
|
||||
"sha256": compute_hash(filepath),
|
||||
"size": os.path.getsize(filepath),
|
||||
"triage": triage,
|
||||
"macro_count": len(macros),
|
||||
"auto_exec_triggers": [e["keyword"] for e in analysis.get("auto_exec", [])],
|
||||
"suspicious_functions": [e["keyword"] for e in analysis.get("suspicious", [])],
|
||||
"iocs": [e["keyword"] for e in analysis.get("iocs", [])],
|
||||
"extracted_urls": deobfuscated_urls,
|
||||
"dde_findings": dde_findings,
|
||||
}
|
||||
return report
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Office Macro Malware Analysis Agent")
|
||||
print("oletools-based VBA extraction and deobfuscation")
|
||||
print("=" * 60)
|
||||
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
|
||||
if target and os.path.exists(target):
|
||||
print(f"\n[*] Analyzing: {target}")
|
||||
print(f"[*] SHA-256: {compute_hash(target)}")
|
||||
|
||||
print("\n--- Document Triage (oleid) ---")
|
||||
triage = triage_document(target)
|
||||
for name, info in triage.items():
|
||||
risk_tag = f" [{info['risk']}]" if info.get("risk") else ""
|
||||
print(f" {name}: {info['value']}{risk_tag}")
|
||||
|
||||
print("\n--- VBA Macro Extraction ---")
|
||||
macros = extract_vba_macros(target)
|
||||
print(f" Macro streams found: {len(macros)}")
|
||||
for m in macros:
|
||||
print(f" - {m['vba_filename']} ({m['code_length']} chars)")
|
||||
|
||||
print("\n--- Suspicious Analysis ---")
|
||||
analysis = analyze_vba_suspicious(target)
|
||||
for trigger in analysis["auto_exec"]:
|
||||
print(f" [!] Auto-exec: {trigger['keyword']}")
|
||||
for sus in analysis["suspicious"]:
|
||||
print(f" [!] Suspicious: {sus['keyword']} - {sus['description']}")
|
||||
for ioc in analysis["iocs"]:
|
||||
print(f" [IOC] {ioc['keyword']}")
|
||||
|
||||
print("\n--- Deobfuscation ---")
|
||||
all_urls = []
|
||||
for m in macros:
|
||||
deobfuscated = full_deobfuscation(m["code"])
|
||||
urls = extract_urls_from_code(deobfuscated)
|
||||
all_urls.extend(urls)
|
||||
for url in set(all_urls):
|
||||
print(f" URL: {url}")
|
||||
|
||||
print("\n--- DDE / Remote Template Check ---")
|
||||
dde = check_dde(target)
|
||||
for d in dde:
|
||||
print(f" [{d['type']}] {d.get('url', d.get('commands', ''))}")
|
||||
|
||||
report = generate_report(target, triage, macros, analysis, list(set(all_urls)), dde)
|
||||
print(f"\n[*] Report: {json.dumps(report, indent=2, default=str)[:500]}...")
|
||||
else:
|
||||
print(f"\n[DEMO] Usage: python agent.py <document.docm|xlsm>")
|
||||
print("[*] Provide an Office document for macro analysis.")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,68 @@
|
||||
# API Reference: urlscan.io URL Analysis
|
||||
|
||||
## Base URL
|
||||
```
|
||||
https://urlscan.io/api/v1
|
||||
```
|
||||
|
||||
## Authentication
|
||||
```
|
||||
API-Key: YOUR_API_KEY
|
||||
```
|
||||
|
||||
## Submit Scan
|
||||
```
|
||||
POST /scan/
|
||||
```
|
||||
```json
|
||||
{"url": "https://example.com", "visibility": "private"}
|
||||
```
|
||||
| Field | Values | Description |
|
||||
|-------|--------|-------------|
|
||||
| `url` | URL string | URL to scan |
|
||||
| `visibility` | public/unlisted/private | Scan visibility |
|
||||
|
||||
Response: `{"uuid": "...", "result": "https://urlscan.io/result/UUID/", "api": "..."}`
|
||||
|
||||
## Get Result
|
||||
```
|
||||
GET /result/{uuid}/
|
||||
```
|
||||
Returns 404 while scanning, 200 when complete.
|
||||
|
||||
## Search
|
||||
```
|
||||
GET /search/?q=domain:example.com&size=100
|
||||
```
|
||||
Query fields: `domain:`, `ip:`, `server:`, `country:`, `filename:`, `hash:`
|
||||
|
||||
## Result Structure
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `page.url` | Final URL after redirects |
|
||||
| `page.domain` | Domain name |
|
||||
| `page.ip` | Resolved IP |
|
||||
| `page.country` | Server country |
|
||||
| `page.status` | HTTP status code |
|
||||
| `page.title` | Page title |
|
||||
| `page.server` | Server header |
|
||||
| `page.tlsIssuer` | TLS certificate issuer |
|
||||
| `verdicts.overall.malicious` | Boolean malicious verdict |
|
||||
| `verdicts.overall.score` | Risk score (0-100) |
|
||||
| `lists.ips` | List of contacted IPs |
|
||||
| `lists.certificates` | TLS certificates observed |
|
||||
| `stats.resourceStats` | Resource type statistics |
|
||||
|
||||
## Screenshot
|
||||
```
|
||||
GET /screenshots/{uuid}.png
|
||||
```
|
||||
|
||||
## DOM Snapshot
|
||||
```
|
||||
GET /dom/{uuid}/
|
||||
```
|
||||
|
||||
## Rate Limits
|
||||
- Free: 100 scans/day, 1000 searches/day
|
||||
- Paid: Higher limits per plan
|
||||
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env python3
|
||||
"""URLScan.io Malicious URL Analysis Agent - Submits and analyzes URLs via the urlscan.io API."""
|
||||
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
URLSCAN_API = "https://urlscan.io/api/v1"
|
||||
|
||||
|
||||
def submit_url(url, api_key, visibility="private"):
|
||||
"""Submit a URL to urlscan.io for scanning."""
|
||||
headers = {"API-Key": api_key, "Content-Type": "application/json"}
|
||||
payload = {"url": url, "visibility": visibility}
|
||||
resp = requests.post(f"{URLSCAN_API}/scan/", headers=headers, json=payload, timeout=30)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
logger.info("Submitted URL: %s -> scan UUID: %s", url, data.get("uuid"))
|
||||
return data
|
||||
|
||||
|
||||
def get_scan_result(uuid, api_key, max_wait=120):
|
||||
"""Poll for scan results until complete."""
|
||||
headers = {"API-Key": api_key}
|
||||
for _ in range(max_wait // 5):
|
||||
try:
|
||||
resp = requests.get(f"{URLSCAN_API}/result/{uuid}/", headers=headers, timeout=30)
|
||||
if resp.status_code == 200:
|
||||
return resp.json()
|
||||
except requests.RequestException:
|
||||
pass
|
||||
time.sleep(5)
|
||||
return None
|
||||
|
||||
|
||||
def search_urlscan(query, api_key, size=100):
|
||||
"""Search urlscan.io for existing scans."""
|
||||
headers = {"API-Key": api_key}
|
||||
params = {"q": query, "size": size}
|
||||
resp = requests.get(f"{URLSCAN_API}/search/", headers=headers, params=params, timeout=30)
|
||||
resp.raise_for_status()
|
||||
return resp.json().get("results", [])
|
||||
|
||||
|
||||
def analyze_result(result):
|
||||
"""Analyze urlscan.io scan result for malicious indicators."""
|
||||
findings = []
|
||||
verdicts = result.get("verdicts", {})
|
||||
overall = verdicts.get("overall", {})
|
||||
urlscan_verdict = verdicts.get("urlscan", {})
|
||||
community = verdicts.get("community", {})
|
||||
|
||||
if overall.get("malicious"):
|
||||
findings.append({"type": "Malicious verdict", "severity": "critical", "source": "overall", "score": overall.get("score", 0)})
|
||||
if urlscan_verdict.get("malicious"):
|
||||
findings.append({"type": "URLScan malicious", "severity": "critical", "score": urlscan_verdict.get("score", 0)})
|
||||
if community.get("score", 0) < 0:
|
||||
findings.append({"type": "Negative community score", "severity": "high", "score": community.get("score")})
|
||||
|
||||
page = result.get("page", {})
|
||||
lists = result.get("lists", {})
|
||||
stats = result.get("stats", {})
|
||||
|
||||
if lists.get("ips", []):
|
||||
for ip in lists["ips"]:
|
||||
if ip.get("malicious"):
|
||||
findings.append({"type": "Malicious IP contacted", "severity": "high", "ip": ip.get("ip"), "asn": ip.get("asn")})
|
||||
|
||||
for cert in lists.get("certificates", []):
|
||||
if cert.get("validTo"):
|
||||
try:
|
||||
exp = datetime.fromisoformat(cert["validTo"].replace("Z", "+00:00"))
|
||||
if exp < datetime.now(exp.tzinfo):
|
||||
findings.append({"type": "Expired TLS certificate", "severity": "medium", "subject": cert.get("subjectName")})
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
js_count = len([r for r in stats.get("resourceStats", []) if "javascript" in r.get("type", "").lower()])
|
||||
if js_count > 20:
|
||||
findings.append({"type": "High JavaScript resource count", "severity": "medium", "count": js_count})
|
||||
|
||||
redirects = stats.get("uniqCountries", 0)
|
||||
if result.get("data", {}).get("requests"):
|
||||
redirect_chain = [r.get("request", {}).get("redirectHasExtraInfo") for r in result["data"]["requests"][:5]]
|
||||
|
||||
return {
|
||||
"url": page.get("url", ""),
|
||||
"domain": page.get("domain", ""),
|
||||
"ip": page.get("ip", ""),
|
||||
"country": page.get("country", ""),
|
||||
"server": page.get("server", ""),
|
||||
"status_code": page.get("status", 0),
|
||||
"title": page.get("title", ""),
|
||||
"mime_type": page.get("mimeType", ""),
|
||||
"tls_issuer": page.get("tlsIssuer", ""),
|
||||
"overall_malicious": overall.get("malicious", False),
|
||||
"overall_score": overall.get("score", 0),
|
||||
"findings": findings,
|
||||
}
|
||||
|
||||
|
||||
def bulk_analyze(urls, api_key):
|
||||
"""Submit and analyze multiple URLs."""
|
||||
results = []
|
||||
for url in urls:
|
||||
try:
|
||||
submission = submit_url(url, api_key)
|
||||
uuid = submission.get("uuid")
|
||||
if uuid:
|
||||
result = get_scan_result(uuid, api_key)
|
||||
if result:
|
||||
analysis = analyze_result(result)
|
||||
results.append(analysis)
|
||||
else:
|
||||
results.append({"url": url, "error": "Scan timeout"})
|
||||
except requests.RequestException as e:
|
||||
results.append({"url": url, "error": str(e)})
|
||||
return results
|
||||
|
||||
|
||||
def generate_report(analyses):
|
||||
"""Generate URL analysis report."""
|
||||
malicious = [a for a in analyses if a.get("overall_malicious")]
|
||||
report = {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"urls_analyzed": len(analyses),
|
||||
"malicious_count": len(malicious),
|
||||
"results": analyses,
|
||||
}
|
||||
print(f"URLSCAN REPORT: {len(analyses)} URLs analyzed, {len(malicious)} malicious")
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="URLScan.io Malicious URL Analysis Agent")
|
||||
parser.add_argument("--api-key", required=True, help="urlscan.io API key")
|
||||
parser.add_argument("--url", help="Single URL to scan")
|
||||
parser.add_argument("--url-file", help="File with URLs (one per line)")
|
||||
parser.add_argument("--search", help="Search query for existing scans")
|
||||
parser.add_argument("--output", default="urlscan_report.json")
|
||||
args = parser.parse_args()
|
||||
|
||||
urls = []
|
||||
if args.url:
|
||||
urls.append(args.url)
|
||||
if args.url_file:
|
||||
with open(args.url_file) as f:
|
||||
urls.extend(line.strip() for line in f if line.strip())
|
||||
|
||||
if args.search:
|
||||
results = search_urlscan(args.search, args.api_key)
|
||||
analyses = [analyze_result(r) for r in results if "page" in r]
|
||||
else:
|
||||
analyses = bulk_analyze(urls, args.api_key)
|
||||
|
||||
report = generate_report(analyses)
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
logger.info("Report saved to %s", args.output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,121 @@
|
||||
# API Reference: Cuckoo Sandbox
|
||||
|
||||
## Cuckoo CLI
|
||||
|
||||
### Sample Submission
|
||||
```bash
|
||||
cuckoo submit /path/to/sample.exe
|
||||
cuckoo submit --timeout 300 /path/to/sample.exe
|
||||
cuckoo submit --machine win10_x64 --package exe sample.exe
|
||||
cuckoo submit --url "http://malicious-url.com"
|
||||
```
|
||||
|
||||
### Status
|
||||
```bash
|
||||
cuckoo status
|
||||
tail -f /opt/cuckoo/log/cuckoo.log
|
||||
```
|
||||
|
||||
## Cuckoo REST API
|
||||
|
||||
### Submit File
|
||||
```bash
|
||||
curl -F "file=@sample.exe" -F "timeout=300" \
|
||||
http://localhost:8090/tasks/create/file
|
||||
```
|
||||
Response: `{"task_id": 1}`
|
||||
|
||||
### Submit URL
|
||||
```bash
|
||||
curl -F "url=http://malicious.com" -F "timeout=300" \
|
||||
http://localhost:8090/tasks/create/url
|
||||
```
|
||||
|
||||
### Check Task Status
|
||||
```bash
|
||||
curl http://localhost:8090/tasks/view/<task_id>
|
||||
```
|
||||
Status values: `pending`, `running`, `completed`, `reported`
|
||||
|
||||
### Get Report
|
||||
```bash
|
||||
curl http://localhost:8090/tasks/report/<task_id>
|
||||
curl http://localhost:8090/tasks/report/<task_id>/json
|
||||
```
|
||||
|
||||
### List Tasks
|
||||
```bash
|
||||
curl http://localhost:8090/tasks/list
|
||||
curl http://localhost:8090/tasks/list?limit=50&offset=0
|
||||
```
|
||||
|
||||
## Report JSON Structure
|
||||
|
||||
### Key Paths
|
||||
| Path | Content |
|
||||
|------|---------|
|
||||
| `info.score` | Threat score (0-10) |
|
||||
| `info.duration` | Analysis duration (seconds) |
|
||||
| `behavior.processes` | Process tree with API calls |
|
||||
| `behavior.summary.files` | Created/modified files |
|
||||
| `behavior.summary.keys` | Modified registry keys |
|
||||
| `network.dns` | DNS resolutions |
|
||||
| `network.http` | HTTP requests |
|
||||
| `network.tcp` | TCP connections |
|
||||
| `dropped` | Dropped files with hashes |
|
||||
| `signatures` | Triggered behavioral signatures |
|
||||
|
||||
### Signature Severity Levels
|
||||
| Level | Meaning |
|
||||
|-------|---------|
|
||||
| 1 | Informational |
|
||||
| 2 | Low |
|
||||
| 3 | Medium |
|
||||
| 4 | High |
|
||||
| 5 | Critical |
|
||||
|
||||
## Analysis Packages
|
||||
|
||||
| Package | File Type |
|
||||
|---------|-----------|
|
||||
| `exe` | Windows executables |
|
||||
| `dll` | DLL files (uses rundll32) |
|
||||
| `doc` | Word documents |
|
||||
| `xls` | Excel spreadsheets |
|
||||
| `pdf` | PDF documents |
|
||||
| `js` | JavaScript files |
|
||||
| `vbs` | VBScript files |
|
||||
| `ps1` | PowerShell scripts |
|
||||
| `zip` | Archives (auto-extracted) |
|
||||
|
||||
## InetSim - Network Simulation
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
inetsim --bind-address 192.168.56.1
|
||||
inetsim --report-dir /var/log/inetsim
|
||||
```
|
||||
|
||||
### Simulated Services
|
||||
- HTTP/HTTPS (ports 80, 443)
|
||||
- DNS (port 53)
|
||||
- SMTP (port 25)
|
||||
- FTP (port 21)
|
||||
- IRC (port 6667)
|
||||
|
||||
## FakeNet-NG - Network Redirection
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
fakenet
|
||||
fakenet -c custom_config.ini
|
||||
```
|
||||
|
||||
## Volatility Integration
|
||||
|
||||
### Syntax
|
||||
```bash
|
||||
vol3 -f /opt/cuckoo/storage/analyses/<id>/memory.dmp windows.pslist
|
||||
vol3 -f /opt/cuckoo/storage/analyses/<id>/memory.dmp windows.malfind
|
||||
vol3 -f /opt/cuckoo/storage/analyses/<id>/memory.dmp windows.netscan
|
||||
```
|
||||
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cuckoo Sandbox behavioral analysis agent for automated malware detonation and reporting."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import hashlib
|
||||
import datetime
|
||||
|
||||
try:
|
||||
import requests
|
||||
HAS_REQUESTS = True
|
||||
except ImportError:
|
||||
HAS_REQUESTS = False
|
||||
|
||||
|
||||
CUCKOO_API = os.environ.get("CUCKOO_API", "http://localhost:8090")
|
||||
CUCKOO_STORAGE = os.environ.get("CUCKOO_STORAGE", "/opt/cuckoo/storage/analyses")
|
||||
|
||||
|
||||
def submit_file(filepath, timeout=300, machine=None, package=None):
|
||||
"""Submit a malware sample to Cuckoo via REST API."""
|
||||
if not HAS_REQUESTS:
|
||||
return None
|
||||
url = f"{CUCKOO_API}/tasks/create/file"
|
||||
files = {"file": (os.path.basename(filepath), open(filepath, "rb"))}
|
||||
data = {"timeout": timeout}
|
||||
if machine:
|
||||
data["machine"] = machine
|
||||
if package:
|
||||
data["package"] = package
|
||||
resp = requests.post(url, files=files, data=data)
|
||||
if resp.status_code == 200:
|
||||
return resp.json().get("task_id")
|
||||
return None
|
||||
|
||||
|
||||
def submit_url(url_to_analyze, timeout=300):
|
||||
"""Submit a URL to Cuckoo for analysis."""
|
||||
if not HAS_REQUESTS:
|
||||
return None
|
||||
url = f"{CUCKOO_API}/tasks/create/url"
|
||||
data = {"url": url_to_analyze, "timeout": timeout}
|
||||
resp = requests.post(url, data=data)
|
||||
if resp.status_code == 200:
|
||||
return resp.json().get("task_id")
|
||||
return None
|
||||
|
||||
|
||||
def get_task_status(task_id):
|
||||
"""Check the status of a Cuckoo analysis task."""
|
||||
if not HAS_REQUESTS:
|
||||
return None
|
||||
url = f"{CUCKOO_API}/tasks/view/{task_id}"
|
||||
resp = requests.get(url)
|
||||
if resp.status_code == 200:
|
||||
return resp.json().get("task", {}).get("status")
|
||||
return None
|
||||
|
||||
|
||||
def load_report(task_id, report_dir=None):
|
||||
"""Load a Cuckoo JSON report from disk."""
|
||||
if report_dir is None:
|
||||
report_dir = CUCKOO_STORAGE
|
||||
report_path = os.path.join(report_dir, str(task_id), "reports", "report.json")
|
||||
if os.path.exists(report_path):
|
||||
with open(report_path, "r") as f:
|
||||
return json.load(f)
|
||||
return None
|
||||
|
||||
|
||||
def analyze_processes(report):
|
||||
"""Extract and analyze the process tree from the Cuckoo report."""
|
||||
processes = []
|
||||
for proc in report.get("behavior", {}).get("processes", []):
|
||||
pid = proc.get("pid")
|
||||
ppid = proc.get("ppid")
|
||||
name = proc.get("process_name")
|
||||
suspicious_apis = []
|
||||
dangerous_apis = [
|
||||
"CreateRemoteThread", "VirtualAllocEx", "WriteProcessMemory",
|
||||
"NtCreateThreadEx", "RegSetValueExA", "URLDownloadToFileA",
|
||||
"ShellExecuteA", "ShellExecuteW", "WinExec", "CreateProcessA",
|
||||
"NtWriteVirtualMemory", "QueueUserAPC",
|
||||
]
|
||||
for call in proc.get("calls", []):
|
||||
if call.get("api") in dangerous_apis:
|
||||
args = {arg["name"]: arg["value"] for arg in call.get("arguments", [])}
|
||||
suspicious_apis.append({"api": call["api"], "args": args})
|
||||
processes.append({
|
||||
"pid": pid,
|
||||
"ppid": ppid,
|
||||
"name": name,
|
||||
"suspicious_api_calls": len(suspicious_apis),
|
||||
"top_suspicious": suspicious_apis[:10],
|
||||
})
|
||||
return processes
|
||||
|
||||
|
||||
def analyze_network(report):
|
||||
"""Extract network activity from the Cuckoo report."""
|
||||
network = report.get("network", {})
|
||||
return {
|
||||
"dns": [
|
||||
{"request": d.get("request"), "answers": d.get("answers", [])}
|
||||
for d in network.get("dns", [])
|
||||
],
|
||||
"http": [
|
||||
{"method": h.get("method"), "host": h.get("host"),
|
||||
"uri": h.get("uri"), "body_size": len(h.get("body", ""))}
|
||||
for h in network.get("http", [])
|
||||
],
|
||||
"tcp_connections": [
|
||||
{"src": t.get("src"), "sport": t.get("sport"),
|
||||
"dst": t.get("dst"), "dport": t.get("dport")}
|
||||
for t in network.get("tcp", [])
|
||||
],
|
||||
"udp_connections": [
|
||||
{"src": u.get("src"), "sport": u.get("sport"),
|
||||
"dst": u.get("dst"), "dport": u.get("dport")}
|
||||
for u in network.get("udp", [])
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def analyze_dropped_files(report):
|
||||
"""Extract dropped file information from the report."""
|
||||
dropped = []
|
||||
for d in report.get("dropped", []):
|
||||
dropped.append({
|
||||
"filepath": d.get("filepath", ""),
|
||||
"sha256": d.get("sha256", ""),
|
||||
"size": d.get("size", 0),
|
||||
"type": d.get("type", ""),
|
||||
})
|
||||
return dropped
|
||||
|
||||
|
||||
def analyze_signatures(report):
|
||||
"""Extract triggered behavioral signatures."""
|
||||
signatures = []
|
||||
for sig in report.get("signatures", []):
|
||||
marks = []
|
||||
for mark in sig.get("marks", []):
|
||||
if mark.get("ioc"):
|
||||
marks.append(mark["ioc"])
|
||||
elif mark.get("call"):
|
||||
marks.append(mark["call"].get("api", ""))
|
||||
signatures.append({
|
||||
"name": sig.get("name"),
|
||||
"severity": sig.get("severity"),
|
||||
"description": sig.get("description"),
|
||||
"marks": marks[:5],
|
||||
})
|
||||
return sorted(signatures, key=lambda x: x.get("severity", 0), reverse=True)
|
||||
|
||||
|
||||
def analyze_registry(report):
|
||||
"""Extract registry modifications from behavior summary."""
|
||||
summary = report.get("behavior", {}).get("summary", {})
|
||||
return {
|
||||
"keys_modified": summary.get("keys", [])[:20],
|
||||
"files_created": summary.get("files", [])[:20],
|
||||
"mutexes": summary.get("mutexes", [])[:10],
|
||||
}
|
||||
|
||||
|
||||
def generate_summary(report, processes, network, dropped, signatures, registry):
|
||||
"""Generate a consolidated analysis summary."""
|
||||
info = report.get("info", {})
|
||||
score = info.get("score", 0)
|
||||
return {
|
||||
"task_id": info.get("id"),
|
||||
"sample": info.get("category", "file"),
|
||||
"analysis_time": info.get("duration", 0),
|
||||
"machine": info.get("machine", {}).get("name", ""),
|
||||
"threat_score": score,
|
||||
"process_count": len(processes),
|
||||
"suspicious_api_total": sum(p["suspicious_api_calls"] for p in processes),
|
||||
"dns_queries": len(network["dns"]),
|
||||
"http_requests": len(network["http"]),
|
||||
"tcp_connections": len(network["tcp_connections"]),
|
||||
"dropped_files": len(dropped),
|
||||
"signatures_triggered": len(signatures),
|
||||
"high_severity_sigs": len([s for s in signatures if s["severity"] >= 3]),
|
||||
"registry_keys_modified": len(registry["keys_modified"]),
|
||||
"files_created": len(registry["files_created"]),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Cuckoo Sandbox Behavioral Analysis Agent")
|
||||
print("Automated malware detonation and report parsing")
|
||||
print("=" * 60)
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
arg = sys.argv[1]
|
||||
|
||||
# Check if argument is a report JSON path
|
||||
if arg.endswith(".json") and os.path.exists(arg):
|
||||
print(f"\n[*] Loading report: {arg}")
|
||||
with open(arg, "r") as f:
|
||||
report = json.load(f)
|
||||
elif arg.isdigit():
|
||||
print(f"\n[*] Loading report for task ID: {arg}")
|
||||
report = load_report(int(arg))
|
||||
elif os.path.exists(arg):
|
||||
print(f"\n[*] Submitting sample: {arg}")
|
||||
sha256 = hashlib.sha256(open(arg, "rb").read()).hexdigest()
|
||||
print(f"[*] SHA-256: {sha256}")
|
||||
task_id = submit_file(arg)
|
||||
if task_id:
|
||||
print(f"[*] Task submitted: ID={task_id}")
|
||||
print(f"[*] Monitor at: {CUCKOO_API.replace('8090', '8080')}/analysis/{task_id}/")
|
||||
else:
|
||||
print("[ERROR] Failed to submit. Check Cuckoo API connection.")
|
||||
sys.exit(0)
|
||||
else:
|
||||
report = None
|
||||
|
||||
if report:
|
||||
processes = analyze_processes(report)
|
||||
network = analyze_network(report)
|
||||
dropped = analyze_dropped_files(report)
|
||||
signatures = analyze_signatures(report)
|
||||
registry = analyze_registry(report)
|
||||
summary = generate_summary(report, processes, network, dropped, signatures, registry)
|
||||
|
||||
print(f"\n--- Analysis Summary ---")
|
||||
print(f" Score: {summary['threat_score']}/10")
|
||||
print(f" Processes: {summary['process_count']}")
|
||||
print(f" Suspicious APIs: {summary['suspicious_api_total']}")
|
||||
print(f" Signatures: {summary['signatures_triggered']} "
|
||||
f"({summary['high_severity_sigs']} high severity)")
|
||||
|
||||
print(f"\n--- Network ---")
|
||||
print(f" DNS: {summary['dns_queries']}, HTTP: {summary['http_requests']}, "
|
||||
f"TCP: {summary['tcp_connections']}")
|
||||
for http in network["http"][:5]:
|
||||
print(f" {http['method']} {http['host']}{http['uri']}")
|
||||
|
||||
print(f"\n--- Dropped Files ---")
|
||||
for d in dropped[:5]:
|
||||
print(f" {d['filepath']} ({d['size']} bytes)")
|
||||
|
||||
print(f"\n--- Top Signatures ---")
|
||||
for s in signatures[:5]:
|
||||
print(f" [{s['severity']}/5] {s['name']}: {s['description']}")
|
||||
else:
|
||||
print(f"\n[DEMO] Usage:")
|
||||
print(f" python agent.py <sample.exe> # Submit to Cuckoo")
|
||||
print(f" python agent.py <task_id> # Parse existing report")
|
||||
print(f" python agent.py <report.json> # Parse JSON report file")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,99 @@
|
||||
# API Reference: Volatility 3 Memory Forensics
|
||||
|
||||
## Core Syntax
|
||||
```bash
|
||||
vol3 -f <memory_dump> <plugin> [options]
|
||||
vol3 -f memory.dmp --help # List all plugins
|
||||
vol3 -f memory.dmp <plugin> --help # Plugin-specific help
|
||||
```
|
||||
|
||||
## Windows Plugins
|
||||
|
||||
### Process Analysis
|
||||
| Plugin | Purpose |
|
||||
|--------|---------|
|
||||
| `windows.pslist` | List active processes |
|
||||
| `windows.pstree` | Process tree (parent-child) |
|
||||
| `windows.psscan` | Pool-tag scan (finds hidden processes) |
|
||||
| `windows.cmdline` | Process command-line arguments |
|
||||
| `windows.envars` | Process environment variables |
|
||||
| `windows.handles` | Process handle table |
|
||||
|
||||
### Code Injection Detection
|
||||
| Plugin | Purpose |
|
||||
|--------|---------|
|
||||
| `windows.malfind` | Detect injected code (RWX memory + PE headers) |
|
||||
| `windows.hollowfind` | Detect process hollowing |
|
||||
| `windows.dlllist` | List loaded DLLs per process |
|
||||
| `windows.ldrmodules` | Detect unlinked DLLs |
|
||||
|
||||
### Network
|
||||
| Plugin | Purpose |
|
||||
|--------|---------|
|
||||
| `windows.netscan` | List network connections and listeners |
|
||||
| `windows.netstat` | Network connections (older Windows) |
|
||||
|
||||
### Kernel / Rootkit
|
||||
| Plugin | Purpose |
|
||||
|--------|---------|
|
||||
| `windows.ssdt` | System Service Descriptor Table hooks |
|
||||
| `windows.callbacks` | Kernel callback registrations |
|
||||
| `windows.driverscan` | Scan for driver objects |
|
||||
| `windows.modules` | Loaded kernel modules |
|
||||
| `windows.idt` | Interrupt Descriptor Table |
|
||||
|
||||
### Credentials
|
||||
| Plugin | Purpose |
|
||||
|--------|---------|
|
||||
| `windows.hashdump` | Dump SAM password hashes |
|
||||
| `windows.cachedump` | Dump cached domain credentials |
|
||||
| `windows.lsadump` | Dump LSA secrets |
|
||||
|
||||
### Registry
|
||||
| Plugin | Purpose |
|
||||
|--------|---------|
|
||||
| `windows.registry.printkey` | Print registry key values |
|
||||
| `windows.registry.hivelist` | List registry hives |
|
||||
| `windows.registry.certificates` | Extract certificates |
|
||||
|
||||
### File System
|
||||
| Plugin | Purpose |
|
||||
|--------|---------|
|
||||
| `windows.filescan` | Scan for file objects |
|
||||
| `windows.dumpfiles` | Extract files from memory |
|
||||
| `windows.memmap` | Dump process memory |
|
||||
|
||||
### YARA Scanning
|
||||
```bash
|
||||
vol3 -f memory.dmp yarascan.YaraScan --yara-file rules.yar
|
||||
vol3 -f memory.dmp yarascan.YaraScan --yara-file rules.yar --pid 2184
|
||||
vol3 -f memory.dmp yarascan.YaraScan --yara-rules "rule Test { strings: $s = \"cmd.exe\" condition: $s }"
|
||||
```
|
||||
|
||||
### Timeline
|
||||
```bash
|
||||
vol3 -f memory.dmp timeliner.Timeliner --output-file timeline.csv
|
||||
```
|
||||
|
||||
## Output Options
|
||||
```bash
|
||||
vol3 -f memory.dmp windows.pslist --output csv > processes.csv
|
||||
vol3 -f memory.dmp windows.pslist --output json > processes.json
|
||||
vol3 -f memory.dmp windows.malfind --dump --pid 2184
|
||||
```
|
||||
|
||||
## Memory Acquisition Tools
|
||||
|
||||
| Tool | Platform | Command |
|
||||
|------|----------|---------|
|
||||
| WinPmem | Windows | `winpmem_mini_x64.exe memdump.raw` |
|
||||
| DumpIt | Windows | `DumpIt.exe` (interactive) |
|
||||
| LiME | Linux | `insmod lime.ko "path=/tmp/mem.lime format=lime"` |
|
||||
| AVML | Linux | `avml /tmp/memory.lime` |
|
||||
|
||||
## Symbols
|
||||
```bash
|
||||
# Download symbol packs
|
||||
# https://downloads.volatilityfoundation.org/volatility3/symbols/
|
||||
# Place in: volatility3/symbols/
|
||||
```
|
||||
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Memory forensics agent using Volatility 3 for malware detection in RAM dumps."""
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import csv
|
||||
import re
|
||||
import io
|
||||
|
||||
|
||||
def run_vol3(memory_dump, plugin, extra_args=""):
|
||||
"""Execute a Volatility 3 plugin and return output."""
|
||||
cmd = f"vol3 -f {memory_dump} {plugin} {extra_args}"
|
||||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=300)
|
||||
return result.stdout.strip(), result.stderr.strip(), result.returncode
|
||||
|
||||
|
||||
def get_os_info(memory_dump):
|
||||
"""Identify the OS from the memory dump."""
|
||||
stdout, _, rc = run_vol3(memory_dump, "windows.info")
|
||||
if rc == 0:
|
||||
return {"os": "windows", "info": stdout}
|
||||
stdout, _, rc = run_vol3(memory_dump, "linux.info")
|
||||
if rc == 0:
|
||||
return {"os": "linux", "info": stdout}
|
||||
return {"os": "unknown", "info": ""}
|
||||
|
||||
|
||||
def list_processes(memory_dump):
|
||||
"""List all running processes using pslist."""
|
||||
stdout, _, rc = run_vol3(memory_dump, "windows.pslist")
|
||||
processes = []
|
||||
if rc == 0:
|
||||
for line in stdout.splitlines()[2:]:
|
||||
parts = line.split()
|
||||
if len(parts) >= 6 and parts[0].isdigit():
|
||||
processes.append({
|
||||
"pid": int(parts[0]),
|
||||
"ppid": int(parts[1]),
|
||||
"name": parts[4] if len(parts) > 4 else "",
|
||||
"offset": parts[0] if not parts[0].isdigit() else "",
|
||||
})
|
||||
return processes
|
||||
|
||||
|
||||
def scan_hidden_processes(memory_dump):
|
||||
"""Scan for hidden/unlinked processes using psscan."""
|
||||
stdout, _, rc = run_vol3(memory_dump, "windows.psscan")
|
||||
processes = []
|
||||
if rc == 0:
|
||||
for line in stdout.splitlines()[2:]:
|
||||
parts = line.split()
|
||||
if len(parts) >= 5 and parts[1].isdigit():
|
||||
processes.append({
|
||||
"offset": parts[0],
|
||||
"pid": int(parts[1]),
|
||||
"ppid": int(parts[2]) if parts[2].isdigit() else 0,
|
||||
"name": parts[4] if len(parts) > 4 else "",
|
||||
})
|
||||
return processes
|
||||
|
||||
|
||||
def find_hidden_processes(pslist_procs, psscan_procs):
|
||||
"""Compare pslist and psscan to identify DKOM-hidden processes."""
|
||||
pslist_pids = {p["pid"] for p in pslist_procs}
|
||||
hidden = [p for p in psscan_procs if p["pid"] not in pslist_pids and p["pid"] > 4]
|
||||
return hidden
|
||||
|
||||
|
||||
def detect_code_injection(memory_dump, pid=None):
|
||||
"""Detect injected code using malfind plugin."""
|
||||
extra = f"--pid {pid}" if pid else ""
|
||||
stdout, _, rc = run_vol3(memory_dump, "windows.malfind", extra)
|
||||
injections = []
|
||||
if rc == 0:
|
||||
current = {}
|
||||
for line in stdout.splitlines():
|
||||
if "PID" in line and "Process" in line:
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) >= 4 and parts[0].isdigit():
|
||||
if current:
|
||||
injections.append(current)
|
||||
current = {
|
||||
"pid": int(parts[0]),
|
||||
"process": parts[1] if len(parts) > 1 else "",
|
||||
"address": parts[2] if len(parts) > 2 else "",
|
||||
"protection": parts[3] if len(parts) > 3 else "",
|
||||
}
|
||||
elif current and line.strip():
|
||||
current["data_preview"] = current.get("data_preview", "") + line.strip() + " "
|
||||
if current:
|
||||
injections.append(current)
|
||||
return injections
|
||||
|
||||
|
||||
def get_network_connections(memory_dump):
|
||||
"""Extract network connections using netscan."""
|
||||
stdout, _, rc = run_vol3(memory_dump, "windows.netscan")
|
||||
connections = []
|
||||
if rc == 0:
|
||||
for line in stdout.splitlines()[2:]:
|
||||
parts = line.split()
|
||||
if len(parts) >= 7:
|
||||
connections.append({
|
||||
"protocol": parts[1] if len(parts) > 1 else "",
|
||||
"local_addr": parts[2] if len(parts) > 2 else "",
|
||||
"local_port": parts[3] if len(parts) > 3 else "",
|
||||
"foreign_addr": parts[4] if len(parts) > 4 else "",
|
||||
"foreign_port": parts[5] if len(parts) > 5 else "",
|
||||
"state": parts[6] if len(parts) > 6 else "",
|
||||
"pid": parts[7] if len(parts) > 7 else "",
|
||||
"owner": parts[8] if len(parts) > 8 else "",
|
||||
})
|
||||
return connections
|
||||
|
||||
|
||||
def get_command_lines(memory_dump):
|
||||
"""Extract process command lines."""
|
||||
stdout, _, rc = run_vol3(memory_dump, "windows.cmdline")
|
||||
cmdlines = []
|
||||
if rc == 0:
|
||||
for line in stdout.splitlines()[2:]:
|
||||
parts = line.split(None, 2)
|
||||
if len(parts) >= 3 and parts[0].isdigit():
|
||||
cmdlines.append({
|
||||
"pid": int(parts[0]),
|
||||
"process": parts[1],
|
||||
"cmdline": parts[2],
|
||||
})
|
||||
return cmdlines
|
||||
|
||||
|
||||
def dump_credentials(memory_dump):
|
||||
"""Extract cached credentials using hashdump and lsadump."""
|
||||
results = {}
|
||||
stdout, _, rc = run_vol3(memory_dump, "windows.hashdump")
|
||||
if rc == 0:
|
||||
results["hashdump"] = stdout
|
||||
stdout, _, rc = run_vol3(memory_dump, "windows.cachedump")
|
||||
if rc == 0:
|
||||
results["cachedump"] = stdout
|
||||
stdout, _, rc = run_vol3(memory_dump, "windows.lsadump")
|
||||
if rc == 0:
|
||||
results["lsadump"] = stdout
|
||||
return results
|
||||
|
||||
|
||||
def scan_with_yara(memory_dump, yara_file=None, yara_rule=None, pid=None):
|
||||
"""Scan memory with YARA rules."""
|
||||
extra = ""
|
||||
if yara_file:
|
||||
extra += f"--yara-file {yara_file}"
|
||||
elif yara_rule:
|
||||
extra += f'--yara-rules "{yara_rule}"'
|
||||
if pid:
|
||||
extra += f" --pid {pid}"
|
||||
stdout, _, rc = run_vol3(memory_dump, "yarascan.YaraScan", extra)
|
||||
return stdout if rc == 0 else ""
|
||||
|
||||
|
||||
def check_suspicious_processes(pslist_procs):
|
||||
"""Check process list for common suspicious indicators."""
|
||||
findings = []
|
||||
expected_parents = {
|
||||
"svchost.exe": ["services.exe"],
|
||||
"csrss.exe": ["smss.exe"],
|
||||
"lsass.exe": ["wininit.exe"],
|
||||
"smss.exe": ["System"],
|
||||
}
|
||||
name_counts = {}
|
||||
for p in pslist_procs:
|
||||
name = p["name"].lower()
|
||||
name_counts[name] = name_counts.get(name, 0) + 1
|
||||
|
||||
if name_counts.get("lsass.exe", 0) > 1:
|
||||
findings.append({"severity": "CRITICAL",
|
||||
"finding": "Multiple lsass.exe instances detected"})
|
||||
|
||||
misspellings = {
|
||||
"scvhost.exe": "svchost.exe", "svch0st.exe": "svchost.exe",
|
||||
"lssas.exe": "lsass.exe", "csrs.exe": "csrss.exe",
|
||||
}
|
||||
for p in pslist_procs:
|
||||
if p["name"].lower() in misspellings:
|
||||
findings.append({
|
||||
"severity": "HIGH",
|
||||
"finding": f"Misspelled process: {p['name']} (PID {p['pid']}) "
|
||||
f"mimicking {misspellings[p['name'].lower()]}",
|
||||
})
|
||||
return findings
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Memory Forensics Agent (Volatility 3)")
|
||||
print("Process analysis, injection detection, credential extraction")
|
||||
print("=" * 60)
|
||||
|
||||
dump_file = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
|
||||
if dump_file and os.path.exists(dump_file):
|
||||
print(f"\n[*] Analyzing memory dump: {dump_file}")
|
||||
print(f"[*] Size: {os.path.getsize(dump_file) / (1024**3):.1f} GB")
|
||||
|
||||
print("\n--- OS Identification ---")
|
||||
os_info = get_os_info(dump_file)
|
||||
print(f" OS: {os_info['os']}")
|
||||
|
||||
print("\n--- Process Analysis ---")
|
||||
procs = list_processes(dump_file)
|
||||
print(f" Active processes: {len(procs)}")
|
||||
suspicious = check_suspicious_processes(procs)
|
||||
for s in suspicious:
|
||||
print(f" [{s['severity']}] {s['finding']}")
|
||||
|
||||
print("\n--- Hidden Process Detection ---")
|
||||
psscan = scan_hidden_processes(dump_file)
|
||||
hidden = find_hidden_processes(procs, psscan)
|
||||
if hidden:
|
||||
for h in hidden:
|
||||
print(f" [!] Hidden process: {h['name']} PID={h['pid']}")
|
||||
else:
|
||||
print(" No hidden processes detected")
|
||||
|
||||
print("\n--- Code Injection Detection ---")
|
||||
injections = detect_code_injection(dump_file)
|
||||
print(f" Injected regions: {len(injections)}")
|
||||
for inj in injections[:5]:
|
||||
print(f" [!] PID {inj['pid']} ({inj.get('process', '')}): {inj.get('protection', '')}")
|
||||
|
||||
print("\n--- Network Connections ---")
|
||||
conns = get_network_connections(dump_file)
|
||||
established = [c for c in conns if "ESTABLISHED" in c.get("state", "")]
|
||||
print(f" Total: {len(conns)}, Established: {len(established)}")
|
||||
for c in established[:10]:
|
||||
print(f" {c.get('owner', '?')} (PID {c.get('pid', '?')}): "
|
||||
f"{c['local_addr']}:{c['local_port']} -> "
|
||||
f"{c['foreign_addr']}:{c['foreign_port']}")
|
||||
else:
|
||||
print(f"\n[DEMO] Usage: python agent.py <memory.dmp>")
|
||||
print("[*] Provide a memory dump for forensic analysis.")
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: analyzing-memory-forensics-with-lime-and-volatility
|
||||
description: >
|
||||
Performs Linux memory acquisition using LiME (Linux Memory Extractor) kernel module
|
||||
and analysis with Volatility 3 framework. Extracts process lists, network connections,
|
||||
bash history, loaded kernel modules, and injected code from Linux memory images.
|
||||
Use when performing incident response on compromised Linux systems.
|
||||
---
|
||||
|
||||
# Analyzing Memory Forensics with LiME and Volatility
|
||||
|
||||
## Instructions
|
||||
|
||||
Acquire Linux memory using LiME kernel module, then analyze with Volatility 3
|
||||
to extract forensic artifacts from the memory image.
|
||||
|
||||
```bash
|
||||
# LiME acquisition
|
||||
insmod lime-$(uname -r).ko "path=/evidence/memory.lime format=lime"
|
||||
|
||||
# Volatility 3 analysis
|
||||
vol3 -f /evidence/memory.lime linux.pslist
|
||||
vol3 -f /evidence/memory.lime linux.bash
|
||||
vol3 -f /evidence/memory.lime linux.sockstat
|
||||
```
|
||||
|
||||
```python
|
||||
import volatility3
|
||||
from volatility3.framework import contexts, automagic
|
||||
from volatility3.plugins.linux import pslist, bash, sockstat
|
||||
|
||||
# Programmatic Volatility 3 usage
|
||||
context = contexts.Context()
|
||||
automagics = automagic.available(context)
|
||||
```
|
||||
|
||||
Key analysis steps:
|
||||
1. Acquire memory with LiME (format=lime or format=raw)
|
||||
2. List processes with linux.pslist, compare with linux.psscan
|
||||
3. Extract bash command history with linux.bash
|
||||
4. List network connections with linux.sockstat
|
||||
5. Check loaded kernel modules with linux.lsmod for rootkits
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Full forensic workflow
|
||||
vol3 -f memory.lime linux.pslist | grep -v "\[kthread\]"
|
||||
vol3 -f memory.lime linux.bash
|
||||
vol3 -f memory.lime linux.malfind
|
||||
vol3 -f memory.lime linux.lsmod
|
||||
```
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
# API Reference: Analyzing Memory Forensics with LiME and Volatility
|
||||
|
||||
## LiME (Linux Memory Extractor)
|
||||
|
||||
```bash
|
||||
# Build LiME module
|
||||
cd LiME/src && make
|
||||
|
||||
# Acquire memory (lime format - includes metadata)
|
||||
insmod lime-$(uname -r).ko "path=/evidence/mem.lime format=lime"
|
||||
|
||||
# Acquire memory (raw format)
|
||||
insmod lime-$(uname -r).ko "path=/evidence/mem.raw format=raw"
|
||||
|
||||
# Acquire over network
|
||||
insmod lime.ko "path=tcp:4444 format=lime"
|
||||
# On forensic workstation: nc target 4444 > mem.lime
|
||||
```
|
||||
|
||||
## Volatility 3 Linux Plugins
|
||||
|
||||
| Plugin | Description |
|
||||
|--------|-------------|
|
||||
| `linux.pslist` | List processes via task_struct |
|
||||
| `linux.psscan` | Brute-force scan for task_struct |
|
||||
| `linux.bash` | Recovered bash command history |
|
||||
| `linux.sockstat` | Network connections |
|
||||
| `linux.lsmod` | Loaded kernel modules |
|
||||
| `linux.malfind` | Detect injected code |
|
||||
| `linux.check_afinfo` | Detect network hooking |
|
||||
| `linux.tty_check` | Detect TTY hooking |
|
||||
| `linux.proc.Maps` | Process memory maps |
|
||||
|
||||
## Volatility 3 CLI
|
||||
|
||||
```bash
|
||||
vol3 -f memory.lime linux.pslist
|
||||
vol3 -f memory.lime linux.bash
|
||||
vol3 -f memory.lime linux.sockstat
|
||||
vol3 -f memory.lime linux.malfind
|
||||
vol3 -f memory.lime linux.lsmod
|
||||
vol3 -f memory.lime linux.check_afinfo
|
||||
```
|
||||
|
||||
## Hidden Process Detection
|
||||
|
||||
```bash
|
||||
# Compare pslist (linked list) vs psscan (brute force)
|
||||
vol3 -f mem.lime linux.pslist > pslist.txt
|
||||
vol3 -f mem.lime linux.psscan > psscan.txt
|
||||
diff pslist.txt psscan.txt
|
||||
```
|
||||
|
||||
### References
|
||||
|
||||
- LiME: https://github.com/504ensicsLabs/LiME
|
||||
- Volatility 3: https://github.com/volatilityfoundation/volatility3
|
||||
- Volatility 3 docs: https://volatility3.readthedocs.io/
|
||||
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Agent for Linux memory forensics using LiME acquisition and Volatility 3."""
|
||||
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def acquire_memory_lime(output_path, lime_format="lime"):
|
||||
"""Acquire memory using LiME kernel module."""
|
||||
kernel_version = subprocess.run(
|
||||
["uname", "-r"], capture_output=True, text=True
|
||||
).stdout.strip()
|
||||
lime_module = f"lime-{kernel_version}.ko"
|
||||
if not Path(lime_module).exists():
|
||||
lime_module = "lime.ko"
|
||||
cmd = ["insmod", lime_module, f"path={output_path}", f"format={lime_format}"]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
return {
|
||||
"status": "success" if result.returncode == 0 else "failed",
|
||||
"output_path": output_path,
|
||||
"format": lime_format,
|
||||
"kernel": kernel_version,
|
||||
"stderr": result.stderr,
|
||||
}
|
||||
|
||||
|
||||
def run_vol3_plugin(image_path, plugin_name, extra_args=None):
|
||||
"""Run a Volatility 3 plugin and capture output."""
|
||||
cmd = ["vol3", "-f", image_path, plugin_name]
|
||||
if extra_args:
|
||||
cmd.extend(extra_args)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=300,
|
||||
)
|
||||
lines = result.stdout.strip().splitlines()
|
||||
return {"plugin": plugin_name, "output": lines, "error": result.stderr.strip()}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"plugin": plugin_name, "output": [], "error": "Timeout"}
|
||||
|
||||
|
||||
def parse_pslist_output(lines):
|
||||
"""Parse Volatility linux.pslist output into structured data."""
|
||||
processes = []
|
||||
for line in lines:
|
||||
parts = line.split()
|
||||
if len(parts) >= 4 and parts[0].isdigit():
|
||||
processes.append({
|
||||
"pid": int(parts[0]),
|
||||
"ppid": int(parts[1]) if parts[1].isdigit() else 0,
|
||||
"name": parts[-1],
|
||||
})
|
||||
return processes
|
||||
|
||||
|
||||
def list_processes(image_path):
|
||||
"""List all processes from memory image."""
|
||||
result = run_vol3_plugin(image_path, "linux.pslist")
|
||||
return parse_pslist_output(result.get("output", []))
|
||||
|
||||
|
||||
def extract_bash_history(image_path):
|
||||
"""Extract bash command history from memory."""
|
||||
result = run_vol3_plugin(image_path, "linux.bash")
|
||||
commands = []
|
||||
for line in result.get("output", []):
|
||||
parts = line.split(None, 3)
|
||||
if len(parts) >= 4 and parts[0].isdigit():
|
||||
commands.append({
|
||||
"pid": int(parts[0]),
|
||||
"name": parts[1],
|
||||
"timestamp": parts[2] if len(parts) > 2 else "",
|
||||
"command": parts[3] if len(parts) > 3 else "",
|
||||
})
|
||||
return commands
|
||||
|
||||
|
||||
def list_network_connections(image_path):
|
||||
"""List network connections from memory."""
|
||||
result = run_vol3_plugin(image_path, "linux.sockstat")
|
||||
connections = []
|
||||
for line in result.get("output", []):
|
||||
if "TCP" in line or "UDP" in line:
|
||||
connections.append(line.strip())
|
||||
return connections
|
||||
|
||||
|
||||
def list_kernel_modules(image_path):
|
||||
"""List loaded kernel modules to detect rootkits."""
|
||||
result = run_vol3_plugin(image_path, "linux.lsmod")
|
||||
modules = []
|
||||
for line in result.get("output", []):
|
||||
parts = line.split()
|
||||
if parts and not parts[0].startswith("Offset"):
|
||||
modules.append({"name": parts[-1] if parts else line.strip()})
|
||||
return modules
|
||||
|
||||
|
||||
def detect_hidden_processes(image_path):
|
||||
"""Compare pslist vs psscan to find hidden processes."""
|
||||
pslist = run_vol3_plugin(image_path, "linux.pslist")
|
||||
psscan = run_vol3_plugin(image_path, "linux.psscan")
|
||||
pslist_pids = set()
|
||||
for line in pslist.get("output", []):
|
||||
parts = line.split()
|
||||
if parts and parts[0].isdigit():
|
||||
pslist_pids.add(int(parts[0]))
|
||||
hidden = []
|
||||
for line in psscan.get("output", []):
|
||||
parts = line.split()
|
||||
if parts and parts[0].isdigit():
|
||||
pid = int(parts[0])
|
||||
if pid not in pslist_pids and pid > 0:
|
||||
hidden.append({"pid": pid, "line": line.strip()})
|
||||
return hidden
|
||||
|
||||
|
||||
def detect_suspicious_commands(bash_history):
|
||||
"""Flag suspicious commands in bash history."""
|
||||
suspicious_patterns = [
|
||||
"curl.*|.*sh", "wget.*&&.*chmod", "base64.*-d",
|
||||
"nc.*-e", "python.*-c.*import.*socket",
|
||||
"nohup", "rm.*-rf.*/var/log", "history.*-c",
|
||||
"iptables.*-F", "chmod.*777", "chattr.*-i",
|
||||
]
|
||||
import re
|
||||
findings = []
|
||||
for entry in bash_history:
|
||||
cmd = entry.get("command", "")
|
||||
for pattern in suspicious_patterns:
|
||||
if re.search(pattern, cmd, re.IGNORECASE):
|
||||
findings.append({
|
||||
"pid": entry["pid"],
|
||||
"command": cmd,
|
||||
"pattern": pattern,
|
||||
"severity": "HIGH",
|
||||
})
|
||||
break
|
||||
return findings
|
||||
|
||||
|
||||
def check_malfind(image_path):
|
||||
"""Run malfind to detect injected code."""
|
||||
result = run_vol3_plugin(image_path, "linux.malfind")
|
||||
return result.get("output", [])
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="LiME + Volatility 3 Forensics Agent")
|
||||
parser.add_argument("--image", help="Path to memory image")
|
||||
parser.add_argument("--acquire", help="Output path for LiME acquisition")
|
||||
parser.add_argument("--output", default="memory_forensics_report.json")
|
||||
parser.add_argument("--action", choices=[
|
||||
"acquire", "pslist", "bash", "network", "modules",
|
||||
"hidden", "malfind", "full_analysis"
|
||||
], default="full_analysis")
|
||||
args = parser.parse_args()
|
||||
|
||||
report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}}
|
||||
|
||||
if args.action == "acquire" and args.acquire:
|
||||
result = acquire_memory_lime(args.acquire)
|
||||
report["findings"]["acquisition"] = result
|
||||
print(f"[+] Memory acquisition: {result['status']}")
|
||||
return
|
||||
|
||||
if not args.image:
|
||||
print("[-] --image required for analysis actions")
|
||||
return
|
||||
|
||||
if args.action in ("pslist", "full_analysis"):
|
||||
procs = list_processes(args.image)
|
||||
report["findings"]["processes"] = procs
|
||||
print(f"[+] Processes: {len(procs)}")
|
||||
|
||||
if args.action in ("bash", "full_analysis"):
|
||||
history = extract_bash_history(args.image)
|
||||
report["findings"]["bash_history"] = history
|
||||
suspicious = detect_suspicious_commands(history)
|
||||
report["findings"]["suspicious_commands"] = suspicious
|
||||
print(f"[+] Bash commands: {len(history)}, Suspicious: {len(suspicious)}")
|
||||
|
||||
if args.action in ("network", "full_analysis"):
|
||||
conns = list_network_connections(args.image)
|
||||
report["findings"]["connections"] = conns
|
||||
print(f"[+] Network connections: {len(conns)}")
|
||||
|
||||
if args.action in ("modules", "full_analysis"):
|
||||
modules = list_kernel_modules(args.image)
|
||||
report["findings"]["kernel_modules"] = modules
|
||||
print(f"[+] Kernel modules: {len(modules)}")
|
||||
|
||||
if args.action in ("hidden", "full_analysis"):
|
||||
hidden = detect_hidden_processes(args.image)
|
||||
report["findings"]["hidden_processes"] = hidden
|
||||
print(f"[+] Hidden processes: {len(hidden)}")
|
||||
|
||||
with open(args.output, "w") as f:
|
||||
json.dump(report, f, indent=2, default=str)
|
||||
print(f"[+] Report saved to {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Anthropic Agent Skills Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: analyzing-network-flow-data-with-netflow
|
||||
description: >-
|
||||
Parse NetFlow v9 and IPFIX records to detect volumetric anomalies, port scanning, data
|
||||
exfiltration, and C2 beaconing patterns. Uses the Python netflow library to decode flow
|
||||
records, builds traffic baselines, and applies statistical analysis to identify flows
|
||||
with abnormal byte counts, connection durations, and periodic timing patterns.
|
||||
---
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Install dependencies: `pip install netflow`
|
||||
2. Collect NetFlow/IPFIX data from routers or use the built-in collector: `python -m netflow.collector -p 9995`
|
||||
3. Parse captured flow data using `netflow.parse_packet()`.
|
||||
4. Analyze flows for:
|
||||
- Port scanning: single source to many destinations on same port
|
||||
- Data exfiltration: high byte-count outbound flows to unusual destinations
|
||||
- C2 beaconing: periodic connections with consistent intervals
|
||||
- Volumetric anomalies: traffic spikes beyond baseline thresholds
|
||||
5. Generate a prioritized findings report.
|
||||
|
||||
```bash
|
||||
python scripts/agent.py --flow-file captured_flows.json --output netflow_report.json
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Parse NetFlow v9 Packet
|
||||
```python
|
||||
import netflow
|
||||
data, _ = netflow.parse_packet(raw_bytes, templates={})
|
||||
for flow in data.flows:
|
||||
print(flow.IPV4_SRC_ADDR, flow.IPV4_DST_ADDR, flow.IN_BYTES)
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
# API Reference: NetFlow v9/IPFIX Analysis
|
||||
|
||||
## Python netflow Library
|
||||
```python
|
||||
import netflow
|
||||
# Parse a raw NetFlow packet
|
||||
packet, templates = netflow.parse_packet(raw_bytes, templates={})
|
||||
# templates must persist between calls for v9/IPFIX
|
||||
for flow in packet.flows:
|
||||
flow.IPV4_SRC_ADDR # Source IP
|
||||
flow.IPV4_DST_ADDR # Destination IP
|
||||
flow.L4_SRC_PORT # Source port
|
||||
flow.L4_DST_PORT # Destination port
|
||||
flow.PROTOCOL # IP protocol (6=TCP, 17=UDP)
|
||||
flow.IN_BYTES # Bytes transferred
|
||||
flow.IN_PKTS # Packet count
|
||||
flow.TCP_FLAGS # TCP flags bitmask
|
||||
flow.FIRST_SWITCHED # Flow start time
|
||||
flow.LAST_SWITCHED # Flow end time
|
||||
```
|
||||
|
||||
## CLI Tools
|
||||
```bash
|
||||
python -m netflow.collector -p 9995 -D /tmp/flows # Collector
|
||||
python -m netflow.analyzer -f /tmp/flows/*.json # Analyzer
|
||||
```
|
||||
|
||||
## NetFlow v9 Field Types
|
||||
| Field | ID | Description |
|
||||
|-------|-----|-------------|
|
||||
| IN_BYTES | 1 | Input bytes |
|
||||
| IN_PKTS | 2 | Input packets |
|
||||
| PROTOCOL | 4 | IP protocol |
|
||||
| L4_SRC_PORT | 7 | Source port |
|
||||
| IPV4_SRC_ADDR | 8 | Source IPv4 |
|
||||
| L4_DST_PORT | 11 | Destination port |
|
||||
| IPV4_DST_ADDR | 12 | Destination IPv4 |
|
||||
| TCP_FLAGS | 6 | TCP flags |
|
||||
| FIRST_SWITCHED | 22 | Flow start sysUpTime |
|
||||
| LAST_SWITCHED | 21 | Flow end sysUpTime |
|
||||
|
||||
## Detection Algorithms
|
||||
| Pattern | Method | Threshold |
|
||||
|---------|--------|-----------|
|
||||
| Port scan | Unique dst_ports per src-dst pair | >20 ports |
|
||||
| Network sweep | Unique dst_ips per source | >50 hosts |
|
||||
| Exfiltration | Total bytes per src-dst pair | >100MB |
|
||||
| C2 beaconing | Interval jitter ratio | <0.15 |
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user