Debt Collector Series: §5 Pipeline — ASAN→KU→AttackGraph Fusion

§5 Pipeline — ASAN→KU→AttackGraph Fusion

Layer §4 of the Debt Collector Stack

Author: Shrikant Bhosale (@debtcollector21)


5.1 What It Is

The fusion pipeline that converts raw crash output from a fuzzer (or any ASAN-compatible crash) into a classified, chained, scored finding in the AttackGraph.

This is the translation layer between dynamic analysis (fuzzing) and the formal knowledge framework (KU engine + AttackGraph).


5.2 Architecture

ASAN Output (raw text)
        ↓
┌─────────────────────────────┐
│   ASAN Output Parser         │
│   Regex-based parser that    │
│   extracts: crash type,      │
│   address, size, stack trace,│
│   register state, memory map │
└───────────┬─────────────────┘
            ↓
┌─────────────────────────────┐
│   Crash → Finding Dict      │
│   Converts parsed ASAN to   │
│   VMF-standard finding:     │
│   { type, severity, file,   │
│     line, cvss, cwe, code } │
└───────────┬─────────────────┘
            ↓
┌─────────────────────────────┐
│   KU Engine Injection        │
│   Checks if crash pattern    │
│   matches known KU. If not,  │
│   creates NKU for novelty.   │
└───────────┬─────────────────┘
            ↓
┌─────────────────────────────┐
│   AttackGraph Injection      │
│   Adds crash as ExploitStep  │
│   edge in AttackGraph.       │
│   Re-runs pathfinding.       │
└───────────┬─────────────────┘
            ↓
┌─────────────────────────────┐
│   Report Generator           │
│   Summarizes: finding chain, │
│   CVSS, KUs matched, NKUs    │
│   created, attack paths.     │
└─────────────────────────────┘

5.2 ASAN Output Parser

The parser handles 8 crash types:

Crash Type Example ASAN Output Parser Regex
heap-buffer-overflow heap-buffer-overflow on address 0x... heap-buffer-overflow
stack-buffer-overflow stack-buffer-overflow on address 0x... stack-buffer-overflow
global-buffer-overflow global-buffer-overflow on address 0x... global-buffer-overflow
use-after-free use-after-free on address 0x... use-after-free
double-free double-free on address 0x... double-free
segv (nullptr) SEGV on unknown address 0x000000000000 SEGV
segv (wild) SEGV on unknown address 0xdeadbeef SEGV with address
abort ABRT ABRT

5.2.1 Parser Implementation

def parse_asan_output(text: str) -> Optional[AsanCrash]:
    """
    Parse ASAN output into structured crash data.

    Returns None if no crash pattern is found (e.g., clean run).
    """
    crash_patterns = {
        "heap-buffer-overflow": {
            "type": "heap_buffer_overflow",
            "pattern": r"heap-buffer-overflow on address (0x[0-9a-f]+)",
            "severity": "critical"
        },
        "use-after-free": {
            "type": "use_after_free",
            "pattern": r"use-after-free on address (0x[0-9a-f]+)",
            "severity": "critical"
        },
        "SEGV": {
            "type": "null_deref",
            "pattern": r"SEGV on unknown address (0x[0-9a-f]+)",
            "severity": "critical",
            "null_addresses": {"0x000000000000", "0x0000000000000000"}
        },
        # ... 7 more patterns
    }

    for crash_type, config in crash_patterns.items():
        if crash_type in text:
            match = re.search(config["pattern"], text)
            if match:
                address = match.group(1)
                stack = extract_stack_trace(text)
                return AsanCrash(
                    type=config["type"],
                    address=address,
                    severity=config["severity"],
                    stack_trace=stack,
                    raw_text=text
                )
    return None

5.2.2 Stack Trace Extraction

def extract_stack_trace(text: str) -> list[StackFrame]:
    """
    Extract stack frames from ASAN output.

    Format:
        #0 0x... in FunctionName /path/file.cc:line:col
    """
    frames = []
    for line in text.split('\n'):
        match = re.match(
            r'\s*#(\d+)\s+0x[0-9a-f]+\s+in\s+(\S+)\s+(\S+):(\d+):(\d+)',
            line
        )
        if match:
            frames.append(StackFrame(
                depth=int(match.group(1)),
                function=match.group(2),
                file=match.group(3),
                line=int(match.group(4)),
                column=int(match.group(5))
            ))
    return frames

5.3 Crash → Finding Dict

Once parsed, the crash is converted to a VMF-standard finding dictionary:

def crash_to_finding_dict(crash: AsanCrash, config: ScanConfig) -> dict:
    """
    Convert parsed ASAN crash to VMF finding dict.

    The finding dict follows the same schema as VMF scan output,
    enabling the pipeline to process static AND dynamic findings
    through the same KU/AttackGraph pipeline.
    """
    finding = {
        "id": f"FUZZ-{datetime.now():%Y%m%d-%H%M%S}",
        "source": "fuzzer",
        "type": crash.type,
        "severity": crash.severity,
        "file": crash.stack_trace[0].file if crash.stack_trace else "unknown",
        "line": crash.stack_trace[0].line if crash.stack_trace else 0,
        "address": crash.address,
        "cvss": compute_cvss(crash.type, config),
        "cwe": crash_type_to_cwe(crash.type),
        "code": extract_code_context(crash),
        "metadata": {
            "crash_type": crash.type,
            "stack_depth": len(crash.stack_trace),
            "asan_raw": crash.raw_text
        }
    }
    return finding

5.3.1 CVSS Assignment

CVSS is computed based on crash type and context:

def compute_cvss(crash_type: str, config: ScanConfig) -> float:
    """
    Compute CVSS v3.1 base score from crash type and target configuration.

    Base scores are worst-case (assume attacker controls the crash input
    and can reach the crash site). The AttackGraph refines these.
    """
    base_scores = {
        "heap_buffer_overflow": 8.1,  # AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
        "use_after_free": 8.1,         # same vector
        "null_deref": 6.5,            # AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H
        "double_free": 8.1,           # same as heap overflow
        "stack_buffer_overflow": 8.1, # same
        "global_buffer_overflow": 8.1,# same
    }

    score = base_scores.get(crash_type, 5.0)

    # Adjust for target type
    if config.target_type == "sandboxed":
        score -= 1.0  # exploitation requires sandbox escape
    elif config.target_type == "kernel":
        score += 1.0  # kernel-level impact

    return score

5.4 KU Engine Injection

The finding dict is passed to the KU engine, which checks if the crash pattern matches a known Knowledge Unit:

def inject_crash_into_ku(finding: dict, ku_engine: KUEngine) -> KUResult:
    """
    Process crash through KU engine.

    1. Match against registered KUs (35+ patterns)
    2. If unmatched, create NegativeKU (NKU) for novelty
    3. Register NKU in engine for future matching
    """
    matched_kus = ku_engine.match(finding)

    if matched_kus:
        return KUResult(
            finding_id=finding["id"],
            matched=True,
            kus=matched_kus,
            nku_created=False,
            novelty_score=0.0  # fully predicted
        )
    else:
        # Novel crash pattern — create NKU
        nku = ku_engine.create_nku(finding)
        return KUResult(
            finding_id=finding["id"],
            matched=False,
            kus=[],
            nku_created=True,
            nku=nku,
            novelty_score=nku.severity
        )

5.4.1 NKU Creation

When a crash pattern does not match any existing KU, an NKU (Negative Knowledge Unit) is created:

def generate_nku_for_unexpected_crash(crash: AsanCrash, finding: dict) -> NegativeKnowledgeUnit:
    """
    Create NKU from unexpected crash.

    An NKU encodes the GAP between what VMF predicted (no finding here)
    and what the fuzzer found (crash here).

    This gap is more valuable than a matched crash — it reveals
    a blind spot in the static analysis.
    """
    nku = NegativeKnowledgeUnit(
        id=f"NKU-{datetime.now():%Y%m%d-%H%M%S}",
        crash_type=crash.type,
        pattern=finding.get("code", ""),
        file=finding["file"],
        line=finding["line"],
        severity=compute_nku_severity(crash, finding),
        description=(
            f"Unexpected {crash.type} at {finding['file']}:{finding['line']}. "
            f"Static analysis did not predict this path. "
            f"Indicates blind spot in detector pattern matching."
        )
    )
    return nku

5.5 AttackGraph Injection

The finding is added as an edge to the AttackGraph:

def inject_crash_into_attack_graph(
    finding: dict,
    attack_graph: AttackGraph,
    ku_result: KUResult
) -> None:
    """
    Add crash to AttackGraph as ExploitStep edge.

    Preconditions: determined by crash type (e.g., null_deref
    requires attacker-controlled input reaching the nulled pointer).

    Postconditions: determined by crash type (e.g., heap_buffer_overflow
    enables adjacent object corruption).
    """
    pre = crash_type_to_preconditions(finding["type"])
    post = crash_type_to_postconditions(finding["type"])

    step = ExploitStep(
        id=finding["id"],
        source=finding["file"],
        vulnerability=finding["type"],
        cvss=finding["cvss"],
        preconditions=pre,
        postconditions=post,
        metadata={
            "source": "fuzzer",
            "crash_type": finding["type"],
            "address": finding.get("address", "unknown"),
            "ku_matched": ku_result.matched,
            "nku_id": ku_result.nku.id if ku_result.nku_created else None
        }
    )

    attack_graph.add_edge(step)
    attack_graph.recompute_paths()  # re-run pathfinding

5.5.1 Precondition/Postcondition Mapping

CRASH_TO_PRECONDITIONS = {
    "heap_buffer_overflow": {"controlled_input"},
    "use_after_free": {"controlled_input", "heap_mutability"},
    "null_deref": {"controlled_input", "null_assignment"},
    "double_free": {"heap_mutability", "free_without_reset"},
}

CRASH_TO_POSTCONDITIONS = {
    "heap_buffer_overflow": {"adjacent_corruption", "write_primitive"},
    "use_after_free": {"arbitrary_read", "type_confusion"},
    "null_deref": {"crash", "dos"},
    "double_free": {"heap_corruption", "arbitrary_write"},
}

5.6 End-to-End: The [Vendor] Wasm Demo

5.6.1 Input (ASAN Output)

=================================================================
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020000000f0 at pc 0x55555556a123 bp 0x7fffffffe000 sp 0x7fffffffdff0
READ of size 1 at 0x6020000000f0 thread T0
    #0 0x55555556a123 in read_leb128_no_bounds harness.cc:47:22
    #1 0x55555556a456 in decode_type_section harness.cc:120:34
    #2 0x55555556a789 in decode_module harness.cc:500:18
    #3 0x55555556a999 in main harness.cc:530:5
0x6020000000f0 is located 0 bytes after 16-byte region [0x6020000000e0,0x6020000000f0)
freed by thread T0 here:
    #0 0x7ffff7890123 in free
previously allocated by thread T0 here:
    #0 0x7ffff7890456 in malloc
=================================================================

5.6.2 Pipeline Execution

# Step 1: Parse ASAN
crash = parse_asan_output(raw_text)
# → heap-buffer-overflow at harness.cc:47, address 0x6020000000f0

# Step 2: Convert to finding dict
finding = crash_to_finding_dict(crash, config)
# → { id: "FUZZ-20260717-104500", type: "heap_buffer_overflow",
#     severity: "critical", file: "harness.cc", line: 47, cvss: 8.1 }

# Step 3: KU matching
ku_result = inject_crash_into_ku(finding, ku_engine)
# → matched: True, KUs: ["heap_buffer_overflow", "wasm_decoder_oob"]
# → nku_created: False

# Step 4: AttackGraph injection
inject_crash_into_attack_graph(finding, attack_graph, ku_result)
# → Added as ExploitStep: pre={controlled_input}, post={adjacent_corruption}
# → Re-ran pathfinding: chain extends from recon_info→code_execution
# → Updated CVSS: 8.65 (chain multiplier)

5.6.3 Output

{
    "pipeline_run": {
        "timestamp": "2026-07-17T10:45:00Z",
        "input": "fuzzer_wasm_run_003.log",
        "crash_found": true,
        "crash_type": "heap_buffer_overflow",
        "finding_id": "FUZZ-20260717-104500",
        "ku_matched": true,
        "kus_used": ["heap_buffer_overflow", "wasm_decoder_oob"],
        "nku_created": false,
        "chain_id": "CC-[Vendor]-003",
        "chain_cvss": 8.65,
        "chain_path": "recon_info → heap_corruption → code_execution"
    }
}

5.7 Validation

Metric Value
Crash types supported 8
Avg parse time <5ms
Finding dict accuracy 100% (43/43 confirmed)
KU match rate (known) 100% (43/43 on [Vendor] Wasm)
NKU creation rate (known) 0% ([Vendor] Wasm — all predicted)
NKU creation rate (novel) TBD (requires new target)
AttackGraph integration <10ms/crash

5.8 CLI Usage

# Parse ASAN output file
python3 -c "from vmf.training.fuzzer_integration import *; print(parse_asan_output(open('/tmp/crash.log').read()))"

# Full pipeline on fuzzer output directory
vmf fuzz --wasm --pipeline --output report.json

# Manual crash injection
python3 -c "
from vmf.training.fuzzer_integration import fuzzer_to_full_pipeline
result = fuzzer_to_full_pipeline('/tmp/asan_output.txt')
print(result['chain_cvss'])
"

# Standalone parser
python3 -m vmf.training.fuzzer_integration /tmp/asan_output.txt --json

Leave a Comment