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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +197 -65
  3. data/README.md +96 -22
  4. data/lib/one_gadget/abi.rb +41 -5
  5. data/lib/one_gadget/builds/libc-2.31-93b46e0027747153e336c3fd9431ce9a5d82ad00.rb +563 -0
  6. data/lib/one_gadget/builds/libc-2.35-891c1403437a4e30e684e0c8e34b87a09e4298e5.rb +434 -0
  7. data/lib/one_gadget/builds/libc-2.39-a1d1cf4badf1f5dfe57ff1d17bd692ccc6fcd5c5.rb +531 -0
  8. data/lib/one_gadget/builds/libc-2.39-cd8f5a207dd67aea370d2b471a54c3e56f44ab18.rb +531 -0
  9. data/lib/one_gadget/builds/libc-2.43-b50ceafbd17dc6bceee344a66671c7eaa152bef4.rb +15 -0
  10. data/lib/one_gadget/emulators/aarch64.rb +5 -2
  11. data/lib/one_gadget/emulators/amd64.rb +1 -0
  12. data/lib/one_gadget/emulators/arm.rb +27 -18
  13. data/lib/one_gadget/emulators/arm_family.rb +35 -160
  14. data/lib/one_gadget/emulators/conditional.rb +28 -22
  15. data/lib/one_gadget/emulators/constraints.rb +269 -0
  16. data/lib/one_gadget/emulators/data_processing.rb +167 -0
  17. data/lib/one_gadget/emulators/i386.rb +4 -6
  18. data/lib/one_gadget/emulators/instruction.rb +24 -1
  19. data/lib/one_gadget/emulators/lambda.rb +3 -1
  20. data/lib/one_gadget/emulators/mips.rb +289 -0
  21. data/lib/one_gadget/emulators/processor.rb +33 -455
  22. data/lib/one_gadget/emulators/register_file.rb +19 -10
  23. data/lib/one_gadget/emulators/riscv64.rb +265 -0
  24. data/lib/one_gadget/emulators/safe_calls.rb +9 -3
  25. data/lib/one_gadget/emulators/tracked_memory.rb +209 -0
  26. data/lib/one_gadget/emulators/x86.rb +32 -22
  27. data/lib/one_gadget/fetchers/aarch64.rb +0 -27
  28. data/lib/one_gadget/fetchers/amd64.rb +0 -24
  29. data/lib/one_gadget/fetchers/argument_resolution.rb +340 -0
  30. data/lib/one_gadget/fetchers/arm.rb +99 -44
  31. data/lib/one_gadget/fetchers/base.rb +142 -640
  32. data/lib/one_gadget/fetchers/candidate_walk.rb +150 -0
  33. data/lib/one_gadget/fetchers/disassembly.rb +252 -0
  34. data/lib/one_gadget/fetchers/dynamic_symbols.rb +104 -0
  35. data/lib/one_gadget/fetchers/i386.rb +5 -8
  36. data/lib/one_gadget/fetchers/mips.rb +478 -0
  37. data/lib/one_gadget/fetchers/objdump.rb +24 -2
  38. data/lib/one_gadget/fetchers/riscv64.rb +78 -0
  39. data/lib/one_gadget/fetchers/x86.rb +19 -0
  40. data/lib/one_gadget/fetchers.rb +22 -6
  41. data/lib/one_gadget/gadget.rb +25 -34
  42. data/lib/one_gadget/helper.rb +19 -5
  43. data/lib/one_gadget/one_gadget.rb +6 -0
  44. data/lib/one_gadget/version.rb +1 -1
  45. data/lib/one_gadget.rb +1 -1
  46. metadata +18 -6
  47. data/lib/one_gadget/builds/libc-2.26-2104f3d4ad5cf68603afbe7ba1a17f5ac99c5988.rb +0 -227
  48. data/lib/one_gadget/builds/libc-2.26-ddcc13122ddbfe5e5ef77d4ebe66d124ae5762c2.rb +0 -300
  49. data/lib/one_gadget/builds/libc-2.26-f65648a832414f2144ce795d75b6045a1ec2e252.rb +0 -199
@@ -0,0 +1,340 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OneGadget
4
+ module Fetchers
5
+ # What a reached +exec+/+posix_spawn+ call requires of its caller. Reads the
6
+ # arguments the emulator left, decides whether each is something a caller can
7
+ # arrange -- a +"/bin/sh"+ pointer, an argv array that is already valid or can
8
+ # be built in place, an acceptable envp -- and states what is left as the
9
+ # gadget's constraints. Anything it cannot describe drops the candidate rather
10
+ # than reporting a gadget that would not run. Mixed into {Base}.
11
+ module ArgumentResolution
12
+ private
13
+
14
+ # Generating constraints for being a valid gadget.
15
+ # @param [OneGadget::Emulators::Processor] processor The processor after executing the gadgets.
16
+ # @return [Hash{Symbol => Array<String>, String}?]
17
+ # The options to create a {OneGadget::Gadget::Gadget} object.
18
+ # Keys might be:
19
+ # 1. constraints: Array<String> List of constraints.
20
+ # 2. effect: String Result function call of this gadget.
21
+ # If the constraints can never be satisfied, +nil+ is returned.
22
+ def resolve(processor)
23
+ call = processor.registers[processor.pc].to_s
24
+ return resolve_posix_spawn(processor) if call.include?('posix_spawn')
25
+ return resolve_execve(processor) if call.include?('execve')
26
+
27
+ resolve_execl(processor) if call.include?('execl')
28
+ end
29
+
30
+ def resolve_execve(processor)
31
+ arg0, arg1, arg2 = (0..2).map { |i| processor.argument(i) }
32
+ res = resolve_execve_args(processor, arg0, arg1, arg2)
33
+ return nil if res.nil?
34
+
35
+ { constraints: res[:constraints], effect: %(execve("/bin/sh", #{arg1}, #{res[:envp]})) }
36
+ end
37
+
38
+ def resolve_execve_args(processor, arg0, arg1, arg2, allow_null_argv: true)
39
+ return unless str_bin_sh?(arg0.to_s)
40
+
41
+ # arg1 == NULL || [arg1] == NULL
42
+ # arg2 == NULL || [arg2] == NULL || arg[2] == envp
43
+ cons = processor.constraints
44
+ con = check_argv(processor, arg1, allow_null_argv)
45
+ cons << con unless con.nil?
46
+ return nil unless cons.all?
47
+
48
+ envp = 'environ'
49
+ return nil unless check_envp(processor, arg2) do |c|
50
+ cons << c
51
+ envp = arg2
52
+ end
53
+
54
+ { constraints: cons, envp: }
55
+ end
56
+
57
+ # Generate the +argv+-related constraint for an +exec*+ call.
58
+ #
59
+ # Terminology shared by all the +argv+ helpers below:
60
+ # * +argv_ptr+ - the *pointer to* the argv array, i.e. the emulated content of the register passed
61
+ # as +argv+ ({OneGadget::Emulators::Processor#argument}), kept as the emulator produced it.
62
+ # Examples: +rsi+, +rsp+0x10+, +[rbp-0x8]+, a global-variable reference, or a bare integer.
63
+ # * +argv+ - the array *pointed to* by +argv_ptr+: its dereferenced entries
64
+ # +[argv[0], argv[1], argv[2], argv[3]]+, each already converted to a string.
65
+ #
66
+ # @param [OneGadget::Emulators::Processor] processor The processor state at the call site.
67
+ # @param [OneGadget::Emulators::Lambda, Integer] argv_ptr The pointer to the argv array. See above.
68
+ # @param [Boolean] allow_null
69
+ # Whether +argv_ptr+ itself may be +NULL+ (true for +execve+, false for +posix_spawn+).
70
+ # @return [String, nil, false] How {#resolve_execve_args} should treat this argv:
71
+ # a +String+ is a constraint it must add; +nil+ means the argv is already
72
+ # valid so no constraint is needed; +false+ means the argv can never launch
73
+ # a shell (e.g. a fixed noexec option), which it consumes as an
74
+ # unsatisfiable constraint and drops the gadget.
75
+ def check_argv(processor, argv_ptr, allow_null)
76
+ argv_ptr = resolve_stack_deref(processor, argv_ptr)
77
+ return check_stack_argv(processor, argv_ptr, allow_null) if resolvable_stack(processor, argv_ptr)
78
+
79
+ check_nonstack_argv(argv_ptr, allow_null)
80
+ end
81
+
82
+ # Whether resolving +lmda+'s target via tracked memory is worth attempting,
83
+ # as opposed to the plain opaque "==NULL || is a valid .." form
84
+ # ({#check_nonstack_argv}/the envp equivalent). Always true for the arch's
85
+ # dedicated stack/frame pointer. For anything else, only when element
86
+ # 0 -- what {#argv_already_valid?}/{#generate_argv_with_sh} branch on --
87
+ # was actually written within this candidate.
88
+ # @example element 0 tracked -- resolvable
89
+ # reg tracked (element 0), reg+0x8 tracked (element 1) => resolvable
90
+ # @example a later, unrelated write must not trigger array resolution
91
+ # reg+0x10 (element 2) tracked, reg/reg+0x8 (elements 0/1) untracked
92
+ # => not resolvable; falls back to the opaque form instead of a garbled array
93
+ # @param [OneGadget::Emulators::Processor] processor
94
+ # @param [OneGadget::Emulators::Lambda, Integer] lmda A pointer operand. A
95
+ # concrete address is never resolvable: nothing was tracked against it.
96
+ # @return [Hash{Integer => OneGadget::Emulators::Lambda}, nil]
97
+ def resolvable_stack(processor, lmda)
98
+ return nil unless lmda.is_a?(OneGadget::Emulators::Lambda)
99
+
100
+ stack, offset = processor.resolve_address(lmda)
101
+ return nil unless stack
102
+ return stack if lmda.deref_count.zero? && OneGadget::ABI.stack_register?(lmda.obj)
103
+
104
+ stack if stack.key?(offset)
105
+ end
106
+
107
+ # Handle the case where +argv_ptr+ points into memory this candidate wrote,
108
+ # so the +argv+ entries can be read off it.
109
+ # @param [OneGadget::Emulators::Lambda] argv_ptr The pointer to the argv array. See {#check_argv}.
110
+ # @return [String, nil, false] The same three-way contract as {#check_argv},
111
+ # which returns this value unchanged: a constraint, +nil+ (already valid),
112
+ # or +false+ (drop the gadget).
113
+ def check_stack_argv(processor, argv_ptr, allow_null)
114
+ stack, offset = processor.resolve_address(argv_ptr)
115
+ # A stack register we don't track a stack for (the frame pointer):
116
+ # fall back to treating it as an opaque pointer.
117
+ return check_nonstack_argv(argv_ptr, allow_null) if stack.nil?
118
+
119
+ argv = (0..3).map { |i| stack[offset + processor.class.bits / 8 * i].to_s }
120
+
121
+ # A shell spawned with a fixed "noexec" option never runs a command, so
122
+ # drop the gadget (see this method's @return for the +false+ contract).
123
+ return false if noexec_shell_argv?(argv)
124
+
125
+ # if argv is already valid, no constraints are needed! (but probably won't happen :p)
126
+ return if argv_already_valid?(argv)
127
+
128
+ return generate_argv_with_sh(argv) if global_var?(argv[0])
129
+
130
+ generate_argv_without_sh(argv_ptr, argv, allow_null)
131
+ end
132
+
133
+ # Whether the array is usable as it stands: an empty argv, or one whose only
134
+ # entry is a libc global.
135
+ def argv_already_valid?(argv)
136
+ argv[0] == '0' || (global_var?(argv[0]) && argv[1] == '0')
137
+ end
138
+
139
+ # The constraint for an argv whose first entry libc has already filled in
140
+ # with the shell's own name, leaving the caller to arrange the rest.
141
+ def generate_argv_with_sh(argv)
142
+ # argv[0] is not controlled by the user, argv[0] probably is "/bin/sh" or "sh" (but actually, the content of
143
+ # argv[0] doesn't quite matter, just need to make sure it's readable)
144
+ # So far (I checked glibc 2.37), we can make argv to be {"/bin/sh", sth, NULL} or {"sh", "-c", sth, NULL}
145
+ # TODO: We need to update this when the above assumption is no longer true
146
+ if argv[2] == '0' && !global_var?(argv[1])
147
+ "#{argv[1]} == NULL || {\"/bin/sh\", #{argv[1]}, NULL} is a valid argv"
148
+ else
149
+ argv_gte3 = argv[3] == '0' ? 'NULL' : "#{argv[3]}, ..."
150
+ if global_var?(argv[1])
151
+ # A leading "sh -c" whose fixed elements (e.g. "-c", the "--" separator)
152
+ # are libc globals -- resolve them so the controllable command operand
153
+ # stands out (e.g. {"sh", "-c", "--", x21, ...}).
154
+ "{\"sh\", #{resolve_argv_element(argv[1])}, #{resolve_argv_element(argv[2])}, #{argv_gte3}} is a valid argv"
155
+ else
156
+ "#{argv[1]} == NULL || {\"sh\", #{argv[1]}, #{argv[2]}, #{argv_gte3}} is a valid argv"
157
+ end
158
+ end
159
+ end
160
+
161
+ # @param [String] argv_ptr The pointer to the argv array. See {#check_argv}.
162
+ # @param [Array<String>] argv The argv entries +argv_ptr+ points to, i.e. +[argv[0], .., argv[3]]+.
163
+ def generate_argv_without_sh(argv_ptr, argv, allow_null)
164
+ argv_cons = "{#{argv[0]}"
165
+ (1..argv.length - 1).each do |i|
166
+ if argv[i] == '0'
167
+ argv_cons += ', NULL'
168
+ break
169
+ elsif global_var?(argv[i])
170
+ # A fixed libc-global entry (e.g. "-c", "--") -- show its true content.
171
+ argv_cons += ", #{resolve_argv_element(argv[i])}"
172
+ else
173
+ argv_cons += ", #{argv[i]}"
174
+ end
175
+ end
176
+ argv_cons += ', ...' unless argv_cons.end_with?('NULL')
177
+ argv_cons += '} is a valid argv'
178
+
179
+ if allow_null && argv.all? { |a| OneGadget::ABI.stack_register?(a) }
180
+ # If libc writes something into the stack, argv_ptr cannot be NULL.
181
+ # TODO: Find a better way to check can argv_ptr be NULL
182
+ "#{argv_ptr} == NULL || #{argv[0]} == NULL || #{argv_cons}"
183
+ else
184
+ "#{argv[0]} == NULL || #{argv_cons}"
185
+ end
186
+ end
187
+
188
+ # Whether +argv+ invokes the shell with a fixed option word that disables
189
+ # command execution. execve's program is always "/bin/sh", so +argv[1]+ is
190
+ # that shell's option word; a libc-global bundle carrying the noexec flag
191
+ # there yields a shell that reaches execve yet can never run a command --
192
+ # a false positive to drop.
193
+ # @param [Array<String>] argv The resolved argv entries. See {#check_stack_argv}.
194
+ # @return [Boolean]
195
+ # @example decided by argv[1]'s libc-global content (via global_str_content)
196
+ # # content "-nc" carries bash's noexec 'n' => true;
197
+ # # "-c" runs the command => false; a non-global (attacker) word => false
198
+ def noexec_shell_argv?(argv)
199
+ opt = global_str_content(argv[1])
200
+ !opt.nil? && opt.match?(/\A-[a-zA-Z]*n[a-zA-Z]*\z/)
201
+ end
202
+
203
+ # Handle the case where +argv_ptr+ is not a plain stack pointer (e.g. a register or global variable).
204
+ # @param [String] argv_ptr The pointer to the argv array. See {#check_argv}.
205
+ def check_nonstack_argv(argv_ptr, allow_null)
206
+ if allow_null
207
+ "[#{argv_ptr}] == NULL || #{argv_ptr} == NULL || #{argv_ptr} is a valid argv"
208
+ else
209
+ "[#{argv_ptr}] == NULL || #{argv_ptr} is a valid argv"
210
+ end
211
+ end
212
+
213
+ # If +ptr+ is a single dereference of a tracked stack slot, resolve it to
214
+ # that slot's own tracked value so the rest of argv/envp resolution can
215
+ # treat it like a bare register instead of an opaque pointer.
216
+ # @param [OneGadget::Emulators::Processor] processor
217
+ # @param [OneGadget::Emulators::Lambda, Integer] ptr An argv_ptr/envp_ptr.
218
+ # @return [OneGadget::Emulators::Lambda, Integer] +ptr+, or the value it
219
+ # resolves to when it simplifies.
220
+ # @example a tracked slot resolves to its source register
221
+ # # mov [ebp-0x30], ecx (earlier in the same candidate)
222
+ # resolve_stack_deref(processor, Lambda.parse('[ebp-0x30]')) #=> the ecx lambda
223
+ # @example an untracked slot is a no-op
224
+ # resolve_stack_deref(processor, Lambda.parse('[ebp-0x40]')) #=> that same lambda
225
+ def resolve_stack_deref(processor, ptr)
226
+ return ptr unless ptr.is_a?(OneGadget::Emulators::Lambda) && ptr.deref_count == 1 &&
227
+ OneGadget::ABI.stack_register?(ptr.obj)
228
+
229
+ stack, offset = processor.resolve_address(ptr.dup.ref!)
230
+ tracked = stack && stack[offset]
231
+ return ptr unless tracked.is_a?(OneGadget::Emulators::Lambda) && tracked.deref_count.zero?
232
+
233
+ tracked
234
+ end
235
+
236
+ # Generate the +envp+-related constraint for an +exec*+ call.
237
+ #
238
+ # Mirrors the +argv+ terminology from {#check_argv}: +envp_ptr+ is the *pointer to* the envp array
239
+ # ({OneGadget::Emulators::Processor#argument}), while +envp+ is the array of dereferenced entries
240
+ # it points to.
241
+ #
242
+ # @param [OneGadget::Emulators::Processor] processor The processor state at the call site.
243
+ # @param [OneGadget::Emulators::Lambda, Integer] envp_ptr The pointer to the envp array.
244
+ # @yieldparam [String] cons The +envp+ constraint, yielded only when one is required.
245
+ # @return [Object, nil] Truthy when +envp+ is acceptable, +nil+ to reject the gadget.
246
+ def check_envp(processor, envp_ptr)
247
+ # A doubly-dereferenced pointer that names a global variable is believed to
248
+ # be environ; one that doesn't drops the gadget. The two dereferences are
249
+ # the slot the address is read from and the variable itself, so an
250
+ # architecture that resolves the first of them lands one earlier -- naming
251
+ # the variable, which it then has to be able to recognise.
252
+ if envp_ptr.is_a?(OneGadget::Emulators::Lambda)
253
+ return global_var?(envp_ptr.to_s) if envp_ptr.deref_count >= 2
254
+ return true if envp_ptr.deref_count == 1 && environ?(envp_ptr.to_s)
255
+ end
256
+
257
+ envp_ptr = resolve_stack_deref(processor, envp_ptr)
258
+ # A concrete integer, or a register with nothing useful tracked for it,
259
+ # falls through to the opaque-pointer case (see {#resolvable_stack}).
260
+ stack = envp_ptr.is_a?(OneGadget::Emulators::Lambda) && envp_ptr.deref_count.zero? &&
261
+ resolvable_stack(processor, envp_ptr)
262
+ if stack
263
+ # I haven't see this case after some tests, but just in case :)
264
+ envp = (0..3).map { |i| stack[envp_ptr.immi + processor.class.bits / 8 * i].to_s }
265
+ cons = global_var?(envp[0]) ? nil : "#{envp_ptr} == NULL || {#{envp.join(', ')}, ...} is a valid envp"
266
+ else
267
+ cons = "[#{envp_ptr}] == NULL || #{envp_ptr} == NULL || #{envp_ptr} is a valid envp"
268
+ end
269
+ # What the gadget itself put in the array is the caller's to arrange too,
270
+ # whichever element it landed in (see {#written_into}).
271
+ processor.writes_through(envp_ptr.to_s).each do |value|
272
+ yield "#{value} == NULL || readable: #{value}"
273
+ end
274
+ return nil if cons.nil?
275
+
276
+ yield cons
277
+ end
278
+
279
+ # Resolve +call execl+ cases.
280
+ def resolve_execl(processor)
281
+ return unless str_bin_sh?(processor.argument(0).to_s)
282
+
283
+ args = []
284
+ arg = processor.argument(1).to_s
285
+ if str_sh?(arg)
286
+ arg = processor.argument(2).to_s
287
+ args << '"sh"'
288
+ end
289
+ return nil if global_var?(arg) # we don't want base-related constraints
290
+
291
+ args << arg
292
+ cons = processor.constraints + ["#{arg} == NULL"]
293
+ { constraints: cons, effect: %(execl("/bin/sh", #{args.join(', ')})) }
294
+ end
295
+
296
+ # posix_spawn (*pid, *path, *file_actions, *attrp, argv[], envp[])
297
+ # Constraints are
298
+ # * pid == NULL || *pid is writable
299
+ # * file_actions == NULL || (int) (file_actions->__used) <= 0
300
+ # * attrp == NULL || attrp->flags == 0
301
+ # Meet all constraints then posix_spawn eventually calls execve(path, argv, envp)
302
+ def resolve_posix_spawn(processor)
303
+ args = Array.new(6) { |i| processor.argument(i) }
304
+ # pid/file_actions/attrp are reasoned about as pointers (Lambdas); a
305
+ # concrete non-zero integer there is a fixed address we can't constrain.
306
+ return nil if [args[0], args[2], args[3]].any? { |a| a.is_a?(Integer) && !a.zero? }
307
+
308
+ res = resolve_execve_args(processor, args[1], args[4], args[5], allow_null_argv: false)
309
+ return nil if res.nil?
310
+
311
+ cons = res[:constraints]
312
+ arg0 = args[0]
313
+ if arg0.to_s != '0'
314
+ if arg0.deref_count.zero? && arg0.to_s.include?(processor.sp)
315
+ # Assume stack is always writable, no additional constraints.
316
+ else
317
+ cons << "#{arg0} == NULL || writable: #{arg0}"
318
+ end
319
+ end
320
+ arg2 = args[2]
321
+ cons << "#{arg2} == NULL || (s32)#{(arg2 + 4).deref} <= 0x0" if arg2.to_s != '0'
322
+ arg3 = args[3]
323
+ cons << "#{arg3} == NULL || (u16)#{arg3.deref} == 0x0" if arg3.to_s != '0'
324
+
325
+ { constraints: cons, effect: %(posix_spawn(#{arg0}, "/bin/sh", #{arg2}, #{arg3}, #{args[4]}, #{res[:envp]})) }
326
+ end
327
+
328
+ # Render one argv/envp entry for a constraint. A libc global that points to a
329
+ # fixed string is shown as that string (its true content); a controllable
330
+ # operand, or a global that isn't a plain string, is shown unchanged.
331
+ # @param [String] element A single argv entry.
332
+ # @example +$base+0x16b250+ -> +"--"+ (the do_system separator); +x21+ -> +x21+.
333
+ # @return [String]
334
+ def resolve_argv_element(element)
335
+ content = global_str_content(element)
336
+ content ? content.inspect : element
337
+ end
338
+ end
339
+ end
340
+ end
@@ -14,36 +14,91 @@ module OneGadget
14
14
 
15
15
  private
16
16
 
17
+ # Read as Thumb rather than A32, which objdump assumes for these bytes.
18
+ THUMB = %w[-M force-thumb].freeze
19
+ private_constant :THUMB
20
+
17
21
  # An ARM function's symbol value carries the Thumb bit; the address is what
18
22
  # is left of it.
19
23
  def symbol_address(value)
20
24
  value & ~1
21
25
  end
22
26
 
27
+ # ARM states its two instruction sets in one file, and objdump switches
28
+ # between them on the mapping symbols an ELF carries -- which a file read as
29
+ # bytes has none of. The dynamic symbol table says the same thing in the low
30
+ # bit of every function's value, so each stretch is disassembled as what it
31
+ # says it is.
32
+ # @param [ELFTools::ELFFile] elf
33
+ # @return [void]
34
+ def record_instruction_sets(elf)
35
+ entries = elf.dynamic.symbols.filter_map do |symbol|
36
+ value = symbol.value
37
+ next if value.zero? || symbol.type != ELFTools::Constants::STT_FUNC
38
+
39
+ [symbol_address(value), value.odd?]
40
+ end
41
+ # Only where it changes: a boundary between two functions encoded the same
42
+ # way is one objdump call more for nothing.
43
+ @instruction_sets = entries.sort_by(&:first).chunk_while { |a, b| a[1] == b[1] }.map(&:first)
44
+ end
45
+
46
+ # Split +lo+ to +hi+ where the instruction set changes (see
47
+ # {#record_instruction_sets}), each piece told which one it is.
48
+ # @param [Integer] lo
49
+ # @param [Integer] hi
50
+ # @return [Array<(Integer, Integer, Array<String>)>]
51
+ def decode_ranges(lo, hi)
52
+ return super if @instruction_sets.nil?
53
+
54
+ bounds = @instruction_sets.map(&:first).select { |addr| addr > lo && addr < hi }
55
+ [lo, *bounds, hi].each_cons(2).map { |from, to| [from, to, thumb_at?(from) ? THUMB : []] }
56
+ end
57
+
58
+ # Which instruction set the code at +addr+ is in: the one the last function
59
+ # starting at or before it declared. Thumb is the answer for an address no
60
+ # symbol precedes, being what the rest of the file overwhelmingly is.
61
+ # @param [Integer] addr
62
+ # @return [Boolean]
63
+ def thumb_at?(addr)
64
+ idx = (@instruction_sets.bsearch_index { |entry| entry.first > addr } || @instruction_sets.size) - 1
65
+ idx.negative? || @instruction_sets[idx][1]
66
+ end
67
+
23
68
  # A32 and Thumb both spell a direct call +BL+, in different encodings, and a
24
69
  # Thumb one is not word-aligned, so every halfword has to be considered.
25
70
  def scan_calls(base, data, targets)
26
71
  halves = data.unpack('v*') # 16-bit little-endian halfwords
27
72
  sites = []
73
+ # A whole .text is hundreds of thousands of halfwords, and all but a few
74
+ # open neither encoding, so each is asked what it could be before
75
+ # anything is read or decoded on its behalf.
28
76
  halves.each_with_index do |high, i|
29
- low = halves[i + 1] or next
30
-
31
- # Decoding a target is the expensive half, and a whole .text is hundreds
32
- # of thousands of halfwords: ask what the encoding could be first.
33
- addr = base + i * 2
34
- if thumb_bl?(high, low) && targets.key?(thumb_bl_target(addr, high, low) & ~1)
35
- sites << addr
36
- next
77
+ if THUMB_BL_LEAD.cover?(high) && (low = halves[i + 1]) && thumb_bl_tail?(low)
78
+ addr = base + (i * 2)
79
+ sites << addr if targets.key?(thumb_bl_target(addr, high, low) & ~1)
37
80
  end
38
- next unless i.even? && a32_bl?(low)
81
+ next unless i.even?
39
82
 
83
+ low = halves[i + 1] or next
84
+ next unless a32_bl?(low)
85
+
86
+ addr = base + (i * 2)
40
87
  sites << addr if targets.key?(a32_bl_target(addr, high | (low << 16)))
41
88
  end
42
89
  sites.uniq
43
90
  end
44
91
 
45
- def thumb_bl?(high, low)
46
- high.between?(0xf000, 0xf7ff) && low.allbits?(0xd000)
92
+ # What a Thumb +BL+'s first halfword names the encoding as.
93
+ THUMB_BL_LEAD = (0xf000..0xf7ff)
94
+ private_constant :THUMB_BL_LEAD
95
+
96
+ # Whether the second halfword of a Thumb +BL+ says it is the long form
97
+ # rather than +BLX+.
98
+ # @param [Integer] low
99
+ # @return [Boolean]
100
+ def thumb_bl_tail?(low)
101
+ low.allbits?(0xd000)
47
102
  end
48
103
 
49
104
  # The +cond|101|1+ of an A32 BL, read off the word's top byte -- which is the
@@ -52,6 +107,9 @@ module OneGadget
52
107
  ((low >> 8) & 0x0f) == 0x0b
53
108
  end
54
109
 
110
+ # Where a Thumb +BL+ goes. Its offset is scattered across both halfwords,
111
+ # with the two middle bits stored inverted against the sign bit, and is
112
+ # taken from the address two instructions on.
55
113
  def thumb_bl_target(addr, high, low)
56
114
  s = (high >> 10) & 1
57
115
  i1 = (~(((low >> 13) & 1) ^ s)) & 1
@@ -184,23 +242,47 @@ module OneGadget
184
242
 
185
243
  lines = disasm_lines
186
244
  pattern = reg_patterns(reg)
187
- add_at = pos.downto([0, pos - 400].max).find { |i| lines[i].match?(pattern[:add]) }
245
+ add_at = pos.downto(reachable_from(pos, SETUP_REACH)).find { |i| lines[i].match?(pattern[:add]) }
188
246
  return if add_at.nil?
189
247
 
190
- ldr_at = add_at.downto([0, add_at - 4].max).find { |i| lines[i].match?(pattern[:ldr]) }
248
+ ldr_at = add_at.downto(reachable_from(add_at, LOAD_REACH)).find { |i| lines[i].match?(pattern[:ldr]) }
191
249
  return if ldr_at.nil?
192
250
 
193
251
  [lines[ldr_at], lines[add_at]]
194
252
  end
195
253
 
254
+ # How far back to look for each half of the pair, in lines.
255
+ SETUP_REACH = 400
256
+ LOAD_REACH = 4
257
+ private_constant :SETUP_REACH, :LOAD_REACH
258
+
259
+ # The earliest line back from +pos+ that the line at +pos+ follows on from.
260
+ # {#disasm_lines} holds a window per terminal call, so a window's first line
261
+ # is where the code around it starts.
262
+ # @param [Integer] pos
263
+ # @param [Integer] reach
264
+ # @return [Integer]
265
+ # @example Where line 10 opens a window, line 12 can see back only that far.
266
+ # window_starts #=> { 10 => true }
267
+ # reachable_from(12, 400) #=> 10
268
+ # reachable_from(9, 400) #=> 0
269
+ def reachable_from(pos, reach)
270
+ floor = [0, pos - reach].max
271
+ (floor..pos).reverse_each.find { |i| window_starts.key?(i) } || floor
272
+ end
273
+
196
274
  def branch_lead_chars
197
275
  'bct'
198
276
  end
199
277
 
200
- # +b+ is unconditional; +bne+/+beq+/... and Thumb +cbz+/+cbnz+ are
201
- # conditional (+bl+/+blx+ are calls, not branches). +bx+/table branches and
202
- # returns via +pop {..,pc}+ / +ldm .. {..,pc}+ / +mov pc,..+ / +ldr pc,..+
203
- # terminate the path.
278
+ # Which kind of transfer +line+ is, or +nil+ for one that is neither -- a
279
+ # call among them.
280
+ # @example Thumb +cbz+/+cbnz+ count as conditional, while +bx+, a table
281
+ # branch and any return through +pc+ end the path.
282
+ # branch_kind('4a1c0: b 4a200') #=> :unconditional
283
+ # branch_kind('4a1c0: bne 4a200') #=> :conditional
284
+ # branch_kind('4a1c0: bl 73ba0') #=> nil
285
+ # branch_kind('4a1c0: pop {r4, pc}') #=> :terminator
204
286
  def branch_kind(line)
205
287
  m = branch_mnemonic(line)
206
288
  return :conditional if conditional_mnemonic?(m)
@@ -224,33 +306,6 @@ module OneGadget
224
306
  def call_str
225
307
  'bl'
226
308
  end
227
-
228
- def bin_sh_offset
229
- @bin_sh_offset ||= str_offset('/bin/sh')
230
- end
231
-
232
- def str_bin_sh?(str)
233
- str.include?('$base') && str.include?(bin_sh_offset.to_s(16))
234
- end
235
-
236
- # Offset of the standalone "sh" string (\0-preceded and \0-terminated) that
237
- # glibc passes as argv[0] in execl("/bin/sh", "sh", ...). Its distance from
238
- # "/bin/sh" is build-specific, so locate it directly instead of guessing.
239
- # +nil+ when the libc has no such string.
240
- def sh_offset
241
- return @sh_offset if defined?(@sh_offset)
242
-
243
- idx = File.binread(file).index("\x00sh\x00")
244
- @sh_offset = idx && idx + 1
245
- end
246
-
247
- def str_sh?(str)
248
- !sh_offset.nil? && str.include?('$base') && str.include?(sh_offset.to_s(16))
249
- end
250
-
251
- def global_var?(str)
252
- base_relative?(str, '$base')
253
- end
254
309
  end
255
310
  end
256
311
  end