augure-profiler 0.0.1 → 0.0.3
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 286018f7712ec4f495a1703eaea59f3c114f49b18196f69e291a0a1e7ff3c699
|
|
4
|
+
data.tar.gz: 4403aef59eb80f8fac2425216d6f25f2cc25c06d8d62017089a2c1e15e03685b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 75a950feec5e2a0e5c225ee0561ae808e3693dc0fe95745f7635af553044457525c3b71a59e9a06117efab1e62aca711c200cf0bb8ef4a29dffe5f6787fb169c
|
|
7
|
+
data.tar.gz: bd1bc7c1c60722ca35038a554edb2311ab7a562e06853a6a6d11ac4e3bba77599fdd9a8451609731f7e5a240ccf17be00a0f7ad6ff2ae7ed0a3b7a2962f90c9a
|
|
@@ -23,6 +23,7 @@ module AugureProfiler
|
|
|
23
23
|
["int80_ret", "cd 80 c3"], # int 0x80 ; ret
|
|
24
24
|
["pop_rdi_ret", "5f c3"], # pop rdi ; ret
|
|
25
25
|
["pop_rsi_ret", "5e c3"], # pop rsi ; ret
|
|
26
|
+
["pop_rsi_r15_ret", "5e 41 5f c3"], # pop rsi ; pop r15 ; ret (the callme flavor)
|
|
26
27
|
["pop_rdx_ret", "5a c3"], # pop rdx ; ret
|
|
27
28
|
["pop_rax_ret", "58 c3"], # pop rax ; ret
|
|
28
29
|
["JMP_STACK", "ff e4"], # jmp esp/rsp - renamed per arch
|
|
@@ -46,9 +47,60 @@ module AugureProfiler
|
|
|
46
47
|
emit_checksec(facts)
|
|
47
48
|
emit_symbols(facts)
|
|
48
49
|
emit_gadgets(facts)
|
|
50
|
+
emit_vuln_facts(facts)
|
|
49
51
|
facts
|
|
50
52
|
end
|
|
51
53
|
|
|
54
|
+
# The vuln facts the binary itself carries, so the operator's
|
|
55
|
+
# suggest stays for the knowledge NO ELF can show (a lib-side
|
|
56
|
+
# overflow, a heap class, a filter). The static frame read is the
|
|
57
|
+
# evidence: an unsafe call site feeding an rbp-frame buffer whose
|
|
58
|
+
# room the copy can reach - the sized family only when the length
|
|
59
|
+
# is not a bounded immediate. The fmtstr class: the printf-family
|
|
60
|
+
# format argument living on the stack.
|
|
61
|
+
def emit_vuln_facts(facts)
|
|
62
|
+
static = StaticOffset.offset(@path)
|
|
63
|
+
if static
|
|
64
|
+
facts.add("vuln", "sof")
|
|
65
|
+
facts.add("vuln_function", static[:function]) if static[:function] && !static[:function].empty?
|
|
66
|
+
facts.add("sink", static[:sink]) if static[:sink]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sink = StaticOffset.stack_format_sink(@path)
|
|
70
|
+
return unless sink
|
|
71
|
+
|
|
72
|
+
facts.add("vuln", "fmtstr")
|
|
73
|
+
facts.add("fmtstr_read", "true")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# the csu byte signatures, both known compiler flavors: the popper
|
|
77
|
+
# = pop rbx,rbp,r12,r13,r14,r15 ; ret (the chain re-enters at the
|
|
78
|
+
# pop rbx), the mov block ends in the csu call signature and starts
|
|
79
|
+
# 9 bytes earlier (three 3-byte register movs, REX.W/REX.WR 0x89
|
|
80
|
+
# opcodes - the mov order is the compiler's choice: r15->rdx on
|
|
81
|
+
# recent gcc, r13->rdx on the classic layout)
|
|
82
|
+
CSU_POPPER_BIN = ["5b5d415c415d415e415fc3"].pack("H*").freeze
|
|
83
|
+
CSU_CALL_BIN = ["41ff14dc"].pack("H*").freeze
|
|
84
|
+
|
|
85
|
+
def self.csu_gadget_addrs(bytes, base, extent)
|
|
86
|
+
win = bytes.is_a?(String) ? bytes.b : bytes.pack("C*").b
|
|
87
|
+
win = win.byteslice(0, extent) if extent && extent < win.bytesize
|
|
88
|
+
popper = win.index(CSU_POPPER_BIN)
|
|
89
|
+
mov = find_csu_mov(win)
|
|
90
|
+
{popper: popper ? base + popper : nil, mov: mov ? base + mov : nil}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.find_csu_mov(win)
|
|
94
|
+
off = 0
|
|
95
|
+
while (call = win.index(CSU_CALL_BIN, off))
|
|
96
|
+
start = call - 9
|
|
97
|
+
return start if start >= 0 && [0x4c, 0x44].include?(win.getbyte(start)) && win.getbyte(start + 1) == 0x89
|
|
98
|
+
|
|
99
|
+
off = call + 1
|
|
100
|
+
end
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
52
104
|
private
|
|
53
105
|
|
|
54
106
|
def emit_checksec(facts)
|
|
@@ -90,6 +142,10 @@ module AugureProfiler
|
|
|
90
142
|
# needs the address, not just the existence.
|
|
91
143
|
@elf.symbols.each do |sym|
|
|
92
144
|
next unless sym.name.to_s.match?(/\Awin\z|win\z|flag\z/)
|
|
145
|
+
# an IMPORT is not a win HERE: the ret2csu shape imports its
|
|
146
|
+
# ret2win from the companion library; the symbol's value is 0
|
|
147
|
+
# and a built [qword win] payload would jump to NULL
|
|
148
|
+
next if sym.shndx.to_s == "UNDEF"
|
|
93
149
|
|
|
94
150
|
facts.add("target_function", "true")
|
|
95
151
|
facts.add("win_symbol", sym.name, sym.value.to_i)
|
|
@@ -189,15 +245,39 @@ module AugureProfiler
|
|
|
189
245
|
hits.map { |(_order, type, addr)| [type, addr] }
|
|
190
246
|
end
|
|
191
247
|
|
|
192
|
-
# Static binaries expose the __libc_csu_init universal gadgets
|
|
193
|
-
#
|
|
248
|
+
# Static binaries expose the __libc_csu_init universal gadgets; their
|
|
249
|
+
# presence IS the gadget fact. The two useful sequences sit at
|
|
250
|
+
# compiler-dependent offsets INSIDE the function (the entry/+0x40
|
|
251
|
+
# guess broke on the newer mov rdx,r15 flavor), so both are located
|
|
252
|
+
# by byte signature within the symbol's extent: the popper fact aims
|
|
253
|
+
# at the pop rbx (the chain entry, the leading add rsp,8 stays one
|
|
254
|
+
# slot earlier for layouts that need it), the mov block by its call
|
|
255
|
+
# signature. The pack teaches the arg mapping for its target.
|
|
194
256
|
def emit_csu_gadgets(facts)
|
|
195
|
-
names = @elf.symbols.map(&:name).compact
|
|
196
|
-
return unless names.include?("__libc_csu_init")
|
|
197
|
-
|
|
198
257
|
sym = @elf.symbols.find { |s| s.name == "__libc_csu_init" }
|
|
199
|
-
|
|
200
|
-
|
|
258
|
+
return unless sym
|
|
259
|
+
|
|
260
|
+
base = sym.value.to_i
|
|
261
|
+
extent = sym.size.to_i
|
|
262
|
+
extent = 0x100 if extent <= 0
|
|
263
|
+
section = executable_sections.find do |s|
|
|
264
|
+
s.addr.to_i <= base && base < s.addr.to_i + s.size.to_i
|
|
265
|
+
end
|
|
266
|
+
return unless section
|
|
267
|
+
|
|
268
|
+
window = bytes_slice(section, base - section.addr.to_i)
|
|
269
|
+
return if window.nil? || window.empty?
|
|
270
|
+
|
|
271
|
+
addrs = self.class.csu_gadget_addrs(window, base, extent)
|
|
272
|
+
facts.add("gadget", "csu_popper", addrs[:popper]) if addrs[:popper]
|
|
273
|
+
facts.add("gadget", "csu_mov", addrs[:mov]) if addrs[:mov]
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def bytes_slice(section, from)
|
|
277
|
+
bytes = section_bytes(section)
|
|
278
|
+
return nil if bytes.nil?
|
|
279
|
+
|
|
280
|
+
bytes[from, bytes.size - from] || []
|
|
201
281
|
end
|
|
202
282
|
end
|
|
203
283
|
end
|
|
@@ -9,7 +9,19 @@ module AugureProfiler
|
|
|
9
9
|
# setup: offset = displacement + saved-rbp + return slot. The debugger
|
|
10
10
|
# stays as the verifier, not the measurer.
|
|
11
11
|
module StaticOffset
|
|
12
|
-
|
|
12
|
+
# The unbounded family: no length argument exists, the copy runs to
|
|
13
|
+
# the NUL (or the newline). Any call site feeding it a stack buffer
|
|
14
|
+
# is the overflow, no further question asked.
|
|
15
|
+
UNSAFE_CALLS = %w[strcpy gets scanf sprintf readline strcat].freeze
|
|
16
|
+
# The sized family: the length rides in an argument (rdx on x64).
|
|
17
|
+
# When that argument is an immediate at most as large as the room
|
|
18
|
+
# between the buffer and the return slot, the copy cannot reach it:
|
|
19
|
+
# a bounded call, not the overflow - the scan moves on. A register
|
|
20
|
+
# (the read's count, the ret2plt memcpy) is unbounded by definition.
|
|
21
|
+
BOUNDED_CALLS = %w[read fgets fread memcpy memmove strncpy].freeze
|
|
22
|
+
# The variadic format users: the format argument is the attacker's
|
|
23
|
+
# bytes only when it comes from a stack buffer.
|
|
24
|
+
PRINTF_LIKE = %w[printf fprintf].freeze
|
|
13
25
|
|
|
14
26
|
module_function
|
|
15
27
|
|
|
@@ -24,12 +36,12 @@ module AugureProfiler
|
|
|
24
36
|
|
|
25
37
|
# Returns {offset:, sink:, function:} or nil when no unsafe call site
|
|
26
38
|
# carries a readable rbp-frame buffer.
|
|
27
|
-
def offset(path)
|
|
39
|
+
def offset(path, calls: UNSAFE_CALLS + BOUNDED_CALLS)
|
|
28
40
|
elf = Metasm::ELF.decode_file(path)
|
|
29
41
|
arch = (elf.header.e_class.to_s == "64") ? :x64 : :x86
|
|
30
42
|
@path = path
|
|
31
43
|
stubs = plt_stubs(elf, arch)
|
|
32
|
-
unsafe = stubs.slice(*
|
|
44
|
+
unsafe = stubs.slice(*calls)
|
|
33
45
|
return nil if unsafe.empty?
|
|
34
46
|
|
|
35
47
|
@unsafe_addrs = unsafe.invert
|
|
@@ -39,7 +51,9 @@ module AugureProfiler
|
|
|
39
51
|
bytes = data.bytes
|
|
40
52
|
# The CHEAP scan first: a linear e8 rel32 walk nets every call to
|
|
41
53
|
# an unsafe stub - the full .text disassembly (minutes on a 1MB
|
|
42
|
-
# text) is only needed if this finds nothing.
|
|
54
|
+
# text) is only needed if this finds nothing. Sites without an
|
|
55
|
+
# rbp-frame buffer (a global, a pointer arg) no longer stop the
|
|
56
|
+
# scan: a later site may carry the overflow.
|
|
43
57
|
call_site = nil
|
|
44
58
|
sink = nil
|
|
45
59
|
bytes.each_index do |i|
|
|
@@ -48,7 +62,14 @@ module AugureProfiler
|
|
|
48
62
|
target = text.addr + i + 5 + bytes[(i + 1)..(i + 4)].pack("C*").unpack1("l<")
|
|
49
63
|
next unless (name = @unsafe_addrs[target])
|
|
50
64
|
|
|
51
|
-
|
|
65
|
+
site = text.addr + i
|
|
66
|
+
frame = frame_bytes(data, site - text.addr, arch)
|
|
67
|
+
next if frame.nil? || !frame[:offset]&.positive?
|
|
68
|
+
|
|
69
|
+
next if BOUNDED_CALLS.include?(name) &&
|
|
70
|
+
size_imm_cannot_reach?(bytes, site - text.addr, frame[:offset], arch, name)
|
|
71
|
+
|
|
72
|
+
call_site = site
|
|
52
73
|
sink = name
|
|
53
74
|
break
|
|
54
75
|
end
|
|
@@ -112,6 +133,100 @@ module AugureProfiler
|
|
|
112
133
|
nil
|
|
113
134
|
end
|
|
114
135
|
|
|
136
|
+
# The sized family's length argument rides rdx (x64): an immediate
|
|
137
|
+
# at most as large as the room between the buffer and the return
|
|
138
|
+
# slot means the copy cannot reach it. x86 passes the length on the
|
|
139
|
+
# stack, no immediate to read: treated as unbounded (the
|
|
140
|
+
# conservative direction: a false vuln beats a missed one).
|
|
141
|
+
def size_imm_cannot_reach?(bytes, call_off, room, arch, sink)
|
|
142
|
+
return false unless arch == :x64
|
|
143
|
+
|
|
144
|
+
# the length argument's register: rdx for the memcpy family, rsi
|
|
145
|
+
# for fgets (buf, size, stream)
|
|
146
|
+
opcodes = (sink == "fgets") ?
|
|
147
|
+
[[0xbe], [0x48, 0xc7, 0xc6], [0x49, 0xc7, 0xc6]] : # mov esi/rsi, imm32
|
|
148
|
+
[[0xba], [0x48, 0xc7, 0xc2], [0x49, 0xc7, 0xc2]] # mov edx/rdx, imm32
|
|
149
|
+
|
|
150
|
+
start = [call_off - 16, 0].max
|
|
151
|
+
(start..(call_off - 5)).each do |j|
|
|
152
|
+
opcodes.each do |ops|
|
|
153
|
+
if ops.size == 1
|
|
154
|
+
return true if bytes[j] == ops[0] && (imm = imm32(bytes, j + 1)) && imm <= room
|
|
155
|
+
elsif j >= ops.size - 1
|
|
156
|
+
opcode_ok = (1...(ops.size)).all? { |k| bytes[j - k] == ops[ops.size - 1 - k] }
|
|
157
|
+
return true if opcode_ok && bytes[j] == ops.last && (imm = imm32(bytes, j + 1)) && imm <= room
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
false
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def imm32(bytes, off)
|
|
165
|
+
return nil if off.negative? || off + 4 > bytes.size
|
|
166
|
+
|
|
167
|
+
bytes[off, 4].pack("C*").unpack1("L<")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# The printf-family sites whose format argument is a STACK buffer:
|
|
171
|
+
# lea reg,[rbp-disp] feeding rdi (directly, 48 8d 7d d8, or through
|
|
172
|
+
# a register move, 48 8d 45 d8 then 48 89 c7) means the format is
|
|
173
|
+
# the attacker's bytes - the fmtstr class. A .rodata format rides
|
|
174
|
+
# an immediate (mov edi, imm32): not a vuln. x86 passes args on the
|
|
175
|
+
# stack, no lea-into-rdi idiom to read: nil (an honest miss).
|
|
176
|
+
def stack_format_sink(path)
|
|
177
|
+
elf = Metasm::ELF.decode_file(path)
|
|
178
|
+
arch = (elf.header.e_class.to_s == "64") ? :x64 : :x86
|
|
179
|
+
return nil unless arch == :x64
|
|
180
|
+
|
|
181
|
+
@path = path # plt_stubs reads the section bytes through @path
|
|
182
|
+
|
|
183
|
+
stubs = plt_stubs(elf, arch)
|
|
184
|
+
like = stubs.slice(*PRINTF_LIKE)
|
|
185
|
+
return nil if like.empty?
|
|
186
|
+
|
|
187
|
+
like_addrs = like.invert
|
|
188
|
+
text = elf.sections.find { |s| s.name == ".text" }
|
|
189
|
+
data = section_data(path, text)
|
|
190
|
+
bytes = data.bytes
|
|
191
|
+
bytes.each_index do |i|
|
|
192
|
+
next unless bytes[i] == 0xe8
|
|
193
|
+
|
|
194
|
+
target = text.addr + i + 5 + bytes[(i + 1)..(i + 4)].pack("C*").unpack1("l<")
|
|
195
|
+
next unless like_addrs[target]
|
|
196
|
+
|
|
197
|
+
return like_addrs[target] if stack_format_rdi?(bytes, i)
|
|
198
|
+
end
|
|
199
|
+
nil
|
|
200
|
+
rescue Metasm::ParseError, Metasm::InvalidExeFormat
|
|
201
|
+
nil
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# a mini-modrm decode of the lea feeding the format argument: REX.W
|
|
205
|
+
# (48) + 8d + modrm(mod=01|10, rm=101=rbp) = lea reg,[rbp-disp8|32].
|
|
206
|
+
# reg=rdi (111) is the direct form; reg=rax (000) needs the
|
|
207
|
+
# immediate mov rdi, rax (48 89 c7) right after the lea. Only 0x48
|
|
208
|
+
# qualifies as REX: REX.B would turn rbp into r13.
|
|
209
|
+
def stack_format_rdi?(bytes, call_off)
|
|
210
|
+
start = [call_off - 24, 0].max
|
|
211
|
+
(start...(call_off - 3)).each do |j|
|
|
212
|
+
next unless bytes[j] == 0x48
|
|
213
|
+
next unless bytes[j + 1] == 0x8d
|
|
214
|
+
|
|
215
|
+
modrm = bytes[j + 2]
|
|
216
|
+
mod = modrm >> 6
|
|
217
|
+
next unless mod == 0b01 || mod == 0b10
|
|
218
|
+
next unless (modrm & 7) == 0b101
|
|
219
|
+
|
|
220
|
+
reg = (modrm >> 3) & 7
|
|
221
|
+
len = (mod == 0b01) ? 4 : 7
|
|
222
|
+
return true if reg == 0b111
|
|
223
|
+
next unless reg == 0b000
|
|
224
|
+
|
|
225
|
+
return true if bytes[j + len, 3] == [0x48, 0x89, 0xc7]
|
|
226
|
+
end
|
|
227
|
+
false
|
|
228
|
+
end
|
|
229
|
+
|
|
115
230
|
# The buffer displacement in the argument setup, then the frame math:
|
|
116
231
|
# on x64: [rbp-N] -> saved rbp at N..N+8, return at N+8..N+16.
|
|
117
232
|
def frame_disp(instrs, call_idx, arch)
|