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,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,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
|