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
SAT versus SMT
Propositional SAT asks whether a Boolean formula can be made true.
(x∨y)∧(¬x∨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. s.t. the formula (x∨y)∧(¬x∨z) is true?"
By contrast, SMT asks whether a formula is satisfiable modulo a first-order theory:
0≤n<64∧n+16>72
Here, the symbols ≤, <, +, 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=5, we must have y=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
Implementation detail: The conflict-driven trace on the previous slide is one illustrative schedule. A real solver may detect this consequence earlier.
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 28
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:
Boolean (SAT solver) search proposes an assignment
Theory solver checks whether it makes sense
If contradiction, theory solver explains the conflict and returns a "lemma"
Boolean search learns, backtracks, and tries again
Figure: DPLL(T) intuition
So the Boolean search learns: not all of A,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...
build and run a target
explore behavior
find suspicious states
prove or demonstrate a vulnerability
repair or mitigate it
validate the result
My claim: cyber reasoning systems will become increasingly important for both defensive and offensive tasks
Figure: the CRS loop
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)
Fuzz. Mutation is nearly free; it finds every shallow path
Stall. No new coverage; every queued input dies at the same branches
Trace. Re-run a stalled input concolically: concrete values, but record the path condition as it goes
Flip. At a branch the fuzzer never took, negate its condition and ask SMT
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 kind
Representative examples
LLM-guided fuzzing
ChatAFL, HLPFuzz, ELFuzz, PANGOLIN
Agentic pentesting
PentestGPT, Cybench
Agent-computer interface
SWE-agent
End-to-end vulnerability lifecycle
BountyBench, AIxCC
Firmware / binary-oriented CRS
FirmAgent
Real-world agentic research
Project 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.
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.
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, 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
found
patched
synthetic (70 injected)
54 (77%)
43 (61%)
real, previously unknown
18
11
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
Stage
Engine that does the work
What decides it worked
Build + harness
OSS-Fuzz build; LLM writes extra harnesses, seeds, dictionaries
it compiles and runs
Discovery
libFuzzer / AFL++ (Jazzer for Java) on many cores
sanitizer crash
Stuck branch
concolic execution (SymCC-style) or an LLM-written input
new coverage
Where to look
static analysis; on a delta scan the LLM reads the commit diff and directs fuzzing at it
the sink gets covered
Triage
LLM reads the sanitizer report and the code, names the root cause
PoV replays deterministically
Patch
LLM proposes several candidate patches
build, project tests, PoV no longer trips
SARIF
LLM plus fuzzer evidence labels the static report
scoring harness
The recurring architecture
Every system here runs one loop: semantic hint → executable experiment → feedback
System
Hint
Experiment
Check
Driller / QSYM
solver model for a negated branch
run it as a seed
new coverage
ChatAFL
LLM grammar, next message
send to the server
coverage
HLPFuzz
LLM solves the stuck branch
run the processor
target block runs
PANGOLIN
LLM entry map and spec
fuzz the device
responses match spec
Autonomous pentester
LLM vulnerability hypothesis
HTTP requests
per-class validator
AIxCC CRS
LLM triage and patch
PoV, patched build
sanitizer, 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.
Fuzzer. Millions of runs a minute, but a random 4-byte magic matches once in 232. Coverage stalls at the if.
Symbolic execution. Re-runs a stalled input concolically. Path condition to the memcpy: len < 64 ∧ magic = 0x41424344.
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.
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.
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.
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! :-)