one_gadget 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +197 -65
- data/README.md +96 -22
- data/lib/one_gadget/abi.rb +41 -5
- data/lib/one_gadget/builds/libc-2.31-93b46e0027747153e336c3fd9431ce9a5d82ad00.rb +563 -0
- data/lib/one_gadget/builds/libc-2.35-891c1403437a4e30e684e0c8e34b87a09e4298e5.rb +434 -0
- data/lib/one_gadget/builds/libc-2.39-a1d1cf4badf1f5dfe57ff1d17bd692ccc6fcd5c5.rb +531 -0
- data/lib/one_gadget/builds/libc-2.39-cd8f5a207dd67aea370d2b471a54c3e56f44ab18.rb +531 -0
- data/lib/one_gadget/builds/libc-2.43-b50ceafbd17dc6bceee344a66671c7eaa152bef4.rb +15 -0
- data/lib/one_gadget/emulators/aarch64.rb +5 -2
- data/lib/one_gadget/emulators/amd64.rb +1 -0
- data/lib/one_gadget/emulators/arm.rb +27 -18
- data/lib/one_gadget/emulators/arm_family.rb +35 -160
- data/lib/one_gadget/emulators/conditional.rb +28 -22
- data/lib/one_gadget/emulators/constraints.rb +269 -0
- data/lib/one_gadget/emulators/data_processing.rb +167 -0
- data/lib/one_gadget/emulators/i386.rb +4 -6
- data/lib/one_gadget/emulators/instruction.rb +24 -1
- data/lib/one_gadget/emulators/lambda.rb +3 -1
- data/lib/one_gadget/emulators/mips.rb +289 -0
- data/lib/one_gadget/emulators/processor.rb +33 -455
- data/lib/one_gadget/emulators/register_file.rb +19 -10
- data/lib/one_gadget/emulators/riscv64.rb +265 -0
- data/lib/one_gadget/emulators/safe_calls.rb +9 -3
- data/lib/one_gadget/emulators/tracked_memory.rb +209 -0
- data/lib/one_gadget/emulators/x86.rb +32 -22
- data/lib/one_gadget/fetchers/aarch64.rb +0 -27
- data/lib/one_gadget/fetchers/amd64.rb +0 -24
- data/lib/one_gadget/fetchers/argument_resolution.rb +340 -0
- data/lib/one_gadget/fetchers/arm.rb +99 -44
- data/lib/one_gadget/fetchers/base.rb +142 -640
- data/lib/one_gadget/fetchers/candidate_walk.rb +150 -0
- data/lib/one_gadget/fetchers/disassembly.rb +252 -0
- data/lib/one_gadget/fetchers/dynamic_symbols.rb +104 -0
- data/lib/one_gadget/fetchers/i386.rb +5 -8
- data/lib/one_gadget/fetchers/mips.rb +478 -0
- data/lib/one_gadget/fetchers/objdump.rb +24 -2
- data/lib/one_gadget/fetchers/riscv64.rb +78 -0
- data/lib/one_gadget/fetchers/x86.rb +19 -0
- data/lib/one_gadget/fetchers.rb +22 -6
- data/lib/one_gadget/gadget.rb +25 -34
- data/lib/one_gadget/helper.rb +19 -5
- data/lib/one_gadget/one_gadget.rb +6 -0
- data/lib/one_gadget/version.rb +1 -1
- data/lib/one_gadget.rb +1 -1
- metadata +18 -6
- data/lib/one_gadget/builds/libc-2.26-2104f3d4ad5cf68603afbe7ba1a17f5ac99c5988.rb +0 -227
- data/lib/one_gadget/builds/libc-2.26-ddcc13122ddbfe5e5ef77d4ebe66d124ae5762c2.rb +0 -300
- data/lib/one_gadget/builds/libc-2.26-f65648a832414f2144ce795d75b6045a1ec2e252.rb +0 -199
|
@@ -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',
|
|
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
|
data/lib/one_gadget/fetchers.rb
CHANGED
|
@@ -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.
|