augure 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +136 -0
- data/LICENSE +21 -0
- data/README.md +217 -0
- data/exe/augure +181 -0
- data/exe/augure-benchmark +67 -0
- data/home/runner/work/augure/augure/docs/agent-snippet.md +31 -0
- data/home/runner/work/augure/augure/docs/auditability.md +160 -0
- data/home/runner/work/augure/augure/docs/build-format.md +119 -0
- data/home/runner/work/augure/augure/docs/explanation-architecture.md +74 -0
- data/home/runner/work/augure/augure/docs/how-to-swap-solver.md +73 -0
- data/home/runner/work/augure/augure/docs/how-to-write-rules.md +71 -0
- data/home/runner/work/augure/augure/docs/pack-format.md +137 -0
- data/home/runner/work/augure/augure/docs/reference.md +293 -0
- data/home/runner/work/augure/augure/docs/tutorial.md +108 -0
- data/home/runner/work/augure/augure/examples/ret2win.facts +11 -0
- data/home/runner/work/augure/augure/examples/split.facts +12 -0
- data/home/runner/work/augure/augure/exe/augure +181 -0
- data/home/runner/work/augure/augure/exe/augure-benchmark +67 -0
- data/home/runner/work/augure/augure/lib/augure/bandit.rb +118 -0
- data/home/runner/work/augure/augure/lib/augure/coverage.rb +56 -0
- data/home/runner/work/augure/augure/lib/augure/ctf_corpus.json +773 -0
- data/home/runner/work/augure/augure/lib/augure/engine.rb +128 -0
- data/home/runner/work/augure/augure/lib/augure/facts.rb +194 -0
- data/home/runner/work/augure/augure/lib/augure/kb_corpus.json +345 -0
- data/home/runner/work/augure/augure/lib/augure/knowledge_base.rb +109 -0
- data/home/runner/work/augure/augure/lib/augure/mcts.rb +178 -0
- data/home/runner/work/augure/augure/lib/augure/pack.rb +244 -0
- data/home/runner/work/augure/augure/lib/augure/pack_loader.rb +93 -0
- data/home/runner/work/augure/augure/lib/augure/parity.rb +69 -0
- data/home/runner/work/augure/augure/lib/augure/pipeline.rb +107 -0
- data/home/runner/work/augure/augure/lib/augure/priors.rb +32 -0
- data/home/runner/work/augure/augure/lib/augure/rules.rb +300 -0
- data/home/runner/work/augure/augure/lib/augure/smt_process.rb +48 -0
- data/home/runner/work/augure/augure/lib/augure/verifier.rb +40 -0
- data/home/runner/work/augure/augure/lib/augure/version.rb +5 -0
- data/home/runner/work/augure/augure/lib/augure.rb +25 -0
- data/home/runner/work/augure/augure/packs/banner_leak.yml +46 -0
- data/home/runner/work/augure/augure/packs/brop.yml +46 -0
- data/home/runner/work/augure/augure/packs/fastbin_hook.yml +45 -0
- data/home/runner/work/augure/augure/packs/got_partial_overwrite.yml +45 -0
- data/home/runner/work/augure/augure/packs/got_partial_overwrite_deref.yml +39 -0
- data/home/runner/work/augure/augure/packs/house_botcake.yml +38 -0
- data/home/runner/work/augure/augure/packs/house_force.yml +37 -0
- data/home/runner/work/augure/augure/packs/house_of_apple2.yml +45 -0
- data/home/runner/work/augure/augure/packs/house_orange.yml +38 -0
- data/home/runner/work/augure/augure/packs/jop.yml +39 -0
- data/home/runner/work/augure/augure/packs/large_bin_attack.yml +39 -0
- data/home/runner/work/augure/augure/packs/one_gadget.yml +38 -0
- data/home/runner/work/augure/augure/packs/orw.yml +55 -0
- data/home/runner/work/augure/augure/packs/ret2csu_v2.yml +55 -0
- data/home/runner/work/augure/augure/packs/ret2dlresolve_x86.yml +39 -0
- data/home/runner/work/augure/augure/packs/ret2partial_overwrite.yml +38 -0
- data/home/runner/work/augure/augure/packs/ret2vdso.yml +39 -0
- data/home/runner/work/augure/augure/packs/setcontext_srop.yml +46 -0
- data/home/runner/work/augure/augure/packs/uaf_tcache.yml +45 -0
- data/home/runner/work/augure/augure/packs/unsorted_bin_attack.yml +38 -0
- metadata +103 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Auditability: what it means, and what it costs each team
|
|
2
|
+
|
|
3
|
+
*Explanation. This page is the "why" behind the `explain` payload.*
|
|
4
|
+
|
|
5
|
+
## The tools (the narrative runs)
|
|
6
|
+
|
|
7
|
+
The pattern has a name now. Kambhampati et al. call it the
|
|
8
|
+
LLM-Modulo framework ("LLMs Can't Plan, But Can Help Planning", ICML
|
|
9
|
+
2024): the LLM proposes, external model-based verifiers dispose.
|
|
10
|
+
Wang et al.'s CHECKMATE (arXiv 2512.11143, 2025) measured it: Claude
|
|
11
|
+
Code + an external classical planner improves penetration success by
|
|
12
|
+
over 20%. augure-lictor IS that shape, with the receipts: the
|
|
13
|
+
verifier is Datalog + a frozen corpus, not prose.
|
|
14
|
+
|
|
15
|
+
> The narrative below is now a TOOL: `augure coverage --flip
|
|
16
|
+
> canary:true` replays the corpus under a hardening override and
|
|
17
|
+
> prints what died (the blue scorecard); `augure rules --mitre
|
|
18
|
+
> --json` exports the ATT&CK Navigator layer; the packs' `detection:`
|
|
19
|
+
> sections give lictor's `report --defender` its per-technique
|
|
20
|
+
> observables. The prose describes the design; the commands run it.
|
|
21
|
+
|
|
22
|
+
## What auditability means here
|
|
23
|
+
|
|
24
|
+
Every decision Augure produces is a set of readable proofs, not a score:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"schema": "augure/decision@1",
|
|
29
|
+
"selected": "ret2plt",
|
|
30
|
+
"explain": {
|
|
31
|
+
"provenance": {
|
|
32
|
+
"ret2plt": [{
|
|
33
|
+
"rule": "app_ret2plt",
|
|
34
|
+
"origin": null,
|
|
35
|
+
"evidence": [["vuln", "sof"], ["nx", "true"], ["pie", "false"], ["plt", "system"]]
|
|
36
|
+
}]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
When sibling rules share a head (the documented way to express OR),
|
|
43
|
+
EVERY firing appears - each attributed to its own rule id and origin.
|
|
44
|
+
|
|
45
|
+
Three properties fall out of that shape:
|
|
46
|
+
|
|
47
|
+
1. **Traceability.** Every technique claim points to a rule ID and the exact
|
|
48
|
+
facts that satisfied it. Nothing is a vibe.
|
|
49
|
+
2. **Reproducibility.** The bandit's draw is seeded. Same facts, same seed,
|
|
50
|
+
same decision - re-runnable years later, in front of a client, in front
|
|
51
|
+
of an auditor.
|
|
52
|
+
3. **Disprovability.** A wrong rule is a *findable* wrong rule. When Augure
|
|
53
|
+
disagrees with your intuition, you can locate the disagreement precisely:
|
|
54
|
+
which fact was missing, which condition is too narrow. That disagreement
|
|
55
|
+
is a bug report or a research result - never a shrug.
|
|
56
|
+
|
|
57
|
+
The original system's motto still applies: *one is a clock, the other is a
|
|
58
|
+
cat*. An LLM agent improvises; a clock ticks the same way every time. You
|
|
59
|
+
can open a clock.
|
|
60
|
+
|
|
61
|
+
## For red team: the defensible choice
|
|
62
|
+
|
|
63
|
+
An engagement report that says *"we chose ret2plt because the model said
|
|
64
|
+
so"* does not survive client scrutiny. One that says *"ret2plt was selected
|
|
65
|
+
by rule `app_ret2plt` because the binary imports `system@plt`, PIE is off,
|
|
66
|
+
and the pop-rdi gadget is present; the chain passed the payload-fit check"* -
|
|
67
|
+
does.
|
|
68
|
+
|
|
69
|
+
Concretely, red teams get:
|
|
70
|
+
|
|
71
|
+
- **Methodology you can defend.** The explain block drops into the report as
|
|
72
|
+
evidence, per target.
|
|
73
|
+
- **After-action replay.** Same facts + same seed = same decision. Re-run
|
|
74
|
+
last quarter's engagement against the current rules and see exactly what
|
|
75
|
+
the rule updates changed.
|
|
76
|
+
- **Benchmarked intuition.** The prior table encodes measured weight
|
|
77
|
+
inversions: human operators overweighted shellcode and `ret2libc` and
|
|
78
|
+
underweighted `ret2plt`. Your team can measure its own priors the same
|
|
79
|
+
way - and the disagreement with Augure's corpus is itself signal.
|
|
80
|
+
|
|
81
|
+
The honest boundary: the priors are **authored, not measured on your
|
|
82
|
+
targets**. Treat Augure's recommendation as a falsifiable hypothesis that
|
|
83
|
+
happens to carry its own proof trail - not as an oracle.
|
|
84
|
+
|
|
85
|
+
## For blue team: the model read backwards
|
|
86
|
+
|
|
87
|
+
The same fact/rule table, read in reverse, is a hardening map:
|
|
88
|
+
|
|
89
|
+
- **Coverage mapping.** "Which techniques does our estate make
|
|
90
|
+
*impossible*?" A rule fires only if its facts hold. `app_shellcode`
|
|
91
|
+
requires NX off - flip the fact, the rule dies. Enumerate which
|
|
92
|
+
protections kill which rules, per asset class.
|
|
93
|
+
- **Hardening prioritization.** Each control eliminates a set of rules. The
|
|
94
|
+
control that kills the most rules on your asset mix is your best next
|
|
95
|
+
spend - computable, because the model is explicit.
|
|
96
|
+
- **Anti-reflex prediction.** The corpus's discriminating cases
|
|
97
|
+
(`stack6`/`stack7`-style: filtered return addresses) show what an
|
|
98
|
+
*adaptive* attacker does when the obvious technique dies: it plans the
|
|
99
|
+
boring alternative. Blue teams can run the same planner on their own
|
|
100
|
+
fact base to see the second-order moves.
|
|
101
|
+
- **Reconnaissance signatures.** Every fact a rule needs is something an
|
|
102
|
+
attacker must *learn* first: `plt("system")`, `gadget("pop_rdi_ret")`,
|
|
103
|
+
`glibc_minor`. The fact list is a detection surface: what your monitoring
|
|
104
|
+
should catch someone enumerating.
|
|
105
|
+
|
|
106
|
+
## For purple team: the shared artifact
|
|
107
|
+
|
|
108
|
+
Red and blue arguing about "likely attack paths" argue from different
|
|
109
|
+
imaginations. Both teams arguing about a *rule table* argue from the same
|
|
110
|
+
object:
|
|
111
|
+
|
|
112
|
+
```mermaid
|
|
113
|
+
graph LR
|
|
114
|
+
RB["blue adds a control"] --> C["fact set changes<br/>(NX on, RELRO full...)"]
|
|
115
|
+
C --> R["corpus re-run:<br/>rules stop firing"]
|
|
116
|
+
R --> P["planner predicts<br/>the next move"]
|
|
117
|
+
P --> D["blue validates<br/>detection for THAT move"]
|
|
118
|
+
D --> RB
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- **One vocabulary.** A technique rule is written once; red side proves it
|
|
122
|
+
fires, blue side proves it hurts, purple side keeps both proofs in one
|
|
123
|
+
repo.
|
|
124
|
+
- **The corpus is double-sided.** The 33 frozen CTF targets are offensive
|
|
125
|
+
regression tests *and* detection validation targets: when blue adds a
|
|
126
|
+
control, re-run the corpus, and the rules that fell are the measured
|
|
127
|
+
progress.
|
|
128
|
+
- **The loop.** Blue adds a control → fact set changes → rules stop firing →
|
|
129
|
+
planner predicts the next move → blue validates detection for *that*
|
|
130
|
+
move. That is a purple-team program with numbers on it.
|
|
131
|
+
|
|
132
|
+
## For compliance and assurance
|
|
133
|
+
|
|
134
|
+
- **Reproducible evidence.** Decision + seed + rules version = a run you
|
|
135
|
+
can re-execute during an audit. Penetration-test reports can cite the
|
|
136
|
+
engine output the way they cite a tool version.
|
|
137
|
+
- **Predictions vs field results.** The benchmark numbers are model
|
|
138
|
+
predictions against frozen corpora. The distinction is in the README and
|
|
139
|
+
it stays there: an auditor cares that you know the difference.
|
|
140
|
+
- **Full trail (Lictor).** When the companion orchestrator executes a
|
|
141
|
+
decision, the persisted chain is decision → rule → module → outcome.
|
|
142
|
+
Every operational action traces back to a proof.
|
|
143
|
+
|
|
144
|
+
## What auditability does NOT cover
|
|
145
|
+
|
|
146
|
+
Say it plainly:
|
|
147
|
+
|
|
148
|
+
1. **Completeness of rules.** What is not encoded is not audited. A
|
|
149
|
+
technique absent from the table is invisible to the decision layer -
|
|
150
|
+
auditability guarantees you can inspect what was considered, not that
|
|
151
|
+
everything was.
|
|
152
|
+
2. **Prior accuracy.** The Beta priors are authored constants calibrated
|
|
153
|
+
from a corpus of write-ups. They are a starting point to be fed back,
|
|
154
|
+
not a measurement of your targets.
|
|
155
|
+
3. **The execution side.** Augure decides; Lictor executes. An audit of the
|
|
156
|
+
decision layer says nothing about what the executor did beyond what its
|
|
157
|
+
own trail records.
|
|
158
|
+
|
|
159
|
+
Auditability is a property of the *decision*, and that is exactly as far as
|
|
160
|
+
it reaches. Everything else still requires a human who reads.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# The build format: from a decision to bytes
|
|
2
|
+
|
|
3
|
+
augure decides; the pack says how the decision becomes a payload.
|
|
4
|
+
This page documents the BUILD format: the slots vocabulary, how a
|
|
5
|
+
technique becomes buildable, and where every number comes from.
|
|
6
|
+
|
|
7
|
+
## The lineage (all published)
|
|
8
|
+
|
|
9
|
+
The build layer descends from a line of published work:
|
|
10
|
+
|
|
11
|
+
- **metasm** - Y. Guillot, *"Metasm: un framework de manipulation de
|
|
12
|
+
code en Ruby"*, SSTIC 2007 (also hack.lu 2007).
|
|
13
|
+
- **Déprotection semi-automatique de binaire** - Guillot & Gazet,
|
|
14
|
+
SSTIC 2008: static unpacking driven by metasm's disassembler.
|
|
15
|
+
- **Exploitation automatique avec metasm** - esec-lab (Sogeti),
|
|
16
|
+
June 2010: the automatic-exploitation loop this project re-derives,
|
|
17
|
+
auditable this time.
|
|
18
|
+
- **miasm** - C. Desclaux, SSTIC 2012: metasm's Python successor. The
|
|
19
|
+
strategy-sim prototype carries a miasm backend in its profiler - the
|
|
20
|
+
docstring says "Python equivalent of Metasm", literally.
|
|
21
|
+
- **Ghost writing** - screwnomore, 2015: shellcode as preprocessor
|
|
22
|
+
templates that mutate per build. Our shellcode stubs are assembly
|
|
23
|
+
SOURCE with equivalent variants picked by a seeded rng.
|
|
24
|
+
|
|
25
|
+
## Where the numbers come from
|
|
26
|
+
|
|
27
|
+
| Number | Source | Provenance in the trail |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| the offset | the frame read statically (`lea reg, [rbp-N]` before the unsafe call) or the cyclic pattern crashed under the debugger | `offset_source: static-frame` / `gdb-cyclic` |
|
|
30
|
+
| the return address | the facts: `win_symbol("win", addr)` or a gadget address | `ret_source: win_symbol:win@0x...` |
|
|
31
|
+
| the gadget | the profiler's byte patterns, then RE-DECODED at the claimed address (a lying gadget is refused) | the gadget fact's address |
|
|
32
|
+
| the shellcode | assembly SOURCE in `augure-profiler/shellcode.rb`, assembled by metasm | the stub name |
|
|
33
|
+
|
|
34
|
+
Nothing is guessed. A number the facts cannot supply is a loud
|
|
35
|
+
`BuildError`, never a magic constant.
|
|
36
|
+
|
|
37
|
+
## The slots vocabulary
|
|
38
|
+
|
|
39
|
+
A build layout is an ordered list of slots (data, in
|
|
40
|
+
`lictor/lib/config/build_layouts.yml` or a pack's `build:` section):
|
|
41
|
+
|
|
42
|
+
| Slot | Arguments | Emits |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `padding` | `measured_offset`, optional `sled` | the measured offset of `A` (or `\x90`) |
|
|
45
|
+
| `qword` | `win` or `gadget:TYPE` | the fact's address, 8 bytes |
|
|
46
|
+
| `dword` | same | 4 bytes (x86), whatever the arch |
|
|
47
|
+
| `shellcode` | `sh`, `bash`, `orw` (+ the flag path as the third element), or `file` with `--payload-file` | the assembled stub |
|
|
48
|
+
| `chain` | one or more refs | one arch-width word per ref, in order |
|
|
49
|
+
|
|
50
|
+
The `chain` refs resolve against the facts, loud `BuildError` on any
|
|
51
|
+
missing piece: `gadget:TYPE`, `plt_addr:NAME`, `got_addr:NAME` (the
|
|
52
|
+
profiler's address facts), `win`, `leak:NAME` (a
|
|
53
|
+
`leaked_address(name)` fact - the harvest, the run's leak capture),
|
|
54
|
+
`const:N` (a literal - a syscall number, an offset). The built-in
|
|
55
|
+
layouts cover `ret2plt` (the puts@got leak chain) and `ret2libc`
|
|
56
|
+
(post-leak: ret-align + pop_rdi + binsh + system); the `orw` layout
|
|
57
|
+
strikes through the leaked buffer address. The `ret2csu_v2` pack
|
|
58
|
+
teaches its own chain (the canonical csu leak) in `build:` - packs
|
|
59
|
+
teach, the builder follows.
|
|
60
|
+
|
|
61
|
+
Under `seccomp("true")` the `sh` stub is a booby trap (execve dies):
|
|
62
|
+
the builder switches the stub to the orw chain (open/read/write,
|
|
63
|
+
clean exit) with the flag path embedded.
|
|
64
|
+
|
|
65
|
+
The armor validates a pack's `build:` section: the technique must be
|
|
66
|
+
the pack's own, the layout must be a slot list from the vocabulary
|
|
67
|
+
(padding, qword, dword, shellcode, chain), the refs must name
|
|
68
|
+
existing predicates. A refused layout never reaches the builder.
|
|
69
|
+
|
|
70
|
+
## What metasm does here
|
|
71
|
+
|
|
72
|
+
1. **The frame read** (`StaticOffset`): decode the ELF, resolve the
|
|
73
|
+
unsafe imports' PLT stubs (`.rela.plt` order; both the classic
|
|
74
|
+
`.plt` and the CFI `.plt.sec` layouts), disassemble `.text` from
|
|
75
|
+
every function symbol, net the `e8 rel32` call to an unsafe sink,
|
|
76
|
+
read the buffer's `rbp` displacement: offset = saved rbp + return
|
|
77
|
+
slot - displacement. Validated against the canonical numbers
|
|
78
|
+
(ROP Emporium ret2win x64 = 40, split = 40).
|
|
79
|
+
2. **The shellcode**: `Metasm::Shellcode.assemble` on committed
|
|
80
|
+
assembly text; the call-back layout keeps every stub
|
|
81
|
+
position-independent; the live-execution test in the suite runs our
|
|
82
|
+
own assembled bytes.
|
|
83
|
+
3. **The gadget verification**: the bytes at a claimed gadget address
|
|
84
|
+
are re-decoded and must match the advertised semantics.
|
|
85
|
+
4. **The crash identification**: the static side names the vulnerable
|
|
86
|
+
function and its sink; the control evidence is the known pattern
|
|
87
|
+
found in the registers - exact, not inferred from 0x41414141 shapes.
|
|
88
|
+
5. **The general gadget hunt**: every offset of the executable
|
|
89
|
+
sections, a decoded chain ending on ret, classified semantically -
|
|
90
|
+
the multi-instruction gadgets (pop rsi; pop r15; ret) the byte
|
|
91
|
+
patterns never see.
|
|
92
|
+
6. **The dynamic layer**: the write-site proof (a watchpoint on the
|
|
93
|
+
return slot catches the instruction that plants the overflow - the
|
|
94
|
+
taint proven, not inferred), the runtime map (libc, stack of a live
|
|
95
|
+
run), and lictor's harvest (the service's banner becomes
|
|
96
|
+
leaked_address facts).
|
|
97
|
+
7. **The encoders**: the keyed self-decoder with the backward-call
|
|
98
|
+
get-pc (a negative rel32 carries ff bytes, never 00), the payload
|
|
99
|
+
nop-padded under 128 bytes, the keys per seed. The shikata
|
|
100
|
+
inheritance, auditable: the seed reproduces the exact bytes. A
|
|
101
|
+
payload wider than 120 bytes on a 0x00 channel is refused honestly.
|
|
102
|
+
|
|
103
|
+
No emulation: the suite's live-execution tests are the truth.
|
|
104
|
+
|
|
105
|
+
## Teaching a buildable technique
|
|
106
|
+
|
|
107
|
+
Write the pack (rules, kb, mcts, priors per the pack format), add its
|
|
108
|
+
`build:` section with the layout, run the armor:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
lictor rule validate my_pack.yml # the armor + the corpus guard
|
|
112
|
+
lictor rule install my_pack.yml # the technique becomes decidable AND buildable
|
|
113
|
+
lictor plan target.facts --packs packs/
|
|
114
|
+
lictor build ret2mything --facts target.facts
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The LLM path goes through the same armor: `validate_pack` and
|
|
118
|
+
`scaffold_exploit` on the MCP surface are the same doors, staged for
|
|
119
|
+
review.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Architecture: why rules-as-data
|
|
2
|
+
|
|
3
|
+
*Explanation. The design rationale.*
|
|
4
|
+
|
|
5
|
+
## The one decision that shapes everything
|
|
6
|
+
|
|
7
|
+
> **Separate what the target IS (facts) from what you DO about it (rules),
|
|
8
|
+
> and make both data.**
|
|
9
|
+
|
|
10
|
+
Everything else follows. The fact schema is a schema; the rules are rows;
|
|
11
|
+
the engine is an interpreter over the rows. That separation is what makes
|
|
12
|
+
an auditable decision layer possible at all:
|
|
13
|
+
|
|
14
|
+
- **Provenance**: only data can answer "which rule fired because of which
|
|
15
|
+
facts" - a case statement cannot explain itself.
|
|
16
|
+
- **A second engine**: rules-as-data can be generated into a the frozen corpus .dl
|
|
17
|
+
program, so a compiled Datalog engine and the Ruby engine can be
|
|
18
|
+
conformance-tested against each other - the frozen corpus catches any
|
|
19
|
+
drift between them.
|
|
20
|
+
- **The reverse reading**: "which protections kill which techniques" is
|
|
21
|
+
a table traversal, not a code trace.
|
|
22
|
+
|
|
23
|
+
## The engine is boring on purpose
|
|
24
|
+
|
|
25
|
+
Forward-chaining fixpoint over a tiny rule set: derive tuples until nothing
|
|
26
|
+
new derives. No backtracking, no unification beyond equality, no cuts. The
|
|
27
|
+
entire semantics fits in one file and one afternoon of reading. Boring is
|
|
28
|
+
the point: auditable systems are systems a reviewer can finish.
|
|
29
|
+
|
|
30
|
+
Negation is stratified by convention (only on input facts) and enforced by
|
|
31
|
+
the corpus spec. Defaults are closed-world (`nx` absent means `nx("true")`)
|
|
32
|
+
|
|
33
|
+
- documented, tested, and the single most surprising line in the engine.
|
|
34
|
+
|
|
35
|
+
## Uncertainty, in three layers
|
|
36
|
+
|
|
37
|
+
```mermaid
|
|
38
|
+
graph TD
|
|
39
|
+
AG["Genetic algorithm<br/>offline, synthetic targets"] -- "finds weight inversions" --> P["prior table<br/>(ret2plt 15,5 vs the human ROP anchor)"]
|
|
40
|
+
P --> B["Bandit: Thompson sampling<br/>online, flat, auditable (seeded)"]
|
|
41
|
+
B -- "blind spot: choices look independent" --> M["MCTS over STAGE_MODEL<br/>a leak is worth what it unlocks"]
|
|
42
|
+
M --> D["plan: fmtstr_leak -> ret2libc"]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
A flat bandit ranks `ret2libc` above a leak; MCTS plans the leak first
|
|
46
|
+
because the transition table says what it unlocks. The planner's lesson,
|
|
47
|
+
as a diagram.
|
|
48
|
+
|
|
49
|
+
1. **Genetic algorithm (offline).** Evolved weight vectors over synthetic
|
|
50
|
+
targets; found the inversions humans miss. Research tool, stays in
|
|
51
|
+
Python - it is the narrative, not the SDK.
|
|
52
|
+
2. **Bandit (online, flat).** Thompson sampling over Beta posteriors.
|
|
53
|
+
Adapts per target, remains auditable (seeded draws, logged feedback).
|
|
54
|
+
Blind spot: treats choices as independent.
|
|
55
|
+
3. **MCTS (sequential).** Plans multi-step trajectories: a leak is worth
|
|
56
|
+
what it unlocks, which no flat weight can express. The STAGE_MODEL is a
|
|
57
|
+
transition table - data again, so the planner stays inspectable.
|
|
58
|
+
|
|
59
|
+
## Subprocesses are the API policy
|
|
60
|
+
|
|
61
|
+
SMT solvers, LLM providers: every external capability enters
|
|
62
|
+
through a serialization boundary (facts text, SMT-LIB text, HTTP). That is
|
|
63
|
+
what makes the zero-runtime-dependency gem possible, keeps native-extension
|
|
64
|
+
attack surface at zero, and lets a consumer swap bitwuzla for cvc5 or an
|
|
65
|
+
LLM backend without touching the core.
|
|
66
|
+
|
|
67
|
+
## New proposal sources, and why they do not change this
|
|
68
|
+
|
|
69
|
+
The profiler, the fuzzer's triage, and any future LLM feature extractor
|
|
70
|
+
all emit *facts* into the same boundary. A hypothetical LLM hypothesis
|
|
71
|
+
generator stays behind the whitelist: it proposes facts, the schema
|
|
72
|
+
rejects anything else, the engine never sees a prose sentence. The
|
|
73
|
+
architecture accepts new proposal sources because the contract was never
|
|
74
|
+
"trust the source"; it is "speak fact, or leave".
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# How to: plug a real SMT solver
|
|
2
|
+
|
|
3
|
+
Assumes augure is installed. Goal: route the numerical checks through z3,
|
|
4
|
+
bitwuzla or cvc5 instead of the arithmetic fast path.
|
|
5
|
+
|
|
6
|
+
## 1. Install a solver
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
apt install z3 # or brew install z3
|
|
10
|
+
# bitwuzla and cvc5 have upstream binaries
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 2. Emit the SMT-LIB and solve it
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
require "augure"
|
|
17
|
+
|
|
18
|
+
smt = Augure::Verifier.payload_fits_smtlib(256, 40)
|
|
19
|
+
puts smt
|
|
20
|
+
# (set-logic QF_LIA)
|
|
21
|
+
# (declare-const buffer_size Int)
|
|
22
|
+
# (assert (= buffer_size 256))
|
|
23
|
+
# (assert (>= buffer_size 40))
|
|
24
|
+
# (check-sat)
|
|
25
|
+
|
|
26
|
+
status = Augure::SmtProcess.solve(smt, solver: "z3")
|
|
27
|
+
# => :sat
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The contract is text-in, status-atom-out:
|
|
31
|
+
|
|
32
|
+
| Return | Meaning |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `:sat` | constraint satisfied |
|
|
35
|
+
| `:unsat` | constraint violated |
|
|
36
|
+
| `:unknown` | solver missing, timed out, or answered garbage |
|
|
37
|
+
|
|
38
|
+
`:unknown` is deliberate: a verification *failure* must be visible and
|
|
39
|
+
logged, never a crash. Treat `:unknown` as "do not claim verified".
|
|
40
|
+
|
|
41
|
+
## 3. Solver options
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
Augure::SmtProcess.solve(smt, solver: "bitwuzla", timeout: 5)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- `solver` - any binary that reads SMT-LIB v2 on stdin.
|
|
48
|
+
- `timeout` - seconds (default 10). Expiry returns `:unknown` and kills the
|
|
49
|
+
child process.
|
|
50
|
+
|
|
51
|
+
## 4. Which path should you use?
|
|
52
|
+
|
|
53
|
+
The arithmetic fast path answers the same questions without a subprocess:
|
|
54
|
+
|
|
55
|
+
| | arithmetic | SMT solver |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| latency | nanoseconds | milliseconds (process spawn) |
|
|
58
|
+
| dependencies | none | solver binary |
|
|
59
|
+
| audit value | provable by inspection | provable by solver certificate |
|
|
60
|
+
|
|
61
|
+
Default to arithmetic; reach for the solver when a check outgrows integer
|
|
62
|
+
arithmetic (bit-vectors, arrays, string constraints). The corpus spec pins
|
|
63
|
+
both paths to the same answers, so switching never silently changes a
|
|
64
|
+
verdict.
|
|
65
|
+
|
|
66
|
+
## Troubleshooting
|
|
67
|
+
|
|
68
|
+
- **`:unknown` with a solver installed** - run the emitted text manually:
|
|
69
|
+
`z3 < smt.txt` and read its stdout. Augure only
|
|
70
|
+
parses a bare `sat`/`unsat` token; solver banners are fine, they are
|
|
71
|
+
ignored.
|
|
72
|
+
- **Permission denied** - the solver path is not executable; give the
|
|
73
|
+
absolute path.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# How to: write a technique rule
|
|
2
|
+
|
|
3
|
+
Assumes you have run the tutorial and understand the fact format. Goal: add
|
|
4
|
+
a rule to the applicability table with test coverage and provenance.
|
|
5
|
+
|
|
6
|
+
## 1. Add the rule to the table
|
|
7
|
+
|
|
8
|
+
Open `lib/augure/rules.rb`. Rules live in one place - the table IS the
|
|
9
|
+
engine. Add yours where it belongs in the reading order:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
b.rule :app_my_technique, ["applicable", "my_technique"],
|
|
13
|
+
source: "one line: when does this fire and why" do |c|
|
|
14
|
+
c.fact "vuln", "sof"
|
|
15
|
+
c.fact "nx", "true"
|
|
16
|
+
c.fact "plt", "pwnme"
|
|
17
|
+
end
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Condition vocabulary:
|
|
21
|
+
|
|
22
|
+
| Condition | Meaning |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `c.fact rel, val` | a tuple `rel("val")` exists |
|
|
25
|
+
| `c.not_fact rel, val` | that tuple is absent (input relations only) |
|
|
26
|
+
| `c.match rel, idx, "pattern"` | arg `idx` matches the regex |
|
|
27
|
+
| `c.cmp rel, :ge, 26` | an integer arg satisfies the comparison |
|
|
28
|
+
|
|
29
|
+
## 2. Test it (before the rule exists in your head, after in the file)
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
# spec/augure/engine_spec.rb
|
|
33
|
+
it "fires my_technique when pwnme is imported" do
|
|
34
|
+
f = "vuln(\"sof\").\nnx(\"true\").\nplt(\"pwnme\").\n"
|
|
35
|
+
expect(applicable(f)).to include("my_technique")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "does not fire my_technique without NX" do
|
|
39
|
+
f = "vuln(\"sof\").\nnx(\"false\").\nplt(\"pwnme\").\n"
|
|
40
|
+
expect(applicable(f)).not_to include("my_technique")
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Run `bundle exec rspec`. If the frozen CTF corpus conformance spec now fails,
|
|
45
|
+
your rule changed an existing decision: that is either a bug in the rule or
|
|
46
|
+
an improvement - update the corpus entry and say which, in the commit.
|
|
47
|
+
|
|
48
|
+
## 3. Check the provenance appears
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
bundle exec ruby -e '
|
|
52
|
+
require "augure"
|
|
53
|
+
r = Augure::Pipeline.analyze(facts: "vuln(\"sof\").\nnx(\"true\").\nplt(\"pwnme\").\n")
|
|
54
|
+
p r[:explain][:provenance]["my_technique"]
|
|
55
|
+
'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
You should see `rule: :app_my_technique` with the evidence pairs. That is
|
|
59
|
+
the audit payload - it is automatic, you earned it by using the table.
|
|
60
|
+
|
|
61
|
+
## Rules of the road
|
|
62
|
+
|
|
63
|
+
- **One rule, one reason.** Split OR-conditions into sibling rules sharing a
|
|
64
|
+
head; the provenance then tells *which* reason fired.
|
|
65
|
+
- **Negation on inputs only.** `not_fact` on a derived relation breaks
|
|
66
|
+
stratification; the corpus conformance spec will catch it loudly.
|
|
67
|
+
- **Defaults are closed-world.** Absent `nx` means `nx("true")` - document
|
|
68
|
+
any rule that depends on absence.
|
|
69
|
+
- **The corpus is the contract.** A rules commit that moves a corpus verdict
|
|
70
|
+
must update `lib/augure/ctf_corpus.json` in the same commit and justify
|
|
71
|
+
the move in the message.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Pack format: teach augure a new technique (as data)
|
|
2
|
+
|
|
3
|
+
*How-to. For operators and LLM agents. You write a YAML file; the armor
|
|
4
|
+
decides whether augure learns it.*
|
|
5
|
+
|
|
6
|
+
## The one rule before anything
|
|
7
|
+
|
|
8
|
+
**You write data, never code.** A pack is a constrained YAML document.
|
|
9
|
+
If your intent needs something the format cannot express, the format
|
|
10
|
+
will refuse it - that is the design, not a bug. The vocabulary exists
|
|
11
|
+
so that a wrong pack is impossible, not merely discouraged.
|
|
12
|
+
|
|
13
|
+
## Anatomy
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
technique: my_new_technique # [a-z0-9_]+ - the NEW technique's name
|
|
17
|
+
author: claude-code (session 2026-10-03) # mandatory: who taught augure
|
|
18
|
+
source: https://... # mandatory: a verifiable reference
|
|
19
|
+
rules:
|
|
20
|
+
- id: app_my_new_technique # convention: app_<technique>
|
|
21
|
+
head: [applicable, my_new_technique]
|
|
22
|
+
conditions: # ONLY the 4 forms below
|
|
23
|
+
- [fact, vuln, sof] # a tuple vuln("sof") exists
|
|
24
|
+
- [not_fact, relro, full] # absent (input facts ONLY)
|
|
25
|
+
- [match, gadget, 0, pop] # gadget arg 0 matches /pop/
|
|
26
|
+
- [cmp, glibc_minor, ge, 26] # integer arg >= 26
|
|
27
|
+
kb:
|
|
28
|
+
- id: my_new_technique_001
|
|
29
|
+
technique: my_new_technique
|
|
30
|
+
description: >
|
|
31
|
+
What the technique is, when it applies, what it needs. This is
|
|
32
|
+
what the agent (or human) will read back from `augure explain`.
|
|
33
|
+
chain: [step_one, step_two]
|
|
34
|
+
unlocks: [libc_base]
|
|
35
|
+
success_rate: 0.7 # 0..1 - authored, say so if uncertain
|
|
36
|
+
mcts:
|
|
37
|
+
requires: [] # capabilities needed before this runs
|
|
38
|
+
provides: [libc_base] # capabilities it unlocks
|
|
39
|
+
terminal: false # true only if it yields a shell
|
|
40
|
+
success: 0.7 # the planner's base success
|
|
41
|
+
priors: [14, 6] # Beta(alpha, beta): mean 0.7 here
|
|
42
|
+
|
|
43
|
+
# All three sections below are OPTIONAL:
|
|
44
|
+
build: # (check 8) the assembly order - packs
|
|
45
|
+
technique: my_new_technique # teach, lictor's builder follows
|
|
46
|
+
layout: # slots: padding/qword/dword/shellcode/chain
|
|
47
|
+
- [padding, measured_offset]
|
|
48
|
+
- [chain, "gadget:pop_rdi_ret", "plt_addr:system"]
|
|
49
|
+
|
|
50
|
+
detection: # (check 9) the DEFENDER side - what the
|
|
51
|
+
- channel: wire # technique looks like to the defenses;
|
|
52
|
+
signature: "cyclic padding then a non-mapped return" # channel from
|
|
53
|
+
note: "the padding phase" # wire/syslog/syscall/crash/file
|
|
54
|
+
|
|
55
|
+
mitre: # (check 10) the ATT&CK mapping
|
|
56
|
+
- T1068
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## The checks (in the order the loader runs them)
|
|
60
|
+
|
|
61
|
+
1. **Condition vocabulary**: only `fact`, `not_fact`, `match`, `cmp`.
|
|
62
|
+
Anything else - including "clever" ones - is refused.
|
|
63
|
+
2. **Existing predicates only**: your conditions may reference the 33
|
|
64
|
+
input facts (see `docs/reference.md#schema` - the binary vocabulary:
|
|
65
|
+
nx/pie/canary/plt/gadget/plt_addr/got_addr...; the network vocabulary,
|
|
66
|
+
emitted from nmap scans: service/software_version/remote; the
|
|
67
|
+
confidence marker: verified; the leaks: leaked_address) and the
|
|
68
|
+
built-in derived relations (`vuln`, `has_reg_control`,
|
|
69
|
+
`has_write_primitive`, `has_syscall_gadget`, `has_csu_gadget`,
|
|
70
|
+
`has_pivot_gadget`, `enough_gadgets`). You cannot invent a fact
|
|
71
|
+
source.
|
|
72
|
+
3. **Stratification**: `not_fact` on input facts only.
|
|
73
|
+
4. **New heads only**: your technique must be NEW. Overriding
|
|
74
|
+
`ret2plt` or any built-in is refused - the corpus owns those verdicts.
|
|
75
|
+
5. **Priors and rates in range**: priors are exactly `[alpha, beta]`
|
|
76
|
+
numbers; success rates in `[0, 1]`.
|
|
77
|
+
6. **Corpus guard**: with your pack loaded, all 33 frozen corpus
|
|
78
|
+
targets must keep their documented ranking (your technique may rank
|
|
79
|
+
BELOW the documented top). A pack that takes the top-1 of a
|
|
80
|
+
documented target is refused - claiming better on frozen truth is a
|
|
81
|
+
dev-time corpus update, not a runtime pack. Two documented
|
|
82
|
+
exceptions: an entry with an EMPTY built-in verdict has nothing to
|
|
83
|
+
move (additive packs extend it), and an entry documenting
|
|
84
|
+
`pack_verdict: <technique>` polices the pack answer's ARRIVAL
|
|
85
|
+
instead - the pwnable/orw case.
|
|
86
|
+
7. **match patterns and cmp operands fail at load**: a `match` pattern
|
|
87
|
+
is precompiled (`RegexpError` at load, not at decision); `cmp`
|
|
88
|
+
operands must be integers and operators from the engine's table -
|
|
89
|
+
`"26"` or a typo'd operator never reaches the engine.
|
|
90
|
+
8. **build: (optional)**: must name its own technique; the layout must
|
|
91
|
+
be a non-empty slot list from the vocabulary
|
|
92
|
+
(padding/qword/dword/shellcode/chain). The builder interprets; the
|
|
93
|
+
armor gates the vocabulary.
|
|
94
|
+
9. **detection: (optional)**: each entry needs a `channel` from
|
|
95
|
+
wire/syslog/syscall/crash/file plus a concrete `signature` and
|
|
96
|
+
`note` - a vague hint is not detection content.
|
|
97
|
+
10. **mitre: (optional)**: an array of ATT&CK technique codes
|
|
98
|
+
(`T1068`, `T1068.001`) only.
|
|
99
|
+
|
|
100
|
+
## The optional `build:` and `detection:` sections
|
|
101
|
+
|
|
102
|
+
`build:` teaches the BUILDER the technique's assembly order (packs
|
|
103
|
+
teach, lictor's builder follows): the layout must be a slot list from
|
|
104
|
+
the vocabulary (padding/qword/dword/shellcode/chain - see
|
|
105
|
+
build-format.md), the technique must be the pack's own. Armor check 8.
|
|
106
|
+
|
|
107
|
+
`detection:` is the DEFENDER side of the technique - the corpus read
|
|
108
|
+
backwards. One entry per observable: `channel` from the constrained
|
|
109
|
+
vocabulary (wire/syslog/syscall/crash/file), a concrete `signature`,
|
|
110
|
+
and a `note` saying what the observable means. A vague hint is not
|
|
111
|
+
detection content - the armor rejects it (check 9). The blue team
|
|
112
|
+
consumes these with `augure rules --mitre` and the hardened-coverage
|
|
113
|
+
flow; a pack without `detection:` teaches the offense but leaves the
|
|
114
|
+
defense blind.
|
|
115
|
+
|
|
116
|
+
## The cycle
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
lictor rule validate my_pack.yml # dry-run: schema + corpus guard
|
|
120
|
+
lictor rule install my_pack.yml # interactive confirm, versioned, trailed
|
|
121
|
+
augure analyze target.facts # your technique is now decidable
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Via an MCP agent: the rules change every future decision - the
|
|
125
|
+
operator reads the validated pack before installing it.
|
|
126
|
+
|
|
127
|
+
## Writer's discipline (for agents especially)
|
|
128
|
+
|
|
129
|
+
- Copy `packs/ret2csu_v2.yml` (the canonical example) - change the
|
|
130
|
+
substance, keep the structure.
|
|
131
|
+
- `success_rate` and `priors` are authored numbers: ground them in the
|
|
132
|
+
source you cite, and say so in the description if they are estimates.
|
|
133
|
+
- If validation refuses you, read the refusal - each check names its
|
|
134
|
+
reason. Fix the data; never look for a way around the armor.
|
|
135
|
+
- One pack = one technique. Split OR-conditions into sibling rules
|
|
136
|
+
sharing the head; the provenance then carries EVERY reason that
|
|
137
|
+
fired, each attributed to its own rule.
|