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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +136 -0
  3. data/LICENSE +21 -0
  4. data/README.md +217 -0
  5. data/exe/augure +181 -0
  6. data/exe/augure-benchmark +67 -0
  7. data/home/runner/work/augure/augure/docs/agent-snippet.md +31 -0
  8. data/home/runner/work/augure/augure/docs/auditability.md +160 -0
  9. data/home/runner/work/augure/augure/docs/build-format.md +119 -0
  10. data/home/runner/work/augure/augure/docs/explanation-architecture.md +74 -0
  11. data/home/runner/work/augure/augure/docs/how-to-swap-solver.md +73 -0
  12. data/home/runner/work/augure/augure/docs/how-to-write-rules.md +71 -0
  13. data/home/runner/work/augure/augure/docs/pack-format.md +137 -0
  14. data/home/runner/work/augure/augure/docs/reference.md +293 -0
  15. data/home/runner/work/augure/augure/docs/tutorial.md +108 -0
  16. data/home/runner/work/augure/augure/examples/ret2win.facts +11 -0
  17. data/home/runner/work/augure/augure/examples/split.facts +12 -0
  18. data/home/runner/work/augure/augure/exe/augure +181 -0
  19. data/home/runner/work/augure/augure/exe/augure-benchmark +67 -0
  20. data/home/runner/work/augure/augure/lib/augure/bandit.rb +118 -0
  21. data/home/runner/work/augure/augure/lib/augure/coverage.rb +56 -0
  22. data/home/runner/work/augure/augure/lib/augure/ctf_corpus.json +773 -0
  23. data/home/runner/work/augure/augure/lib/augure/engine.rb +128 -0
  24. data/home/runner/work/augure/augure/lib/augure/facts.rb +194 -0
  25. data/home/runner/work/augure/augure/lib/augure/kb_corpus.json +345 -0
  26. data/home/runner/work/augure/augure/lib/augure/knowledge_base.rb +109 -0
  27. data/home/runner/work/augure/augure/lib/augure/mcts.rb +178 -0
  28. data/home/runner/work/augure/augure/lib/augure/pack.rb +244 -0
  29. data/home/runner/work/augure/augure/lib/augure/pack_loader.rb +93 -0
  30. data/home/runner/work/augure/augure/lib/augure/parity.rb +69 -0
  31. data/home/runner/work/augure/augure/lib/augure/pipeline.rb +107 -0
  32. data/home/runner/work/augure/augure/lib/augure/priors.rb +32 -0
  33. data/home/runner/work/augure/augure/lib/augure/rules.rb +300 -0
  34. data/home/runner/work/augure/augure/lib/augure/smt_process.rb +48 -0
  35. data/home/runner/work/augure/augure/lib/augure/verifier.rb +40 -0
  36. data/home/runner/work/augure/augure/lib/augure/version.rb +5 -0
  37. data/home/runner/work/augure/augure/lib/augure.rb +25 -0
  38. data/home/runner/work/augure/augure/packs/banner_leak.yml +46 -0
  39. data/home/runner/work/augure/augure/packs/brop.yml +46 -0
  40. data/home/runner/work/augure/augure/packs/fastbin_hook.yml +45 -0
  41. data/home/runner/work/augure/augure/packs/got_partial_overwrite.yml +45 -0
  42. data/home/runner/work/augure/augure/packs/got_partial_overwrite_deref.yml +39 -0
  43. data/home/runner/work/augure/augure/packs/house_botcake.yml +38 -0
  44. data/home/runner/work/augure/augure/packs/house_force.yml +37 -0
  45. data/home/runner/work/augure/augure/packs/house_of_apple2.yml +45 -0
  46. data/home/runner/work/augure/augure/packs/house_orange.yml +38 -0
  47. data/home/runner/work/augure/augure/packs/jop.yml +39 -0
  48. data/home/runner/work/augure/augure/packs/large_bin_attack.yml +39 -0
  49. data/home/runner/work/augure/augure/packs/one_gadget.yml +38 -0
  50. data/home/runner/work/augure/augure/packs/orw.yml +55 -0
  51. data/home/runner/work/augure/augure/packs/ret2csu_v2.yml +55 -0
  52. data/home/runner/work/augure/augure/packs/ret2dlresolve_x86.yml +39 -0
  53. data/home/runner/work/augure/augure/packs/ret2partial_overwrite.yml +38 -0
  54. data/home/runner/work/augure/augure/packs/ret2vdso.yml +39 -0
  55. data/home/runner/work/augure/augure/packs/setcontext_srop.yml +46 -0
  56. data/home/runner/work/augure/augure/packs/uaf_tcache.yml +45 -0
  57. data/home/runner/work/augure/augure/packs/unsorted_bin_attack.yml +38 -0
  58. metadata +103 -0
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Augure
4
+ # The pipeline facade: facts -> engine -> bandit -> MCTS -> decision.
5
+ # Augure decides; it never executes. The returned hash is the audit
6
+ # payload: applicable, verified, selected, plan and the per-technique
7
+ # rule provenance.
8
+ module Pipeline
9
+ module_function
10
+
11
+ # Decide on a fact base and return the auditable decision.
12
+ #
13
+ # @param facts [String, Facts] raw fact text or a parsed fact base
14
+ # @param seed [Integer, nil] seed for deterministic bandit/MCTS draws
15
+ # @param priors [Hash{String => [Numeric, Numeric]}, nil] prior table
16
+ # overriding the AG+EXTENDED+KB defaults (see Priors.from_trail
17
+ # usage in lictor for the closed loop)
18
+ # @param buffer_size [Integer] stack budget for payload-fit checks
19
+ # @return [Hash] the decision:
20
+ # {applicable:, verified:, ranking:, selected:, plan:, explain:}
21
+ # The --json payload is the same hash stamped with
22
+ # "schema": "augure/decision@1" (stable contract).
23
+ # The verify layer is the ARITHMETIC feasibility check
24
+ # (Verifier.payload_fits). The SMT path is a separate, tested seam
25
+ # (Verifier.payload_fits_smtlib + SmtProcess) wired by consumers who
26
+ # need it - the pipeline does not run a solver behind the scenes.
27
+ # plan: false skips the MCTS search (the corpus guard needs only the
28
+ # deterministic applicable + prior-mean ranking; MCTS is the costly part).
29
+ def analyze(facts:, seed: nil, priors: nil, buffer_size: 256, packs: nil, plan: true)
30
+ facts = Facts.parse(facts) if facts.is_a?(String)
31
+ engine_rules = packs ? Rules.all + packs.flat_map(&:rules) : Rules.all
32
+ effective_priors = PackLoader.priors_with(packs, priors || default_priors)
33
+ engine_result = Engine.new(facts, rules: engine_rules).run
34
+ applicable = engine_result.applicable
35
+
36
+ verified = verify(applicable, facts: facts, buffer_size: buffer_size)
37
+
38
+ # The seed drives the WHOLE draw, selection included: a decision
39
+ # replayed with the same seed must select the same technique.
40
+ bandit = selector(priors: effective_priors, rng: seed ? Random.new(seed) : nil)
41
+ ranking = bandit.ranking_of(applicable)
42
+ candidates = verified.select { |_, v| v[:status] == :sat }.keys
43
+ selected = bandit.select(candidates.empty? ? applicable : candidates)
44
+ plan = plan ? plan_for(applicable, seed: seed, packs: packs) : []
45
+
46
+ {
47
+ applicable: applicable,
48
+ verified: verified,
49
+ ranking: ranking.map(&:first),
50
+ selected: selected,
51
+ plan: plan,
52
+ explain: {provenance: explain_provenance(engine_result)}
53
+ }
54
+ end
55
+
56
+ # The explain payload: per applicable technique, the rules that
57
+ # fired it with their evidence (the audit contract).
58
+ def explain_provenance(engine_result)
59
+ heads = engine_result.provenance_by_head.select { |head, _| head[0] == Rules::APPLICABLE }
60
+ heads.transform_keys { |head| head[1] }.transform_values do |provs|
61
+ provs.map do |p|
62
+ {rule: p[:rule], evidence: p[:evidence], origin: p[:origin]}
63
+ end
64
+ end
65
+ end
66
+
67
+ # The selector every consumer shares: AG inversions + KB-informed priors.
68
+ def selector(priors: nil, rng: nil)
69
+ Bandit.new(priors: priors || default_priors, rng: rng || Random.new)
70
+ end
71
+
72
+ def default_priors
73
+ @default_priors ||= begin
74
+ # Disjoint by design: EXTENDED_PRIORS covers techniques the AG
75
+ # simulation never scored.
76
+ merged = AG_PRIORS.merge(EXTENDED_PRIORS)
77
+ KnowledgeBase.priors.each do |tech, (a, b)|
78
+ merged[tech] = if merged[tech]
79
+ [merged[tech][0] + a, merged[tech][1] + b]
80
+ else
81
+ [a, b]
82
+ end
83
+ end
84
+ merged
85
+ end
86
+ end
87
+
88
+ def verify(applicable, facts:, buffer_size:)
89
+ # Arithmetic feasibility only. The SMT seam (payload_fits_smtlib +
90
+ # SmtProcess) is available to consumers; the pipeline does not
91
+ # claim to run a solver.
92
+ applicable.each_with_object({}) do |tech, out|
93
+ checks = {}
94
+ checks[:payload_fits] = Verifier.payload_fits(buffer_size: buffer_size, payload_min: 40)
95
+ out[tech] = {status: (checks.values.all? { |s| s == :sat }) ? :sat : :unsat, checks: checks}
96
+ end
97
+ end
98
+
99
+ def plan_for(applicable, seed: nil, packs: nil)
100
+ return [] if applicable.empty?
101
+
102
+ model = packs ? Mcts.merged_model(packs) : Mcts::STAGE_MODEL
103
+ _first, path = Mcts.plan(applicable, iterations: 2000, seed: seed || 42, model: model)
104
+ path
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Augure
4
+ # AG-derived priors (200 synthetic targets, seed 42) encoding the weight
5
+ # inversions the genetic algorithm found against human intuition:
6
+ # humans overweighted shellcode/ret2libc and underweighted ret2plt/fmtstr.
7
+ AG_PRIORS = {
8
+ "shellcode" => [5, 13],
9
+ "ret2libc" => [2, 18],
10
+ "rop" => [10, 10],
11
+ "fmtstr_write" => [12, 8],
12
+ "ret2plt" => [15, 5],
13
+ "ret2func" => [16, 4],
14
+ "srop" => [4, 6],
15
+ "dlresolve" => [3, 7],
16
+ "heap_fastbin" => [3, 7],
17
+ "heap_tcache" => [4, 6],
18
+ "heap_overwrite" => [12, 8],
19
+ "ret2plt_leak" => [8, 4]
20
+ }.freeze
21
+
22
+ # Priors for techniques the AG simulation did not cover. Authored from
23
+ # documented public suites (ROP Emporium, Protostar, exploit.education
24
+ # Phoenix write-ups) and calibrated with KB pseudo-counts - declared as
25
+ # authored constants, not as measured results.
26
+ EXTENDED_PRIORS = {
27
+ "fmtstr_leak" => [13, 7], # leaks are enablers: they defeat PIE/canary
28
+ "ret2csu" => [14, 6], # reliable when the csu gadgets exist
29
+ "stack_pivot" => [13, 7], # required under stack-space constraints
30
+ "got_overwrite" => [13, 7] # partial RELRO makes GOT redirection cheap
31
+ }.freeze
32
+ end
@@ -0,0 +1,300 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Augure
4
+ # Rules-as-data: the single source of truth for technique applicability.
5
+ #
6
+ # Each rule is data: an id (the provenance handle), a head relation/value,
7
+ # and a list of conditions. The Engine interprets the table; a future
8
+ # generator emits the same table as a Souffle .dl program, so the two
9
+ # engines cannot diverge the way the Python prototype's did.
10
+ #
11
+ # Condition forms:
12
+ # [:fact, rel, val] any tuple rel(val) present
13
+ # [:not_fact, rel, val] rel(val) absent (inputs only)
14
+ # [:match, rel, idx, pattern] arg idx matches /pattern/
15
+ # [:cmp, rel, op, n] integer arg satisfies V <op> n
16
+ class Rules
17
+ Rule = Struct.new(:id, :head, :conditions, :source, :origin)
18
+
19
+ APPLICABLE = "applicable"
20
+
21
+ # Input-relation defaults, matching the prototype's closed-world
22
+ # semantics: an absent fact means the default below, not absence.
23
+ DEFAULTS = {
24
+ "nx" => "true", "pie" => "true", "canary" => "false"
25
+ }.freeze
26
+
27
+ class << self
28
+ def all
29
+ @all ||= build.freeze
30
+ end
31
+
32
+ def applicable_rules
33
+ all.select { |r| r.head[0] == APPLICABLE }
34
+ end
35
+
36
+ def inputs
37
+ Facts::SCHEMA
38
+ end
39
+
40
+ private
41
+
42
+ def build
43
+ builder = Builder.new
44
+ builder.tap { |b| populate(b) }.rules
45
+ end
46
+
47
+ def populate(b)
48
+ # -- derived: vulnerability classes from hints -------------------
49
+ b.rule :vuln_sof_gets, %w[vuln sof],
50
+ source: "gets() presence implies stack overflow" do |c|
51
+ c.match "vuln_hint", 0, "unsafe_func:gets"
52
+ end
53
+ b.rule :vuln_sof_strcpy, %w[vuln sof],
54
+ source: "strcpy() presence implies stack overflow" do |c|
55
+ c.match "vuln_hint", 0, "unsafe_func:strcpy"
56
+ end
57
+ b.rule :vuln_fmtstr_printlike, %w[vuln fmtstr],
58
+ source: "printf-like unsized format functions" do |c|
59
+ c.match "vuln_hint", 0, "printf|sprintf"
60
+ end
61
+
62
+ # -- derived: gadget families ------------------------------------
63
+ b.rule :fam_reg_control, %w[has_reg_control true],
64
+ source: "any pop gadget gives register control" do |c|
65
+ c.match "gadget", 0, "pop"
66
+ end
67
+ b.rule :fam_write_primitive, %w[has_write_primitive true],
68
+ source: "any mov-store gadget gives a write primitive" do |c|
69
+ c.match "gadget", 0, "mov"
70
+ end
71
+ b.rule :fam_syscall_gadget, %w[has_syscall_gadget true],
72
+ source: "syscall or int80 gadget" do |c|
73
+ c.match "gadget", 0, "syscall|int80"
74
+ end
75
+ b.rule :fam_csu_gadget, %w[has_csu_gadget true],
76
+ source: "__libc_csu_init popper/mov gadget family" do |c|
77
+ c.match "gadget", 0, "csu"
78
+ end
79
+ b.rule :fam_pivot_gadget, %w[has_pivot_gadget true],
80
+ source: "stack pivot gadget (xchg/mov rsp)" do |c|
81
+ c.match "gadget", 0, "xchg"
82
+ end
83
+ b.rule :enough_from_reg, %w[enough_gadgets true],
84
+ source: "register control alone sustains a ROP chain" do |c|
85
+ c.fact "has_reg_control", "true"
86
+ end
87
+ b.rule :enough_from_write, %w[enough_gadgets true],
88
+ source: "a write primitive alone sustains a ROP chain" do |c|
89
+ c.fact "has_write_primitive", "true"
90
+ end
91
+
92
+ # -- applicable: stack techniques (vuln = sof) --------------------
93
+ b.rule :app_shellcode, ["applicable", "shellcode"],
94
+ source: "NX off + no canary + unfiltered return: inject and jump" do |c|
95
+ c.fact "vuln", "sof"
96
+ c.fact "nx", "false"
97
+ c.fact "canary", "false"
98
+ c.not_fact "return_addr_filtered", "true"
99
+ # the technique as-modeled ends in execve: dead under a
100
+ # filter - the orw pack owns the seccomp case
101
+ c.not_fact "seccomp", "true"
102
+ end
103
+ b.rule :app_ret2func, ["applicable", "ret2func"],
104
+ source: "win function present at fixed address, no PIE" do |c|
105
+ c.fact "vuln", "sof"
106
+ c.fact "pie", "false"
107
+ c.fact "target_function", "true"
108
+ end
109
+ b.rule :app_ret2plt, ["applicable", "ret2plt"],
110
+ source: "system@plt imported: call it directly, no leak" do |c|
111
+ c.fact "vuln", "sof"
112
+ c.fact "nx", "true"
113
+ c.fact "pie", "false"
114
+ c.fact "plt", "system"
115
+ end
116
+ b.rule :app_ret2libc_plt, ["applicable", "ret2libc"],
117
+ source: "__libc_start_main@plt exposes libc base" do |c|
118
+ c.fact "vuln", "sof"
119
+ c.fact "plt", "__libc_start_main"
120
+ c.not_fact "seccomp", "true"
121
+ end
122
+ b.rule :app_ret2libc_libc, ["applicable", "ret2libc"],
123
+ source: "libc known/version pinned: offsets computable" do |c|
124
+ c.fact "vuln", "sof"
125
+ c.fact "libc_present", "true"
126
+ c.not_fact "seccomp", "true"
127
+ end
128
+ b.rule :app_rop, ["applicable", "rop"],
129
+ source: "NX on with gadget supply: chain primitives" do |c|
130
+ c.fact "vuln", "sof"
131
+ c.fact "nx", "true"
132
+ c.fact "enough_gadgets", "true"
133
+ c.not_fact "seccomp", "true"
134
+ end
135
+ b.rule :app_srop, ["applicable", "srop"],
136
+ source: "sigreturn frame + syscall gadget: full register set" do |c|
137
+ c.fact "vuln", "sof"
138
+ c.fact "nx", "true"
139
+ c.fact "sigreturn_frame", "true"
140
+ c.fact "has_syscall_gadget", "true"
141
+ c.not_fact "seccomp", "true"
142
+ end
143
+ b.rule :app_ret2plt_leak, ["applicable", "ret2plt_leak"],
144
+ source: "PIE on: leak via puts@plt with register control" do |c|
145
+ c.fact "vuln", "sof"
146
+ c.fact "nx", "true"
147
+ c.fact "pie", "true"
148
+ c.fact "plt", "puts"
149
+ c.fact "has_reg_control", "true"
150
+ end
151
+
152
+ # -- applicable: format string -----------------------------------
153
+ b.rule :app_fmtstr_write, ["applicable", "fmtstr_write"],
154
+ source: "format string bug: arbitrary write, NX-independent" do |c|
155
+ c.fact "vuln", "fmtstr"
156
+ end
157
+ # The leak is a strategy of its own when there is a read primitive or
158
+ # a protection worth bypassing: %x/%s reads defeat PIE and canaries.
159
+ b.rule :app_fmtstr_leak_read, ["applicable", "fmtstr_leak"],
160
+ source: "read primitive: arbitrary stack read via format string" do |c|
161
+ c.fact "vuln", "fmtstr"
162
+ c.fact "fmtstr_read", "true"
163
+ end
164
+ b.rule :app_fmtstr_leak_pie, ["applicable", "fmtstr_leak"],
165
+ source: "PIE on: leak a code pointer to defeat ASLR" do |c|
166
+ c.fact "vuln", "fmtstr"
167
+ c.fact "pie", "true"
168
+ end
169
+ b.rule :app_fmtstr_leak_canary, ["applicable", "fmtstr_leak"],
170
+ source: "canary on: leak the stack cookie before smashing" do |c|
171
+ c.fact "vuln", "fmtstr"
172
+ c.fact "canary", "true"
173
+ end
174
+
175
+ # -- applicable: deferred symbol resolution ----------------------
176
+ b.rule :app_ret2dlresolve, ["applicable", "ret2dlresolve"],
177
+ source: "lazy binding + writable relocation: fake DT_SYMTAB " \
178
+ "resolves an arbitrary symbol (ROP Emporium " \
179
+ "ret2dlresolve, documented)" do |c|
180
+ c.fact "vuln", "sof"
181
+ c.fact "nx", "true"
182
+ c.fact "pie", "false"
183
+ c.fact "dt_lazy", "true"
184
+ c.fact "reloc_writable", "true"
185
+ end
186
+
187
+ # -- applicable: __libc_csu_init gadget universe -----------------
188
+ b.rule :app_ret2csu, ["applicable", "ret2csu"],
189
+ source: "csu gadgets populate rdx/rsi/rdi in sparse binaries " \
190
+ "(ROP Emporium ret2csu, documented)" do |c|
191
+ c.fact "vuln", "sof"
192
+ c.fact "nx", "true"
193
+ c.fact "has_csu_gadget", "true"
194
+ end
195
+
196
+ # -- applicable: stack pivot under space constraints --------------
197
+ b.rule :app_stack_pivot, ["applicable", "stack_pivot"],
198
+ source: "limited stack space: pivot to a larger buffer first " \
199
+ "(ROP Emporium pivot, documented)" do |c|
200
+ c.fact "vuln", "sof"
201
+ c.fact "nx", "true"
202
+ c.fact "limited_stack", "true"
203
+ c.fact "has_pivot_gadget", "true"
204
+ end
205
+
206
+ # -- applicable: GOT overwrite ------------------------------------
207
+ b.rule :app_got_overwrite_sof, ["applicable", "got_overwrite"],
208
+ source: "writable GOT target: overwrite an entry to redirect " \
209
+ "a call via a ROP write primitive (documented)" do |c|
210
+ c.fact "vuln", "sof"
211
+ c.fact "nx", "true"
212
+ c.fact "got_overwrite_target", "true"
213
+ c.not_fact "relro", "full"
214
+ end
215
+ b.rule :app_got_overwrite_fmtstr, ["applicable", "got_overwrite"],
216
+ source: "writable GOT + format string: %n writes the GOT " \
217
+ "entry and redirects the call (phoenix format-four, " \
218
+ "documented)" do |c|
219
+ c.fact "vuln", "fmtstr"
220
+ c.fact "got_overwrite_target", "true"
221
+ c.not_fact "relro", "full"
222
+ end
223
+
224
+ # -- applicable: heap ---------------------------------------------
225
+ b.rule :app_heap_tcache_alloc, ["applicable", "heap_tcache"],
226
+ source: "tcache allocator: tcache poisoning" do |c|
227
+ c.fact "vuln", "heap"
228
+ c.fact "allocator", "tcache"
229
+ end
230
+ b.rule :app_heap_tcache_glibc, ["applicable", "heap_tcache"],
231
+ source: "glibc 2.26..2.33: tcache exists by default" do |c|
232
+ c.fact "vuln", "heap"
233
+ c.cmp "glibc_minor", :ge, 26
234
+ c.cmp "glibc_minor", :lt, 34
235
+ end
236
+ b.rule :app_heap_fastbin_glibc, ["applicable", "heap_fastbin"],
237
+ source: "glibc < 2.26: fastbin is the mechanism" do |c|
238
+ c.fact "vuln", "heap"
239
+ c.cmp "glibc_minor", :lt, 26
240
+ end
241
+ b.rule :app_heap_fastbin_alloc, ["applicable", "heap_fastbin"],
242
+ source: "ptmalloc2 fastbin context" do |c|
243
+ c.fact "vuln", "heap"
244
+ c.fact "allocator", "ptmalloc2"
245
+ end
246
+ b.rule :app_heap_overwrite_dlmalloc, ["applicable", "heap_overwrite"],
247
+ source: "dlmalloc: adjacency overwrite" do |c|
248
+ c.fact "vuln", "heap"
249
+ c.fact "allocator", "dlmalloc"
250
+ end
251
+ b.rule :app_heap_overwrite_fp, ["applicable", "heap_overwrite"],
252
+ source: "function pointer on heap: overwrite it" do |c|
253
+ c.fact "vuln", "heap"
254
+ c.fact "function_pointer_on_heap", "true"
255
+ end
256
+ end
257
+ end
258
+
259
+ # Tiny DSL used to declare rules as data.
260
+ class Builder
261
+ attr_reader :rules
262
+
263
+ def initialize
264
+ @rules = []
265
+ end
266
+
267
+ def rule(id, head, source:, &block)
268
+ collector = ConditionCollector.new
269
+ block.call(collector)
270
+ @rules << Rule.new(
271
+ id: id, head: head, conditions: collector.conditions, source: source
272
+ )
273
+ end
274
+
275
+ class ConditionCollector
276
+ attr_reader :conditions
277
+
278
+ def initialize
279
+ @conditions = []
280
+ end
281
+
282
+ def fact(rel, val)
283
+ @conditions << [:fact, rel, val]
284
+ end
285
+
286
+ def not_fact(rel, val)
287
+ @conditions << [:not_fact, rel, val]
288
+ end
289
+
290
+ def match(rel, idx, pattern)
291
+ @conditions << [:match, rel, idx, pattern]
292
+ end
293
+
294
+ def cmp(rel, op, n)
295
+ @conditions << [:cmp, rel, op, n]
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "timeout"
5
+
6
+ module Augure
7
+ # Thin subprocess boundary to any SMT-LIB solver (z3, bitwuzla, cvc5).
8
+ # Contract: SMT-LIB text in, status atom out. Unknown/timeout = :unknown,
9
+ # never an exception: a verification failure must be visible, not fatal.
10
+ module SmtProcess
11
+ module_function
12
+
13
+ def solve(smt_text, solver:, timeout: 10)
14
+ out = nil
15
+ ok = false
16
+ Open3.popen3(solver) do |stdin, stdout, _stderr, waiter|
17
+ stdin.write(smt_text)
18
+ stdin.close
19
+ begin
20
+ Timeout.timeout(timeout) do
21
+ out = stdout.read
22
+ ok = waiter.value.success?
23
+ end
24
+ rescue Timeout::Error
25
+ kill_quietly(waiter)
26
+ return :unknown
27
+ end
28
+ end
29
+ parse_status(out) if ok
30
+ rescue
31
+ :unknown
32
+ end
33
+
34
+ def parse_status(stdout)
35
+ case stdout
36
+ when /\bsat\b/ then :sat
37
+ when /\bunsat\b/ then :unsat
38
+ else :unknown
39
+ end
40
+ end
41
+
42
+ def kill_quietly(waiter)
43
+ Process.kill("KILL", waiter.pid)
44
+ rescue
45
+ nil
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "timeout"
5
+
6
+ module Augure
7
+ # Numerical feasibility checks. The fast path is constant arithmetic in
8
+ # plain Ruby; the same checks emit SMT-LIB v2 text for a real solver via
9
+ # SmtProcess. The formal story survives because both paths run on the same
10
+ # corpus and must agree.
11
+ module Verifier
12
+ module_function
13
+
14
+ def payload_fits(buffer_size:, payload_min:)
15
+ (buffer_size >= payload_min) ? :sat : :unsat
16
+ end
17
+
18
+ def bad_bytes?(payload:, bad_bytes:)
19
+ payload.any? { |byte| bad_bytes.include?(byte) }
20
+ end
21
+
22
+ def rop_chain_feasible?(required_gadgets:, present_gadgets:, payload:, bad_bytes:, stack_size:)
23
+ return false unless (required_gadgets - present_gadgets).empty?
24
+ return false if bad_bytes?(payload: payload, bad_bytes: bad_bytes)
25
+ return false if payload.size > stack_size
26
+
27
+ true
28
+ end
29
+
30
+ def payload_fits_smtlib(buffer_size, payload_min)
31
+ <<~SMT
32
+ (set-logic QF_LIA)
33
+ (declare-const buffer_size Int)
34
+ (assert (= buffer_size #{buffer_size.to_i}))
35
+ (assert (>= buffer_size #{payload_min.to_i}))
36
+ (check-sat)
37
+ SMT
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Augure
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "augure/version"
4
+
5
+ module Augure
6
+ class Error < StandardError; end
7
+ class MalformedFact < Error; end
8
+ class UnknownPredicate < Error; end
9
+ class EngineError < Error; end
10
+ class PackError < Error; end
11
+ end
12
+
13
+ require_relative "augure/facts"
14
+ require_relative "augure/rules"
15
+ require_relative "augure/engine"
16
+ require_relative "augure/bandit"
17
+ require_relative "augure/mcts"
18
+ require_relative "augure/pack"
19
+ require_relative "augure/pack_loader"
20
+ require_relative "augure/knowledge_base"
21
+ require_relative "augure/verifier"
22
+ require_relative "augure/smt_process"
23
+ require_relative "augure/priors"
24
+ require_relative "augure/coverage"
25
+ require_relative "augure/pipeline"
@@ -0,0 +1,46 @@
1
+ technique: banner_leak
2
+ author: Gregory Epp
3
+ source: lictor lab - the banner practice is everywhere in CTF services
4
+
5
+ rules:
6
+ - id: app_banner_leak
7
+ head: [applicable, banner_leak]
8
+ conditions:
9
+ - [fact, service, unknown]
10
+ - [fact, verified, remote]
11
+ - [fact, software_version, 0]
12
+
13
+ kb:
14
+ - id: banner_leak_001
15
+ technique: banner_leak
16
+ description: >
17
+ The service prints its own addresses: "win() at 0x4011d6" is an
18
+ address handed over before the first input. The verified remote
19
+ fact anchors the observation; the leak feeds the build slots.
20
+ chain: [banner_read, address_parse, payload_assemble]
21
+ unlocks: [addresses]
22
+ success_rate: 0.60
23
+
24
+ mcts:
25
+ requires: []
26
+ provides: [addresses]
27
+ terminal: false
28
+ success: 0.60
29
+
30
+ priors: [7, 13]
31
+
32
+
33
+ # The defender side (the corpus read backwards): what this technique
34
+ # looks like to the defenses. Armored by check 9.
35
+ detection:
36
+ - channel: wire
37
+ signature: "the service's banner carries raw hex addresses (%p prints)"
38
+ note: "the leak itself: addresses handed over before the first input"
39
+ - channel: syslog
40
+ signature: "short-lived connections that read only the greeting"
41
+ note: "a leak pass: connect-read-close, no input sent"
42
+
43
+ # The ATT&CK mapping (the navigator export consumes it)
44
+ mitre:
45
+ - T1592
46
+ - T1595
@@ -0,0 +1,46 @@
1
+ technique: brop
2
+ author: Gregory Epp
3
+ source: https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/brop/
4
+
5
+ rules:
6
+ - id: app_brop
7
+ head: [applicable, brop]
8
+ conditions:
9
+ - [fact, remote, true]
10
+ - [fact, verified, remote]
11
+ - [fact, nx, true]
12
+
13
+ kb:
14
+ - id: brop_001
15
+ technique: brop
16
+ description: >
17
+ Blind ROP: no binary, only a remote oracle that faults. The crash
18
+ differential (the verified fact) anchors the model; stop-gadget
19
+ hunting and a BROP gadget leak enough libc to call system.
20
+ First rule that consumes a verified observation, not a modeled one.
21
+ chain: [stop_gadget, brop_gadget, dump_libc, system_call]
22
+ unlocks: [libc_base]
23
+ success_rate: 0.30
24
+
25
+ mcts:
26
+ requires: []
27
+ provides: [libc_base]
28
+ terminal: false
29
+ success: 0.30
30
+
31
+ priors: [6, 14]
32
+
33
+
34
+ # The defender side (the corpus read backwards): what this technique
35
+ # looks like to the defenses. Armored by check 9.
36
+ detection:
37
+ - channel: wire
38
+ signature: "highly regular connect-probe cycles with alternating payloads"
39
+ note: "the differential probing pattern (fault oracle discovery)"
40
+ - channel: crash
41
+ signature: "child SIGSEGV on long inputs, the service keeps accepting"
42
+ note: "the crash oracle being exercised"
43
+
44
+ # The ATT&CK mapping (the navigator export consumes it)
45
+ mitre:
46
+ - T1068