§7 AttackGraph — Formal Exploit Chain Model
Layer §6 of the Debt Collector Stack
Author: Shrikant Bhosale (@debtcollector21)
7.1 What It Is
AttackGraph is a formal mathematical model of exploit chains as a directed graph G = (V, E):
- V = StateNode: A security-relevant program state (e.g.,
recon_info,heap_corruption,code_execution) - E = ExploitStep: A finding that transitions from one state to another (e.g., a buffer overflow transitions from
recon_infotoheap_corruption)
This is BloodHound for code — a graph model that enables pathfinding through vulnerability space.
7.2 Formal Definition
7.2.1 State Nodes
@dataclass
class StateNode:
state: str # e.g., "recon_info", "heap_corruption", "code_execution"
category: str # "recon", "corruption", "execution"
severity: str # "info", "low", "medium", "high", "critical"
Built-in states:
| State | Category | Severity | Description |
|---|---|---|---|
no_access |
initial | info | No access to the target |
recon_info |
recon | info | Information gathering (addresses, layout, data) |
foothold |
access | medium | Initial code execution (sandboxed/user-level) |
code_execution |
control | critical | Arbitrary code execution |
data_exfil |
exfil | high | Data extraction |
heap_corruption |
corruption | critical | Heap metadata/non-metadata corruption |
stack_corruption |
corruption | critical | Stack buffer overflow |
use_after_free |
corruption | critical | UAF condition established |
type_confusion |
corruption | high | Type confusion achieved |
info_leak |
recon | high | Sensitive data leaked |
privilege_escalation |
access | critical | Privilege boundary crossed |
dos |
impact | medium | Denial of service |
crash |
impact | low | Program crash (no exploitation) |
7.2.2 Exploit Step Edges
@dataclass
class ExploitStep:
id: str # Finding ID (VMF-xxx or FUZZ-xxx)
source: str # file:line
vulnerability: str # Type (heap_buffer_overflow, null_deref, etc.)
cvss: float # CVSS v3.1 base score
preconditions: set[str] # Required postconditions from previous steps
postconditions: set[str] # Enabled states after this step
metadata: dict # Source, crash type, ASAN output, etc.
7.2.3 Node Identity
Critical design decision: Nodes are identified by STATE VALUE, not by file:line.
This means findings at different locations that produce the same state transition merge into a single abstract graph node. For example:
scanner.cc:342memcpy OOB →heap_corruptiondecoder.cc:88section count overflow →heap_corruption
Both findings map to the SAME transition recon_info → heap_corruption because they have the same pre/post conditions. This merging is what enables multi-step pathfinding across disconnected files.
7.3 Pathfinding
AttackGraph implements 5 pathfinding strategies:
7.3.1 Shortest Path (Dijkstra, Unweighted)
def shortest_path(graph: AttackGraph, start: str, end: str) -> list[ExploitStep]:
"""
Minimum number of steps from start to end state.
Uses Dijkstra with unit weights (each edge = 1 step).
Returns the shortest path in terms of step count.
"""
return nx.shortest_path(graph.digraph, source=start, target=end)
Use case: “What’s the fastest way from no_access to code_execution?”
7.3.2 Fastest Path (Weighted by Complexity)
def fastest_path(graph: AttackGraph, start: str, end: str) -> list[ExploitStep]:
"""
Minimum complexity path from start to end.
Edge weight = 1 / (cvss * complexity_factor)
Higher CVSS = lower weight = preferred path.
Complexity factor penalizes steps that require:
- Physical access (×2)
- Admin privileges (×3)
- Specific hardware (×4)
"""
for edge in graph.digraph.edges(data=True):
step = edge[2]['step']
weight = 1.0 / (step.cvss * complexity_factor(step))
graph.digraph[edge[0]][edge[1]]['weight'] = weight
return nx.shortest_path(
graph.digraph, source=start, target=end, weight='weight'
)
Use case: “What’s the easiest path to exploitation?”
7.3.3 Stealthiest Path (Maximin Stealth)
def stealthiest_path(graph: AttackGraph, start: str, end: str) -> list[ExploitStep]:
"""
Path that maximizes the minimum stealth factor.
Stealth factor ∈ [0, 1] based on:
- Crash/noise generation (noisy = 0.2, quiet = 0.9)
- Logging presence (logged = 0.3, no log = 0.8)
- Probability of detection (high = 0.1, low = 0.9)
Uses maximin pathfinding: maximize the minimum stealth
factor along the path.
"""
paths = list(nx.all_simple_paths(graph.digraph, source=start, target=end))
best_path = None
best_stealth = 0
for path in paths:
min_stealth = min(
graph.get_stealth(edge)
for edge in zip(path[:-1], path[1:])
)
if min_stealth > best_stealth:
best_stealth = min_stealth
best_path = path
return best_path
Use case: “How do I exploit this without getting caught?”
7.3.4 Most Reliable Path (Max Probability Product)
def most_reliable_path(graph: AttackGraph, start: str, end: str) -> list[ExploitStep]:
"""
Path with highest overall success probability.
Each edge has a success probability (0-1) derived from:
- CVSS exploitability subscore
- Fuzzer crash rate
- PoC verification status
- D_e of the target function
Total = product of all edge probabilities along the path.
"""
for edge in graph.digraph.edges(data=True):
step = edge[2]['step']
prob = step.success_probability()
# Use negative log for shortest-path
graph.digraph[edge[0]][edge[1]]['weight'] = -log(prob)
return nx.shortest_path(
graph.digraph, source=start, target=end, weight='weight'
)
Use case: “Which chain am I most confident will work in practice?”
7.3.5 K-Shortest Paths (Top-K by Complexity)
def k_shortest_paths(graph: AttackGraph, start: str, end: str, k: int) -> list[list[ExploitStep]]:
"""
Top-k paths ordered by complexity (ascending).
Uses Yen's algorithm for k-shortest simple paths.
Starts with shortest (Dijkstra), then finds alternatives.
"""
return list(nx.shortest_simple_paths(
graph.digraph, source=start, target=end, weight='complexity'
)[:k])
Use case: “Show me the 5 most practical exploit chains.”
7.4 Bottleneck Analysis
AttackGraph uses betweenness centrality to identify critical nodes:
def bottleneck_analysis(graph: AttackGraph) -> dict[str, float]:
"""
Compute betweenness centrality for each state node.
Betweenness = fraction of all shortest paths that pass
through this node. High betweenness = bottleneck.
A single well-placed fix at a bottleneck node can
collapse entire classes of attack paths.
"""
return nx.betweenness_centrality(graph.digraph, normalized=True)
Empirical example ([Vendor] analysis):
| State Node | Betweenness | Bottleneck? |
|---|---|---|
heap_corruption |
0.42 | YES — 42% of all paths pass through it |
recon_info |
0.18 | No |
type_confusion |
0.31 | Moderate |
code_execution |
0.09 | No (sink, not transit) |
Interpretation: Fixing heap corruption (e.g., adding bounds checks to the Wasm decoder) would collapse 42% of all attack paths. This is the highest-leverage mitigation point.
7.5 Structure Classification
Every chain is classified by its graph structure:
| Structure | Pattern | Example | Mitigation Strategy |
|---|---|---|---|
| Linear | A → B → C → D | OOB → UAF → code exec | Break any single link |
| Branching | A → {B, C} → D | Info leak enables 2 paths | Collapse B and C |
| Looping | A → B → A | State corruption loop | Fix invariant |
| Kill Chain | Recon → Weaponize → Deliver → Exploit → Control → Exfil | Full MITRE ATT&CK | Detection at each phase |
| Singleton | A (isolated) | Info leak, No chain path | Low priority |
7.6 Chain Templates
AttackGraph supports 4 real-world chain templates:
| Template | Steps | Example |
|---|---|---|
| Web-to-Full-Compromise | SSRF → internal_recon → cred_access → priv_esc → data_exfil | Server-side attack |
| Supply Chain | dep_confusion → build_inject → signed_binary → widespread_install | Dependency attack |
| Browser-to-Kernel | [Vendor]_oob → sandbox_escape → kernel_exploit → persistence | Browser pwn2own |
| Memory Corruption | info_leak → heap_spray → uaf → code_exec | Classic binary exploitation |
7.7 Chain Validation
Each chain must satisfy the chain validity condition:
def chain_valid(chains: list[ExploitStep]) -> bool:
"""
Every step's preconditions must be satisfied by the
UNION of all preceding steps' postconditions.
This uses abstract union (not sequential), because
the attacker can choose which postconditions are active
at each stage.
"""
accumulated_postconditions = set()
for step in chains:
if not step.preconditions.issubset(accumulated_postconditions):
return False # Missing prerequisite
accumulated_postconditions |= step.postconditions
return True
7.8 CVSS Chain Multiplier
When findings are chained, the CVSS is adjusted:
def chain_cvss(chains: list[ExploitStep]) -> float:
"""
Chain CVSS = max(base_cvss) × chain_multiplier
Chain multiplier:
- 1 chain crossing 2 trust boundaries: ×1.3
- 1 chain with 3+ steps: ×1.15
- Cross-language chain: ×1.1
- Multiple chains converging: ×1.2
"""
base = max(step.cvss for step in chains)
multiplier = 1.0
trust_boundaries = count_trust_boundaries(chains)
if trust_boundaries >= 2:
multiplier = max(multiplier, 1.3)
if len(chains) >= 3:
multiplier = max(multiplier, 1.15)
languages = {step.metadata.get('language') for step in chains}
if len(languages) > 1:
multiplier = max(multiplier, 1.1)
return round(min(10.0, base * multiplier), 1)
7.9 Integration with Feynman Gate
Every chain passes through the Feynman Gate before scoring:
def feynman_validate_chain(chains: list[ExploitStep]) -> ChainVerdict:
"""
Feynman Gate for chains:
- Q1: Does every step have a PoC? (must all pass)
- Q2: Are all pre/post conditions verified? (chain_valid)
- Q3: What would disprove this chain?
- Q4: Is the chain realistic in practice (not just theoretical)?
"""
# Q1: All steps need PoC
all_poc = all(step.metadata.get("poc_exists", False) for step in chains)
if not all_poc:
return ChainVerdict.FAIL
# Q2: Pre/post condition chain
if not chain_valid(chains):
return ChainVerdict.FAIL
# Q3: Disproof test
disproof = identify_mitigations(chains)
if len(disproof) >= 2: # Two independent mitigations block the chain
return ChainVerdict.FAIL
# Q4: Realism check
realism = assess_chain_realism(chains)
if realism < 0.5:
return ChainVerdict.WARN
return ChainVerdict.PASS
7.10 Empirical Results
| Target | Chains | Top CVSS | Bottleneck | Structure |
|---|---|---|---|---|
| [Vendor] (Parcel) | 4 | 8.8 | heap_corruption (0.38) |
Linear + Branching |
| [Vendor] (4 subsystems) | 3 | 8.0 | heap_corruption (0.42) |
Cross-subsystem Linear |
| Chrome Mojo | 2 | 8.3 | type_confusion (0.31) |
Linear |
| Cash App (Java) | 3 | 8.0 | resource_leak (0.28) |
Linear |
| [Vendor] (14 apps) | 1 | 7.5 | oob_access (0.35) |
Linear |
| Wasm Fuzzer (KU-integrated) | 1 | 8.65 | heap_corruption (0.42) |
Linear |
7.11 CLI Usage
# Show AttackGraph summary
vmf report --graph
# Find paths from start to end
vmf chain --from recon_info --to code_execution
# Find fastest path
vmf chain --from no_access --to data_exfil --strategy fastest
# K shortest paths
vmf chain --from no_access --to code_execution --strategy k_shortest --k 5
# Bottleneck analysis
vmf report --bottlenecks
# Chain validation
vmf chain --validate