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
@@ -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
- gadgets = []
82
- # Overlapping candidate paths share tails, so the same suffix (a start line
83
- # and everything after it) recurs across candidates; emulate each once.
84
- seen = {}
85
- candidates.each do |cand|
86
- executed_windows(cand.lines) do |suffix|
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+ -- a start line and everything after it, longest
100
- # last -- cut down to the part that runs: emulation ends at the terminal
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
- # The mnemonic must be a call, so a branch whose target symbol merely looks
126
- # similar (+<execlp@@GLIBC_2.4+0x136>+) doesn't end the window.
93
+ # @param [String] line One disassembled line.
94
+ # @return [Boolean]
127
95
  def terminal_call_line?(line)
128
- return false unless line[/\A\s*[0-9a-f]+:\s*(\S+)/, 1]&.match?(/\A#{call_str}x?(?:\.[wn])?\z/)
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
- # Generating constraints for being a valid gadget.
185
- # @param [OneGadget::Emulators::Processor] processor The processor after executing the gadgets.
186
- # @return [Hash{Symbol => Array<String>, String}?]
187
- # The options to create a {OneGadget::Gadget::Gadget} object.
188
- # Keys might be:
189
- # 1. constraints: Array<String> List of constraints.
190
- # 2. effect: String Result function call of this gadget.
191
- # If the constraints can never be satisfied, +nil+ is returned.
192
- def resolve(processor)
193
- call = processor.registers[processor.pc].to_s
194
- return resolve_posix_spawn(processor) if call.include?('posix_spawn')
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
- # Generate the +argv+-related constraint for an +exec*+ call.
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
- # @param [String] argv_ptr The pointer to the argv array. See {#check_argv}.
328
- # @param [Array<String>] argv The argv entries +argv_ptr+ points to, i.e. +[argv[0], .., argv[3]]+.
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
- argv_cons += ', ...' unless argv_cons.end_with?('NULL')
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 +argv+ invokes the shell with a fixed option word that disables
355
- # command execution. execve's program is always "/bin/sh", so +argv[1]+ is
356
- # that shell's option word; a libc-global bundle carrying the noexec flag
357
- # there yields a shell that reaches execve yet can never run a command --
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 decided by argv[1]'s libc-global content (via global_str_content)
362
- # # content "-nc" carries bash's noexec 'n' => true;
363
- # # "-c" runs the command => false; a non-global (attacker) word => false
364
- def noexec_shell_argv?(argv)
365
- opt = global_str_content(argv[1])
366
- !opt.nil? && opt.match?(/\A-[a-zA-Z]*n[a-zA-Z]*\z/)
367
- end
368
-
369
- # Handle the case where +argv_ptr+ is not a plain stack pointer (e.g. a register or global variable).
370
- # @param [String] argv_ptr The pointer to the argv array. See {#check_argv}.
371
- def check_nonstack_argv(argv_ptr, allow_null)
372
- if allow_null
373
- "[#{argv_ptr}] == NULL || #{argv_ptr} == NULL || #{argv_ptr} is a valid argv"
374
- else
375
- "[#{argv_ptr}] == NULL || #{argv_ptr} is a valid argv"
376
- end
377
- end
378
-
379
- # If +ptr+ is a single dereference of a tracked stack slot, resolve it to
380
- # that slot's own tracked value so the rest of argv/envp resolution can
381
- # treat it like a bare register instead of an opaque pointer.
382
- # @param [OneGadget::Emulators::Processor] processor
383
- # @param [OneGadget::Emulators::Lambda, Integer] ptr An argv_ptr/envp_ptr.
384
- # @return [OneGadget::Emulators::Lambda, Integer] +ptr+, or the value it
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
- # Resolve +call execl+ cases.
437
- def resolve_execl(processor)
438
- return unless str_bin_sh?(processor.argument(0).to_s)
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
- args = []
441
- arg = processor.argument(1).to_s
442
- if str_sh?(arg)
443
- arg = processor.argument(2).to_s
444
- args << '"sh"'
445
- end
446
- return nil if global_var?(arg) # we don't want base-related constraints
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
- def global_var?(_str); raise NotImplementedError
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
- def str_bin_sh?(_str); raise NotImplementedError
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
- def str_sh?(_str); raise NotImplementedError
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: the same expression twice, two
623
- # concrete values, or two offsets from one base (+r1+ against +r1+0x4+ can
624
- # never be equal). +nil+ leaves the relation a real constraint.
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 target's objdump disassembly as stripped +"ADDR: insn"+ lines.
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