● Zion Boggan
repos/Cyclops V2/oracle/oracle.py
zionboggan.com ↗
119 lines · python
History for this file →
1
"""
2
Cyclops v2 - Pillar 3, the dynamic oracle.
3
 
4
The ONLY component allowed to declare a finding real. It does not read code and
5
opine; it executes and observes. For the memory-corruption / parser archetype it:
6
  1. compiles a libFuzzer harness against the target with ASan/UBSan,
7
  2. fuzzes for a time budget,
8
  3. on a crash: saves the reproducer + sanitizer report,
9
  4. TRIAGES the crash (write vs read primitive, PC control, exploitability),
10
  5. writes a Proof node and - only if the triage clears the severity bar -
11
     promotes the hypothesis to a Finding through memory's gate.
12
 
13
A crash is ground truth. memory.promote_to_finding refuses anything without it.
14
"""
15
import os, sys, re, subprocess, glob, shutil, time
16
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "memory"))
17
from memory import Memory
18
 
19
CYCLOPS_HOME = os.environ.get("CYCLOPS_HOME", "/opt/cyclops-v2")
20
CRASH_DIR    = os.path.join(CYCLOPS_HOME, "oracle", "crashes")
21
CLANG        = os.environ.get("CYCLOPS_CLANG", "clang")
22
 
23
ASAN_CLASS = [
24
    (r"heap-buffer-overflow.*WRITE",   ("heap OOB WRITE",   "CRITICAL")),
25
    (r"stack-buffer-overflow.*WRITE",  ("stack OOB WRITE",  "CRITICAL")),
26
    (r"global-buffer-overflow.*WRITE", ("global OOB WRITE", "HIGH")),
27
    (r"heap-use-after-free",           ("use-after-free",   "CRITICAL")),
28
    (r"double-free",                   ("double-free",      "HIGH")),
29
    (r"WRITE of size",                 ("OOB WRITE",        "CRITICAL")),
30
    (r"heap-buffer-overflow.*READ",    ("heap OOB READ",    "MEDIUM")),
31
    (r"stack-buffer-overflow.*READ",   ("stack OOB READ",   "MEDIUM")),
32
    (r"SEGV on unknown address",       ("SEGV",             "HIGH")),
33
    (r"stack-overflow",                ("stack exhaustion", "LOW")),
34
    (r"out-of-memory",                 ("OOM",              "LOW")),
35
]
36
 
37
 
38
def compile_harness(harness_c, target_srcs, out_bin, extra_flags=None):
39
    """clang -fsanitize=fuzzer,address. Returns (ok, log)."""
40
    cmd = [CLANG, "-g", "-O1", "-fsanitize=fuzzer,address,undefined",
41
           "-fno-omit-frame-pointer", harness_c] + list(target_srcs) + \
42
          (extra_flags or []) + ["-o", out_bin]
43
    p = subprocess.run(cmd, capture_output=True, text=True)
44
    return p.returncode == 0, (p.stderr or "") + (p.stdout or "")
45
 
46
 
47
def triage(asan_text):
48
    """Classify the sanitizer report -> (primitive, severity, exploitable)."""
49
    for pat, (prim, sev) in ASAN_CLASS:
50
        if re.search(pat, asan_text, re.I):
51
            exploitable = sev in ("CRITICAL", "HIGH") and "WRITE" in prim.upper() \
52
                          or "use-after-free" in prim or prim == "SEGV"
53
            return prim, sev, exploitable
54
    return "unknown-fault", "MEDIUM", False
55
 
56
 
57
def run_fuzz(fuzz_bin, seconds=120, corpus=None):
58
    """Run libFuzzer. Returns (crashed, crash_input_path, asan_text)."""
59
    os.makedirs(CRASH_DIR, exist_ok=True)
60
    artifact = os.path.join(CRASH_DIR, "crash-")
61
    args = [fuzz_bin, f"-max_total_time={seconds}", "-print_final_stats=1",
62
            f"-artifact_prefix={artifact}"]
63
    if corpus:
64
        args.append(corpus)
65
    p = subprocess.run(args, capture_output=True, text=True)
66
    txt = (p.stderr or "") + (p.stdout or "")
67
    crashed = ("ERROR: AddressSanitizer" in txt or "ERROR: libFuzzer" in txt
68
               or "SUMMARY: " in txt or "runtime error:" in txt)
69
    crash_input = None
70
    found = sorted(glob.glob(artifact + "*"))
71
    if found:
72
        crash_input = found[-1]
73
    return crashed, crash_input, txt
74
 
75
 
76
def prove_memory_corruption(target_id, hypothesis_id, impact_family,
77
                            harness_c, target_srcs, extra_flags=None,
78
                            seconds=120, repo=""):
79
    """Full oracle run for one parser target. Returns a result dict."""
80
    m = Memory()
81
    os.makedirs(CRASH_DIR, exist_ok=True)
82
    out_bin = os.path.join(CRASH_DIR, f"fuzz_{target_id[:8]}")
83
    ok, log = compile_harness(harness_c, target_srcs, out_bin, extra_flags)
84
    if not ok:
85
        return {"status": "BUILD_FAILED", "log": log[-2000:]}
86
 
87
    crashed, crash_input, asan = run_fuzz(out_bin, seconds=seconds)
88
    if not crashed:
89
        m.add_proof(hypothesis_id, "DISPROVEN", "libfuzzer+asan", "",
90
                    f"fuzzed {seconds}s, no crash reached the sink")
91
        return {"status": "NO_CRASH", "seconds": seconds}
92
 
93
    prim, sev, exploitable = triage(asan)
94
    report_path = os.path.join(CRASH_DIR, f"report_{target_id[:8]}.txt")
95
    with open(report_path, "w") as fh:
96
        fh.write(asan)
97
    summary = f"{prim}; ASan-confirmed; reproducer={crash_input}"
98
    pid = m.add_proof(hypothesis_id, "REPLICA_CONFIRMED", "libfuzzer+asan",
99
                      report_path, summary)
100
 
101
    result = {"status": "CRASH", "primitive": prim, "severity": sev,
102
              "exploitable": exploitable, "reproducer": crash_input,
103
              "report": report_path, "proof": pid}
104
 
105
    if exploitable:
106
        fid = m.promote_to_finding(
107
            hypothesis_id, impact_family, sev,
108
            f"{repo}: {prim} in parser -> {impact_family}",
109
            novel=False, cve_checked=False)
110
        result["finding"] = fid
111
    else:
112
        m.clear_axis(target_id, "memory-corruption", f"{prim}@{hypothesis_id[:8]}",
113
                     "LOW", f"{prim} reproduced but not an RCE-capable primitive")
114
        result["note"] = "crash is not RCE-capable; logged as low axis, not a P1 finding"
115
    return result
116
 
117
 
118
if __name__ == "__main__":
119
    print("oracle module - invoked by orchestrator. See prove_memory_corruption().")