one_gadget 2.0.0 → 2.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 +4 -4
- data/CHANGELOG.md +197 -65
- data/README.md +96 -22
- data/lib/one_gadget/abi.rb +41 -5
- data/lib/one_gadget/builds/libc-2.31-93b46e0027747153e336c3fd9431ce9a5d82ad00.rb +563 -0
- data/lib/one_gadget/builds/libc-2.35-891c1403437a4e30e684e0c8e34b87a09e4298e5.rb +434 -0
- data/lib/one_gadget/builds/libc-2.39-a1d1cf4badf1f5dfe57ff1d17bd692ccc6fcd5c5.rb +531 -0
- data/lib/one_gadget/builds/libc-2.39-cd8f5a207dd67aea370d2b471a54c3e56f44ab18.rb +531 -0
- data/lib/one_gadget/builds/libc-2.43-b50ceafbd17dc6bceee344a66671c7eaa152bef4.rb +15 -0
- data/lib/one_gadget/emulators/aarch64.rb +5 -2
- data/lib/one_gadget/emulators/amd64.rb +1 -0
- data/lib/one_gadget/emulators/arm.rb +27 -18
- data/lib/one_gadget/emulators/arm_family.rb +35 -160
- data/lib/one_gadget/emulators/conditional.rb +28 -22
- data/lib/one_gadget/emulators/constraints.rb +269 -0
- data/lib/one_gadget/emulators/data_processing.rb +167 -0
- data/lib/one_gadget/emulators/i386.rb +4 -6
- data/lib/one_gadget/emulators/instruction.rb +24 -1
- data/lib/one_gadget/emulators/lambda.rb +3 -1
- data/lib/one_gadget/emulators/mips.rb +289 -0
- data/lib/one_gadget/emulators/processor.rb +33 -455
- data/lib/one_gadget/emulators/register_file.rb +19 -10
- data/lib/one_gadget/emulators/riscv64.rb +265 -0
- data/lib/one_gadget/emulators/safe_calls.rb +9 -3
- data/lib/one_gadget/emulators/tracked_memory.rb +209 -0
- data/lib/one_gadget/emulators/x86.rb +32 -22
- data/lib/one_gadget/fetchers/aarch64.rb +0 -27
- data/lib/one_gadget/fetchers/amd64.rb +0 -24
- data/lib/one_gadget/fetchers/argument_resolution.rb +340 -0
- data/lib/one_gadget/fetchers/arm.rb +99 -44
- data/lib/one_gadget/fetchers/base.rb +142 -640
- data/lib/one_gadget/fetchers/candidate_walk.rb +150 -0
- data/lib/one_gadget/fetchers/disassembly.rb +252 -0
- data/lib/one_gadget/fetchers/dynamic_symbols.rb +104 -0
- data/lib/one_gadget/fetchers/i386.rb +5 -8
- data/lib/one_gadget/fetchers/mips.rb +478 -0
- data/lib/one_gadget/fetchers/objdump.rb +24 -2
- data/lib/one_gadget/fetchers/riscv64.rb +78 -0
- data/lib/one_gadget/fetchers/x86.rb +19 -0
- data/lib/one_gadget/fetchers.rb +22 -6
- data/lib/one_gadget/gadget.rb +25 -34
- data/lib/one_gadget/helper.rb +19 -5
- data/lib/one_gadget/one_gadget.rb +6 -0
- data/lib/one_gadget/version.rb +1 -1
- data/lib/one_gadget.rb +1 -1
- metadata +18 -6
- data/lib/one_gadget/builds/libc-2.26-2104f3d4ad5cf68603afbe7ba1a17f5ac99c5988.rb +0 -227
- data/lib/one_gadget/builds/libc-2.26-ddcc13122ddbfe5e5ef77d4ebe66d124ae5762c2.rb +0 -300
- data/lib/one_gadget/builds/libc-2.26-f65648a832414f2144ce795d75b6045a1ec2e252.rb +0 -199
|
@@ -4,6 +4,10 @@ require 'elftools'
|
|
|
4
4
|
|
|
5
5
|
require 'one_gadget/emulators/processor'
|
|
6
6
|
require 'one_gadget/error'
|
|
7
|
+
require 'one_gadget/fetchers/argument_resolution'
|
|
8
|
+
require 'one_gadget/fetchers/candidate_walk'
|
|
9
|
+
require 'one_gadget/fetchers/disassembly'
|
|
10
|
+
require 'one_gadget/fetchers/dynamic_symbols'
|
|
7
11
|
require 'one_gadget/fetchers/objdump'
|
|
8
12
|
|
|
9
13
|
module OneGadget
|
|
@@ -12,54 +16,31 @@ module OneGadget
|
|
|
12
16
|
# instruction sequences - a backward control-flow walk from each
|
|
13
17
|
# +exec+/+posix_spawn+ call - and turns a solved candidate into a
|
|
14
18
|
# {OneGadget::Gadget::Gadget}. A subclass supplies only the arch-specific
|
|
15
|
-
# pieces (call mnemonic, string/global recognition, branch classification)
|
|
19
|
+
# pieces (call mnemonic, string/global recognition, branch classification),
|
|
20
|
+
# every one of which is declared here whatever module implements the engine
|
|
21
|
+
# that calls it: {CandidateWalk} for the control-flow walk, {Disassembly} for
|
|
22
|
+
# reading the file, and {ArgumentResolution} for describing a reached call.
|
|
16
23
|
#
|
|
17
24
|
# To add an architecture, see +docs/adding-an-architecture.md+; +AArch64+ is
|
|
18
25
|
# the simplest example.
|
|
19
26
|
class Base
|
|
27
|
+
include ArgumentResolution
|
|
28
|
+
include CandidateWalk
|
|
29
|
+
include Disassembly
|
|
30
|
+
include DynamicSymbols
|
|
31
|
+
|
|
20
32
|
# The absolute path to glibc.
|
|
21
33
|
# @return [String] The filename.
|
|
22
34
|
attr_reader :file
|
|
23
35
|
|
|
24
|
-
# What a terminal function's name starts with, and enough bytes of a name to
|
|
25
|
-
# tell: a symbol table's string table is read as one blob, so a name is a
|
|
26
|
-
# slice of it.
|
|
27
|
-
TERMINAL_PREFIXES = %w[exec posix_spawn].freeze
|
|
28
|
-
TERMINAL_PREFIX_BYTES = 12
|
|
29
|
-
private_constant :TERMINAL_PREFIXES, :TERMINAL_PREFIX_BYTES
|
|
30
|
-
|
|
31
|
-
# Give up on a control-flow path once it has crossed this many conditional branches.
|
|
32
|
-
MAX_FORKS = 4
|
|
33
|
-
# Hard cap on a single path's length (loop/runaway guard).
|
|
34
|
-
PATH_BUDGET = 80
|
|
35
|
-
|
|
36
|
-
# How much to disassemble around each terminal call when an architecture can
|
|
37
|
-
# locate the calls cheaply (see {#terminal_call_sites} / {#windowed_disasm}).
|
|
38
|
-
#
|
|
39
|
-
# The walk runs backwards from the call, but what it reaches does not: a
|
|
40
|
-
# branch predecessor can sit *after* the call, so the window has to hold the
|
|
41
|
-
# loop or later block that jumps back into the region.
|
|
42
|
-
#
|
|
43
|
-
# Both directions were measured by windowing every architecture against its
|
|
44
|
-
# own full disassembly across the spec corpus. Gadgets start being lost below
|
|
45
|
-
# 0x400 either way, and a window too small to hold a predecessor invents them
|
|
46
|
-
# as well: the line before the first of a window is the last of another, and
|
|
47
|
-
# nothing about it follows. These leave 16x that, and still disassemble about
|
|
48
|
-
# a fifth of a libc -- terminal calls cluster into a handful of merged
|
|
49
|
-
# windows, so a wider one costs little.
|
|
50
|
-
#
|
|
51
|
-
# A gadget whose code and branch-predecessors exceed the window is missed,
|
|
52
|
-
# which is why windowing is opt-in per arch (fast disassembly arches keep the
|
|
53
|
-
# full, exhaustive scan) and falls back to full disassembly if the call scan
|
|
54
|
-
# comes up empty.
|
|
55
|
-
WINDOW_BACK = 0x4000
|
|
56
|
-
# As far past the call, for a predecessor that branches back into the region.
|
|
57
|
-
WINDOW_FWD = 0x4000
|
|
58
|
-
|
|
59
36
|
# Cache values that are a deterministic function of an objdump command
|
|
60
37
|
# (its output and everything derived from it), so re-analysing the same
|
|
61
38
|
# file - common in the specs, harmless for the CLI which reads a file once -
|
|
62
39
|
# doesn't redo the disassembly or the whole-binary scans.
|
|
40
|
+
# @param [Symbol] kind What is being cached, so two kinds may share a command.
|
|
41
|
+
# @param [String] command The objdump command the value is a function of.
|
|
42
|
+
# @yieldreturn [Object] The value, computed only on a miss.
|
|
43
|
+
# @return [Object] The cached value.
|
|
63
44
|
def self.cached(kind, command)
|
|
64
45
|
@cached ||= Hash.new { |h, k| h[k] = {} }
|
|
65
46
|
@cached[kind][command] ||= yield
|
|
@@ -78,38 +59,25 @@ module OneGadget
|
|
|
78
59
|
# @return [Array<OneGadget::Gadget::Gadget>] Gadgets found.
|
|
79
60
|
def find
|
|
80
61
|
str_offset('/bin/sh') # ensure it's glibc-like; raises "not glibc?" if not found
|
|
81
|
-
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
next if seen.key?(key = suffix.join)
|
|
88
|
-
|
|
89
|
-
seen[key] = true
|
|
90
|
-
next if refused_before_call?(suffix)
|
|
91
|
-
|
|
92
|
-
gadget = resolve_suffix(suffix)
|
|
93
|
-
gadgets << gadget unless gadget.nil?
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
gadgets
|
|
62
|
+
# Reading the disassembly first settles the command the answer is a
|
|
63
|
+
# function of: how a file with no section headers is read is decided
|
|
64
|
+
# there. Each gadget is handed out as a copy, since a caller may write to
|
|
65
|
+
# one (see {OneGadget::Gadget::Gadget#base}).
|
|
66
|
+
disassembly
|
|
67
|
+
Base.cached(:gadgets, @objdump.command) { search }.map(&:dup)
|
|
97
68
|
end
|
|
98
69
|
|
|
99
|
-
# Every suffix of +lines
|
|
100
|
-
#
|
|
101
|
-
# call, so anything past one never executes. A suffix reaches beyond a
|
|
102
|
-
# terminal call when the function holds several and the candidate was walked
|
|
103
|
-
# back from a later one. Bounding each window here lets everything
|
|
104
|
-
# downstream -- {#emulate} and its overrides, the dedup key in {#find} --
|
|
105
|
-
# read a window as executed code.
|
|
106
|
-
#
|
|
107
|
-
# Each line is classified once per candidate rather than once per suffix
|
|
108
|
-
# containing it: walking the start backwards, the first terminal call at or
|
|
109
|
-
# after it only moves when the start is itself one.
|
|
70
|
+
# Every suffix of +lines+, longest last, cut at the terminal call it reaches
|
|
71
|
+
# -- emulation ends there, so anything past one never executes.
|
|
110
72
|
# @param [Array<String>] lines One candidate, as a line list.
|
|
111
73
|
# @yieldparam [Array<String>] window
|
|
112
74
|
# @return [void]
|
|
75
|
+
# @example A candidate holding two terminal calls; no window runs past the
|
|
76
|
+
# one it reaches.
|
|
77
|
+
# lines = ['1000: mov r0, r1', '1004: bl <execve>',
|
|
78
|
+
# '1008: mov r1, r2', '100c: bl <execl>']
|
|
79
|
+
# [].tap { |a| executed_windows(lines) { |w| a << w.map { |l| l[/\A\w+/] } } }
|
|
80
|
+
# #=> [['1008', '100c'], ['1004'], ['1000', '1004']]
|
|
113
81
|
def executed_windows(lines)
|
|
114
82
|
stop = lines.size - 1 if terminal_call_line?(lines.last)
|
|
115
83
|
(lines.size - 2).downto(0) do |i|
|
|
@@ -122,10 +90,10 @@ module OneGadget
|
|
|
122
90
|
# stops on ({OneGadget::Emulators::Processor#terminal_call?}). Not
|
|
123
91
|
# {#terminal_call_regexp}, which is looser so it can locate call sites: it
|
|
124
92
|
# also matches the +posix_spawn+ setup helpers, which emulation runs through.
|
|
125
|
-
#
|
|
126
|
-
#
|
|
93
|
+
# @param [String] line One disassembled line.
|
|
94
|
+
# @return [Boolean]
|
|
127
95
|
def terminal_call_line?(line)
|
|
128
|
-
return false unless
|
|
96
|
+
return false unless call_line?(line)
|
|
129
97
|
|
|
130
98
|
name = line[/<([^@>]+)/, 1]
|
|
131
99
|
!name.nil? && OneGadget::Emulators::Processor::TERMINAL_CALL_RE.match?(name)
|
|
@@ -146,6 +114,8 @@ module OneGadget
|
|
|
146
114
|
end
|
|
147
115
|
|
|
148
116
|
# Emulate a candidate suffix and turn it into a gadget, or +nil+ if it isn't one.
|
|
117
|
+
# @param [Array<String>] lines The suffix, ending at the terminal call.
|
|
118
|
+
# @return [OneGadget::Gadget::Gadget, nil]
|
|
149
119
|
def resolve_suffix(lines)
|
|
150
120
|
processor = emulate(lines)
|
|
151
121
|
(@refused ||= {})[processor.refused_line] = true if processor.refused_line
|
|
@@ -166,323 +136,86 @@ module OneGadget
|
|
|
166
136
|
OneGadget::Gadget::Gadget.new(offset_of(lines.first), **options)
|
|
167
137
|
end
|
|
168
138
|
|
|
169
|
-
# Fetch candidates that end with call exec*.
|
|
170
|
-
#
|
|
171
|
-
# Provide a block to filter gadget candidates.
|
|
172
|
-
# @yieldparam [String] cand
|
|
173
|
-
# Is this candidate valid?
|
|
174
|
-
# @yieldreturn [Boolean]
|
|
175
|
-
# True for valid.
|
|
176
|
-
# @return [Array<String>]
|
|
177
|
-
# Each +String+ returned is multi-lines of assembly code.
|
|
178
|
-
def candidates(&)
|
|
179
|
-
branch_aware_candidates(&)
|
|
180
|
-
end
|
|
181
|
-
|
|
182
139
|
private
|
|
183
140
|
|
|
184
|
-
#
|
|
185
|
-
#
|
|
186
|
-
# @return [
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return resolve_execve(processor) if call.include?('execve')
|
|
196
|
-
|
|
197
|
-
resolve_execl(processor) if call.include?('execl')
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
def resolve_execve(processor)
|
|
201
|
-
arg0, arg1, arg2 = (0..2).map { |i| processor.argument(i) }
|
|
202
|
-
res = resolve_execve_args(processor, arg0, arg1, arg2)
|
|
203
|
-
return nil if res.nil?
|
|
204
|
-
|
|
205
|
-
{ constraints: res[:constraints], effect: %(execve("/bin/sh", #{arg1}, #{res[:envp]})) }
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
def resolve_execve_args(processor, arg0, arg1, arg2, allow_null_argv: true)
|
|
209
|
-
return unless str_bin_sh?(arg0.to_s)
|
|
210
|
-
|
|
211
|
-
# arg1 == NULL || [arg1] == NULL
|
|
212
|
-
# arg2 == NULL || [arg2] == NULL || arg[2] == envp
|
|
213
|
-
cons = processor.constraints
|
|
214
|
-
con = check_argv(processor, arg1, allow_null_argv)
|
|
215
|
-
cons << con unless con.nil?
|
|
216
|
-
return nil unless cons.all?
|
|
217
|
-
|
|
218
|
-
envp = 'environ'
|
|
219
|
-
return nil unless check_envp(processor, arg2) do |c|
|
|
220
|
-
cons << c
|
|
221
|
-
envp = arg2
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
{ constraints: cons, envp: }
|
|
225
|
-
end
|
|
141
|
+
# Every gadget in the disassembly: each candidate walked back from a
|
|
142
|
+
# terminal call, cut into the windows that reach it, emulated one apiece.
|
|
143
|
+
# @return [Array<OneGadget::Gadget::Gadget>]
|
|
144
|
+
def search
|
|
145
|
+
gadgets = []
|
|
146
|
+
# Overlapping candidate paths share tails, so the same suffix (a start line
|
|
147
|
+
# and everything after it) recurs across candidates; emulate each once.
|
|
148
|
+
seen = {}
|
|
149
|
+
candidates.each do |cand|
|
|
150
|
+
executed_windows(cand.lines) do |suffix|
|
|
151
|
+
next if seen.key?(key = suffix.join)
|
|
226
152
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
# Terminology shared by all the +argv+ helpers below:
|
|
230
|
-
# * +argv_ptr+ - the *pointer to* the argv array, i.e. the emulated content of the register passed
|
|
231
|
-
# as +argv+ ({OneGadget::Emulators::Processor#argument}), kept as the emulator produced it.
|
|
232
|
-
# Examples: +rsi+, +rsp+0x10+, +[rbp-0x8]+, a global-variable reference, or a bare integer.
|
|
233
|
-
# * +argv+ - the array *pointed to* by +argv_ptr+: its dereferenced entries
|
|
234
|
-
# +[argv[0], argv[1], argv[2], argv[3]]+, each already converted to a string.
|
|
235
|
-
#
|
|
236
|
-
# @param [OneGadget::Emulators::Processor] processor The processor state at the call site.
|
|
237
|
-
# @param [OneGadget::Emulators::Lambda, Integer] argv_ptr The pointer to the argv array. See above.
|
|
238
|
-
# @param [Boolean] allow_null
|
|
239
|
-
# Whether +argv_ptr+ itself may be +NULL+ (true for +execve+, false for +posix_spawn+).
|
|
240
|
-
# @return [String, nil, false] How {#resolve_execve_args} should treat this argv:
|
|
241
|
-
# a +String+ is a constraint it must add; +nil+ means the argv is already
|
|
242
|
-
# valid so no constraint is needed; +false+ means the argv can never launch
|
|
243
|
-
# a shell (e.g. a fixed noexec option), which it consumes as an
|
|
244
|
-
# unsatisfiable constraint and drops the gadget.
|
|
245
|
-
def check_argv(processor, argv_ptr, allow_null)
|
|
246
|
-
argv_ptr = resolve_stack_deref(processor, argv_ptr)
|
|
247
|
-
return check_stack_argv(processor, argv_ptr, allow_null) if resolvable_stack(processor, argv_ptr)
|
|
248
|
-
|
|
249
|
-
check_nonstack_argv(argv_ptr, allow_null)
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
# Whether resolving +lmda+'s target via tracked memory is worth attempting,
|
|
253
|
-
# as opposed to the plain opaque "==NULL || is a valid .." form
|
|
254
|
-
# ({#check_nonstack_argv}/the envp equivalent). Always true for the arch's
|
|
255
|
-
# dedicated stack/frame pointer. For anything else, only when element
|
|
256
|
-
# 0 -- what {#argv_already_valid?}/{#generate_argv_with_sh} branch on --
|
|
257
|
-
# was actually written within this candidate.
|
|
258
|
-
# @example element 0 tracked -- resolvable
|
|
259
|
-
# reg tracked (element 0), reg+0x8 tracked (element 1) => resolvable
|
|
260
|
-
# @example a later, unrelated write must not trigger array resolution
|
|
261
|
-
# reg+0x10 (element 2) tracked, reg/reg+0x8 (elements 0/1) untracked
|
|
262
|
-
# => not resolvable; falls back to the opaque form instead of a garbled array
|
|
263
|
-
# @param [OneGadget::Emulators::Processor] processor
|
|
264
|
-
# @param [OneGadget::Emulators::Lambda, Integer] lmda A pointer operand. A
|
|
265
|
-
# concrete address is never resolvable: nothing was tracked against it.
|
|
266
|
-
# @return [Hash{Integer => OneGadget::Emulators::Lambda}, nil]
|
|
267
|
-
def resolvable_stack(processor, lmda)
|
|
268
|
-
return nil unless lmda.is_a?(OneGadget::Emulators::Lambda)
|
|
269
|
-
|
|
270
|
-
stack, offset = processor.resolve_address(lmda)
|
|
271
|
-
return nil unless stack
|
|
272
|
-
return stack if lmda.deref_count.zero? && OneGadget::ABI.stack_register?(lmda.obj)
|
|
273
|
-
|
|
274
|
-
stack if stack.key?(offset)
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
# Handle the case where +argv_ptr+ points into memory this candidate wrote,
|
|
278
|
-
# so the +argv+ entries can be read off it.
|
|
279
|
-
# @param [OneGadget::Emulators::Lambda] argv_ptr The pointer to the argv array. See {#check_argv}.
|
|
280
|
-
# @return [String, nil, false] The same three-way contract as {#check_argv},
|
|
281
|
-
# which returns this value unchanged: a constraint, +nil+ (already valid),
|
|
282
|
-
# or +false+ (drop the gadget).
|
|
283
|
-
def check_stack_argv(processor, argv_ptr, allow_null)
|
|
284
|
-
stack, offset = processor.resolve_address(argv_ptr)
|
|
285
|
-
# A stack register we don't track a stack for (the frame pointer):
|
|
286
|
-
# fall back to treating it as an opaque pointer.
|
|
287
|
-
return check_nonstack_argv(argv_ptr, allow_null) if stack.nil?
|
|
288
|
-
|
|
289
|
-
argv = (0..3).map { |i| stack[offset + processor.class.bits / 8 * i].to_s }
|
|
290
|
-
|
|
291
|
-
# A shell spawned with a fixed "noexec" option never runs a command, so
|
|
292
|
-
# drop the gadget (see this method's @return for the +false+ contract).
|
|
293
|
-
return false if noexec_shell_argv?(argv)
|
|
294
|
-
|
|
295
|
-
# if argv is already valid, no constraints are needed! (but probably won't happen :p)
|
|
296
|
-
return if argv_already_valid?(argv)
|
|
297
|
-
|
|
298
|
-
return generate_argv_with_sh(argv) if global_var?(argv[0])
|
|
299
|
-
|
|
300
|
-
generate_argv_without_sh(argv_ptr, argv, allow_null)
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
def argv_already_valid?(argv)
|
|
304
|
-
argv[0] == '0' || (global_var?(argv[0]) && argv[1] == '0')
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
def generate_argv_with_sh(argv)
|
|
308
|
-
# argv[0] is not controlled by the user, argv[0] probably is "/bin/sh" or "sh" (but actually, the content of
|
|
309
|
-
# argv[0] doesn't quite matter, just need to make sure it's readable)
|
|
310
|
-
# So far (I checked glibc 2.37), we can make argv to be {"/bin/sh", sth, NULL} or {"sh", "-c", sth, NULL}
|
|
311
|
-
# TODO: We need to update this when the above assumption is no longer true
|
|
312
|
-
if argv[2] == '0' && !global_var?(argv[1])
|
|
313
|
-
"#{argv[1]} == NULL || {\"/bin/sh\", #{argv[1]}, NULL} is a valid argv"
|
|
314
|
-
else
|
|
315
|
-
argv_gte3 = argv[3] == '0' ? 'NULL' : "#{argv[3]}, ..."
|
|
316
|
-
if global_var?(argv[1])
|
|
317
|
-
# A leading "sh -c" whose fixed elements (e.g. "-c", the "--" separator)
|
|
318
|
-
# are libc globals -- resolve them so the controllable command operand
|
|
319
|
-
# stands out (e.g. {"sh", "-c", "--", x21, ...}).
|
|
320
|
-
"{\"sh\", #{resolve_argv_element(argv[1])}, #{resolve_argv_element(argv[2])}, #{argv_gte3}} is a valid argv"
|
|
321
|
-
else
|
|
322
|
-
"#{argv[1]} == NULL || {\"sh\", #{argv[1]}, #{argv[2]}, #{argv_gte3}} is a valid argv"
|
|
323
|
-
end
|
|
324
|
-
end
|
|
325
|
-
end
|
|
153
|
+
seen[key] = true
|
|
154
|
+
next if refused_before_call?(suffix)
|
|
326
155
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
def generate_argv_without_sh(argv_ptr, argv, allow_null)
|
|
330
|
-
argv_cons = "{#{argv[0]}"
|
|
331
|
-
(1..argv.length - 1).each do |i|
|
|
332
|
-
if argv[i] == '0'
|
|
333
|
-
argv_cons += ', NULL'
|
|
334
|
-
break
|
|
335
|
-
elsif global_var?(argv[i])
|
|
336
|
-
# A fixed libc-global entry (e.g. "-c", "--") -- show its true content.
|
|
337
|
-
argv_cons += ", #{resolve_argv_element(argv[i])}"
|
|
338
|
-
else
|
|
339
|
-
argv_cons += ", #{argv[i]}"
|
|
156
|
+
gadget = resolve_suffix(suffix)
|
|
157
|
+
gadgets << gadget unless gadget.nil?
|
|
340
158
|
end
|
|
341
159
|
end
|
|
342
|
-
|
|
343
|
-
argv_cons += '} is a valid argv'
|
|
344
|
-
|
|
345
|
-
if allow_null && argv.all? { |a| OneGadget::ABI.stack_register?(a) }
|
|
346
|
-
# If libc writes something into the stack, argv_ptr cannot be NULL.
|
|
347
|
-
# TODO: Find a better way to check can argv_ptr be NULL
|
|
348
|
-
"#{argv_ptr} == NULL || #{argv[0]} == NULL || #{argv_cons}"
|
|
349
|
-
else
|
|
350
|
-
"#{argv[0]} == NULL || #{argv_cons}"
|
|
351
|
-
end
|
|
160
|
+
gadgets
|
|
352
161
|
end
|
|
353
162
|
|
|
354
|
-
# Whether +
|
|
355
|
-
#
|
|
356
|
-
#
|
|
357
|
-
#
|
|
358
|
-
# a false positive to drop.
|
|
359
|
-
# @param [Array<String>] argv The resolved argv entries. See {#check_stack_argv}.
|
|
163
|
+
# Whether +line+ is a call, whatever it calls. The mnemonic must be the call
|
|
164
|
+
# itself, so a branch to a symbol whose name merely looks like one is not
|
|
165
|
+
# counted.
|
|
166
|
+
# @param [String] line One disassembled line.
|
|
360
167
|
# @return [Boolean]
|
|
361
|
-
# @example
|
|
362
|
-
#
|
|
363
|
-
#
|
|
364
|
-
def
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
#
|
|
370
|
-
# @
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
end
|
|
378
|
-
|
|
379
|
-
#
|
|
380
|
-
#
|
|
381
|
-
#
|
|
382
|
-
# @
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
# resolves to when it simplifies.
|
|
386
|
-
# @example a tracked slot resolves to its source register
|
|
387
|
-
# # mov [ebp-0x30], ecx (earlier in the same candidate)
|
|
388
|
-
# resolve_stack_deref(processor, Lambda.parse('[ebp-0x30]')) #=> the ecx lambda
|
|
389
|
-
# @example an untracked slot is a no-op
|
|
390
|
-
# resolve_stack_deref(processor, Lambda.parse('[ebp-0x40]')) #=> that same lambda
|
|
391
|
-
def resolve_stack_deref(processor, ptr)
|
|
392
|
-
return ptr unless ptr.is_a?(OneGadget::Emulators::Lambda) && ptr.deref_count == 1 &&
|
|
393
|
-
OneGadget::ABI.stack_register?(ptr.obj)
|
|
394
|
-
|
|
395
|
-
stack, offset = processor.resolve_address(ptr.dup.ref!)
|
|
396
|
-
tracked = stack && stack[offset]
|
|
397
|
-
return ptr unless tracked.is_a?(OneGadget::Emulators::Lambda) && tracked.deref_count.zero?
|
|
398
|
-
|
|
399
|
-
tracked
|
|
400
|
-
end
|
|
401
|
-
|
|
402
|
-
# Generate the +envp+-related constraint for an +exec*+ call.
|
|
403
|
-
#
|
|
404
|
-
# Mirrors the +argv+ terminology from {#check_argv}: +envp_ptr+ is the *pointer to* the envp array
|
|
405
|
-
# ({OneGadget::Emulators::Processor#argument}), while +envp+ is the array of dereferenced entries
|
|
406
|
-
# it points to.
|
|
407
|
-
#
|
|
408
|
-
# @param [OneGadget::Emulators::Processor] processor The processor state at the call site.
|
|
409
|
-
# @param [OneGadget::Emulators::Lambda, Integer] envp_ptr The pointer to the envp array.
|
|
410
|
-
# @yieldparam [String] cons The +envp+ constraint, yielded only when one is required.
|
|
411
|
-
# @return [Object, nil] Truthy when +envp+ is acceptable, +nil+ to reject the gadget.
|
|
412
|
-
def check_envp(processor, envp_ptr)
|
|
413
|
-
# A doubly-dereferenced pointer that names a global variable is believed to
|
|
414
|
-
# be environ; one that doesn't drops the gadget.
|
|
415
|
-
return global_var?(envp_ptr.to_s) if envp_ptr.is_a?(OneGadget::Emulators::Lambda) &&
|
|
416
|
-
envp_ptr.deref_count >= 2
|
|
417
|
-
|
|
418
|
-
envp_ptr = resolve_stack_deref(processor, envp_ptr)
|
|
419
|
-
# A concrete integer, or a register with nothing useful tracked for it,
|
|
420
|
-
# falls through to the opaque-pointer case (see {#resolvable_stack}).
|
|
421
|
-
stack = envp_ptr.is_a?(OneGadget::Emulators::Lambda) && envp_ptr.deref_count.zero? &&
|
|
422
|
-
resolvable_stack(processor, envp_ptr)
|
|
423
|
-
if stack
|
|
424
|
-
# I haven't see this case after some tests, but just in case :)
|
|
425
|
-
envp = (0..3).map { |i| stack[envp_ptr.immi + processor.class.bits / 8 * i].to_s }
|
|
426
|
-
# TODO: Handle the case when libc will write something into envp
|
|
427
|
-
cons = global_var?(envp[0]) ? nil : "#{envp_ptr} == NULL || {#{envp.join(', ')}, ...} is a valid envp"
|
|
428
|
-
else
|
|
429
|
-
cons = "[#{envp_ptr}] == NULL || #{envp_ptr} == NULL || #{envp_ptr} is a valid envp"
|
|
430
|
-
end
|
|
431
|
-
return nil if cons.nil?
|
|
432
|
-
|
|
433
|
-
yield cons
|
|
168
|
+
# @example A call, and a branch into the middle of the same function.
|
|
169
|
+
# call_line?('e6570: call 94180 <execve>') #=> true
|
|
170
|
+
# call_line?('a34d0: b a3210 <execlp+0x1a8>') #=> false
|
|
171
|
+
def call_line?(line)
|
|
172
|
+
line[/\A\s*[0-9a-f]+:\s*(\S+)/, 1]&.match?(call_mnemonic) || false
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# The mnemonics {#call_str} names, as the whole of one. Built once: it comes
|
|
176
|
+
# out of a constant, and a search asks it of every line of a libc.
|
|
177
|
+
# @return [Regexp]
|
|
178
|
+
# @example On arm, where a call carries an +x+ and a width suffix.
|
|
179
|
+
# call_mnemonic.match?('blx') #=> true
|
|
180
|
+
# call_mnemonic.match?('bl.w') #=> true
|
|
181
|
+
# call_mnemonic.match?('b') #=> false
|
|
182
|
+
def call_mnemonic
|
|
183
|
+
@call_mnemonic ||= /\A#{call_str}x?(?:\.[wn])?\z/
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Whether +line+ transfers control, so the address it names is a place in
|
|
187
|
+
# the file rather than a value it works with.
|
|
188
|
+
# @param [String] line One disassembled line.
|
|
189
|
+
# @return [Boolean]
|
|
190
|
+
def control_transfer?(line)
|
|
191
|
+
!branch_kind(line).nil? || call_line?(line)
|
|
434
192
|
end
|
|
435
193
|
|
|
436
|
-
#
|
|
437
|
-
|
|
438
|
-
|
|
194
|
+
# What a libc calls the variable holding the environment a process started
|
|
195
|
+
# with, allowing for the aliases exported beside it (+_environ+, +__environ+).
|
|
196
|
+
ENVIRON = /\A_*environ\z/
|
|
197
|
+
private_constant :ENVIRON
|
|
439
198
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
args << arg
|
|
449
|
-
cons = processor.constraints + ["#{arg} == NULL"]
|
|
450
|
-
{ constraints: cons, effect: %(execl("/bin/sh", #{args.join(', ')})) }
|
|
451
|
-
end
|
|
452
|
-
|
|
453
|
-
# posix_spawn (*pid, *path, *file_actions, *attrp, argv[], envp[])
|
|
454
|
-
# Constraints are
|
|
455
|
-
# * pid == NULL || *pid is writable
|
|
456
|
-
# * file_actions == NULL || (int) (file_actions->__used) <= 0
|
|
457
|
-
# * attrp == NULL || attrp->flags == 0
|
|
458
|
-
# Meet all constraints then posix_spawn eventually calls execve(path, argv, envp)
|
|
459
|
-
def resolve_posix_spawn(processor)
|
|
460
|
-
args = Array.new(6) { |i| processor.argument(i) }
|
|
461
|
-
# pid/file_actions/attrp are reasoned about as pointers (Lambdas); a
|
|
462
|
-
# concrete non-zero integer there is a fixed address we can't constrain.
|
|
463
|
-
return nil if [args[0], args[2], args[3]].any? { |a| a.is_a?(Integer) && !a.zero? }
|
|
464
|
-
|
|
465
|
-
res = resolve_execve_args(processor, args[1], args[4], args[5], allow_null_argv: false)
|
|
466
|
-
return nil if res.nil?
|
|
467
|
-
|
|
468
|
-
cons = res[:constraints]
|
|
469
|
-
arg0 = args[0]
|
|
470
|
-
if arg0.to_s != '0'
|
|
471
|
-
if arg0.deref_count.zero? && arg0.to_s.include?(processor.sp)
|
|
472
|
-
# Assume stack is always writable, no additional constraints.
|
|
473
|
-
else
|
|
474
|
-
cons << "#{arg0} == NULL || writable: #{arg0}"
|
|
475
|
-
end
|
|
476
|
-
end
|
|
477
|
-
arg2 = args[2]
|
|
478
|
-
cons << "#{arg2} == NULL || (s32)#{(arg2 + 4).deref} <= 0x0" if arg2.to_s != '0'
|
|
479
|
-
arg3 = args[3]
|
|
480
|
-
cons << "#{arg3} == NULL || (u16)#{arg3.deref} == 0x0" if arg3.to_s != '0'
|
|
481
|
-
|
|
482
|
-
{ constraints: cons, effect: %(posix_spawn(#{arg0}, "/bin/sh", #{arg2}, #{arg3}, #{args[4]}, #{res[:envp]})) }
|
|
483
|
-
end
|
|
199
|
+
# Whether +str+ names the address of that variable. An architecture that can
|
|
200
|
+
# say which symbol an address belongs to answers this; the rest cannot, and
|
|
201
|
+
# recognise the environment by the shape of the pointer instead (see
|
|
202
|
+
# {ArgumentResolution#check_envp}).
|
|
203
|
+
# @param [String] _str A rendered value.
|
|
204
|
+
# @return [Boolean]
|
|
205
|
+
def environ?(_str) = false
|
|
484
206
|
|
|
485
|
-
|
|
207
|
+
# Whether +str+ references a libc global, i.e. an address the caller does not
|
|
208
|
+
# choose. An arch reaching its globals through a register (i386's GOT)
|
|
209
|
+
# overrides this against that register instead.
|
|
210
|
+
# @param [String] str A rendered value.
|
|
211
|
+
# @return [Boolean]
|
|
212
|
+
# @example The +$base+-relative form an arch produces once it concretizes a
|
|
213
|
+
# pc-relative operand.
|
|
214
|
+
# global_var?('$base+0x3ed8e0') #=> true
|
|
215
|
+
# global_var?('[$base+0x10]') #=> true
|
|
216
|
+
# global_var?('rax') #=> false
|
|
217
|
+
def global_var?(str)
|
|
218
|
+
base_relative?(str, '$base')
|
|
486
219
|
end
|
|
487
220
|
|
|
488
221
|
# Whether +str+ names a value at a fixed offset from +base+, i.e. an address
|
|
@@ -518,10 +251,38 @@ module OneGadget
|
|
|
518
251
|
"#{holder} is the GOT address of libc"
|
|
519
252
|
end
|
|
520
253
|
|
|
521
|
-
|
|
254
|
+
# Whether +str+ references the +"/bin/sh"+ string. The default recognises the
|
|
255
|
+
# +$base+-relative form (see {#global_var?}); an arch that renders the address
|
|
256
|
+
# differently overrides it.
|
|
257
|
+
# @param [String] str A rendered value.
|
|
258
|
+
# @return [Boolean]
|
|
259
|
+
def str_bin_sh?(str)
|
|
260
|
+
str.include?('$base') && str.include?(bin_sh_offset.to_s(16))
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Whether +str+ references the standalone +"sh"+ string glibc passes as
|
|
264
|
+
# argv[0] in +execl("/bin/sh", "sh", ...)+. False for a libc that has none.
|
|
265
|
+
# @param [String] str A rendered value.
|
|
266
|
+
# @return [Boolean]
|
|
267
|
+
def str_sh?(str)
|
|
268
|
+
!sh_offset.nil? && str.include?('$base') && str.include?(sh_offset.to_s(16))
|
|
522
269
|
end
|
|
523
270
|
|
|
524
|
-
|
|
271
|
+
# File offset of the +"/bin/sh"+ string.
|
|
272
|
+
# @return [Integer]
|
|
273
|
+
def bin_sh_offset
|
|
274
|
+
@bin_sh_offset ||= str_offset('/bin/sh')
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# File offset of the standalone +"sh"+ string (\0-preceded and
|
|
278
|
+
# \0-terminated). Its distance from +"/bin/sh"+ is build-specific, so locate
|
|
279
|
+
# it directly instead of guessing.
|
|
280
|
+
# @return [Integer?] +nil+ when the libc has no such string.
|
|
281
|
+
def sh_offset
|
|
282
|
+
return @sh_offset if defined?(@sh_offset)
|
|
283
|
+
|
|
284
|
+
idx = file_bytes.index("\x00sh\x00")
|
|
285
|
+
@sh_offset = idx && idx + 1
|
|
525
286
|
end
|
|
526
287
|
|
|
527
288
|
def call_str; raise NotImplementedError
|
|
@@ -556,17 +317,6 @@ module OneGadget
|
|
|
556
317
|
@file_bytes ||= File.binread(file)
|
|
557
318
|
end
|
|
558
319
|
|
|
559
|
-
# Render one argv/envp entry for a constraint. A libc global that points to a
|
|
560
|
-
# fixed string is shown as that string (its true content); a controllable
|
|
561
|
-
# operand, or a global that isn't a plain string, is shown unchanged.
|
|
562
|
-
# @param [String] element A single argv entry.
|
|
563
|
-
# @example +$base+0x16b250+ -> +"--"+ (the do_system separator); +x21+ -> +x21+.
|
|
564
|
-
# @return [String]
|
|
565
|
-
def resolve_argv_element(element)
|
|
566
|
-
content = global_str_content(element)
|
|
567
|
-
content ? content.inspect : element
|
|
568
|
-
end
|
|
569
|
-
|
|
570
320
|
# The NUL-terminated printable string a resolved-offset global (+$base+<off>+)
|
|
571
321
|
# points to, or +nil+ when +element+ isn't such a global or the bytes aren't a
|
|
572
322
|
# short printable string. Architectures whose globals aren't yet
|
|
@@ -619,13 +369,10 @@ module OneGadget
|
|
|
619
369
|
end
|
|
620
370
|
|
|
621
371
|
# The two sides of a relation as plain numbers, when they can be compared
|
|
622
|
-
# without the caller arranging anything
|
|
623
|
-
#
|
|
624
|
-
#
|
|
625
|
-
#
|
|
626
|
-
# Offsets are only comparable undereferenced. +[r1]+ and +[r1+0x4]+ address
|
|
627
|
-
# different slots, but the values in them are unrelated -- nothing says they
|
|
628
|
-
# differ.
|
|
372
|
+
# without the caller arranging anything.
|
|
373
|
+
# @example Offsets from one base compare; the slots they address do not.
|
|
374
|
+
# comparable_values('r1', 'r1+0x4') #=> [0, 4]
|
|
375
|
+
# comparable_values('[r1]', '[r1+0x4]') #=> nil
|
|
629
376
|
# @param [String] lhs The relation's left side, cast already stripped.
|
|
630
377
|
# @param [String] rhs The relation's right side.
|
|
631
378
|
# @return [(Numeric, Numeric), nil] Both sides as numbers, or nil when they
|
|
@@ -654,118 +401,6 @@ module OneGadget
|
|
|
654
401
|
trivial_relation(con) == false
|
|
655
402
|
end
|
|
656
403
|
|
|
657
|
-
# Regexp (as a String) matching an objdump line that calls a terminal
|
|
658
|
-
# function (+exec*+ / +posix_spawn*+) we can turn into a gadget.
|
|
659
|
-
def terminal_call_regexp
|
|
660
|
-
"#{call_str}.*<(exec[^+]*|posix_spawn[^+]*)>$"
|
|
661
|
-
end
|
|
662
|
-
|
|
663
|
-
# Enumerate gadget candidates by walking the control-flow graph *backward*
|
|
664
|
-
# from each terminal call along its predecessors (the instruction that falls
|
|
665
|
-
# through to it, plus any branch that targets it), forking at conditional
|
|
666
|
-
# edges up to {MAX_FORKS} times. Each emitted candidate is a flat,
|
|
667
|
-
# address-preserving line-list ending at the terminal call, consumed
|
|
668
|
-
# unchanged by {#find}; the emulator turns each crossed conditional edge into
|
|
669
|
-
# a constraint (see {OneGadget::Emulators::Conditional}).
|
|
670
|
-
def branch_aware_candidates(&)
|
|
671
|
-
cands = []
|
|
672
|
-
re = /#{terminal_call_regexp}/
|
|
673
|
-
disasm_lines.each_with_index do |line, idx|
|
|
674
|
-
next unless line.match?(re)
|
|
675
|
-
|
|
676
|
-
back_walk(idx, 0, Set.new, []) { |lines| cands << lines.join("\n") }
|
|
677
|
-
end
|
|
678
|
-
cands.uniq!
|
|
679
|
-
cands.select!(&) if block_given?
|
|
680
|
-
cands
|
|
681
|
-
end
|
|
682
|
-
|
|
683
|
-
# Depth-first backward walk. +visited+ (a Set) and +path+ are mutated with
|
|
684
|
-
# backtracking so a fork explores independently without per-step copies;
|
|
685
|
-
# +path+ is built in forward order (the terminal call stays last). Emits a
|
|
686
|
-
# candidate at each leaf - {#find} then tries every start line within it.
|
|
687
|
-
def back_walk(idx, forks, visited, path, &blk)
|
|
688
|
-
addr = addr_at(idx)
|
|
689
|
-
return if path.size >= PATH_BUDGET
|
|
690
|
-
|
|
691
|
-
visited.add(addr)
|
|
692
|
-
path.unshift(disasm_lines[idx])
|
|
693
|
-
edges = predecessors(idx).reject do |pidx, cond|
|
|
694
|
-
visited.include?(addr_at(pidx)) || (cond && forks >= MAX_FORKS)
|
|
695
|
-
end
|
|
696
|
-
if edges.empty?
|
|
697
|
-
blk.call(path.dup)
|
|
698
|
-
else
|
|
699
|
-
edges.each { |pidx, cond| back_walk(pidx, forks + (cond ? 1 : 0), visited, path, &blk) }
|
|
700
|
-
end
|
|
701
|
-
path.shift
|
|
702
|
-
visited.delete(addr)
|
|
703
|
-
end
|
|
704
|
-
|
|
705
|
-
# The address of the instruction at +idx+, remembered as it is asked for.
|
|
706
|
-
# Only the lines the backward walk reaches ever need one, a small part of a
|
|
707
|
-
# whole libc's disassembly.
|
|
708
|
-
def addr_at(idx)
|
|
709
|
-
(@addr_at ||= {})[idx] ||= offset_of(disasm_lines[idx])
|
|
710
|
-
end
|
|
711
|
-
|
|
712
|
-
# Predecessors of +disasm_lines[idx]+ as +[pred_index, conditional_edge?]+:
|
|
713
|
-
# the instruction that falls through to it, plus any branch that targets it.
|
|
714
|
-
# Computed lazily (the walk only touches lines near terminal calls) and cached.
|
|
715
|
-
def predecessors(idx)
|
|
716
|
-
(@predecessors ||= {})[idx] ||= begin
|
|
717
|
-
preds = []
|
|
718
|
-
# Nothing falls through into the first line of a window: the line before
|
|
719
|
-
# it is the last of another window, a different part of the binary.
|
|
720
|
-
if idx.positive? && !window_starts.key?(idx)
|
|
721
|
-
kind = branch_kind(disasm_lines[idx - 1])
|
|
722
|
-
preds << [idx - 1, kind == :conditional] unless %i[unconditional terminator].include?(kind)
|
|
723
|
-
end
|
|
724
|
-
(branch_pred_map[addr_at(idx)] || []).each do |b|
|
|
725
|
-
preds << [b, branch_kind(disasm_lines[b]) == :conditional]
|
|
726
|
-
end
|
|
727
|
-
preds
|
|
728
|
-
end
|
|
729
|
-
end
|
|
730
|
-
|
|
731
|
-
# Map of target-address => indexes of (conditional or unconditional) direct
|
|
732
|
-
# branches that jump there. Scans the whole disassembly once (a branch can
|
|
733
|
-
# target a call region from anywhere), so keep the per-line test cheap.
|
|
734
|
-
def branch_pred_map
|
|
735
|
-
@branch_pred_map ||= Base.cached(:branch_pred, @objdump.command) do
|
|
736
|
-
lead = branch_lead_regex
|
|
737
|
-
disasm_lines.each_with_index.with_object(Hash.new { |h, k| h[k] = [] }) do |(line, i), map|
|
|
738
|
-
# Cheap precompiled reject (no per-line allocation) before the full test.
|
|
739
|
-
next unless line.match?(lead)
|
|
740
|
-
next unless %i[conditional unconditional].include?(branch_kind(line))
|
|
741
|
-
|
|
742
|
-
tgt = branch_target(line)
|
|
743
|
-
map[tgt] << i if tgt
|
|
744
|
-
end
|
|
745
|
-
end
|
|
746
|
-
end
|
|
747
|
-
|
|
748
|
-
# Precompiled over-approximation of a branch line, built from the arch's
|
|
749
|
-
# {#branch_lead_chars}, to skip the full test for the (majority) non-branch
|
|
750
|
-
# lines during the whole-binary scan.
|
|
751
|
-
def branch_lead_regex
|
|
752
|
-
@branch_lead_regex ||= /\A[0-9a-f]+:\s+[#{branch_lead_chars}]/
|
|
753
|
-
end
|
|
754
|
-
|
|
755
|
-
# Parse the (direct) target address of a branch line, or +nil+ if indirect.
|
|
756
|
-
def branch_target(line)
|
|
757
|
-
line.sub(/\A[0-9a-f]+:\s*\S+\s*/, '')[/\b([0-9a-f]+)\b\s*(?:<|\z)/, 1]&.to_i(16)
|
|
758
|
-
end
|
|
759
|
-
|
|
760
|
-
# The mnemonic of an objdump line, memoized because each line is tested by
|
|
761
|
-
# several branch predicates during the CFG scan.
|
|
762
|
-
# @example
|
|
763
|
-
# mnemonic('4a1d0: b.ne 4a200 <foo>') #=> 'b.ne'
|
|
764
|
-
# mnemonic('4a1c0: cbz x0, 4a200') #=> 'cbz'
|
|
765
|
-
def mnemonic(line)
|
|
766
|
-
(@mnemonic ||= {})[line] ||= line[/\A[0-9a-f]+:\s*(\S+)/, 1] || ''
|
|
767
|
-
end
|
|
768
|
-
|
|
769
404
|
# Classify a disassembly +line+ for the control-flow walk (arch-specific):
|
|
770
405
|
# :conditional - may or may not be taken; the walk explores both edges
|
|
771
406
|
# and turns the decision into a gadget constraint
|
|
@@ -785,102 +420,13 @@ module OneGadget
|
|
|
785
420
|
def branch_lead_chars; raise NotImplementedError
|
|
786
421
|
end
|
|
787
422
|
|
|
788
|
-
# The
|
|
789
|
-
def disasm_lines
|
|
790
|
-
disassembly[:lines]
|
|
791
|
-
end
|
|
792
|
-
|
|
793
|
-
# Where in {#disasm_lines} each window begins, keyed for lookup. Empty when
|
|
794
|
-
# the whole file was disassembled, since then every line does follow the one
|
|
795
|
-
# before it.
|
|
796
|
-
# @return [Hash{Integer => true}]
|
|
797
|
-
def window_starts
|
|
798
|
-
disassembly[:starts]
|
|
799
|
-
end
|
|
800
|
-
|
|
801
|
-
# The disassembly and the shape it was taken in, cached (per objdump command)
|
|
802
|
-
# for the lifetime of the fetcher.
|
|
803
|
-
# @return [Hash{Symbol => Array<String>, Hash}]
|
|
804
|
-
def disassembly
|
|
805
|
-
@disassembly ||= Base.cached(:disasm, @objdump.command) do
|
|
806
|
-
sites = terminal_call_sites
|
|
807
|
-
sites.nil? || sites.empty? ? full_disasm : windowed_disasm(sites)
|
|
808
|
-
end
|
|
809
|
-
end
|
|
810
|
-
|
|
811
|
-
# Disassemble the whole file (the exhaustive default).
|
|
812
|
-
def full_disasm
|
|
813
|
-
{ lines: objdump_lines, starts: {} }
|
|
814
|
-
end
|
|
815
|
-
|
|
816
|
-
# Disassemble only [call-WINDOW_BACK, call+WINDOW_FWD] around each call site,
|
|
817
|
-
# merging overlaps. Used when {#terminal_call_sites} located the calls without
|
|
818
|
-
# a full disassembly (the win on the slow-to-objdump Thumb-2 arm libcs).
|
|
819
|
-
def windowed_disasm(sites)
|
|
820
|
-
windows = sites.sort.map { |a| [[a - WINDOW_BACK, 0].max, a + WINDOW_FWD] }
|
|
821
|
-
# One objdump per window, all at once: they are separate processes that
|
|
822
|
-
# wait on each other for nothing, and a libc comes to a handful of windows.
|
|
823
|
-
disassembled = merge_ranges(windows)
|
|
824
|
-
.map { |lo, hi| Thread.new { objdump_lines(start: lo, stop: hi) } }
|
|
825
|
-
.map(&:value)
|
|
826
|
-
starts = {}
|
|
827
|
-
lines = disassembled.each_with_object([]) do |window, acc|
|
|
828
|
-
starts[acc.size] = true
|
|
829
|
-
acc.concat(window)
|
|
830
|
-
end
|
|
831
|
-
{ lines:, starts: }
|
|
832
|
-
end
|
|
833
|
-
|
|
834
|
-
# Merge a list of sorted [lo, hi] ranges, coalescing any that overlap.
|
|
835
|
-
def merge_ranges(ranges)
|
|
836
|
-
ranges.each_with_object([]) do |(lo, hi), merged|
|
|
837
|
-
if merged.last && lo <= merged.last[1]
|
|
838
|
-
merged.last[1] = hi if hi > merged.last[1]
|
|
839
|
-
else
|
|
840
|
-
merged << [lo, hi]
|
|
841
|
-
end
|
|
842
|
-
end
|
|
843
|
-
end
|
|
844
|
-
|
|
845
|
-
# An objdump line carries an instruction when it opens with its address.
|
|
846
|
-
DISASSEMBLED = /\A[0-9a-f]+:/
|
|
847
|
-
private_constant :DISASSEMBLED
|
|
848
|
-
|
|
849
|
-
def objdump_lines(start: nil, stop: nil)
|
|
850
|
-
# One pass, one string per line: a whole libc is hundreds of thousands of
|
|
851
|
-
# them, and only the instructions are wanted.
|
|
852
|
-
`#{@objdump.command(start:, stop:)}`.each_line.filter_map do |line|
|
|
853
|
-
line = line.strip
|
|
854
|
-
line if DISASSEMBLED.match?(line)
|
|
855
|
-
end
|
|
856
|
-
end
|
|
857
|
-
|
|
858
|
-
# Addresses of the calls reaching a terminal function, found without
|
|
859
|
-
# disassembling anything: the +exec*+/+posix_spawn*+ symbols are read out of
|
|
860
|
-
# the ELF and +.text+ is scanned for direct calls into them (see
|
|
861
|
-
# {#scan_calls}). +nil+ when the file cannot be read that way, so the caller
|
|
862
|
-
# disassembles everything instead.
|
|
863
|
-
# @return [Array<Integer>, nil]
|
|
864
|
-
def terminal_call_sites
|
|
865
|
-
File.open(file) do |fd|
|
|
866
|
-
elf = ELFTools::ELFFile.new(fd)
|
|
867
|
-
return nil unless elf.endian == :little
|
|
868
|
-
|
|
869
|
-
targets = terminal_symbol_addresses(elf)
|
|
870
|
-
return [] if targets.empty?
|
|
871
|
-
|
|
872
|
-
text = elf.section_by_name('.text')
|
|
873
|
-
return nil if text.nil?
|
|
874
|
-
|
|
875
|
-
scan_calls(text.header.sh_addr.to_i, text.data, targets)
|
|
876
|
-
end
|
|
877
|
-
rescue ELFTools::ELFError
|
|
878
|
-
nil # not something we can scan; disassemble everything
|
|
879
|
-
end
|
|
880
|
-
|
|
881
|
-
# The addresses in +data+ (loaded at +base+) of direct calls into +targets+.
|
|
423
|
+
# The addresses in +data+ (loaded at +base+) of calls into +targets+.
|
|
882
424
|
# Over-approximating is fine -- a false positive only adds a window nothing
|
|
883
425
|
# is found in -- while a missed call costs every gadget around it.
|
|
426
|
+
#
|
|
427
|
+
# How an instruction is spelled in those bytes is the architecture's to say,
|
|
428
|
+
# not the file's: ARM and AArch64 keep theirs little-endian in a big-endian
|
|
429
|
+
# file, so both read words little-endian whatever the file says.
|
|
884
430
|
# @param [Integer] _base
|
|
885
431
|
# @param [String] _data
|
|
886
432
|
# @param [Hash{Integer => true}] _targets
|
|
@@ -888,44 +434,6 @@ module OneGadget
|
|
|
888
434
|
# which disassembles everything instead.
|
|
889
435
|
def scan_calls(_base, _data, _targets) = nil
|
|
890
436
|
|
|
891
|
-
# Where the +exec*+/+posix_spawn*+ symbols live, keyed for lookup. Read
|
|
892
|
-
# straight out of the tables rather than built into an object per symbol: a
|
|
893
|
-
# libc has thousands, and only two fields of each are wanted.
|
|
894
|
-
# @param [ELFTools::ELFFile] elf
|
|
895
|
-
# @return [Hash{Integer => true}]
|
|
896
|
-
def terminal_symbol_addresses(elf)
|
|
897
|
-
addrs = {}
|
|
898
|
-
%w[.dynsym .symtab].each do |name|
|
|
899
|
-
sec = elf.section_by_name(name)
|
|
900
|
-
next if sec.nil?
|
|
901
|
-
|
|
902
|
-
strtab = elf.sections[sec.header.sh_link.to_i].data
|
|
903
|
-
each_symbol(elf, sec) do |name_offset, value|
|
|
904
|
-
symbol = strtab.byteslice(name_offset, TERMINAL_PREFIX_BYTES)
|
|
905
|
-
addrs[symbol_address(value)] = true if symbol&.start_with?(*TERMINAL_PREFIXES)
|
|
906
|
-
end
|
|
907
|
-
end
|
|
908
|
-
addrs
|
|
909
|
-
end
|
|
910
|
-
|
|
911
|
-
# Each symbol's name offset and value, unpacked from the table's bytes. A
|
|
912
|
-
# 32-bit entry is four words with the value second; a 64-bit one is six, the
|
|
913
|
-
# value being the pair from the third.
|
|
914
|
-
# @param [ELFTools::ELFFile] elf
|
|
915
|
-
# @param [ELFTools::Sections::Section] section
|
|
916
|
-
# @yieldparam [Integer] name_offset
|
|
917
|
-
# @yieldparam [Integer] value
|
|
918
|
-
# @return [void]
|
|
919
|
-
def each_symbol(elf, section)
|
|
920
|
-
wide = elf.elf_class == 64
|
|
921
|
-
section.data.unpack('V*').each_slice(wide ? 6 : 4) do |words|
|
|
922
|
-
value = wide ? words[2] && words[3] && (words[2] | (words[3] << 32)) : words[1]
|
|
923
|
-
next if value.nil? || value.zero?
|
|
924
|
-
|
|
925
|
-
yield(words[0], value)
|
|
926
|
-
end
|
|
927
|
-
end
|
|
928
|
-
|
|
929
437
|
# The address a symbol's value names. Overridden where the value carries
|
|
930
438
|
# something besides the address (ARM keeps the Thumb bit in it).
|
|
931
439
|
# @param [Integer] value
|
|
@@ -933,12 +441,6 @@ module OneGadget
|
|
|
933
441
|
def symbol_address(value)
|
|
934
442
|
value
|
|
935
443
|
end
|
|
936
|
-
|
|
937
|
-
# Map from an instruction's address to its index in {#disasm_lines}, so a
|
|
938
|
-
# given address can be located in the disassembly in O(1).
|
|
939
|
-
def disasm_index
|
|
940
|
-
@disasm_index ||= disasm_lines.each_with_index.to_h { |line, i| [line[/\A([0-9a-f]+):/, 1].to_i(16), i] }
|
|
941
|
-
end
|
|
942
444
|
end
|
|
943
445
|
end
|
|
944
446
|
end
|