CIS 400 / 600 • Syracuse University

Cyber Reasoning Systems

SMT, vulnerability discovery, and AI-assisted program analysis

Prof. Kristopher Micinski

Disclaimer: how I make slides

  • I use an AI-integrated slide-making app here:

    • https://github.com/kmicinski/slides
  • I initially dictate the slides at a high level, and then generate a markdown document

  • I use an AI agent to make figures for the slides

  • Writing on the slides and the general structure is developed by me

    • But I often use AI for copyediting on my slides
  • Please let me know if you notice any mistakes, kkmicins@syr.edu

High-level picture: what's the goal?

  • We want to find a safety violation: identify a bug in a program
  • We can often see this in terms of reachability:
    • Is it possible to reach / drive the program execution to an exploited state starting from its normal path of execution?

For example...

if (len < 64 && magic == 0x41424344) {
  memcpy(buf, input, len + 16);
}

We might ask...

Is there any input that reaches this point while violating a memory safety condition?

Many ways to identify safety violations

  • Computer science has explored many ways of finding (memory, etc.) safety violations
    • Testing
    • Fuzzing
    • Reachability-based program analysis / abstract interpretation / etc.
    • Symbolic execution
    • Program logics
  • All of these rely upon different formal foundations.
    • For example, abstract interpretation uses properties of lattices and monotonic functions defined over these lattices.
    • Meanwhile, symbolic execution uses SAT/SMT

This lecture

  • This class we'll talk about SMT and symbolic execution, along with cyber-reasoning systems that put LLMs in a loop with tools including SMT, SE, etc.

  • Next class we'll take a look at program analysis and how it can be implemented.

  • Later in the class we'll study how we can harmonize these things, adding program analysis as a tool to the cyber-reasoning pipeline...

Symbolic execution versus program analysis

Symbolic execution enumerate paths, one at a time Program analysis over-approximate all behaviors at once infeasible program program exit bug found one complete path ? ? ? ? ? bug? unexplored: a bug may still be down here partial trace: stopped early, no verdict finds real bugs; cannot prove their absence ⇒ a form of testing over-approximation lattices · fixpoints · dataflow actual behaviors every real run bad states bad states ✓ provably unreachable false alarm in the math, not in reality proves absence; may raise false alarms ⇒ a form of proof

SAT versus SMT

Propositional SAT asks whether a Boolean formula can be made true.

(x∨y)∧(¬x∨z) (x \lor y) \land (\neg x \lor z)

  • Can use SAT to ask: "does a counterexample exist?". The variables are implicitly existentially quantified. So it's really, "does there ∃x,y,z.\exists x,y,z. s.t. the formula (x∨y)∧(¬x∨z)(x \lor y) \land (\neg x \lor z) is true?"

By contrast, SMT asks whether a formula is satisfiable modulo a first-order theory:

0≤n<64∧n+16>72 0 \le n < 64 \land n + 16 > 72

Here, the symbols ≤\leq, <<, ++, and >> appeal to a theory of arithmetic

  • Can't be all of arithmetic, that would be impossible (Gödel's incompleteness theorems)

Background: de Moura and Bjørner, "Z3: An Efficient SMT Solver," TACAS 2008; Barrett et al., "The SMT-LIB Standard."

How is SMT solving useful in vulnerability discovery?

Security questions often mix control flow with low-level data facts:

  • Can attacker input reach this branch?
  • Can a length arithmetic operation wrap around?
  • Can two pointers alias?
  • Can a string satisfy a parser's checks?

SMT is useful because these are not just Boolean questions. They involve machine integers, memory, strings, and path reachability.

The challenge is how to phrase our attack-relevant statements in the language of SMT

Is "SMT" really AI?

  • SAT solving is classic symbolic AI.
    • SMT solvers still incorporate aspects of SAT solving, e.g., DPLL(T), CDCL, clause learning, etc.
  • SMT solvers are an extension of SAT to first-order theories
  • What about modern LLMs? Do we really need SMT in the age of modern LLMs and tools?
    • As far as I know, this is an open question--one we will try to explore...

How SMT solving works, the 1000-foot view

  • SMT tries to build an assignment by guessing at unconstrained variables and then forcing implications. For example, if we guess x = 5 and we have x<0→y=5x < 0 \to y = 5, we must have y=5y=5

    • This is called propagation; in this case theory propagation, since we are appealing to the theory of arithmetic
  • A SAT engine explores the SAT fragment of the problem

  • Theory-specific solvers reason about specific theories: numbers, memory, equality

  • Learned explanations allow generalizing knowledge to learn from previous mistakes

How Boolean search and theory reasoning cooperate State 0: three boxes, Boolean search (CDCL), Theory reasoning, and a Learned clauses store under Boolean search. State 1: an arrow labelled assigned conditions goes from Boolean search to theory reasoning. State 2: a return arrow labelled consequences or conflicts comes back. State 3: a branch from the return path carries a conflict explanation into the learned-clause store, and a short arrow returns retained clauses to Boolean search. Boolean search CDCL: decide, propagate, learn Theory reasoning numbers · memory · equality Learned clauses results of learning not every theory fact is stored assigned conditions e.g. “n ≤ 64 holds”: a Boolean choice, not a value for n consequences or conflicts back and forth during the search conflict explanation, kept as a clause retained clauses

Bjørner et al., Z3 Internals, §2.1.

A theory-specific conflict can yield a learned lemma

One conflict, many avoided attempts Atoms: a is n at most 64, b is n greater than 80, c is tag equals 7. Query: (a or c) and b. State 1: the trail records b = true, required by the query, and the number line shows n greater than 80 as an open interval to the right of 80. State 2: the trail adds a = true as a decision, and the number line shows n at most 64 as a closed interval to the left of 64. State 3: the gap between 64 and 80 is marked No overlap. State 4: the theory lemma not a or not b appears, with a and b highlighted as its premises; c is not involved. a : n ≤ 64 b : n > 80 c : tag = 7 query: (a ∨ c) ∧ b assignment trail premise premise b = true required by query a = true decision values of n 48 64 80 96 b : n > 80 a : n ≤ 64 No overlap ✗ theory lemma ¬a ∨ ¬b These two conditions cannot both hold Learned lemma, add to database

Barbosa et al., cvc5, §2.1.

The solver's inner loop

The CDCL(T) loop, continuing the example Left: a loop. Choose feeds a decision into Propagate. From Propagate, a conflict leads to Explain and learn, then Backtrack, then back to Propagate; a root conflict exits to UNSAT. A candidate leads to Final theory checks; passing checks reach SAT; new lemmas return to Propagate. Right: the example. State 1: theory lemma not a or not b. State 2: resolving it with b gives the learned clause not a. State 3: the decision a = true is undone. State 4: a = false is propagated from the learned clause, then c = true from a or c. State 5: candidate values n = 81, tag = 7, not yet checked. State 6: the checks pass and the result is SAT. Choose make a decision Propagate forced consequences Explain and learn remember why it failed Final theory checks check the candidate model Backtrack undo failed choices SAT UNSAT decision choose again conflict candidate new lemmas root conflict checks pass SAT the example, right after the conflict b = true n > 80 required by query level 0 a = true n ≤ 64 decision level 1 a = false n > 64 learned clause c = true tag = 7 from a ∨ c theory lemma ¬a ∨ ¬b learned clause ¬a resolve the lemma with b n = 81, tag = 7 candidate values, not yet checked n = 81, tag = 7 one satisfying input (many exist) b: 81 > 80 ✓   c: tag = 7 ✓ SAT ↓ optional: earlier theory propagation

Barbosa et al., cvc5, §2.1; theory interface.

Optional: earlier theory propagation

The same example with early theory propagation The same loop drawing. Right: the example replayed from b alone. State 1: arithmetic propagates a = false, with the lemma not a or not b as its explanation. State 2: Boolean propagation derives c = true from a or c. State 3: candidate values n = 81, tag = 7, not yet checked. State 4: the checks pass and the result is SAT. No decision was made and no conflict occurred. Choose make a decision Propagate forced consequences Explain and learn remember why it failed Final theory checks check the candidate model Backtrack undo failed choices SAT UNSAT decision choose again conflict candidate new lemmas root conflict checks pass SAT the same query, from b alone b = true n > 80 required by query a = false n > 64 theory propagation explanation, on request ¬a ∨ ¬b c = true tag = 7 from a ∨ c no decision, no conflict, nothing to undo n = 81, tag = 7 candidate values, not yet checked n = 81, tag = 7 one satisfying input (many exist) b: 81 > 80 ✓   c: tag = 7 ✓ SAT ↑ back to the conflict-driven trace

Implementation detail: The conflict-driven trace on the previous slide is one illustrative schedule. A real solver may detect this consequence earlier.

Barbosa et al., cvc5, §2.1; theory interface.

The solver's wall-clock time depends on workload

  • Changing the way we send formulas to SMT can radically change its performance!
WorkloadWork that can dominate
Bit-vectorsBuilding Boolean circuits and searching them
EqualityMaintaining which terms must be equal
ArithmeticUpdating bounds and solving numerical constraints
QuantifiersGenerating and checking formula instances

Navigating the Universe of Z3 Theory Solvers; Fazekas et al., IPASIR-UP, SAT 2023.

From concrete execution to symbolic execution

Concrete execution

input = "AAAA"
x = 0x41414141
branch: x == 0xdeadbeef  // false

One run. One answer.

Symbolic execution

input = α0 α1 α2 α3
x = concat(α0, α1, α2, α3)
branch condition: x == 0xdeadbeef

A set of inputs at once.

Now the question is not "what happened on this run?" It is: is there any input that makes the branch go the other way?

Classic system: Cadar, Dunbar, and Engler, "KLEE," OSDI 2008.

Path conditions

A path condition is the conjunction of branch decisions needed to reach a program point.

if (n < 64) {
  if (tag == 0x1337) {
    sink(buf[n + 8]);
  }
}

Path to sink:

n<64∧tag=0x1337 \text{n} < 64 \land \text{tag} = 0x1337

Potential out-of-bounds query:

n<64∧tag=0x1337∧n+8≥∣buf∣ \text{n} < 64 \land \text{tag} = 0x1337 \land \text{n} + 8 \ge |\text{buf}|

Figure: building a path condition

program execution tree query if (n < 64) { if (tag == 0x1337) { sink(buf[n + 8]); } } n < 64 ? exit tag == 0x1337 ? exit sink(buf[n+8]) F T F T path condition n < 64 ∧ tag = 0x1337 ∧ n + 8 ≥ 64← bug condition, |buf| = 64 satn = 56, tag = 0x1337 with n + 8 ≥ 1000 instead:unsatno input satisfies the query

Useful theories for reverse engineering...

SMT-LIB is a uniform standard for defining SMT queries via S-expressions and allows us to reuse a variety of solvers via a simple, uniform interface.

For vulnerability identification, the most important theories are usually:

  • Bit-vectors: machine integers, overflow, shifts, masks
  • Arrays: memory as select and store
  • Linear arithmetic: sizes, bounds, counters
  • Strings / regexes: web input, parsers, validation logic
  • Uninterpreted functions: abstraction when exact behavior is unknown

Not every theory is equally important for every target. For example, strings / regex is much more useful for doing web exploitation, whereas bit-vectors are useful for binaries / C-style apps

Bit-vectors: machine arithmetic, not integers

  • C integers are fixed-width.

  • For example, in 8-bit unsigned arithmetic we have 250 + 10 = 4, because arithmetic is modulo 282^8

A vulnerability query may depend on exactly this behavior:

uint8_t total = len + header;
if (total < 64) memcpy(buf, input, len);
  • If len + header wraps, the check may validate the wrong quantity!

  • Thus, a bit-vector theory has to track the size of each bit vector, and operations must properly account for things like overflow, etc.

How SMT solvers work, roughly

Modern SMT solving often looks like:

  1. Boolean (SAT solver) search proposes an assignment
  2. Theory solver checks whether it makes sense
  3. If contradiction, theory solver explains the conflict and returns a "lemma"
  4. Boolean search learns, backtracks, and tries again

Figure: DPLL(T) intuition

Boolean search (SAT) Theory solver (arithmetic) A ⇒ x < y B ⇒ y < z C ⇒ z < x assignment A = true B = true C = true C = false (backtrack) propose A, B, C learn ¬A ∨ ¬B ∨ ¬C x y z x < y ay < z z < x cycle x < y < z < x — conflict no cycle — consistent ✓

So the Boolean search learns: not all of A,B,CA, B, C can be true.

Section • 02

Modern Cyber Reasoning Systems

Does SMT still matter?

What is a cyber-reasoning system?

Term I made up; but I mean a frontier-LLM-orchestrated pipeline which does the following...

  1. build and run a target
  2. explore behavior
  3. find suspicious states
  4. prove or demonstrate a vulnerability
  5. repair or mitigate it
  6. validate the result
  • My claim: cyber reasoning systems will become increasingly important for both defensive and offensive tasks

Figure: the CRS loop

thick edges = machine-checked input crash constraint hypothesis candidate patch target program source + build + harness Build harness Exploitability checker Coverage-guided fuzzer Validator Symbolic executor SMT Patch generator Static analysis data-flow, etc. LLM agent A — the fuzzer finds a crash and sends the input + sanitizer report to triage B — static analysis points the LLM agent at a sink and a dataflow path C — the symbolic executor solves a hard branch and returns a fresh seed D — the LLM agent states a hypothesis and proposes a candidate patch E — the validator builds, reruns the PoV, runs tests, and restarts fuzzing

Which parts should be neural, which symbolic, which concrete?

Why not just fuzz? Why not just symbolic execution?

Fuzzing is excellent when:

  • executions are cheap and coverage tracks progress
  • bugs are shallow enough to stumble into

It struggles when:

  • inputs are deeply structured or need semantic consistency
  • the useful path is guarded by narrow constraints or state sequences

Symbolic execution is excellent when:

  • the relevant path is narrow and the query is precise
  • constraints are within solver reach

It struggles when:

  • path explosion dominates or solvers time out
  • memory, system interactions, or a missing harness get in the way

A CRS allocates each engine where it has leverage.

The classic hybrid: Stephens et al., "Driller: Augmenting Fuzzing Through Selective Symbolic Execution," NDSS 2016.

The handoff, concretely: hybrid fuzzing

Driller's loop (QSYM and SymCC made it fast)

  1. Fuzz. Mutation is nearly free; it finds every shallow path
  2. Stall. No new coverage; every queued input dies at the same branches
  3. Trace. Re-run a stalled input concolically: concrete values, but record the path condition as it goes
  4. Flip. At a branch the fuzzer never took, negate its condition and ask SMT
  5. Seed. SAT means the model is a new input. Push it into the fuzzer's queue; go to 1
if (hdr.magic != 0x41424344) return; // 1 in 2^32 by mutation
if (hdr.len > body_len)      return; // relation between fields
if (!json_parse(body, &doc)) return; // 10k-line parser
use(doc);                            // the bug is here
  • Fuzzer alone: stuck on line 1
  • + SMT: lines 1–2 fall in milliseconds; then the solver explodes inside the parser
  • + LLM (ChatAFL, HLPFuzz): writes a valid body; the fuzzer mutates from there
Symbolic execution is a subroutine here, not the driver: it is called only on the branches where its expensive query pays for itself, and its answer is still just a seed the fuzzer has to run.

Stephens et al., Driller, NDSS 2016; Yun et al., QSYM, USENIX Security 2018; Poeplau and Francillon, SymCC, USENIX Security 2020.

Why not just ask an LLM?

LLMs are useful for:

  • code comprehension
  • hypothesis generation
  • recognizing protocols and formats
  • explaining tool output
  • proposing harnesses or patches

Main idea: Guess / generate hypotheses with LLM, then check with tools to provide ground truth

  • Tests, symbolic execution, fuzzing (e.g., with seed generated by LLM)

Section • 03

Recent systems

What changed in the last 2–4 years?

Reading map

There are several different kinds of cyber-reasoning systems in the literature:

System kindRepresentative examples
LLM-guided fuzzingChatAFL, HLPFuzz, ELFuzz, PANGOLIN
Agentic pentestingPentestGPT, Cybench
Agent-computer interfaceSWE-agent
End-to-end vulnerability lifecycleBountyBench, AIxCC
Firmware / binary-oriented CRSFirmAgent
Real-world agentic researchProject Naptime / Big Sleep
  • Disclaimer: I have not read all of these papers myself, though I've skimmed them; we will cover these papers based on student interest this semester.

ChatAFL — LLM-guided protocol fuzzing

NDSS 2024, Meng et al., built on AFLNet. The LLM has already read the RFCs: ask it narrow questions, and let coverage judge every answer.

Three insertion points in the fuzzing loop:

  • Grammar, once per protocol → structure-aware mutation
  • Seeds: add the message types the recorded traces lack
  • Plateau: coverage stalls → "what message comes next?"

Result vs. AFLNet, six servers: +47.6% state transitions, nine zero-days.

Few-shot grammar prompt and the LLM's reply (Fig. 6)

Source: Meng et al., "Large Language Model guided Protocol Fuzzing," NDSS 2024.

Tool-using agents — PentestGPT and Project Naptime

PentestGPT — USENIX Security 2024 (Deng et al.)

  • Benchmark first: 13 HackTheBox / VulnHub machines, 182 sub-tasks
  • Plain LLMs do the pieces but lose the overall picture and fixate on the latest output
  • Fix: a Pentesting Task Tree owned by a Reasoning module; Generation writes the commands, Parsing condenses tool output

Project Naptime / Big Sleep — Project Zero, DeepMind

  • Code browser, debugger, Python sandbox, verifier
  • Industrial case study, not a benchmark
  • Every claim checked by concrete execution

Both are the CRS loop (explore → observe → reason → act → validate); the hard part is keeping state across many tool calls. SWE-agent made the same point: the agent-computer interface decides what the agent can find.

Sources: Deng et al., PentestGPT, USENIX Security 2024; Project Naptime; SWE-agent, NeurIPS 2024.

In the wild: autonomous pentesting agents

Target in scope: a web app, black box. Goal: a finding the validator will sign.
Thought 1: /api/preview?url= fetches URLs server-side. Hypothesis: SSRF.
Act 1: GET /api/preview?url=http://canary-7f3a.example/
Obs 1: canary logs a hit from the target's egress IP. The server fetches attacker URLs.
Act 2: GET /api/preview?url=http://169.254.169.254/latest/meta-data/
Obs 2: 200 OK; body lists iam/ and hostname. Internal network is reachable.
Act 3: Validate[SSRF]: replay Act 1 from a clean session; require the canary hit.
Obs 3: PASS. Report drafted for human review.
  • Tools: proxy, crawler, headless browser, scripting sandbox; the agent sees requests and responses
  • One agent per hypothesis: many runs per target in parallel, each exploring a class: SSRF, SQLi, XSS, IDOR, path traversal
  • One validator per class: a finding "counts" only when we can reproduce it
  • Human before disclosure: the validator removes false positives; a human judges scope and impact

Composite of public engineering write-ups from commercial autonomous pentesters (e.g. XBOW, RunSybil); no single vendor's internals are shown. Leaderboard claim: XBOW blog, 2025.

PANGOLIN: LLM-recovered specs for firmware fuzzing

Venue: USENIX Security 2026 — Jia et al. Targets the web interfaces of IoT firmware. Core idea: real firmware crosses language boundaries (C binaries calling Lua/PHP/shell scripts) and its reachable interfaces are undocumented. A fuzzer cannot test a format nobody wrote down, so the LLM recovers the spec and the fuzzer tests against it.

  • Entry map (§4.1): statistics isolate the dispatch code; the LLM reads only that and maps every URI, hidden ones included, to its handler
  • Parameter spec (§4.2): one agent prunes the cross-language call graph down to parsing and sanitizer calls; another writes a JSON spec per URI, marking each field Muta or NoMuta
  • Correction loop (§4.3): the fuzzer keeps NoMuta fields fixed and mutates the rest on the real device; a response that contradicts the spec goes back to the agents
Neural steps propose the entry map and the spec; the concrete step, the device, decides. A wrong spec finds nothing, and that silence is the signal.

Results: 68 zero-days, 45 of them on hidden interfaces (EAGLEYE found 4); 2.96× the previous best, LABRADOR.

Source: Jia et al., "PANGOLIN: Fuzzing Multilingual IoT Firmware with LLM-Driven Code Analysis," USENIX Security 2026. Press down for the paper's own overview figure.

PANGOLIN — the paper's own overview

Figure 4 from the paper: Entry Map Construction (§4.1), Parameter Specification Generation (§4.2), Correction and Specification Guidance (§4.3)

Source: Jia et al., PANGOLIN, USENIX Security 2026, Figure 4. Red arrows are the response-driven correction loop.

The DARPA AIxCC Competition

Final round, DEF CON 33, August 2025. Seven fully autonomous CRSs, no human in the loop, against 70 vulnerabilities injected into OSS-Fuzz-style C and Java projects (SQLite, libxml2, FreeRDP, Apache Tika, ZooKeeper, …).

What a CRS had to submit

  • PoV: an input to a named fuzz harness that trips a sanitizer when the organisers replay it
  • Patch: must build, pass the project's own tests, and stop the PoV; no human reviews it
  • SARIF triage: label a static-analysis report true or false
  • Targets arrive as a full scan of a repository or a delta scan of one commit
  • An accuracy multiplier penalised rejected PoVs and broken patches, so guessing costs!

Results

foundpatched
synthetic (70 injected)54 (77%)43 (61%)
real, previously unknown1811

Headline pace: about 45 minutes per discovery, roughly 152 USD of LLM and compute per task.

Finding bugs is easier than repairing: 77% found, only 61% also patched.

1st Team Atlanta (4M USD) • 2nd Trail of Bits (3M USD) • 3rd Theori (1.5M USD). Source: DARPA AIxCC final results, Aug 2025. All seven CRSs were open-sourced after the final.

Inside an AIxCC-style CRS: who does what

StageEngine that does the workWhat decides it worked
Build + harnessOSS-Fuzz build; LLM writes extra harnesses, seeds, dictionariesit compiles and runs
DiscoverylibFuzzer / AFL++ (Jazzer for Java) on many coressanitizer crash
Stuck branchconcolic execution (SymCC-style) or an LLM-written inputnew coverage
Where to lookstatic analysis; on a delta scan the LLM reads the commit diff and directs fuzzing at itthe sink gets covered
TriageLLM reads the sanitizer report and the code, names the root causePoV replays deterministically
PatchLLM proposes several candidate patchesbuild, project tests, PoV no longer trips
SARIFLLM plus fuzzer evidence labels the static reportscoring harness

The recurring architecture

Every system here runs one loop: semantic hint → executable experiment → feedback

SystemHintExperimentCheck
Driller / QSYMsolver model for a negated branchrun it as a seednew coverage
ChatAFLLLM grammar, next messagesend to the servercoverage
HLPFuzzLLM solves the stuck branchrun the processortarget block runs
PANGOLINLLM entry map and specfuzz the deviceresponses match spec
Autonomous pentesterLLM vulnerability hypothesisHTTP requestsper-class validator
AIxCC CRSLLM triage and patchPoV, patched buildsanitizer, tests, replay

Viewing a single bug through from POV of many tools...

Back to the very first example:

char buf[64];
if (len < 64 && magic == 0x41424344) {
  memcpy(buf, input, len + 16);
}
Fuzzing supplies ground truth. Symbolic execution phrases the question. SMT answers it. The LLM does the guessing.
  1. Fuzzer. Millions of runs a minute, but a random 4-byte magic matches once in 232. Coverage stalls at the if.
  2. Symbolic execution. Re-runs a stalled input concolically. Path condition to the memcpy: len < 64 ∧ magic = 0x41424344.
  3. SMT. Bit-vector query, milliseconds: SAT with magic = 0x41424344, len = 0. If you know the safety property, ask it directly: len < 64 ∧ len + 16 > 64 is SAT with len = 49.
  4. Fuzzer. The model is a seed. It passes the check; a few mutations of len later, ASan reports a stack-buffer-overflow at the memcpy. Input + report = PoV.
  5. LLM. Reads the report and the function, names the cause (the check bounds len, the copy uses len + 16), proposes len + 16 <= sizeof buf, writes a regression test.
  6. Validator. Build, tests, replay the PoV: no crash. Fuzzing restarts on the patched binary.

So, does SMT still matter?

Yes, as a component, not the driver. Each engine is trusted for something different:

  • Fuzzing: cheap and never wrong, but blind.
  • Symbolic execution + SMT: exact, and can say "no such input exists," but can't prove
  • LLM: great at guessing, use in a loop with conventional tools

To me, the big question is: how does LLM+X change the game versus traditional tools?

We'll explore this question as the answer evolves over the course of the semester! :-)