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,478 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'elftools'
4
+
5
+ require 'one_gadget/emulators/mips'
6
+ require 'one_gadget/fetchers/base'
7
+
8
+ module OneGadget
9
+ module Fetchers
10
+ # Fetcher for MIPS (32-bit, o32).
11
+ #
12
+ # Two things about this architecture are unlike every other one supported, and
13
+ # both are answered here so that nothing about them reaches the engine:
14
+ #
15
+ # * a call states no target -- it goes through a register loaded from the GOT,
16
+ # so the callee's name has to be resolved and written where every other arch
17
+ # has one already ({#name_got_calls});
18
+ # * a branch or call has a *delay slot*: the instruction after it runs before
19
+ # it takes effect. {OneGadget::Emulators::Mips#process!} holds the transfer
20
+ # back so both run in that order, which leaves the disassembly in the order
21
+ # objdump wrote it and every address meaning what it says. Two seams follow
22
+ # from it, and they are the only ones: the instruction after a call belongs
23
+ # to the window that ends at the call ({#emulate}), and the edge into a
24
+ # branch's target leaves from the delay slot rather than the branch
25
+ # ({#branch_pred_map}).
26
+ class Mips < Base
27
+ # Everything this arch reaches -- its calls and its globals alike -- goes
28
+ # through the GOT base in +gp+, which is a precondition the caller arranges
29
+ # by setting that register. Say so, and drop the read/write requirements
30
+ # rooted there: the GOT is a fixed, mapped libc address, so reaching through
31
+ # it asks nothing further of the caller (as i386 does for its own GOT
32
+ # register).
33
+ # @param [OneGadget::Emulators::Processor] processor
34
+ # @return [Hash, nil]
35
+ def resolve(processor)
36
+ res = super
37
+ return if res.nil?
38
+
39
+ got = got_base_constraint(processor, GOT_BASE) or return nil
40
+
41
+ res[:constraints].unshift(*got_preconditions(processor, got))
42
+ res[:constraints].reject! { |con| con.match?(/\A(?:writable|readable): \[*#{GOT_BASE}\b/) }
43
+ res
44
+ end
45
+
46
+ # This arch reads a global through the GOT and resolves the slot to the
47
+ # address it holds, so what reaches a call is one dereference of the variable
48
+ # rather than two of the slot naming it. Which variable that is comes from
49
+ # the symbols already read for the table.
50
+ # @param [String] str A rendered value.
51
+ # @return [Boolean]
52
+ def environ?(str)
53
+ got = mips_got or return false
54
+ offset = string_file_offset(str.delete('[]')) or return false
55
+
56
+ ENVIRON.match?(got[:names][offset].to_s)
57
+ end
58
+
59
+ # What the caller must arrange for this window to reach the GOT. Normally
60
+ # just the register itself -- but o32 has the *caller* restore it after
61
+ # every call, because the callee establishes its own, so a window that runs
62
+ # past a call reads the table through whatever it restored from. Every call
63
+ # it makes after that point was named on the assumption that this is the
64
+ # GOT, so say so rather than leaving it unsaid.
65
+ # @param [OneGadget::Emulators::Processor] processor
66
+ # @param [String] got The constraint naming the register itself.
67
+ # @return [Array<String>]
68
+ # @example a window that restores gp from its frame
69
+ # got_preconditions(processor, 'gp is the GOT address of libc')
70
+ # #=> ['gp is the GOT address of libc', '[sp+0x18] is the GOT address of libc']
71
+ def got_preconditions(processor, got)
72
+ held = processor.registers[GOT_BASE].to_s
73
+ return [got] if held == GOT_BASE
74
+
75
+ [got, "#{held} is the GOT address of libc"]
76
+ end
77
+
78
+ # A candidate may begin at a delay slot -- entering there runs it and falls
79
+ # past the transfer it belongs to -- but it may not then *follow* that
80
+ # transfer, which never executed. Such a window shows it by its second line
81
+ # not being the next instruction along; entering one instruction earlier, at
82
+ # the transfer itself, is the separate and valid window that does follow it.
83
+ # @param [Array<String>] lines One candidate, as a line list.
84
+ # @yieldparam [Array<String>] window
85
+ # @return [void]
86
+ def executed_windows(lines)
87
+ super do |window|
88
+ next if follows_a_transfer_it_skipped?(window) || enters_at_an_unset_call?(window)
89
+ next if calls_without_the_callee_in_t9?(window)
90
+
91
+ yield(window)
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ # This arch spells every instruction in one word.
98
+ INSTRUCTION_SIZE = 4
99
+ private_constant :INSTRUCTION_SIZE
100
+
101
+ # Whether the window opens on a call through a register it never set, so that
102
+ # where it goes is whatever the caller happened to leave there. Refusing
103
+ # costs nothing: the window one instruction earlier loads it itself.
104
+ # @param [Array<String>] window
105
+ # @return [Boolean]
106
+ # @example The load that names the call is behind the window's first line.
107
+ # enters_at_an_unset_call?(['4b3e0: jalr t9 <posix_spawnattr_init>']) #=> true
108
+ # enters_at_an_unset_call?(['4b3dc: lw t9,-31652(gp)', '4b3e0: jalr t9']) #=> false
109
+ def enters_at_an_unset_call?(window)
110
+ mnemonic(window.first) == 'jalr'
111
+ end
112
+
113
+ # Whether the window reaches a call whose callee derives its own GOT base
114
+ # from +t9+, without the window having aimed +t9+ at it.
115
+ # @param [Array<String>] window
116
+ # @return [Boolean]
117
+ # @example The same call, with and without the load that aims at it.
118
+ # aimed = ['66860: lw t9,-30292(gp)', '66864: bal 6653c <posix_spawnattr_init>']
119
+ # calls_without_the_callee_in_t9?(aimed) #=> false
120
+ # calls_without_the_callee_in_t9?(aimed[1..]) #=> true
121
+ def calls_without_the_callee_in_t9?(window)
122
+ held = nil
123
+ window.each do |line|
124
+ if (load = line.match(GOT_LOAD))
125
+ held = got_address(load[1].to_i)
126
+ elsif (call = line.match(DIRECT_CALL))
127
+ target = call[1].to_i(16)
128
+ return true if held != target && derives_got_base?(target)
129
+ elsif line.match?(TARGET_WRITE)
130
+ held = nil
131
+ end
132
+ end
133
+ false
134
+ end
135
+
136
+ # A call that states its destination, as opposed to one through a register.
137
+ DIRECT_CALL = /:\s*(?:bal|jal)\s+([0-9a-f]+)/
138
+ private_constant :DIRECT_CALL
139
+
140
+ # Whether the function at +address+ derives its own GOT base, which o32 asks
141
+ # a callee to do from the address the caller leaves in +t9+ -- so a caller
142
+ # that has not put it there is calling a function that cannot find anything.
143
+ # @param [Integer] address
144
+ # @return [Boolean]
145
+ # @example The prologue this recognises, and a leaf that has none.
146
+ # derives_got_base?(0x6653c) #=> true (lui gp / addiu gp / addu gp,gp,t9)
147
+ # derives_got_base?(0x66594) #=> false (sltiu v0,a1,256 / ...)
148
+ def derives_got_base?(address)
149
+ code = loaded_code or return false
150
+
151
+ index = (address - code[:base]) / INSTRUCTION_SIZE
152
+ return false if index.negative?
153
+
154
+ PROLOGUE_WORDS.times.any? { |i| code[:words][index + i] == GOT_BASE_FROM_TARGET }
155
+ end
156
+
157
+ # How far into a function to look for that prologue: it is the first thing a
158
+ # function does, ahead of anything that could need what it computes.
159
+ PROLOGUE_WORDS = 4
160
+ private_constant :PROLOGUE_WORDS
161
+
162
+ # +addu gp,gp,t9+, the instruction the prologue ends with.
163
+ GOT_BASE_FROM_TARGET = 0x0399e021
164
+ private_constant :GOT_BASE_FROM_TARGET
165
+
166
+ # The code the file loads, so that an instruction can be read at an address
167
+ # without disassembling anything around it.
168
+ # @return [Hash{Symbol => Integer, Array<Integer>}, nil]
169
+ # @example The word posix_spawnattr_init opens with, +lui gp,0x6+.
170
+ # code = loaded_code
171
+ # code[:words][(0x6653c - code[:base]) / 4] #=> 0x3c1c0006
172
+ def loaded_code
173
+ return @loaded_code if defined?(@loaded_code)
174
+
175
+ @loaded_code = File.open(file) do |fd|
176
+ elf = ELFTools::ELFFile.new(fd)
177
+ segment = executable_segment(elf)
178
+ segment && { base: segment.mem_head,
179
+ words: segment.data.unpack(elf.endian == :big ? 'N*' : 'V*') }
180
+ end
181
+ end
182
+
183
+ # @param [Array<String>] window
184
+ # @return [Boolean] Whether it starts at a delay slot and then takes the
185
+ # branch that delay slot belongs to.
186
+ def follows_a_transfer_it_skipped?(window)
187
+ return false if window.size < 2
188
+
189
+ offset_of(window[1]) != offset_of(window.first) + INSTRUCTION_SIZE
190
+ end
191
+
192
+ # The register o32 states the GOT base in.
193
+ GOT_BASE = 'gp'
194
+ private_constant :GOT_BASE
195
+
196
+ # Every instruction is one word here and word-aligned, so the whole scan is
197
+ # a masked compare per word. A call spells its destination two ways: a
198
+ # direct one carries it, and the indirect one PIC code reaches everything
199
+ # through names the GOT slot it is taken from.
200
+ def scan_calls(base, data, targets)
201
+ got = mips_got or return nil
202
+
203
+ sites = []
204
+ data.unpack(got[:big] ? 'N*' : 'V*').each_with_index do |word, i|
205
+ next unless CALL_OPCODES.key?(word >> OPCODE_SHIFT)
206
+
207
+ address = base + (i * INSTRUCTION_SIZE)
208
+ destination = call_destination(word, address)
209
+ sites << address if destination && targets.key?(destination)
210
+ end
211
+ sites
212
+ end
213
+
214
+ # How far the opcode sits from the bottom of a word.
215
+ OPCODE_SHIFT = 26
216
+ private_constant :OPCODE_SHIFT
217
+
218
+ # The opcodes a call can be spelled in -- the load, the branch-and-link and
219
+ # the jump-and-link below -- so that a word which is none of them is passed
220
+ # over without being decoded. Most of a .text is.
221
+ CALL_OPCODES = { 0x23 => true, 0x01 => true, 0x03 => true }.freeze
222
+ private_constant :CALL_OPCODES
223
+
224
+ # Where a call at +address+ goes.
225
+ # @param [Integer] word The instruction word.
226
+ # @param [Integer] address
227
+ # @return [Integer, nil] +nil+ when the word is not a call, or is one whose
228
+ # destination these bytes alone do not say.
229
+ # @example
230
+ # # lw t9,-32744(gp), whose GOT slot holds _setjmp
231
+ # call_destination(0x8f998018, 0x4b3dc) #=> 0x39190
232
+ # # bal 4b3f0
233
+ # call_destination(0x04110004, 0x4b3dc) #=> 0x4b3f0
234
+ def call_destination(word, address)
235
+ following = address + INSTRUCTION_SIZE
236
+ case word & OPCODE_AND_TARGET_REGISTERS
237
+ when GOT_CALL_LOAD then got_address(immediate(word))
238
+ when BAL then following + (immediate(word) * INSTRUCTION_SIZE)
239
+ else ((following & JAL_REGION) | ((word & JAL_TARGET) << 2) if (word >> OPCODE_SHIFT) == JAL_OPCODE)
240
+ end
241
+ end
242
+
243
+ # The high half of a word, which is opcode plus whichever registers an
244
+ # instruction of that opcode names there.
245
+ OPCODE_AND_TARGET_REGISTERS = 0xffff0000
246
+ private_constant :OPCODE_AND_TARGET_REGISTERS
247
+
248
+ # +lw t9,<imm>(gp)+ -- the load of a call's destination out of the GOT.
249
+ GOT_CALL_LOAD = 0x8f990000
250
+ private_constant :GOT_CALL_LOAD
251
+
252
+ # +bal <imm>+, the pc-relative direct call, spelled +bgezal zero,<imm>+.
253
+ BAL = 0x04110000
254
+ private_constant :BAL
255
+
256
+ # +jal <target>+, the direct call that states an absolute address, which it
257
+ # can only do within the 256MB region it is itself in.
258
+ JAL_OPCODE = 3
259
+ JAL_TARGET = 0x03ffffff
260
+ JAL_REGION = 0xf0000000
261
+ private_constant :JAL_OPCODE, :JAL_TARGET, :JAL_REGION
262
+
263
+ # The signed offset an instruction states in its low half.
264
+ # @param [Integer] word The instruction word.
265
+ # @return [Integer]
266
+ def immediate(word)
267
+ half = word & 0xffff
268
+ half >= 0x8000 ? half - 0x10000 : half
269
+ end
270
+
271
+ # A call, however it is spelled: through a register (which is how PIC code
272
+ # reaches everything) or directly.
273
+ def call_str = '(?:jalr|jal|bal)'
274
+
275
+ def branch_lead_chars = 'bj'
276
+
277
+ # +b+ is the unconditional branch, the compare-and-branch family is
278
+ # conditional, and +jr+ (which is how a return is written) ends the path.
279
+ # Calls are not branches: the walk stitches the window they target.
280
+ def branch_kind(line)
281
+ mnem = mnemonic(line)
282
+ return :conditional if OneGadget::Emulators::Mips::COND.key?(mnem)
283
+ return :unconditional if mnem == 'b'
284
+
285
+ :terminator if %w[jr j].include?(mnem)
286
+ end
287
+
288
+ def emulator = OneGadget::Emulators::Mips.new
289
+
290
+ # Rewrite the disassembly into what the engine reads everywhere else: every
291
+ # call named.
292
+ def objdump_lines(start: nil, stop: nil, extra: [])
293
+ state_got_values(name_got_calls(super))
294
+ end
295
+
296
+ # A window ends at the call that ends the gadget, but that call's delay slot
297
+ # runs before control leaves -- it is where an argument is often set -- so it
298
+ # is part of what the window executes.
299
+ # @param [Array<String>] cmds
300
+ # @return [OneGadget::Emulators::Processor]
301
+ def emulate(cmds)
302
+ super(terminal_call_line?(cmds.last) ? cmds + delay_slot_after(cmds.last) : cmds)
303
+ end
304
+
305
+ # The instruction a transfer delays behind, as a one-element list, or none
306
+ # when the disassembly does not carry it (it starts another window).
307
+ # @param [String] line
308
+ # @return [Array<String>]
309
+ def delay_slot_after(line)
310
+ index = disasm_index[offset_of(line)]
311
+ return [] if index.nil? || window_starts.key?(index + 1)
312
+
313
+ [disasm_lines[index + 1]].compact
314
+ end
315
+
316
+ # A branch takes effect only after its delay slot, so the edge is recorded as
317
+ # leaving the instruction *after* the branch -- a path through it then reads
318
+ # in the order it executes.
319
+ # @return [Hash{Integer => Array<Integer>}]
320
+ def branch_pred_map
321
+ @branch_pred_map ||= delayed_edges(super)
322
+ end
323
+
324
+ # Move each edge on by one instruction, dropping any whose delay slot the
325
+ # disassembly does not carry because another window starts there.
326
+ # @param [Hash{Integer => Array<Integer>}] map
327
+ # @return [Hash{Integer => Array<Integer>}]
328
+ def delayed_edges(map)
329
+ map.to_h { |target, indexes| [target, indexes.filter_map { |i| i + 1 unless window_starts.key?(i + 1) }] }
330
+ end
331
+
332
+ # The call this arch actually uses names no target: the callee is loaded out
333
+ # of the GOT into +t9+ and jumped to. Resolve the slot and write the name
334
+ # beside the call, which is where the engine reads one.
335
+ # @param [Array<String>] lines
336
+ # @return [Array<String>]
337
+ # @example
338
+ # name_got_calls(['4b3dc: lw t9,-31652(gp)', '4b3e0: jalr t9'])
339
+ # #=> ['4b3dc: lw t9,-31652(gp)', '4b3e0: jalr t9 <posix_spawnattr_init>']
340
+ def name_got_calls(lines)
341
+ loaded = nil
342
+ lines.map do |line|
343
+ if (m = line.match(GOT_LOAD))
344
+ loaded = m[1].to_i
345
+ elsif line.match?(INDIRECT_CALL)
346
+ name = loaded && got_symbol(loaded)
347
+ loaded = nil
348
+ next name ? "#{line} <#{name}>" : line
349
+ elsif line.match?(TARGET_WRITE)
350
+ # the call no longer goes where that offset said
351
+ loaded = nil
352
+ end
353
+ line
354
+ end
355
+ end
356
+
357
+ # A libc global is reached through the GOT as well: the slot holds the
358
+ # address, and an +addiu+ applies the offset within it. What the slot holds
359
+ # is in the file, so state it beside the load -- which is what lets the
360
+ # value read as +$base+<off>+, the form every other architecture produces
361
+ # for a global.
362
+ # @param [Array<String>] lines
363
+ # @return [Array<String>]
364
+ # @example
365
+ # state_got_values(['77b44: lw a0,-32496(gp)'])
366
+ # #=> ['77b44: lw a0,-32496(gp) # b0000']
367
+ def state_got_values(lines)
368
+ lines.map do |line|
369
+ m = line.match(GOT_GLOBAL_LOAD) or next line
370
+
371
+ address = got_address(m[1].to_i)
372
+ address.nil? || address.zero? ? line : "#{line} # #{format('%x', address)}"
373
+ end
374
+ end
375
+
376
+ # Loading anything but the call target out of the GOT.
377
+ GOT_GLOBAL_LOAD = /:\s*lw\s+(?!t9,)\w+,(-?\d+)\(gp\)/
378
+ private_constant :GOT_GLOBAL_LOAD
379
+
380
+ # Loading the call target out of the GOT, which is +gp+-relative.
381
+ GOT_LOAD = /:\s*lw\s+t9,(-?\d+)\(gp\)/
382
+ private_constant :GOT_LOAD
383
+
384
+ # The call through it. o32 requires the callee's address in +t9+ -- that is
385
+ # how the callee computes its own +gp+ -- so a call always reads that
386
+ # register, optionally naming the one the return address goes to.
387
+ INDIRECT_CALL = /:\s*jalr\s+(?:\w+,)?t9\s*\z/
388
+ private_constant :INDIRECT_CALL
389
+
390
+ # Anything else that writes the call register. Only the load that last wrote
391
+ # +t9+ says where the call goes: most calls reach it another way (out of a
392
+ # struct, or from a register), and carrying a GOT offset over one of those
393
+ # would name the call after a function it never reaches. A store reads the
394
+ # register rather than writing it.
395
+ TARGET_WRITE = /:\s*(?!s[whb]\b)\S+\s+t9,/
396
+ private_constant :TARGET_WRITE
397
+
398
+ # What a +gp+-relative GOT slot holds, as +[address, name]+, or +nil+ for one
399
+ # this cannot read. Kept, since a libc reaches the same slot from every call
400
+ # site that goes through it.
401
+ # @param [Integer] gp_offset The offset as the instruction writes it.
402
+ # @return [(Integer, String), nil]
403
+ def got_entry(gp_offset)
404
+ return @got_entries[gp_offset] if @got_entries&.key?(gp_offset)
405
+
406
+ (@got_entries ||= {})[gp_offset] = read_got_entry(gp_offset)
407
+ end
408
+
409
+ # The slot itself, out of the file. This arch states its GOT in the dynamic
410
+ # segment, so the answer is there even for a file with no sections at all.
411
+ # @param [Integer] gp_offset The offset as the instruction writes it.
412
+ # @return [(Integer, String), nil]
413
+ # @example An entry below +DT_MIPS_LOCAL_GOTNO+ holds an address outright;
414
+ # the rest correspond one for one with the dynamic symbols.
415
+ # read_got_entry(-32744) #=> [0x39190, '_setjmp']
416
+ # read_got_entry(-32752) #=> [0, nil]
417
+ def read_got_entry(gp_offset)
418
+ got = mips_got or return nil
419
+
420
+ index = (GP_BIAS + gp_offset) / 4
421
+ if index < got[:local]
422
+ address = local_got_entry(got, index)
423
+ return address && [address, got[:names][address]]
424
+ end
425
+ got[:symbols][got[:gotsym] + index - got[:local]]
426
+ end
427
+
428
+ # @param [Integer] gp_offset
429
+ # @return [String, nil] The symbol the slot names.
430
+ def got_symbol(gp_offset) = got_entry(gp_offset)&.last
431
+
432
+ # @param [Integer] gp_offset
433
+ # @return [Integer, nil] The address the slot holds.
434
+ def got_address(gp_offset) = got_entry(gp_offset)&.first
435
+
436
+ # +gp+ points this far into the GOT, so that one signed 16-bit offset reaches
437
+ # the most of it. Every +gp+-relative offset is read against it.
438
+ GP_BIAS = 0x7ff0
439
+ private_constant :GP_BIAS
440
+
441
+ # What a local GOT entry holds: the address itself, written in the file.
442
+ # @param [Hash] got
443
+ # @param [Integer] index
444
+ # @return [Integer, nil]
445
+ def local_got_entry(got, index)
446
+ file_bytes[got[:offset] + (index * 4), 4]&.unpack1(got[:big] ? 'N' : 'V')
447
+ end
448
+
449
+ # Where this file's GOT is and how to read it. The entries below
450
+ # +DT_MIPS_LOCAL_GOTNO+ hold an address outright; the rest correspond one for
451
+ # one with the dynamic symbols, starting at +DT_MIPS_GOTSYM+.
452
+ # @return [Hash, nil] +nil+ when the file states no GOT.
453
+ def mips_got
454
+ return @mips_got if defined?(@mips_got)
455
+
456
+ @mips_got = File.open(file) { |fd| read_mips_got(ELFTools::ELFFile.new(fd)) }
457
+ end
458
+
459
+ # @param [ELFTools::ELFFile] elf
460
+ # @return [Hash, nil]
461
+ def read_mips_got(elf)
462
+ dynamic = elf.segment_by_type(:dynamic) or return nil
463
+ address = dynamic.tag_by_type(:pltgot)&.value or return nil
464
+ local = dynamic.tag_by_type(:mips_local_gotno)&.value or return nil
465
+ gotsym = dynamic.tag_by_type(:mips_gotsym)&.value or return nil
466
+ segment = elf.segments_by_type(:load).find { |seg| seg.vma_in?(address) } or return nil
467
+
468
+ # Read out of the symbols now, rather than holding them: they are lazy, and
469
+ # the file they would read from is closed as soon as this returns.
470
+ symbols = dynamic.symbols.map { |symbol| [symbol.value, symbol.name] }
471
+ names = symbols.to_h { |value, name| [value, name] }
472
+ .reject { |value, name| value.zero? || name.empty? }
473
+ { local:, gotsym:, symbols:, names:, big: elf.endian == :big,
474
+ offset: segment.vma_to_offset(address) }
475
+ end
476
+ end
477
+ end
478
+ end
@@ -19,22 +19,35 @@ module OneGadget
19
19
  @options = []
20
20
  end
21
21
 
22
+ # Read the file as a raw blob at +vma+ instead of as an ELF, for one whose
23
+ # section headers are gone: objdump disassembles sections, and a file with
24
+ # none disassembles to nothing at all.
25
+ # @param [String] machine The objdump architecture name, as {OneGadget::Helper.objdump_arch} gives it.
26
+ # @param [Symbol] endian +:little+ or +:big+.
27
+ # @param [Integer] vma What the first byte of the file is loaded at.
28
+ # @return [void]
29
+ def read_raw(machine:, endian:, vma:)
30
+ @raw = { machine:, endian:, vma: }
31
+ end
32
+
22
33
  # Set the extra options to be passed to objdump.
23
34
  # @param [Array<String>] options The options.
24
35
  # @example
25
36
  # objdump.extra_options = %w[-M intel]
37
+ # @return [void]
26
38
  def extra_options=(options)
27
39
  @options = options
28
40
  end
29
41
 
30
42
  # @param [Integer] start The start address to be dumpped from.
31
43
  # @param [Integer] stop The end address.
44
+ # @param [Array<String>] extra Options for this range alone, on top of {#extra_options=}.
32
45
  # @return [String] The CLI command to be executed.
33
- def command(start: nil, stop: nil)
46
+ def command(start: nil, stop: nil, extra: [])
34
47
  # --dwarf-start=0 is to make sure `suppress_bfd_header` is true to eliminate the file path in the output, see
35
48
  # issue #204 for more details.
36
49
  # Note: We might need to update this when the objdump act differently in the future.
37
- cmd = [bin, '--dwarf-start=0', '--no-show-raw-insn', '-w', '-d', *@options, @file]
50
+ cmd = [bin, '--dwarf-start=0', '--no-show-raw-insn', '-w', *disassemble_options, *@options, *extra, @file]
38
51
  cmd.push('--start-address', start) if start
39
52
  cmd.push('--stop-address', stop) if stop
40
53
  ::Shellwords.join(cmd)
@@ -42,6 +55,15 @@ module OneGadget
42
55
 
43
56
  private
44
57
 
58
+ # +-d+ walks the sections; a file read as a blob has none, so everything in
59
+ # it is disassembled (+-D+) and told what it is and where it lives.
60
+ def disassemble_options
61
+ return ['-d'] if @raw.nil?
62
+
63
+ ['-D', '-b', 'binary', '-m', @raw[:machine], @raw[:endian] == :big ? '-EB' : '-EL',
64
+ "--adjust-vma=#{@raw[:vma]}"]
65
+ end
66
+
45
67
  def bin
46
68
  OneGadget::Helper.find_objdump(@arch).tap do |bin|
47
69
  install_objdump_guide! if bin.nil?
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'one_gadget/emulators/riscv64'
4
+ require 'one_gadget/fetchers/base'
5
+
6
+ module OneGadget
7
+ module Fetchers
8
+ # Fetcher for RISC-V (RV64).
9
+ class Riscv64 < Base
10
+ private
11
+
12
+ # +jal+ is the only direct call and is four bytes wide, carrying its target as
13
+ # a signed offset from itself. Instructions are two-byte aligned, since a
14
+ # compressed one is half a word, so every two-byte position is read as a
15
+ # possible +jal+ rather than every fourth: a false positive only adds a
16
+ # window nothing is found in, while a missed call costs the gadgets around it.
17
+ def scan_calls(base, data, targets)
18
+ halves = data.unpack('v*')
19
+ sites = []
20
+ halves.each_with_index do |low, i|
21
+ next unless (low & 0x7f) == JAL_OPCODE
22
+
23
+ high = halves[i + 1]
24
+ break if high.nil?
25
+
26
+ addr = base + (i * 2)
27
+ sites << addr if targets.key?(addr + jal_offset(low | (high << 16)))
28
+ end
29
+ sites
30
+ end
31
+
32
+ # The J-type opcode, which lives in the low half of the instruction word.
33
+ JAL_OPCODE = 0x6f
34
+ private_constant :JAL_OPCODE
35
+
36
+ # The signed offset a +jal+ carries. The encoding scatters its bits --
37
+ # +[20|10:1|11|19:12]+, with bit 0 always zero since a target is two-byte
38
+ # aligned -- so they are put back in order before the sign is applied.
39
+ # @param [Integer] word The instruction word.
40
+ # @return [Integer]
41
+ def jal_offset(word)
42
+ imm = (((word >> 31) & 0x1) << 20) |
43
+ (((word >> 12) & 0xff) << 12) |
44
+ (((word >> 20) & 0x1) << 11) |
45
+ (((word >> 21) & 0x3ff) << 1)
46
+ imm.anybits?(1 << 20) ? imm - (1 << 21) : imm
47
+ end
48
+
49
+ def emulator
50
+ OneGadget::Emulators::Riscv64.new
51
+ end
52
+
53
+ def branch_lead_chars
54
+ 'bj'
55
+ end
56
+
57
+ # Every conditional branch compares two registers directly -- there is no
58
+ # flag register and no compare instruction -- so the whole family, including
59
+ # the pseudo-instructions the assembler spells against +zero+, starts with
60
+ # +b+. +jalr+ ends a path: it is an indirect jump, and where it goes is not
61
+ # something the disassembly says.
62
+ def branch_kind(line)
63
+ m = mnemonic(line)
64
+ return :conditional if m.start_with?('b')
65
+ return :unconditional if m == 'j'
66
+
67
+ :terminator if %w[ret jr jalr].include?(m)
68
+ end
69
+
70
+ # +jal+ is the direct call. A tail +j+ into an +exec*+ entry reaches one too,
71
+ # but it is a jump, not a call, and is left out for the same reason x86 counts
72
+ # only +call+: the walker stitches such a jump into the window it targets.
73
+ def call_str
74
+ 'jal'
75
+ end
76
+ end
77
+ end
78
+ end
@@ -12,6 +12,25 @@ module OneGadget
12
12
 
13
13
  private
14
14
 
15
+ # Emulate only the candidates that could still become a gadget. x86 writes
16
+ # the address of a string as a displacement, so a window that reaches
17
+ # +"/bin/sh"+ names it outright -- except through +posix_spawn+, which takes
18
+ # the path as an argument a window need not have materialised itself.
19
+ #
20
+ # This lives here rather than in {Base} because an architecture building such
21
+ # an address in pieces (+adrp+ then +add+) can state no such rule: filtering
22
+ # aarch64 on the same test drops all but one of its gadgets.
23
+ def candidates
24
+ reference = bin_sh_reference.to_s(16)
25
+ super do |candidate|
26
+ candidate.match?(TERMINAL_SPAWN) || candidate.include?(reference)
27
+ end
28
+ end
29
+
30
+ # The number a window holds when it points at the +"/bin/sh"+ string.
31
+ # @return [Integer]
32
+ def bin_sh_reference = bin_sh_offset
33
+
15
34
  # A direct near call, +E8+ then a 32-bit displacement from the instruction
16
35
  # after it.
17
36
  CALL_REL32 = "\xe8".b.freeze
@@ -5,6 +5,8 @@ require 'one_gadget/fetchers/aarch64'
5
5
  require 'one_gadget/fetchers/amd64'
6
6
  require 'one_gadget/fetchers/arm'
7
7
  require 'one_gadget/fetchers/i386'
8
+ require 'one_gadget/fetchers/mips'
9
+ require 'one_gadget/fetchers/riscv64'
8
10
  require 'one_gadget/gadget'
9
11
  require 'one_gadget/helper'
10
12
 
@@ -15,6 +17,17 @@ module OneGadget
15
17
  # down to the easiest-to-reach set (see {ClassMethods#from_file}).
16
18
  RAW_LEVEL = 2
17
19
 
20
+ # The fetcher each architecture is searched with, and so the set of
21
+ # architectures this tool supports (see {ClassMethods#supported_architecture?}).
22
+ FETCHERS = {
23
+ aarch64: OneGadget::Fetchers::AArch64,
24
+ amd64: OneGadget::Fetchers::Amd64,
25
+ arm: OneGadget::Fetchers::Arm,
26
+ i386: OneGadget::Fetchers::I386,
27
+ mips: OneGadget::Fetchers::Mips,
28
+ riscv64: OneGadget::Fetchers::Riscv64
29
+ }.freeze
30
+
18
31
  # Define class methods here.
19
32
  module ClassMethods
20
33
  # Fetch one-gadget offsets of this build id.
@@ -42,17 +55,20 @@ module OneGadget
42
55
  # Array of all found gadgets is returned.
43
56
  def from_file(file, level: 0)
44
57
  arch = OneGadget::Helper.architecture(file)
45
- klass = {
46
- aarch64: OneGadget::Fetchers::AArch64,
47
- amd64: OneGadget::Fetchers::Amd64,
48
- arm: OneGadget::Fetchers::Arm,
49
- i386: OneGadget::Fetchers::I386
50
- }[arch]
58
+ klass = FETCHERS[arch]
51
59
  raise Error::UnsupportedArchitectureError, arch if klass.nil?
52
60
 
53
61
  for_level(klass.new(file).find, level)
54
62
  end
55
63
 
64
+ # Whether gadgets can be searched for in +arch+. The ELF reader recognises
65
+ # plenty this tool cannot emulate.
66
+ # @param [Symbol] arch As {OneGadget::Helper.architecture} names it.
67
+ # @return [Boolean]
68
+ def supported_architecture?(arch)
69
+ FETCHERS.key?(arch)
70
+ end
71
+
56
72
  private
57
73
 
58
74
  # Narrow a complete gadget set down to what an output level asks for.