augure-profiler 0.0.1
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/exe/augure-profile +42 -0
- data/home/runner/work/augure/augure/profiler/exe/augure-profile +42 -0
- data/home/runner/work/augure/augure/profiler/lib/augure-profiler/dynamic_trace.rb +80 -0
- data/home/runner/work/augure/augure/profiler/lib/augure-profiler/fuzzer.rb +291 -0
- data/home/runner/work/augure/augure/profiler/lib/augure-profiler/gadget_hunt.rb +109 -0
- data/home/runner/work/augure/augure/profiler/lib/augure-profiler/profiler.rb +203 -0
- data/home/runner/work/augure/augure/profiler/lib/augure-profiler/shellcode.rb +329 -0
- data/home/runner/work/augure/augure/profiler/lib/augure-profiler/static_offset.rb +265 -0
- data/home/runner/work/augure/augure/profiler/lib/augure-profiler/version.rb +5 -0
- data/home/runner/work/augure/augure/profiler/lib/augure-profiler.rb +12 -0
- metadata +84 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "metasm"
|
|
4
|
+
|
|
5
|
+
module AugureProfiler
|
|
6
|
+
# The gdb-free offset: read it straight off the frame. Decode the ELF,
|
|
7
|
+
# resolve the unsafe imports' PLT stubs (the .rela.plt order), find the
|
|
8
|
+
# call site, and read the buffer's rbp displacement in the argument
|
|
9
|
+
# setup: offset = displacement + saved-rbp + return slot. The debugger
|
|
10
|
+
# stays as the verifier, not the measurer.
|
|
11
|
+
module StaticOffset
|
|
12
|
+
UNSAFE_CALLS = %w[strcpy gets scanf sprintf read readline strcat].freeze
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# metasm leaves section.encoded nil on decode: the raw bytes come
|
|
17
|
+
# from the file at the section's offset (same as the profiler).
|
|
18
|
+
def self.section_data(path, sec)
|
|
19
|
+
File.open(path) do |f|
|
|
20
|
+
f.seek(sec.offset.to_i)
|
|
21
|
+
f.read(sec.size.to_i)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Returns {offset:, sink:, function:} or nil when no unsafe call site
|
|
26
|
+
# carries a readable rbp-frame buffer.
|
|
27
|
+
def offset(path)
|
|
28
|
+
elf = Metasm::ELF.decode_file(path)
|
|
29
|
+
arch = (elf.header.e_class.to_s == "64") ? :x64 : :x86
|
|
30
|
+
@path = path
|
|
31
|
+
stubs = plt_stubs(elf, arch)
|
|
32
|
+
unsafe = stubs.slice(*UNSAFE_CALLS)
|
|
33
|
+
return nil if unsafe.empty?
|
|
34
|
+
|
|
35
|
+
@unsafe_addrs = unsafe.invert
|
|
36
|
+
|
|
37
|
+
text = elf.sections.find { |s| s.name == ".text" }
|
|
38
|
+
data = section_data(@path, text)
|
|
39
|
+
bytes = data.bytes
|
|
40
|
+
# The CHEAP scan first: a linear e8 rel32 walk nets every call to
|
|
41
|
+
# an unsafe stub - the full .text disassembly (minutes on a 1MB
|
|
42
|
+
# text) is only needed if this finds nothing.
|
|
43
|
+
call_site = nil
|
|
44
|
+
sink = nil
|
|
45
|
+
bytes.each_index do |i|
|
|
46
|
+
next unless bytes[i] == 0xe8
|
|
47
|
+
|
|
48
|
+
target = text.addr + i + 5 + bytes[(i + 1)..(i + 4)].pack("C*").unpack1("l<")
|
|
49
|
+
next unless (name = @unsafe_addrs[target])
|
|
50
|
+
|
|
51
|
+
call_site = text.addr + i
|
|
52
|
+
sink = name
|
|
53
|
+
break
|
|
54
|
+
end
|
|
55
|
+
unless call_site
|
|
56
|
+
# LAST RESORT: the flow disassembly of the whole .text (minutes
|
|
57
|
+
# on a 1MB text - the e8 scan above almost always wins).
|
|
58
|
+
cpu = (arch == :x64) ? Metasm::X64.new : Metasm::Ia32.new
|
|
59
|
+
sc = Metasm::Shellcode.new(cpu)
|
|
60
|
+
sc.base_addr = text.addr
|
|
61
|
+
sc.encoded = Metasm::EncodedData.new(data)
|
|
62
|
+
ds = sc.disassemble(text.addr)
|
|
63
|
+
elf.symbols.select do |sym|
|
|
64
|
+
sym.type.to_s == "FUNC" && sym.value.to_i >= text.addr && sym.value.to_i < text.addr + text.size.to_i
|
|
65
|
+
end
|
|
66
|
+
.each { |sym| ds.disassemble(sym.value) }
|
|
67
|
+
instrs = ds.decoded.to_a.sort_by { |a, _| a }.map { |a, di| [a, di.instruction] }
|
|
68
|
+
call_idx = instrs.index do |_a, instr|
|
|
69
|
+
next false unless instr.opname == "call"
|
|
70
|
+
|
|
71
|
+
arg = instr.args.first
|
|
72
|
+
arg.respond_to?(:expression) ? @unsafe_addrs.key?(arg.expression.reduce.to_i) : false
|
|
73
|
+
end
|
|
74
|
+
call_site = call_idx && instrs.dig(call_idx, 0)
|
|
75
|
+
if call_site
|
|
76
|
+
arg = instrs.dig(call_idx, 1)&.args&.first
|
|
77
|
+
sink = arg&.respond_to?(:expression) ? @unsafe_addrs[arg.expression.reduce.to_i] : nil
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
result = if call_site.is_a?(Integer) # nil or Integer: nil.is_a? is false
|
|
81
|
+
frame_bytes(data, call_site - text.addr, arch)
|
|
82
|
+
elsif call_site
|
|
83
|
+
frame_disp(instrs, call_idx, arch)
|
|
84
|
+
end
|
|
85
|
+
result&.tap do |r|
|
|
86
|
+
r[:sink] ||= sink
|
|
87
|
+
site = call_site
|
|
88
|
+
r[:function] = enclosing_function(elf, site.to_i)
|
|
89
|
+
end
|
|
90
|
+
rescue Metasm::ParseError, Metasm::InvalidExeFormat
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Byte-pattern net: lea reg, [rbp-disp8] anywhere in the window
|
|
95
|
+
# before the unsafe call. x64: [REX.W] 8d modrm(mod=01,rm=101) disp8;
|
|
96
|
+
# x86: 8d modrm(mod=01,rm=101) disp8 (rbp/ebp addressed frames only).
|
|
97
|
+
def frame_bytes(data, call_off, arch)
|
|
98
|
+
width = (arch == :x64) ? 8 : 4
|
|
99
|
+
saved = width
|
|
100
|
+
rex_w = (arch == :x64) ? [0x48, 0x49, 0x4c, 0x4d] : nil
|
|
101
|
+
start = [call_off - 64, 2].max
|
|
102
|
+
(call_off - 2).downto(start) do |j|
|
|
103
|
+
modrm = data.getbyte(j)
|
|
104
|
+
next unless (modrm & 0xC7) == 0x45
|
|
105
|
+
next unless data.getbyte(j - 1) == 0x8d
|
|
106
|
+
next unless (arch == :x64) ? rex_w.include?(data.getbyte(j - 2)) : true
|
|
107
|
+
|
|
108
|
+
disp = data.getbyte(j + 1)
|
|
109
|
+
disp -= 256 if disp > 127
|
|
110
|
+
return {offset: saved - disp, sink: nil}
|
|
111
|
+
end
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The buffer displacement in the argument setup, then the frame math:
|
|
116
|
+
# on x64: [rbp-N] -> saved rbp at N..N+8, return at N+8..N+16.
|
|
117
|
+
def frame_disp(instrs, call_idx, arch)
|
|
118
|
+
saved = (arch == :x64) ? 8 : 4
|
|
119
|
+
base = (arch == :x64) ? "rbp" : "ebp"
|
|
120
|
+
instrs[[call_idx - 8, 0].max...call_idx].reverse_each do |di|
|
|
121
|
+
s = di.instruction.to_s
|
|
122
|
+
next unless di.opname == "lea" && s =~ /\[#{base}(-0x[0-9a-f]+)\]/i
|
|
123
|
+
|
|
124
|
+
disp = Regexp.last_match(1).to_i(16)
|
|
125
|
+
return {offset: saved - disp,
|
|
126
|
+
sink: @unsafe_addrs[instrs[call_idx].instruction.args.first.expression.reduce.to_i]}
|
|
127
|
+
end
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def enclosing_function(elf, addr)
|
|
132
|
+
func = elf.symbols.select { |s| s.type.to_s == "FUNC" && s.value.to_i > 0 }
|
|
133
|
+
.find { |s| addr.to_i >= s.value.to_i && addr.to_i < s.value.to_i + s.size.to_i }
|
|
134
|
+
func&.name.to_s
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# .rela.plt order. CFI toolchains put the call targets in .plt.sec
|
|
138
|
+
# (16 * i, same order); the classic .plt has PLT0 first (16 * (i + 1)).
|
|
139
|
+
# The symbol index indexes .dynsym.
|
|
140
|
+
def plt_stubs(elf, _arch)
|
|
141
|
+
pltsec = elf.sections.find { |s| s.name == ".plt.sec" }
|
|
142
|
+
plt = pltsec || elf.sections.find { |s| s.name == ".plt" }
|
|
143
|
+
rela = elf.sections.find { |s| s.name == ".rela.plt" }
|
|
144
|
+
dynsym = elf.sections.find { |s| s.name == ".dynsym" }
|
|
145
|
+
dynstr = elf.sections.find { |s| s.name == ".dynstr" }
|
|
146
|
+
return {} unless plt && rela && dynsym && dynstr
|
|
147
|
+
|
|
148
|
+
sym_data = section_data(@path, dynsym)
|
|
149
|
+
str_data = section_data(@path, dynstr)
|
|
150
|
+
name_at = lambda do |idx|
|
|
151
|
+
off = sym_data[idx * 24, 4].unpack1("V")
|
|
152
|
+
str_data[off..].unpack1("Z*")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
entries = section_data(@path, rela)
|
|
156
|
+
count = entries.size / 24
|
|
157
|
+
stubs = {}
|
|
158
|
+
count.times do |i|
|
|
159
|
+
r_info = entries[i * 24 + 8, 8].unpack1("Q<")
|
|
160
|
+
name = name_at.call(r_info >> 32)
|
|
161
|
+
stubs[name] = (plt.name == ".plt.sec") ? plt.addr + 16 * i : plt.addr + 16 * (i + 1)
|
|
162
|
+
end
|
|
163
|
+
stubs
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Gadget honesty: a fact claims "at this address there is a ret" -
|
|
167
|
+
# re-decode the bytes and confirm the claim before anything consumes
|
|
168
|
+
# it. A lying gadget (wrong address, mid-instruction c3) is refused.
|
|
169
|
+
# Returns true when the decoded chain matches the type's semantics.
|
|
170
|
+
def self.verify_gadget(path, type, addr)
|
|
171
|
+
elf = Metasm::ELF.decode_file(path)
|
|
172
|
+
sec = elf.sections.find do |s|
|
|
173
|
+
s.flags.map(&:to_s).include?("EXECINSTR") &&
|
|
174
|
+
addr.to_i >= s.addr.to_i && addr.to_i < s.addr.to_i + s.size.to_i
|
|
175
|
+
end
|
|
176
|
+
return false unless sec
|
|
177
|
+
|
|
178
|
+
arch = (elf.header.e_class.to_s == "64") ? :x64 : :x86
|
|
179
|
+
cpu = (arch == :x64) ? Metasm::X64.new : Metasm::Ia32.new
|
|
180
|
+
data = section_data(path, sec)
|
|
181
|
+
sc = Metasm::Shellcode.new(cpu)
|
|
182
|
+
sc.base_addr = sec.addr.to_i
|
|
183
|
+
sc.encoded = Metasm::EncodedData.new(data)
|
|
184
|
+
ds = sc.disassemble(addr.to_i)
|
|
185
|
+
di = ds.di_at(addr.to_i)
|
|
186
|
+
return false unless di
|
|
187
|
+
|
|
188
|
+
expected = expected_chain(type, arch)
|
|
189
|
+
return false if expected.empty?
|
|
190
|
+
|
|
191
|
+
cur = addr.to_i
|
|
192
|
+
expected.each do |want|
|
|
193
|
+
di = ds.di_at(cur) || (ds.disassemble(cur) && ds.di_at(cur))
|
|
194
|
+
return false unless di
|
|
195
|
+
|
|
196
|
+
instr = di.instruction.to_s
|
|
197
|
+
case want
|
|
198
|
+
when /\Apop (.+)\z/
|
|
199
|
+
return false unless di.instruction.opname == "pop" && di.instruction.args.first.to_s == Regexp.last_match(1)
|
|
200
|
+
when /\Ajmp (.+)\z/
|
|
201
|
+
return false unless di.instruction.opname == "jmp" && instr.include?(Regexp.last_match(1))
|
|
202
|
+
else
|
|
203
|
+
return false unless instr == want || instr.start_with?(want)
|
|
204
|
+
end
|
|
205
|
+
cur += di.bin_length.to_i
|
|
206
|
+
end
|
|
207
|
+
true
|
|
208
|
+
rescue Metasm::ParseError, Metasm::InvalidExeFormat
|
|
209
|
+
false
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def self.expected_chain(type, arch)
|
|
213
|
+
sp = (arch == :x64) ? "rsp" : "esp"
|
|
214
|
+
case type
|
|
215
|
+
when "ret_align" then ["ret"]
|
|
216
|
+
when "jmp_rsp" then ["jmp #{sp}"]
|
|
217
|
+
when "jmp_esp" then ["jmp #{sp}"]
|
|
218
|
+
when "syscall_ret" then %w[syscall ret]
|
|
219
|
+
when "int80_ret" then ["int"]
|
|
220
|
+
when /\Apop_.*_ret\z/
|
|
221
|
+
type.delete_prefix("pop_").delete_suffix("_ret").split("_").map { |r| "pop #{r}" } + ["ret"]
|
|
222
|
+
else
|
|
223
|
+
[]
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# The crash, identified instead of guessed. The runtime rip cannot be
|
|
228
|
+
# slid back through ASLR, so the identification comes from the STATIC
|
|
229
|
+
# side: the vulnerable function and its sink are facts the frame read
|
|
230
|
+
# already established, and the CONTROL evidence is the pattern the
|
|
231
|
+
# cyclic input planted in the registers (rbp/rip carrying pattern
|
|
232
|
+
# bytes = the saved frame IS ours - the old tool inferred this from
|
|
233
|
+
# 0x41414141 shapes; here the pattern is known, so the proof is exact).
|
|
234
|
+
def self.crash_facts(path, report)
|
|
235
|
+
return {} unless report.is_a?(Hash) && report[:signal] == "SIGSEGV"
|
|
236
|
+
|
|
237
|
+
static = offset(path) || {}
|
|
238
|
+
facts = {vuln_function: static[:function], sink: static[:sink]}.compact
|
|
239
|
+
|
|
240
|
+
pattern = nil
|
|
241
|
+
if static[:offset]
|
|
242
|
+
sweep = begin
|
|
243
|
+
AugureProfiler::Fuzzer.run(path, inputs: 40, max_len: 1024)
|
|
244
|
+
rescue
|
|
245
|
+
nil
|
|
246
|
+
end
|
|
247
|
+
crash_len = sweep&.first&.dig(:input)&.bytesize
|
|
248
|
+
pattern = Triage.cyclic(crash_len + 64) if crash_len
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
controlled = %w[rip rsp rbp rax rbx rcx rdx rdi rsi r8 r9 r10 r11 r12 r13 r14 r15]
|
|
252
|
+
.any? do |reg|
|
|
253
|
+
val = report.dig(:registers, reg)
|
|
254
|
+
next false unless val.is_a?(String) && val.start_with?("0x")
|
|
255
|
+
|
|
256
|
+
raw = [val[2..].to_i(16)].pack("Q<")
|
|
257
|
+
# the register carries OUR bytes: a pattern subsequence (truncated
|
|
258
|
+
# to 4 bytes covers the classic ret-slot window) or the A-fill
|
|
259
|
+
raw.include?("AAAA") || (pattern && [8, 4].any? { |w| pattern.include?(raw[0, w]) })
|
|
260
|
+
end
|
|
261
|
+
facts[:crash_site] = controlled ? "ret" : "unknown"
|
|
262
|
+
facts
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "augure-profiler/version"
|
|
4
|
+
require_relative "augure-profiler/profiler"
|
|
5
|
+
require_relative "augure-profiler/fuzzer"
|
|
6
|
+
require_relative "augure-profiler/shellcode"
|
|
7
|
+
require_relative "augure-profiler/static_offset"
|
|
8
|
+
require_relative "augure-profiler/dynamic_trace"
|
|
9
|
+
require_relative "augure-profiler/gadget_hunt"
|
|
10
|
+
|
|
11
|
+
module AugureProfiler
|
|
12
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: augure-profiler
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Gregory Epp
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: augure
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: metasm
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.0'
|
|
40
|
+
description: 'Generates augure fact files from ELF binaries via metasm: checksec protections,
|
|
41
|
+
PLT imports, unsafe-symbol hints and semantically classified gadgets. The profiler
|
|
42
|
+
speaks facts; augure decides.'
|
|
43
|
+
email:
|
|
44
|
+
- log0u7@users.noreply.github.com
|
|
45
|
+
executables:
|
|
46
|
+
- augure-profile
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- "/home/runner/work/augure/augure/profiler/exe/augure-profile"
|
|
51
|
+
- "/home/runner/work/augure/augure/profiler/lib/augure-profiler.rb"
|
|
52
|
+
- "/home/runner/work/augure/augure/profiler/lib/augure-profiler/dynamic_trace.rb"
|
|
53
|
+
- "/home/runner/work/augure/augure/profiler/lib/augure-profiler/fuzzer.rb"
|
|
54
|
+
- "/home/runner/work/augure/augure/profiler/lib/augure-profiler/gadget_hunt.rb"
|
|
55
|
+
- "/home/runner/work/augure/augure/profiler/lib/augure-profiler/profiler.rb"
|
|
56
|
+
- "/home/runner/work/augure/augure/profiler/lib/augure-profiler/shellcode.rb"
|
|
57
|
+
- "/home/runner/work/augure/augure/profiler/lib/augure-profiler/static_offset.rb"
|
|
58
|
+
- "/home/runner/work/augure/augure/profiler/lib/augure-profiler/version.rb"
|
|
59
|
+
- exe/augure-profile
|
|
60
|
+
homepage: https://github.com/log0u7/augure
|
|
61
|
+
licenses:
|
|
62
|
+
- MIT
|
|
63
|
+
metadata:
|
|
64
|
+
homepage_uri: https://github.com/log0u7/augure
|
|
65
|
+
source_code_uri: https://github.com/log0u7/augure
|
|
66
|
+
changelog_uri: https://github.com/log0u7/augure/blob/main/CHANGELOG.md
|
|
67
|
+
rdoc_options: []
|
|
68
|
+
require_paths:
|
|
69
|
+
- lib
|
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.4'
|
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - ">="
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '0'
|
|
80
|
+
requirements: []
|
|
81
|
+
rubygems_version: 3.6.9
|
|
82
|
+
specification_version: 4
|
|
83
|
+
summary: 'Binary profiler for the augure decision layer: ELF in, Datalog facts out.'
|
|
84
|
+
test_files: []
|