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,293 @@
|
|
|
1
|
+
# Reference: fact format, rules table, API
|
|
2
|
+
|
|
3
|
+
Version: augure 0.1.x.
|
|
4
|
+
|
|
5
|
+
## Fact format
|
|
6
|
+
|
|
7
|
+
One fact per line: `predicate("string-atom").` or `predicate("atom", 42).`
|
|
8
|
+
Blank lines and `// comments` ignored. Rules (`:-`) rejected. Unknown
|
|
9
|
+
predicate, wrong arity or wrong argument type raises an explicit error with
|
|
10
|
+
the line number. So does:
|
|
11
|
+
|
|
12
|
+
- a **value outside a closed domain** (18 predicates are validated: the
|
|
13
|
+
booleans take `"true"`/`"false"` only, `relro` takes `none`/`partial`/`full`,
|
|
14
|
+
`verified` takes `"remote"` - a typo'd atom parses under a string-only
|
|
15
|
+
schema, disables the closed-world default and silently kills every rule
|
|
16
|
+
of the predicate; the validation makes it loud instead);
|
|
17
|
+
- an **atom carrying quotes, backslashes, parens or newlines** (the
|
|
18
|
+
grammar keeps the fact stream a stream).
|
|
19
|
+
|
|
20
|
+
Free-form predicates (`vuln`, `vuln_hint`, `allocator`, `gadget`,
|
|
21
|
+
`win_symbol`, `plt`...) take any atom.
|
|
22
|
+
|
|
23
|
+
### A complete example
|
|
24
|
+
|
|
25
|
+
This is the shape of a `target.facts` file (see the
|
|
26
|
+
[tutorial](tutorial.md) for the full walkthrough):
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
// the target
|
|
30
|
+
nx("true").
|
|
31
|
+
pie("false").
|
|
32
|
+
canary("false").
|
|
33
|
+
relro("partial").
|
|
34
|
+
|
|
35
|
+
// what it imports
|
|
36
|
+
plt("system").
|
|
37
|
+
plt("puts").
|
|
38
|
+
|
|
39
|
+
// what gadgets exist inside it
|
|
40
|
+
gadget("pop_rdi_ret", 4198400).
|
|
41
|
+
gadget("ret_align", 4198404).
|
|
42
|
+
|
|
43
|
+
// what the profiler noticed
|
|
44
|
+
vuln_hint("unsafe_func:gets").
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Schema
|
|
48
|
+
|
|
49
|
+
| Predicate | Arity | Arg types | Meaning |
|
|
50
|
+
|---|---|---|---|
|
|
51
|
+
| `nx` | 1 | string | data execution prevention (`"true"`/`"false"`) |
|
|
52
|
+
| `win_symbol` | 2 | string, integer | the winning function's name and address (emitted by the profiler) |
|
|
53
|
+
| `crash_site` | 1 | string | the crash's identification: `ret` when the control is confirmed by the planted pattern |
|
|
54
|
+
| `leaked_address` | 2 | string, integer | an address the service's own banner leaked (name + value, emitted by lictor's harvest) |
|
|
55
|
+
| `plt_addr` | 2 | string, integer | a JMP_SLOT import's PLT stub address (name + value, emitted by the profiler from the section layout) |
|
|
56
|
+
| `got_addr` | 2 | string, integer | a JMP_SLOT import's GOT entry address (the relocation offset) |
|
|
57
|
+
| `vuln_function` | 1 | string | the vulnerable function the static frame read named |
|
|
58
|
+
| `sink` | 1 | string | the unsafe sink (strcpy, gets, ...) the static read identified |
|
|
59
|
+
| `pie` | 1 | string | position-independent executable |
|
|
60
|
+
| `canary` | 1 | string | stack canary present |
|
|
61
|
+
| `relro` | 1 | string | `"none"`, `"partial"`, `"full"` |
|
|
62
|
+
| `plt` | 1 | string | imported function name |
|
|
63
|
+
| `gadget` | 2 | string, integer | gadget semantic type + address |
|
|
64
|
+
| `vuln_hint` | 1 | string | profiler hint, e.g. `"unsafe_func:gets"` |
|
|
65
|
+
| `vuln` | 1 | string | explicit vuln class: `sof`, `fmtstr`, `heap` |
|
|
66
|
+
| `target_function` | 1 | string | win-style function present |
|
|
67
|
+
| `libc_present` | 1 | string | libc known/version pinned |
|
|
68
|
+
| `allocator` | 1 | string | `tcache`, `ptmalloc2`, `dlmalloc` |
|
|
69
|
+
| `glibc_minor` | 1 | integer | glibc minor version |
|
|
70
|
+
| `return_addr_filtered` | 1 | string | return addresses constrained (stack6-style) |
|
|
71
|
+
| `got_overwrite_target` | 1 | string | GOT overwrite viable |
|
|
72
|
+
| `function_pointer_on_heap` | 1 | string | heap function pointer |
|
|
73
|
+
| `seccomp` | 1 | string | seccomp filter context |
|
|
74
|
+
| `shellcode_input` | 1 | string | input reachable and executable |
|
|
75
|
+
| `sigreturn_frame` | 1 | string | sigreturn frame constructible |
|
|
76
|
+
| `fmtstr_read` | 1 | string | format string has a read primitive |
|
|
77
|
+
| `reloc_writable` | 1 | string | relocations writable (ret2dlresolve) |
|
|
78
|
+
| `dt_lazy` | 1 | string | lazy binding in effect |
|
|
79
|
+
| `limited_stack` | 1 | string | stack space constrains the chain |
|
|
80
|
+
| `service` | 1 | string | remote service name (nmap: `ssh`, `http`...) |
|
|
81
|
+
| `software_version` | 1 | string | remote product + version (from the scan) |
|
|
82
|
+
| `remote` | 1 | string | the facts describe a remote service, not a local ELF |
|
|
83
|
+
| `verified` | 1 | string | `"remote"` = validated by a differential probe: the remote faults like the local model |
|
|
84
|
+
|
|
85
|
+
Defaults (closed world): absent `nx` = `"true"`, absent `pie` = `"true"`,
|
|
86
|
+
absent `canary` = `"false"`.
|
|
87
|
+
|
|
88
|
+
## Rules table (applicability)
|
|
89
|
+
|
|
90
|
+
| Rule ID | Head | Fires when |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `app_shellcode` | `shellcode` | sof, NX off, no canary, return not filtered, NOT seccomp |
|
|
93
|
+
| `app_ret2func` | `ret2func` | sof, PIE off, target function present |
|
|
94
|
+
| `app_ret2plt` | `ret2plt` | sof, NX on, PIE off, `system` imported |
|
|
95
|
+
| `app_ret2libc_plt` | `ret2libc` | sof, `__libc_start_main` imported, NOT seccomp |
|
|
96
|
+
| `app_ret2libc_libc` | `ret2libc` | sof, libc present, NOT seccomp |
|
|
97
|
+
| `app_rop` | `rop` | sof, NX on, gadget supply (pop or mov-store), NOT seccomp |
|
|
98
|
+
| `app_srop` | `srop` | sof, sigreturn frame, syscall gadget, NOT seccomp |
|
|
99
|
+
| `app_ret2plt_leak` | `ret2plt_leak` | sof, NX on, PIE on, `puts`, reg control |
|
|
100
|
+
| `app_fmtstr_write` | `fmtstr_write` | format-string vuln (NX-independent) |
|
|
101
|
+
| `app_fmtstr_leak_read` | `fmtstr_leak` | fmtstr + read primitive |
|
|
102
|
+
| `app_fmtstr_leak_pie` | `fmtstr_leak` | fmtstr + PIE on |
|
|
103
|
+
| `app_fmtstr_leak_canary` | `fmtstr_leak` | fmtstr + canary on |
|
|
104
|
+
| `app_ret2dlresolve` | `ret2dlresolve` | sof, NX on, PIE off, lazy binding, writable reloc |
|
|
105
|
+
| `app_ret2csu` | `ret2csu` | sof, NX on, csu gadget family |
|
|
106
|
+
| `app_stack_pivot` | `stack_pivot` | sof, NX on, limited stack, pivot gadget |
|
|
107
|
+
| `app_got_overwrite_sof` | `got_overwrite` | sof, NX on, GOT target, not full RELRO |
|
|
108
|
+
| `app_got_overwrite_fmtstr` | `got_overwrite` | fmtstr, GOT target, not full RELRO |
|
|
109
|
+
| `app_heap_tcache_alloc` | `heap_tcache` | heap, allocator tcache |
|
|
110
|
+
| `app_heap_tcache_glibc` | `heap_tcache` | heap, glibc 2.26..2.33 |
|
|
111
|
+
| `app_heap_fastbin_glibc` | `heap_fastbin` | heap, glibc < 2.26 |
|
|
112
|
+
| `app_heap_fastbin_alloc` | `heap_fastbin` | heap, allocator ptmalloc2 |
|
|
113
|
+
| `app_heap_overwrite_dlmalloc` | `heap_overwrite` | heap, dlmalloc |
|
|
114
|
+
| `app_heap_overwrite_fp` | `heap_overwrite` | heap, function pointer on heap |
|
|
115
|
+
|
|
116
|
+
("NOT seccomp" = the rule carries `[not_fact, seccomp, "true"]`: the
|
|
117
|
+
technique ends in execve, dead under any filter that kills execve; the
|
|
118
|
+
condition passes when the fact is absent.)
|
|
119
|
+
|
|
120
|
+
Derived relations: `vuln` (from hints), `has_reg_control`,
|
|
121
|
+
`has_write_primitive`, `has_syscall_gadget`, `has_csu_gadget`,
|
|
122
|
+
`has_pivot_gadget`, `enough_gadgets`.
|
|
123
|
+
|
|
124
|
+
## CLI
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
augure analyze <facts-file | -> [--json] [--seed N] [--packs DIR]
|
|
128
|
+
augure coverage [--flip NAME:VALUE ...] [--facts FILE] [--json]
|
|
129
|
+
augure rules [--mitre] [--packs DIR] [--json]
|
|
130
|
+
augure explain <technique>
|
|
131
|
+
augure doctor
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
| Flag | Effect |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `--json`, `-j` | machine-readable decision (analyze) / the coverage scorecard (coverage) / the Navigator layer (rules --mitre) |
|
|
137
|
+
| `--seed N`, `-s N` | deterministic bandit/MCTS draws |
|
|
138
|
+
| `--packs DIR` | merge technique packs (analyze, explain, rules --mitre's source) |
|
|
139
|
+
| `--flip NAME:VALUE` | coverage: a hardening override (`nx:true`, `canary:true`, `relro:full`; `NAME:nil` removes) |
|
|
140
|
+
| `--facts FILE` | coverage: one entry instead of the whole corpus |
|
|
141
|
+
| (stdin) | `-` reads facts from stdin |
|
|
142
|
+
|
|
143
|
+
Exit codes: `0` decision emitted, non-zero on malformed facts (message on
|
|
144
|
+
stderr names the line). `coverage` always exits 0 (an analysis, not a gate).
|
|
145
|
+
|
|
146
|
+
## Ruby API
|
|
147
|
+
|
|
148
|
+
### `Augure::Facts`
|
|
149
|
+
|
|
150
|
+
- `.parse(text) -> Facts` - strict parse, raises `MalformedFact` /
|
|
151
|
+
`UnknownPredicate` with line numbers.
|
|
152
|
+
- `.from_file(path) -> Facts`
|
|
153
|
+
- `.new` + `#add(name, *args)` - the builder API (the profiler's
|
|
154
|
+
contract; the same domain + grammar validation as `parse`).
|
|
155
|
+
- `#rel(name) -> Array<[arg, ...]> | nil` - tuples by relation.
|
|
156
|
+
- `#each`, `#size`, `#[]` - iteration, count, direct lookup.
|
|
157
|
+
- `#merge(other) -> Facts` - combined copy.
|
|
158
|
+
- `#to_s -> String` - deterministic emission; round-trips.
|
|
159
|
+
|
|
160
|
+
### `Augure::Engine`
|
|
161
|
+
|
|
162
|
+
- `.new(facts, rules: Rules.all)`
|
|
163
|
+
- `#run -> Result`
|
|
164
|
+
- `Result#applicable -> [String]` - rule-table order.
|
|
165
|
+
- `Result#derived(rel) -> [tuple]`
|
|
166
|
+
- `Result#provenance(technique) -> [{rule:, origin:, evidence:}]` - every sibling firing, each attributed
|
|
167
|
+
|
|
168
|
+
### `Augure::Bandit`
|
|
169
|
+
|
|
170
|
+
- `.new(priors: {"tech" => [alpha, beta]}, rng: Random.new)`
|
|
171
|
+
- `#arm(tech) -> Arm` (auto-arms unknown with (1, 1))
|
|
172
|
+
- `#select(verified) -> String | nil` - Thompson sample argmax.
|
|
173
|
+
- `#feedback(tech, success)` - updates posterior, logs history.
|
|
174
|
+
- `#ranking_of(techniques) -> [[tech, mean], ...]` - prior-mean pairs,
|
|
175
|
+
deterministic (the method the corpus freezes; the pipeline maps
|
|
176
|
+
`&:first` over it).
|
|
177
|
+
- `#rankings -> [[tech, mean], ...]` - ALL arms sorted by prior mean
|
|
178
|
+
(deterministic, not a draw).
|
|
179
|
+
- `#mean(tech) -> Float` - the prior mean (alpha / (alpha + beta)).
|
|
180
|
+
- `#select(verified)` - the Thompson sampler (the draw).
|
|
181
|
+
|
|
182
|
+
### `Augure::Mcts`
|
|
183
|
+
|
|
184
|
+
- `.plan(allowed, iterations: 2000, seed:) -> [first_move, path]`
|
|
185
|
+
(`model:` overrides the transition graph; technique packs merge into
|
|
186
|
+
it via `merged_model(packs)`)
|
|
187
|
+
- `.available(caps, allowed)`, `.transition(caps, tech)`,
|
|
188
|
+
`.terminal?(caps, model:)` - terminal = any capability provided by a
|
|
189
|
+
`terminal: true` stage of the model (packs own their semantics; the
|
|
190
|
+
built-in model's terminals all provide `shell`).
|
|
191
|
+
- `.stage(tech)` - one stage's `[requires, provides, terminal, success]`.
|
|
192
|
+
- `STAGE_MODEL` - the technique transition graph.
|
|
193
|
+
|
|
194
|
+
### `Augure::Pack` / `PackLoader`
|
|
195
|
+
|
|
196
|
+
- `Pack.load_file(path)` - validates a technique pack (YAML) through
|
|
197
|
+
the **10 armor checks** (see [pack-format.md](pack-format.md)): shape,
|
|
198
|
+
stratification, new heads, provenance, kb/mcts/priors shapes, match
|
|
199
|
+
regexes + cmp types (7), the optional `build:` (8), `detection:` (9),
|
|
200
|
+
`mitre:` (10).
|
|
201
|
+
- `PackLoader.load_dir(dir) -> [Pack]`, `.priors_with(packs, base)`,
|
|
202
|
+
`.mcts_model(packs)`.
|
|
203
|
+
- `.corpus_guard(packs)` - refuses any pack that reorders an existing
|
|
204
|
+
technique or takes a documented target's top-1; honors a documented
|
|
205
|
+
`pack_verdict` entry (the pack answer must ARRIVE) and tolerates
|
|
206
|
+
empty frozen verdicts.
|
|
207
|
+
- The gem SHIPS all 20 packs (`packs/` in the gem); `packs/ret2csu_v2.yml`
|
|
208
|
+
is the canonical authoring example.
|
|
209
|
+
|
|
210
|
+
### `Augure::Verifier`
|
|
211
|
+
|
|
212
|
+
- `.payload_fits(buffer_size:, payload_min:) -> :sat | :unsat`
|
|
213
|
+
- `.bad_bytes?(payload:, bad_bytes:) -> bool`
|
|
214
|
+
- `.rop_chain_feasible?(...) -> bool`
|
|
215
|
+
- `.payload_fits_smtlib(buffer_size, payload_min) -> String`
|
|
216
|
+
|
|
217
|
+
### `Augure::SmtProcess`
|
|
218
|
+
|
|
219
|
+
- `.solve(smt_text, solver:, timeout: 10) -> :sat | :unsat | :unknown`
|
|
220
|
+
|
|
221
|
+
### `Augure::Pipeline`
|
|
222
|
+
|
|
223
|
+
- `.analyze(facts:, seed: nil, priors: nil, buffer_size: 256,
|
|
224
|
+
packs: nil, plan: true) -> {applicable:, verified:, ranking:, selected:,
|
|
225
|
+
plan:, explain:}` - `packs:` merges technique packs (rules, priors,
|
|
226
|
+
MCTS transitions); `plan: false` skips the MCTS search (the corpus
|
|
227
|
+
guard uses it).
|
|
228
|
+
|
|
229
|
+
### Machine payload contract
|
|
230
|
+
|
|
231
|
+
`augure analyze --json` emits the decision hash stamped with a stable
|
|
232
|
+
schema tag:
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
{ "schema": "augure/decision@1", "applicable": [...], ... }
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Consumers (CI, agents, the MCP server) pin `augure/decision@1`; a
|
|
239
|
+
breaking change bumps the tag and is documented in the CHANGELOG.
|
|
240
|
+
|
|
241
|
+
### Taught technique packs
|
|
242
|
+
|
|
243
|
+
The shipped packs extend the rule table as data (`packs/`, loaded with
|
|
244
|
+
`augure analyze --packs DIR` or taught to lictor with
|
|
245
|
+
`lictor rule install`): `house_force`, `house_orange`, `house_botcake`,
|
|
246
|
+
`unsorted_bin_attack`, `fastbin_hook`, `ret2dlresolve_x86`,
|
|
247
|
+
`got_partial_overwrite`, `brop` (the first rule that consumes
|
|
248
|
+
`verified("remote")` - an observation anchors it), `ret2partial_overwrite`,
|
|
249
|
+
`one_gadget`, `banner_leak`, `got_partial_overwrite_deref`,
|
|
250
|
+
`house_of_apple2`, `jop`, `large_bin_attack`, `orw`, `ret2vdso`,
|
|
251
|
+
`setcontext_srop`, `uaf_tcache` - plus the canonical `ret2csu_v2`
|
|
252
|
+
example: **20 packs** ship. Each carries its rules, knowledge-base
|
|
253
|
+
entries, MCTS transition and Beta priors, the author and source as
|
|
254
|
+
provenance, and (optionally) `build:` (the assembly order),
|
|
255
|
+
`detection:` (the defender side) and `mitre:` (the ATT&CK mapping).
|
|
256
|
+
Pack priors fill the gaps of the prior table; a consumer-provided or
|
|
257
|
+
outcome-adapted entry always wins.
|
|
258
|
+
|
|
259
|
+
### `Augure::KnowledgeBase`
|
|
260
|
+
|
|
261
|
+
- `.corpus -> [entry]` (22 documented patterns)
|
|
262
|
+
- `.corpus_with(extra) -> [entry]` (seed corpus + technique-pack knowledge)
|
|
263
|
+
- `.priors -> {"tech" => [alpha, beta]}` (N=10 pseudo-counts)
|
|
264
|
+
- `.transitions -> {"tech" => [unlock, ...]}`
|
|
265
|
+
- `#query(text, technique: nil, n_results: 3) -> [{doc:, score:}]`
|
|
266
|
+
|
|
267
|
+
## MCTS stage model
|
|
268
|
+
|
|
269
|
+
| Technique | Requires | Provides | Terminal | Base success |
|
|
270
|
+
|---|---|---|---|---|
|
|
271
|
+
| `fmtstr_leak` | - | libc_base | no | 0.75 |
|
|
272
|
+
| `fmtstr_canary` | - | canary | no | 0.70 |
|
|
273
|
+
| `ret2plt_leak` | - | libc_base | no | 0.68 |
|
|
274
|
+
| `ret2libc` | libc_base | shell | yes | 0.85 |
|
|
275
|
+
| `rop` | libc_base | shell | yes | 0.75 |
|
|
276
|
+
| `rop_nocontext` | - | shell | yes | 0.25 |
|
|
277
|
+
| `ret2plt` | - | shell | yes | 0.75 |
|
|
278
|
+
| `shellcode` | - | shell | yes | 0.90 |
|
|
279
|
+
| `stack_pivot` | - | pivot | no | 0.60 |
|
|
280
|
+
| `ret2plt_leak_big` | pivot | libc_base | no | 0.68 |
|
|
281
|
+
| `ret2libc_big` | pivot+libc_base | shell | yes | 0.85 |
|
|
282
|
+
| `ret2csu` | - | libc_base | no | 0.70 |
|
|
283
|
+
| `dlresolve` | - | shell | yes | 0.55 |
|
|
284
|
+
| `got_overwrite` | - | shell | yes | 0.65 |
|
|
285
|
+
| `io_uring_register` | - | iouring | no | 0.90 |
|
|
286
|
+
| `rds_pin_steal` | iouring | pin_underflow | no | 0.70 |
|
|
287
|
+
| `page_free` | pin_underflow | freed_page | no | 0.75 |
|
|
288
|
+
| `pagecache_reclaim` | freed_page | pagecache_ctrl | no | 0.65 |
|
|
289
|
+
| `io_uring_write` | pagecache_ctrl+iouring | suid_overwrite | no | 0.70 |
|
|
290
|
+
| `suid_exec` | suid_overwrite | shell | yes | 0.90 |
|
|
291
|
+
|
|
292
|
+
The last six are the PinTheft chain (V12 Security), an externally documented
|
|
293
|
+
ordering - augure only tests whether the planner reproduces it.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Get your first auditable decision in 10 minutes
|
|
2
|
+
|
|
3
|
+
*Tutorial. By the end you will have run Augure on a real fact file, read its
|
|
4
|
+
proof trail, and made it disagree with you on purpose.*
|
|
5
|
+
|
|
6
|
+
Prerequisites: Ruby 3.4+.
|
|
7
|
+
|
|
8
|
+
## 1. Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
gem install augure
|
|
12
|
+
augure --help
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
You should see the usage banner. **Check:** `Usage: augure analyze`.
|
|
16
|
+
|
|
17
|
+
## 2. Write a fact file
|
|
18
|
+
|
|
19
|
+
> **Where do facts come from?** From the profiler, not your keyboard:
|
|
20
|
+
> `augure-profile app.elf -o target.facts` generates exactly this file from
|
|
21
|
+
> a binary (checksec, imports, unsafe symbols, gadgets) via metasm - gem
|
|
22
|
+
> `augure-profiler`. Hand-writing facts is still useful to learn the
|
|
23
|
+
> format the profiler speaks.
|
|
24
|
+
|
|
25
|
+
Facts are one target's properties, one per line. Create `target.facts`:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
// the target
|
|
29
|
+
nx("true").
|
|
30
|
+
pie("false").
|
|
31
|
+
canary("false").
|
|
32
|
+
relro("partial").
|
|
33
|
+
|
|
34
|
+
// what it imports
|
|
35
|
+
plt("system").
|
|
36
|
+
plt("puts").
|
|
37
|
+
|
|
38
|
+
// what gadgets exist inside it
|
|
39
|
+
gadget("pop_rdi_ret", 4198400).
|
|
40
|
+
gadget("ret_align", 4198404).
|
|
41
|
+
|
|
42
|
+
// what the profiler noticed
|
|
43
|
+
vuln_hint("unsafe_func:gets").
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Check:** the file has no trailing `:-` and every line ends with `.`.
|
|
47
|
+
|
|
48
|
+
## 3. Decide
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
augure analyze target.facts
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
You should see:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
applicable: ret2plt, rop
|
|
58
|
+
selected: ret2plt
|
|
59
|
+
plan: ret2plt
|
|
60
|
+
# (selected is a seeded Thompson draw: same facts + same --seed = same
|
|
61
|
+
# selection, always. Unseeded, the draw explores the ranking.)
|
|
62
|
+
ret2plt <- app_ret2plt (vuln=sof, nx=true, pie=false, plt=system)
|
|
63
|
+
rop <- app_rop (vuln=sof, nx=true, enough_gadgets=true)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Augure picked `ret2plt` - the technique human operators routinely skip in
|
|
67
|
+
favor of a ROP chain. The rules, not the reflex, made the call.
|
|
68
|
+
|
|
69
|
+
## 4. Read the proof
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
augure analyze target.facts --json | head -30
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Find the `explain.provenance` block (the JSON is long: `grep -A 20 provenance`
|
|
76
|
+
after the pipe) - the rule ID, the evidence pairs, and the origin that
|
|
77
|
+
fired it. That block is the whole product: hand it to a colleague, they can
|
|
78
|
+
verify the decision without rerunning anything.
|
|
79
|
+
|
|
80
|
+
## 5. Reproduce it
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
augure analyze target.facts --json --seed 42 > run1.json
|
|
84
|
+
augure analyze target.facts --json --seed 42 > run2.json
|
|
85
|
+
diff run1.json run2.json && echo "identical"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Same input, same seed, same decision. **Check:** the diff is empty.
|
|
89
|
+
|
|
90
|
+
## 6. Make it disagree with you
|
|
91
|
+
|
|
92
|
+
Remove the `plt("system")` line and run again:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
grep -v 'plt("system")' target.facts > no-system.facts
|
|
96
|
+
augure analyze no-system.facts
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`ret2plt` vanished - and the proof trail explains why: the rule
|
|
100
|
+
`app_ret2plt` requires the fact you just removed. This is the loop the
|
|
101
|
+
whole tool is built on: change the facts, watch the decision move, know
|
|
102
|
+
exactly which rule moved it.
|
|
103
|
+
|
|
104
|
+
## Next steps
|
|
105
|
+
|
|
106
|
+
- [Write your own technique rules](how-to-write-rules.md)
|
|
107
|
+
- [The full reference](reference.md) - fact schema and API
|
|
108
|
+
- [What the proof trail buys each team](auditability.md)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "optparse"
|
|
6
|
+
require_relative "../lib/augure"
|
|
7
|
+
|
|
8
|
+
VERSION_LINE = "augure #{Augure::VERSION}"
|
|
9
|
+
|
|
10
|
+
options = {json: false, seed: nil}
|
|
11
|
+
parser = OptionParser.new do |opts|
|
|
12
|
+
opts.banner = <<~USAGE
|
|
13
|
+
|
|
14
|
+
#{VERSION_LINE} - the auditable exploitation decision layer
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
augure analyze <facts-file | -> [options] decide on facts
|
|
18
|
+
augure coverage [options] the corpus read backwards (defender side)
|
|
19
|
+
augure doctor verify the environment
|
|
20
|
+
augure rules print the rules-as-data table
|
|
21
|
+
augure explain <technique> rules + knowledge for one technique
|
|
22
|
+
augure --version print the version
|
|
23
|
+
|
|
24
|
+
analyze options:
|
|
25
|
+
USAGE
|
|
26
|
+
opts.on("-j", "--json", "Emit the decision as JSON (schema: augure/decision@1)") { options[:json] = true }
|
|
27
|
+
opts.on("-s", "--seed N", Integer, "Seed for deterministic bandit/MCTS") { |n| options[:seed] = n }
|
|
28
|
+
opts.on("--packs DIR", "Load technique packs (armor + corpus guard) for analyze/explain") { |v| options[:packs] = v }
|
|
29
|
+
opts.on("--flip NAME:VALUE", "Coverage: a hardening flip (nx:true, canary:true, relro:full; NAME:nil removes)") do |v|
|
|
30
|
+
name, value = v.split(":", 2)
|
|
31
|
+
(options[:flips] ||= {})[name] = ((value == "nil") ? nil : value)
|
|
32
|
+
end
|
|
33
|
+
opts.on("--facts FILE", "Coverage: one facts entry instead of the corpus") { |v| options[:facts_entry] = v }
|
|
34
|
+
opts.on("--mitre", "rules: export the ATT&CK navigator layer (from the packs' mitre: fields)") { options[:mitre] = true }
|
|
35
|
+
opts.on("--version", "Print the version") do
|
|
36
|
+
puts VERSION_LINE
|
|
37
|
+
exit 0
|
|
38
|
+
end
|
|
39
|
+
opts.on("-h", "--help", "Show this help") do
|
|
40
|
+
puts opts
|
|
41
|
+
exit 0
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
parser.parse!
|
|
45
|
+
|
|
46
|
+
command = ARGV.shift
|
|
47
|
+
|
|
48
|
+
case command
|
|
49
|
+
when "--version", "-v"
|
|
50
|
+
puts VERSION_LINE
|
|
51
|
+
when "doctor"
|
|
52
|
+
checks = []
|
|
53
|
+
checks << ["ruby >= 3.4", (RUBY_VERSION >= "3.4") ? nil : "upgrade ruby (mise install ruby@3.4)"]
|
|
54
|
+
checks << [VERSION_LINE, nil]
|
|
55
|
+
|
|
56
|
+
# The corpus self-verifies: parse a shipped example, decide, compare.
|
|
57
|
+
begin
|
|
58
|
+
example = File.join(__dir__, "../examples/ret2win.facts")
|
|
59
|
+
result = Augure::Pipeline.analyze(facts: File.read(example))
|
|
60
|
+
checks << if result[:ranking].first == "ret2func"
|
|
61
|
+
["corpus self-test (selected: ret2func)", nil]
|
|
62
|
+
else
|
|
63
|
+
["corpus self-test", "corpus example decided differently - report a bug"]
|
|
64
|
+
end
|
|
65
|
+
rescue => e
|
|
66
|
+
checks << ["corpus self-test", e.message]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
solvers = %w[z3 bitwuzla cvc5].select do |s|
|
|
70
|
+
system("sh", "-c", "command -v '#{s}' >/dev/null 2>&1")
|
|
71
|
+
end
|
|
72
|
+
checks << ["SMT solvers",
|
|
73
|
+
solvers.empty? ? "optional - none found; the arithmetic path works without (apt install z3 for the SMT path)" : "optional, found: #{solvers.join(", ")}"]
|
|
74
|
+
|
|
75
|
+
failed = false
|
|
76
|
+
checks.each do |name, problem|
|
|
77
|
+
if problem&.start_with?("optional")
|
|
78
|
+
puts " opt #{name} - #{problem}"
|
|
79
|
+
elsif problem
|
|
80
|
+
failed = true
|
|
81
|
+
puts " FAIL #{name} - fix: #{problem}"
|
|
82
|
+
else
|
|
83
|
+
puts " ok #{name}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
if failed
|
|
87
|
+
n = checks.count { |_, p| p && !p.start_with?("optional") }
|
|
88
|
+
abort("augure: doctor found #{n} issue(s) - fixes above")
|
|
89
|
+
end
|
|
90
|
+
when "rules"
|
|
91
|
+
if options[:mitre]
|
|
92
|
+
mitre_by_pack = {}
|
|
93
|
+
packs_dir = options[:packs] || "packs"
|
|
94
|
+
Dir.glob(File.expand_path(File.join(packs_dir, "*.yml"))).each do |path|
|
|
95
|
+
pack = Augure::Pack.load_file(path)
|
|
96
|
+
(mitre_by_pack[pack.technique] = pack.mitre) if pack.mitre
|
|
97
|
+
rescue
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
if options[:json]
|
|
101
|
+
puts JSON.pretty_generate("schema" => "augure/mitre-navigator@1", "layers" => [{
|
|
102
|
+
"name" => "augure techniques", "versions" => {"attack" => "14", "navigator" => "4.9"},
|
|
103
|
+
"techniques" => mitre_by_pack.map { |tech, codes| codes.map { |c| {"techniqueID" => c, "score" => 1, "comment" => tech} } }.flatten
|
|
104
|
+
}])
|
|
105
|
+
exit 0
|
|
106
|
+
end
|
|
107
|
+
mitre_by_pack.each { |tech, codes| puts "#{tech}\t#{codes.join(", ")}" }
|
|
108
|
+
exit 0
|
|
109
|
+
end
|
|
110
|
+
Augure::Rules.all.each do |rule|
|
|
111
|
+
puts rule.id
|
|
112
|
+
puts " head: #{rule.head.join("=")}"
|
|
113
|
+
puts " why: #{rule.source}"
|
|
114
|
+
end
|
|
115
|
+
when "explain"
|
|
116
|
+
technique = ARGV.shift or abort(parser.to_s)
|
|
117
|
+
packs = options[:packs] && Augure::PackLoader.load_dir(options[:packs]).tap { |ps| Augure::PackLoader.corpus_guard(ps) }
|
|
118
|
+
rules = Augure::Rules.all + packs.to_a.flat_map(&:rules)
|
|
119
|
+
rules = rules.select { |r| r.head == [Augure::Rules::APPLICABLE, technique] }
|
|
120
|
+
entries = Augure::KnowledgeBase.corpus_with(packs.to_a.flat_map(&:kb))
|
|
121
|
+
.select { |e| e["technique"] == technique }
|
|
122
|
+
if rules.empty? && entries.empty?
|
|
123
|
+
known = Augure::Rules.applicable_rules.map { |r| r.head[1] }.uniq.sort
|
|
124
|
+
abort("augure: unknown technique '#{technique}' - known: #{known.join(", ")}")
|
|
125
|
+
end
|
|
126
|
+
puts "technique: #{technique}"
|
|
127
|
+
rules.each do |rule|
|
|
128
|
+
puts " rule: #{rule.id} - #{rule.source}"
|
|
129
|
+
end
|
|
130
|
+
entries.each do |e|
|
|
131
|
+
puts " kb: #{e["id"]} (success_rate #{e["success_rate"]})"
|
|
132
|
+
puts " #{e["description"]}"
|
|
133
|
+
end
|
|
134
|
+
when "coverage"
|
|
135
|
+
entries = if options[:facts_entry]
|
|
136
|
+
[{"suite" => "facts", "name" => File.basename(options[:facts_entry]),
|
|
137
|
+
"facts" => File.read(options[:facts_entry])}]
|
|
138
|
+
else
|
|
139
|
+
corpus = JSON.parse(File.read(File.expand_path("../lib/augure/ctf_corpus.json", __dir__)))
|
|
140
|
+
corpus["suites"].values.flatten
|
|
141
|
+
end
|
|
142
|
+
unless options[:flips]
|
|
143
|
+
abort("augure: coverage needs at least one --flip NAME:VALUE (nx:true, canary:true, relro:full)")
|
|
144
|
+
end
|
|
145
|
+
report = Augure::Coverage.report(entries, flips: options[:flips])
|
|
146
|
+
if options[:json]
|
|
147
|
+
puts JSON.pretty_generate("schema" => "augure/coverage@1", **report)
|
|
148
|
+
exit 0
|
|
149
|
+
end
|
|
150
|
+
entries.size
|
|
151
|
+
report[:entries].count { |e| e[:lost].empty? && e[:gained].empty? }
|
|
152
|
+
puts "the corpus read backwards: #{entries.size} targets, flips #{options[:flips].inspect}"
|
|
153
|
+
puts "targets that change their applicable set: #{report[:entries].count { |e| e[:lost] || e[:gained] } - report[:entries].count { |e| e[:lost].empty? && e[:gained].empty? }}"
|
|
154
|
+
report[:died].each { |tech, n| puts " died: #{tech} (#{n} target(s))" }
|
|
155
|
+
report[:gained].each { |tech, n| puts " gained: #{tech} (#{n} target(s))" }
|
|
156
|
+
puts "untouched targets: #{report[:survived_count]}"
|
|
157
|
+
puts "the remaining applicable techniques are the attacker's next moves - harden accordingly."
|
|
158
|
+
when "analyze"
|
|
159
|
+
path = ARGV.shift or abort(parser.to_s)
|
|
160
|
+
input = (path == "-") ? $stdin.read : File.read(path)
|
|
161
|
+
begin
|
|
162
|
+
packs = options[:packs] && Augure::PackLoader.load_dir(options[:packs]).tap { |ps| Augure::PackLoader.corpus_guard(ps) }
|
|
163
|
+
result = Augure::Pipeline.analyze(facts: input, seed: options[:seed], packs: packs)
|
|
164
|
+
if options[:json]
|
|
165
|
+
puts JSON.pretty_generate({schema: "augure/decision@1"}.merge(result))
|
|
166
|
+
else
|
|
167
|
+
puts "applicable: #{result[:applicable].join(", ")}"
|
|
168
|
+
puts "selected: #{result[:selected]}"
|
|
169
|
+
puts "plan: #{result[:plan].join(" -> ")}"
|
|
170
|
+
result[:explain][:provenance].each do |tech, provs|
|
|
171
|
+
provs.each do |p|
|
|
172
|
+
puts " #{tech} <- #{p[:rule]} (#{p[:evidence].map { |e| e.join("=") }.join(", ")})"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
rescue Augure::MalformedFact, Augure::UnknownPredicate => e
|
|
177
|
+
abort("augure: #{e.message}")
|
|
178
|
+
end
|
|
179
|
+
else
|
|
180
|
+
abort(parser.to_s)
|
|
181
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "optparse"
|
|
6
|
+
require_relative "../lib/augure"
|
|
7
|
+
|
|
8
|
+
# Concordance benchmark against the frozen CTF corpus, in the spirit of the
|
|
9
|
+
# Python prototype's benchmark harness: predictions vs documented truth,
|
|
10
|
+
# per suite, with an explicit exit code gate.
|
|
11
|
+
|
|
12
|
+
threshold = 1.00
|
|
13
|
+
OptionParser.new do |opts|
|
|
14
|
+
opts.banner = "Usage: augure-benchmark <corpus.json> [--min-top1 N]"
|
|
15
|
+
opts.on("--min-top1 N", Float, "Fail below this top-1 concordance (default 1.00)") { |v| threshold = v }
|
|
16
|
+
opts.on("--version", "Print the version") do
|
|
17
|
+
puts "augure #{Augure::VERSION} (benchmark)"
|
|
18
|
+
exit 0
|
|
19
|
+
end
|
|
20
|
+
end.parse!(ARGV)
|
|
21
|
+
|
|
22
|
+
path = ARGV.shift or abort("Usage: augure-benchmark <corpus.json> [--min-top1 N]")
|
|
23
|
+
corpus = JSON.parse(File.read(path))
|
|
24
|
+
meta_priors = corpus.dig("meta", "priors")
|
|
25
|
+
|
|
26
|
+
puts " augure-benchmark: prediction vs documented ground truth"
|
|
27
|
+
puts
|
|
28
|
+
|
|
29
|
+
correct_top1 = 0
|
|
30
|
+
correct_top3 = 0
|
|
31
|
+
n = 0
|
|
32
|
+
|
|
33
|
+
corpus["suites"].each do |suite, entries|
|
|
34
|
+
suite_ok1 = 0
|
|
35
|
+
suite_ok3 = 0
|
|
36
|
+
entries.each do |entry|
|
|
37
|
+
result = Augure::Pipeline.analyze(facts: entry["facts"], priors: meta_priors)
|
|
38
|
+
top3 = result[:ranking].first(3)
|
|
39
|
+
# An empty frozen verdict is a verdict: concordance = the engine
|
|
40
|
+
# reproduces it exactly (the pack layer owns the technique; the
|
|
41
|
+
# built-in table intentionally says nothing).
|
|
42
|
+
if entry["applicable"].empty?
|
|
43
|
+
ok = result[:applicable].empty?
|
|
44
|
+
suite_ok1 += 1 if ok
|
|
45
|
+
suite_ok3 += 1 if ok
|
|
46
|
+
next
|
|
47
|
+
end
|
|
48
|
+
suite_ok1 += 1 if result[:ranking].first == entry["truth"]
|
|
49
|
+
suite_ok3 += 1 if top3.include?(entry["truth"])
|
|
50
|
+
end
|
|
51
|
+
count = entries.size
|
|
52
|
+
n += count
|
|
53
|
+
correct_top1 += suite_ok1
|
|
54
|
+
correct_top3 += suite_ok3
|
|
55
|
+
puts format(" %-12s %d/%d top-1 %d/%d top-3",
|
|
56
|
+
suite, suite_ok1, count, suite_ok3, count)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
puts
|
|
60
|
+
pct = correct_top1.to_f / n
|
|
61
|
+
puts format(" Top-1 concordance: %d/%d (%.0f%%)", correct_top1, n, pct * 100)
|
|
62
|
+
puts format(" Top-3 concordance: %d/%d (%.0f%%)", correct_top3, n, (correct_top3.to_f / n) * 100)
|
|
63
|
+
|
|
64
|
+
if pct < threshold
|
|
65
|
+
puts format(" BELOW THRESHOLD (%.0f%% < %.0f%%)", pct * 100, threshold * 100)
|
|
66
|
+
exit 1
|
|
67
|
+
end
|