#!/usr/bin/env python3 """Agent for analyzing heap spray exploitation in memory dumps. Detects heap spray artifacts using Volatility3 by scanning for NOP sled patterns, large contiguous allocations, and injected executable regions in process virtual address space. """ # For authorized forensic analysis only import argparse import hashlib import json import os import re import subprocess from collections import defaultdict from datetime import datetime from pathlib import Path NOP_PATTERNS = { "x86_nop": b"\x90" * 16, "heap_spray_0c": b"\x0c" * 16, "heap_spray_0d": b"\x0d" * 16, "heap_spray_0a": b"\x0a" * 16, "heap_spray_04": b"\x04" * 16, "heap_spray_41": b"\x41" * 16, } SHELLCODE_MARKERS = [ b"\xfc\xe8", # CLD; CALL b"\x60\xe8", # PUSHAD; CALL b"\xeb\x10\x5a", # JMP SHORT; POP EDX b"\x31\xc0\x50\x68", # XOR EAX; PUSH; PUSH b"\xe8\xff\xff\xff\xff", # CALL $+5 (self-locating) ] SUSPICIOUS_ALLOC_THRESHOLD = 0x100000 # 1 MB class HeapSprayAnalyzer: """Detects heap spray exploitation artifacts in memory dumps.""" def __init__(self, memory_dump, output_dir="./heap_spray_analysis"): self.memory_dump = memory_dump self.output_dir = Path(output_dir) self.output_dir.mkdir(parents=True, exist_ok=True) self.findings = [] def _run_vol3(self, plugin, extra_args=None): """Run a Volatility3 plugin and return stdout.""" cmd = ["vol", "-f", self.memory_dump, plugin] if extra_args: cmd.extend(extra_args) try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) return result.stdout except (FileNotFoundError, subprocess.TimeoutExpired): return "" def run_malfind(self): """Run windows.malfind to detect injected executable memory.""" output = self._run_vol3("windows.malfind") entries = [] current = {} for line in output.splitlines(): parts = line.split() if len(parts) >= 6 and parts[0].isdigit(): if current: entries.append(current) current = { "pid": int(parts[0]), "process": parts[1], "start_addr": parts[2], "end_addr": parts[3], "protection": parts[5] if len(parts) > 5 else "", } elif current and line.strip().startswith("0x"): hex_match = re.findall(r"[0-9a-fA-F]{2}", line.split(" ")[0] if " " in line else line) if "hex_bytes" not in current: current["hex_bytes"] = "" current["hex_bytes"] += "".join(hex_match) if current: entries.append(current) return entries def run_vadinfo(self): """Run windows.vadinfo to find large suspicious allocations.""" output = self._run_vol3("windows.vadinfo") large_allocs = [] for line in output.splitlines(): parts = line.split() if len(parts) >= 5 and parts[0].isdigit(): try: pid = int(parts[0]) start = int(parts[2], 16) if parts[2].startswith("0x") else 0 end = int(parts[3], 16) if parts[3].startswith("0x") else 0 size = end - start if size >= SUSPICIOUS_ALLOC_THRESHOLD: large_allocs.append({ "pid": pid, "process": parts[1], "start": hex(start), "end": hex(end), "size_bytes": size, "size_mb": round(size / (1024 * 1024), 2), }) except (ValueError, IndexError): continue return large_allocs def scan_dump_for_patterns(self, dump_path): """Scan a memory dump file for NOP sled and shellcode patterns.""" matches = {"nop_sleds": [], "shellcode_markers": []} try: with open(dump_path, "rb") as f: data = f.read() except (FileNotFoundError, PermissionError): return matches for name, pattern in NOP_PATTERNS.items(): offset = 0 count = 0 while True: idx = data.find(pattern, offset) if idx == -1: break count += 1 offset = idx + len(pattern) if count > 100: break if count > 0: matches["nop_sleds"].append({"pattern": name, "occurrences": count}) for marker in SHELLCODE_MARKERS: idx = data.find(marker) if idx != -1: context = data[idx:idx + 64] matches["shellcode_markers"].append({ "offset": hex(idx), "bytes": context.hex()[:128], "sha256": hashlib.sha256(context).hexdigest(), }) return matches def dump_process_memory(self, pid): """Dump a process's memory using Volatility3 memmap.""" dump_dir = self.output_dir / f"pid_{pid}" dump_dir.mkdir(exist_ok=True) self._run_vol3("windows.memmap", ["--pid", str(pid), "--dump", "--output-dir", str(dump_dir)]) dumps = list(dump_dir.glob("*.dmp")) return [str(d) for d in dumps] def analyze(self): """Run full heap spray analysis pipeline.""" malfind_results = self.run_malfind() large_allocs = self.run_vadinfo() spray_candidates = defaultdict(list) for alloc in large_allocs: spray_candidates[alloc["pid"]].append(alloc) for pid, allocs in spray_candidates.items(): total_mb = sum(a["size_mb"] for a in allocs) if total_mb > 50: self.findings.append({ "severity": "high", "type": "Heap Spray Indicator", "detail": f"PID {pid}: {total_mb:.1f} MB in {len(allocs)} large allocations", }) for entry in malfind_results: hex_bytes = entry.get("hex_bytes", "") if hex_bytes.count("90") > 20 or hex_bytes.count("0c") > 20: self.findings.append({ "severity": "critical", "type": "NOP Sled in Injected Region", "detail": f"PID {entry['pid']} ({entry['process']}): " f"NOP sled at {entry['start_addr']}", }) return { "malfind_entries": malfind_results, "large_allocations": large_allocs, "spray_candidate_pids": list(spray_candidates.keys()), } def generate_report(self): analysis = self.analyze() report = { "report_date": datetime.utcnow().isoformat(), "memory_dump": self.memory_dump, "malfind_count": len(analysis["malfind_entries"]), "large_allocation_count": len(analysis["large_allocations"]), **analysis, "findings": self.findings, "total_findings": len(self.findings), } out = self.output_dir / "heap_spray_report.json" with open(out, "w") as f: json.dump(report, f, indent=2, default=str) print(json.dumps(report, indent=2, default=str)) return report def main(): parser = argparse.ArgumentParser( description="Analyze memory dumps for heap spray exploitation artifacts" ) parser.add_argument("memory_dump", help="Path to memory dump file (.raw, .vmem, .dmp)") parser.add_argument("--output-dir", default="./heap_spray_analysis", help="Output directory for report and dumps") parser.add_argument("--alloc-threshold", type=int, default=0x100000, help="Minimum allocation size in bytes to flag (default: 1MB)") args = parser.parse_args() os.makedirs(args.output_dir, exist_ok=True) analyzer = HeapSprayAnalyzer(args.memory_dump, output_dir=args.output_dir) analyzer.generate_report() if __name__ == "__main__": main()