Files

419 lines
17 KiB
Python

#!/usr/bin/env python3
"""
Volatile Evidence Collection Script
Collects volatile forensic evidence from a live system following
RFC 3227 order of volatility. Runs from external media.
Collects:
- Network connections and state
- Running processes with command lines
- Logged-in users and sessions
- System configuration and state
- DNS cache, ARP table, routing table
- Hashes all evidence files
Requirements:
pip install psutil
"""
import argparse
import csv
import hashlib
import json
import logging
import os
import platform
import socket
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
try:
import psutil
except ImportError:
print("Install psutil: pip install psutil")
sys.exit(1)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
logger = logging.getLogger("volatile_evidence")
class EvidenceCollector:
"""Collect volatile evidence from a live system."""
def __init__(self, output_dir: str, case_id: str):
self.case_id = case_id
self.hostname = socket.gethostname()
self.timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
self.output_dir = os.path.join(output_dir, f"{self.hostname}_{self.timestamp}")
os.makedirs(self.output_dir, exist_ok=True)
self.evidence_manifest = []
self.collection_log = []
# Start collection log
self._log_action("Collection initiated", f"Case: {case_id}, Host: {self.hostname}")
self._log_action("System time", datetime.now(timezone.utc).isoformat())
self._log_action("Platform", f"{platform.system()} {platform.release()}")
self._log_action("Collector", os.getenv("USERNAME", os.getenv("USER", "unknown")))
def _log_action(self, action: str, details: str):
entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"action": action,
"details": details,
}
self.collection_log.append(entry)
logger.info(f"{action}: {details}")
def _save_evidence(self, filename: str, content: str, category: str):
filepath = os.path.join(self.output_dir, filename)
with open(filepath, "w", encoding="utf-8", errors="replace") as f:
f.write(content)
file_hash = self._hash_file(filepath)
self.evidence_manifest.append({
"filename": filename,
"category": category,
"sha256": file_hash,
"size_bytes": os.path.getsize(filepath),
"collected_at": datetime.now(timezone.utc).isoformat(),
})
self._log_action(f"Evidence collected: {filename}", f"SHA256: {file_hash}")
return filepath
def _save_evidence_csv(self, filename: str, data: list, category: str):
if not data:
self._log_action(f"No data for {filename}", "Empty result set")
return None
filepath = os.path.join(self.output_dir, filename)
with open(filepath, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
file_hash = self._hash_file(filepath)
self.evidence_manifest.append({
"filename": filename,
"category": category,
"sha256": file_hash,
"size_bytes": os.path.getsize(filepath),
"collected_at": datetime.now(timezone.utc).isoformat(),
})
self._log_action(f"Evidence collected: {filename}", f"SHA256: {file_hash}, Rows: {len(data)}")
return filepath
@staticmethod
def _hash_file(filepath: str) -> str:
sha256 = hashlib.sha256()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
sha256.update(chunk)
return sha256.hexdigest()
def collect_network_connections(self):
"""Collect active network connections with process information."""
self._log_action("Collecting", "Network connections")
connections = []
for conn in psutil.net_connections(kind="all"):
try:
proc_name = psutil.Process(conn.pid).name() if conn.pid else "N/A"
except (psutil.NoSuchProcess, psutil.AccessDenied):
proc_name = "N/A"
connections.append({
"fd": conn.fd,
"family": str(conn.family),
"type": str(conn.type),
"local_address": f"{conn.laddr.ip}:{conn.laddr.port}" if conn.laddr else "",
"remote_address": f"{conn.raddr.ip}:{conn.raddr.port}" if conn.raddr else "",
"status": conn.status,
"pid": conn.pid or "",
"process_name": proc_name,
})
self._save_evidence_csv("network_connections.csv", connections, "network")
# Also save in text format
text_output = f"Network Connections - {datetime.now(timezone.utc).isoformat()}\n"
text_output += f"{'='*80}\n"
text_output += f"Total connections: {len(connections)}\n\n"
for conn in connections:
text_output += f"PID:{conn['pid']} ({conn['process_name']}) "
text_output += f"{conn['local_address']} -> {conn['remote_address']} "
text_output += f"[{conn['status']}]\n"
self._save_evidence("network_connections.txt", text_output, "network")
return connections
def collect_arp_table(self):
"""Collect ARP cache."""
self._log_action("Collecting", "ARP table")
try:
if platform.system() == "Windows":
result = subprocess.run(["arp", "-a"], capture_output=True, text=True, timeout=10)
else:
result = subprocess.run(["ip", "neigh"], capture_output=True, text=True, timeout=10)
self._save_evidence("arp_cache.txt", result.stdout, "network")
except Exception as e:
self._log_action("ARP collection failed", str(e))
def collect_routing_table(self):
"""Collect routing table."""
self._log_action("Collecting", "Routing table")
try:
if platform.system() == "Windows":
result = subprocess.run(["route", "print"], capture_output=True, text=True, timeout=10)
else:
result = subprocess.run(["ip", "route", "show"], capture_output=True, text=True, timeout=10)
self._save_evidence("routing_table.txt", result.stdout, "network")
except Exception as e:
self._log_action("Routing table collection failed", str(e))
def collect_dns_cache(self):
"""Collect DNS resolver cache."""
self._log_action("Collecting", "DNS cache")
try:
if platform.system() == "Windows":
result = subprocess.run(["ipconfig", "/displaydns"], capture_output=True, text=True, timeout=30)
if result.stdout:
self._save_evidence("dns_cache.txt", result.stdout, "network")
else:
# Linux - attempt systemd-resolved
result = subprocess.run(
["systemd-resolve", "--statistics"],
capture_output=True, text=True, timeout=10,
)
if result.stdout:
self._save_evidence("dns_stats.txt", result.stdout, "network")
except Exception as e:
self._log_action("DNS cache collection failed", str(e))
def collect_running_processes(self):
"""Collect detailed information about all running processes."""
self._log_action("Collecting", "Running processes")
processes = []
for proc in psutil.process_iter(
["pid", "ppid", "name", "username", "cmdline", "exe",
"create_time", "status", "cpu_percent", "memory_percent",
"num_threads", "connections"]
):
try:
info = proc.info
cmdline = " ".join(info["cmdline"]) if info["cmdline"] else ""
create_time = datetime.fromtimestamp(info["create_time"]).isoformat() if info["create_time"] else ""
num_conns = len(info["connections"]) if info["connections"] else 0
processes.append({
"pid": info["pid"],
"ppid": info["ppid"],
"name": info["name"],
"username": info["username"] or "",
"command_line": cmdline[:500],
"executable": info["exe"] or "",
"create_time": create_time,
"status": info["status"],
"cpu_percent": info["cpu_percent"],
"memory_percent": round(info["memory_percent"] or 0, 2),
"threads": info["num_threads"],
"network_connections": num_conns,
})
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
self._save_evidence_csv("running_processes.csv", processes, "processes")
# Process tree text format
tree_output = f"Process Tree - {datetime.now(timezone.utc).isoformat()}\n{'='*80}\n"
tree_output += f"Total processes: {len(processes)}\n\n"
for p in sorted(processes, key=lambda x: x["pid"]):
tree_output += f"PID:{p['pid']} PPID:{p['ppid']} User:{p['username']} "
tree_output += f"{p['name']} [{p['status']}]\n"
if p["command_line"]:
tree_output += f" CMD: {p['command_line']}\n"
self._save_evidence("process_tree.txt", tree_output, "processes")
return processes
def collect_open_files(self):
"""Collect open file handles for all processes."""
self._log_action("Collecting", "Open file handles")
open_files = []
for proc in psutil.process_iter(["pid", "name"]):
try:
for f in proc.open_files():
open_files.append({
"pid": proc.info["pid"],
"process_name": proc.info["name"],
"file_path": f.path,
"fd": f.fd,
"mode": getattr(f, "mode", ""),
})
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
if open_files:
self._save_evidence_csv("open_files.csv", open_files, "processes")
return open_files
def collect_logged_in_users(self):
"""Collect information about currently logged-in users."""
self._log_action("Collecting", "Logged-in users")
users = []
for user in psutil.users():
users.append({
"username": user.name,
"terminal": user.terminal or "",
"host": user.host or "",
"started": datetime.fromtimestamp(user.started).isoformat(),
"pid": getattr(user, "pid", ""),
})
self._save_evidence_csv("logged_in_users.csv", users, "users")
# Text format
text = f"Logged-in Users - {datetime.now(timezone.utc).isoformat()}\n{'='*80}\n"
for u in users:
text += f"User: {u['username']} | Terminal: {u['terminal']} | "
text += f"Host: {u['host']} | Since: {u['started']}\n"
self._save_evidence("logged_in_users.txt", text, "users")
return users
def collect_system_info(self):
"""Collect system configuration and state."""
self._log_action("Collecting", "System information")
boot_time = datetime.fromtimestamp(psutil.boot_time())
info = {
"hostname": self.hostname,
"platform": platform.platform(),
"architecture": platform.machine(),
"processor": platform.processor(),
"python_version": platform.python_version(),
"boot_time": boot_time.isoformat(),
"uptime_seconds": (datetime.now() - boot_time).total_seconds(),
"cpu_count_physical": psutil.cpu_count(logical=False),
"cpu_count_logical": psutil.cpu_count(logical=True),
"memory_total_gb": round(psutil.virtual_memory().total / (1024**3), 2),
"memory_used_percent": psutil.virtual_memory().percent,
"disk_partitions": [
{"device": p.device, "mountpoint": p.mountpoint, "fstype": p.fstype}
for p in psutil.disk_partitions()
],
"network_interfaces": {
name: [{"address": addr.address, "family": str(addr.family)}
for addr in addrs]
for name, addrs in psutil.net_if_addrs().items()
},
}
self._save_evidence(
"system_info.json",
json.dumps(info, indent=2),
"system",
)
return info
def collect_services(self):
"""Collect running services (platform-specific)."""
self._log_action("Collecting", "Running services")
try:
if platform.system() == "Windows":
result = subprocess.run(
["sc", "queryex", "type=", "service", "state=", "all"],
capture_output=True, text=True, timeout=30,
)
self._save_evidence("services_all.txt", result.stdout, "system")
else:
result = subprocess.run(
["systemctl", "list-units", "--type=service", "--all", "--no-pager"],
capture_output=True, text=True, timeout=30,
)
self._save_evidence("systemd_services.txt", result.stdout, "system")
except Exception as e:
self._log_action("Service collection failed", str(e))
def collect_scheduled_tasks(self):
"""Collect scheduled tasks / cron jobs."""
self._log_action("Collecting", "Scheduled tasks")
try:
if platform.system() == "Windows":
result = subprocess.run(
["schtasks", "/query", "/fo", "CSV", "/v"],
capture_output=True, text=True, timeout=30,
)
self._save_evidence("scheduled_tasks.csv", result.stdout, "system")
else:
result = subprocess.run(
["crontab", "-l"],
capture_output=True, text=True, timeout=10,
)
self._save_evidence("crontab.txt", result.stdout or "No crontab", "system")
except Exception as e:
self._log_action("Scheduled task collection failed", str(e))
def collect_environment_variables(self):
"""Collect environment variables."""
self._log_action("Collecting", "Environment variables")
env_data = "\n".join(f"{k}={v}" for k, v in sorted(os.environ.items()))
self._save_evidence("environment_variables.txt", env_data, "system")
def finalize(self):
"""Generate evidence manifest and collection log."""
# Save collection log
log_path = os.path.join(self.output_dir, "collection_log.json")
with open(log_path, "w") as f:
json.dump(self.collection_log, f, indent=2)
# Save evidence manifest
manifest_path = os.path.join(self.output_dir, "evidence_manifest.json")
manifest = {
"case_id": self.case_id,
"hostname": self.hostname,
"collection_start": self.collection_log[0]["timestamp"] if self.collection_log else "",
"collection_end": datetime.now(timezone.utc).isoformat(),
"total_evidence_items": len(self.evidence_manifest),
"evidence": self.evidence_manifest,
}
with open(manifest_path, "w") as f:
json.dump(manifest, f, indent=2)
# Generate SHA256 manifest text file
hash_manifest = os.path.join(self.output_dir, "sha256_manifest.txt")
with open(hash_manifest, "w") as f:
for item in self.evidence_manifest:
f.write(f"{item['sha256']} {item['filename']}\n")
logger.info(f"Collection complete. Evidence directory: {self.output_dir}")
logger.info(f"Total evidence items: {len(self.evidence_manifest)}")
return self.output_dir
def main():
parser = argparse.ArgumentParser(description="Volatile Evidence Collection Tool")
parser.add_argument("--case-id", required=True, help="Case/incident ID")
parser.add_argument("--output-dir", default="./evidence_collection", help="Output directory")
parser.add_argument("--skip-memory", action="store_true", help="Skip memory dump (use dedicated tool)")
parser.add_argument("--collect-all", action="store_true", help="Collect all available evidence types")
args = parser.parse_args()
collector = EvidenceCollector(args.output_dir, args.case_id)
if not args.skip_memory:
logger.info("NOTE: For memory acquisition, use dedicated tools (WinPmem/LiME) from forensic USB")
# Collect in order of volatility
collector.collect_network_connections()
collector.collect_arp_table()
collector.collect_dns_cache()
collector.collect_routing_table()
collector.collect_running_processes()
collector.collect_open_files()
collector.collect_logged_in_users()
collector.collect_system_info()
collector.collect_services()
collector.collect_scheduled_tasks()
collector.collect_environment_variables()
evidence_dir = collector.finalize()
print(f"\nEvidence collection complete")
print(f"Output directory: {evidence_dir}")
print(f"Total items: {len(collector.evidence_manifest)}")
if __name__ == "__main__":
main()