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,265 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'one_gadget/abi'
|
|
4
|
+
require 'one_gadget/emulators/instruction'
|
|
5
|
+
require 'one_gadget/emulators/processor'
|
|
6
|
+
|
|
7
|
+
module OneGadget
|
|
8
|
+
module Emulators
|
|
9
|
+
# Emulator of RISC-V (RV64).
|
|
10
|
+
class Riscv64 < Processor
|
|
11
|
+
# Instantiate a {Riscv64} object.
|
|
12
|
+
def initialize
|
|
13
|
+
super(OneGadget::ABI.riscv64, 'sp')
|
|
14
|
+
@registers['zero'] = 0 # hardwired
|
|
15
|
+
@pc = 'pc'
|
|
16
|
+
setup_frame_pointer('s0') # track argv/data staged off the frame pointer
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @see OneGadget::Emulators::X86#process!
|
|
20
|
+
# @param [String] cmd One line from result of objdump.
|
|
21
|
+
# @return [Boolean] If successfully processed.
|
|
22
|
+
def process!(cmd)
|
|
23
|
+
resolve_pending_branch(cmd)
|
|
24
|
+
@cur_addr = cmd[/\A\s*([0-9a-f]+):/, 1]&.to_i(16)
|
|
25
|
+
|
|
26
|
+
mnem = mnemonic(cmd)
|
|
27
|
+
return handle_branch(mnem, cmd) != :fail if branch_mnem?(mnem)
|
|
28
|
+
|
|
29
|
+
inst, args = parse(cmd)
|
|
30
|
+
__send__(inst.handler, *args) != :fail
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A conditional branch of this arch compares its operands itself -- there is
|
|
34
|
+
# no flag register and no compare instruction -- so each mnemonic names the
|
|
35
|
+
# relation the operands must stand in for the branch to be taken. The
|
|
36
|
+
# assembler's spellings against zero (+beqz+, +blez+, ...) name one register
|
|
37
|
+
# and the reversed ones (+bgt+, +ble+, ...) read a relation the other way
|
|
38
|
+
# round; each is listed as the relation it states, so a constraint reads as
|
|
39
|
+
# the comparison was written.
|
|
40
|
+
COND = {
|
|
41
|
+
'beq' => :eq, 'bne' => :ne,
|
|
42
|
+
'blt' => :slt, 'bge' => :sge, 'bltu' => :ult, 'bgeu' => :uge,
|
|
43
|
+
'bgt' => :sgt, 'ble' => :sle, 'bgtu' => :ugt, 'bleu' => :ule,
|
|
44
|
+
'beqz' => :eq, 'bnez' => :ne,
|
|
45
|
+
'bltz' => :slt, 'bgez' => :sge, 'blez' => :sle, 'bgtz' => :sgt
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
# The data-processing instructions, mapped to the Ruby operator that folds
|
|
49
|
+
# them and renders them alike. An immediate form differs from its register
|
|
50
|
+
# one only in how the right operand is written, which the operand reader
|
|
51
|
+
# already handles, so both name the same operator. Arithmetic shift right is
|
|
52
|
+
# absent: a value is held masked to its width, where +>>+ is the logical
|
|
53
|
+
# shift, and naming the arithmetic one as that would state a different
|
|
54
|
+
# instruction.
|
|
55
|
+
DATA_OPS = {
|
|
56
|
+
'and' => :&, 'andi' => :&,
|
|
57
|
+
'or' => :|, 'ori' => :|,
|
|
58
|
+
'xor' => :^, 'xori' => :^,
|
|
59
|
+
'sll' => :<<, 'slli' => :<<,
|
|
60
|
+
'srl' => :>>, 'srli' => :>>
|
|
61
|
+
}.freeze
|
|
62
|
+
private_constant :DATA_OPS
|
|
63
|
+
|
|
64
|
+
# The loads, mapped to how many bytes each reads.
|
|
65
|
+
LOADS = { 'ld' => 8, 'lw' => 4, 'lwu' => 4, 'lh' => 2, 'lhu' => 2, 'lb' => 1, 'lbu' => 1 }.freeze
|
|
66
|
+
private_constant :LOADS
|
|
67
|
+
|
|
68
|
+
# The stores, mapped to how many bytes each writes.
|
|
69
|
+
STORES = { 'sd' => 8, 'sw' => 4, 'sh' => 2, 'sb' => 1 }.freeze
|
|
70
|
+
private_constant :STORES
|
|
71
|
+
|
|
72
|
+
# Supported instruction set. Anything not listed aborts the candidate.
|
|
73
|
+
# @return [Array<Instruction>] The supported instructions.
|
|
74
|
+
def instructions
|
|
75
|
+
[
|
|
76
|
+
Instruction.new('add', 3),
|
|
77
|
+
Instruction.new('addi', 3),
|
|
78
|
+
Instruction.new('auipc', 2),
|
|
79
|
+
Instruction.new('jal', 1..2),
|
|
80
|
+
Instruction.new('li', 2),
|
|
81
|
+
Instruction.new('lui', 2),
|
|
82
|
+
Instruction.new('mv', 2),
|
|
83
|
+
Instruction.new('nop', 0),
|
|
84
|
+
Instruction.new('sub', 3),
|
|
85
|
+
Instruction.new('not', 2)
|
|
86
|
+
] + (LOADS.keys + STORES.keys).map { |mnem| Instruction.new(mnem, 2) } +
|
|
87
|
+
DATA_OPS.keys.map { |mnem| Instruction.new(mnem, 3) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Return the argument value of calling a function.
|
|
91
|
+
# @param [Integer] idx The 0-based index of the argument.
|
|
92
|
+
# @return [Lambda, Integer] The value held in register +a<idx>+, used for the +idx+-th argument.
|
|
93
|
+
def argument(idx)
|
|
94
|
+
registers["a#{idx}"]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def branch_mnem?(mnem)
|
|
100
|
+
mnem == 'j' || COND.key?(mnem)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Operands of +cmd+ (mnemonic dropped), each stripped of a trailing +<symbol>+.
|
|
104
|
+
def operands(cmd)
|
|
105
|
+
cmd.sub(/\A[0-9a-f]+:\s*\S+\s*/, '').split(',').map { |o| o.strip.sub(/\s*<.*>\z/, '') }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Record the comparison and decide the branch together, since one
|
|
109
|
+
# instruction is both (see {COND}). The zero spellings leave their second
|
|
110
|
+
# operand out; +zero+ names it, and reads as the +0x0+ it holds.
|
|
111
|
+
def handle_branch(mnem, cmd)
|
|
112
|
+
return true if mnem == 'j' # unconditional: control handled by the stitched path
|
|
113
|
+
|
|
114
|
+
ops = operands(cmd)
|
|
115
|
+
lhs, rhs = mnem.end_with?('z') ? [ops[0], 'zero'] : ops[0..1]
|
|
116
|
+
record_compare(:sub, operand_str(lhs), operand_str(rhs))
|
|
117
|
+
branch_on_compare(COND[mnem], ops.last.to_i(16))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# A direct call: record the terminal +exec*+ target, accept a known-safe
|
|
121
|
+
# syscall wrapper, or +:fail+ to abort the candidate. The two-operand form
|
|
122
|
+
# names the register the return address is written to, which is +ra+ for a
|
|
123
|
+
# call and is not modelled either way.
|
|
124
|
+
def inst_jal(*args)
|
|
125
|
+
addr = args.last
|
|
126
|
+
return reach_terminal_call(addr) if terminal_call?(addr)
|
|
127
|
+
|
|
128
|
+
dispatch_safe_call(addr)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# +auipc dst, imm+ is how this arch names an address relative to the
|
|
132
|
+
# instruction itself: the immediate objdump prints is the upper 20 bits, so
|
|
133
|
+
# the value is +pc + (imm << 12)+. The +addi+/load that follows applies the
|
|
134
|
+
# low half, which is what makes the pair resolve to the +$base+<off>+ form
|
|
135
|
+
# the rest of the engine reads as a libc global.
|
|
136
|
+
# @example the pair objdump resolves to 171fc0 in its own comment
|
|
137
|
+
# 9f48e: auipc a5,0xd3 # a5 = $base+0x17248e
|
|
138
|
+
# 9f492: ld a5,-1230(a5) # a5 = [$base+0x171fc0]
|
|
139
|
+
def inst_auipc(dst, imm)
|
|
140
|
+
check_register!(dst)
|
|
141
|
+
|
|
142
|
+
registers[dst] = pc_value + (imm.to_i(16) << 12)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# +lui dst, imm+ loads the same upper 20 bits, but as a plain value rather
|
|
146
|
+
# than an address, so nothing about the instruction's own location enters it.
|
|
147
|
+
def inst_lui(dst, imm)
|
|
148
|
+
check_register!(dst)
|
|
149
|
+
|
|
150
|
+
registers[dst] = (imm.to_i(16) << 12) & width_mask
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# +li+ is the assembler's spelling of loading a constant, whatever the
|
|
154
|
+
# instructions it expands to.
|
|
155
|
+
def inst_li(dst, imm)
|
|
156
|
+
check_register!(dst)
|
|
157
|
+
|
|
158
|
+
registers[dst] = Integer(imm)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# This arch spells no modifier on an operand and has no 2-operand shorthand,
|
|
162
|
+
# so the arithmetic is the plain three-operand form; +addi+ differs from
|
|
163
|
+
# +add+ only in that its right operand is written as a literal.
|
|
164
|
+
def inst_add(dst, src, op2) = arith(:+, dst, src, op2)
|
|
165
|
+
alias inst_addi inst_add
|
|
166
|
+
|
|
167
|
+
# +sub dst, src, op2+. See {#inst_add}.
|
|
168
|
+
def inst_sub(dst, src, op2) = arith(:-, dst, src, op2)
|
|
169
|
+
|
|
170
|
+
def inst_mv(dst, src)
|
|
171
|
+
check_register!(dst)
|
|
172
|
+
|
|
173
|
+
registers[dst] = arg_to_lambda(src)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Each data-processing mnemonic handled the one way, since they differ only
|
|
177
|
+
# in the operator applied (see {Processor#data_op} for the operands).
|
|
178
|
+
DATA_OPS.each do |mnem, op|
|
|
179
|
+
define_method(Instruction.handler_name(mnem)) do |dst, src, op2|
|
|
180
|
+
data_op(op, dst, src, op2, name: mnem)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# +not dst, src+ is every bit of +src+ flipped. Only a concrete value has a
|
|
185
|
+
# complement this emulator can name ({Processor#complement}); a symbolic one
|
|
186
|
+
# aborts rather than being recorded as a mask it isn't.
|
|
187
|
+
def inst_not(dst, src)
|
|
188
|
+
check_register!(dst)
|
|
189
|
+
|
|
190
|
+
registers[dst] = complement('not', src, dst, src)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Each load and store handled the one way, since within a family they differ
|
|
194
|
+
# only in the width they touch (see {#load_value} and {#store_value}).
|
|
195
|
+
LOADS.each do |mnem, size|
|
|
196
|
+
define_method(Instruction.handler_name(mnem)) { |dst, mem| load_value(dst, mem, size) }
|
|
197
|
+
end
|
|
198
|
+
STORES.each do |mnem, size|
|
|
199
|
+
define_method(Instruction.handler_name(mnem)) { |src, mem| store_value(src, mem, size) }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# A load. The address is read like any other, so what the caller has to
|
|
203
|
+
# arrange about it is recorded the same way. A load narrower than a register
|
|
204
|
+
# only takes part of the word this emulator tracks, which is not a value it
|
|
205
|
+
# can name, so the register then holds what a call would have left: a path
|
|
206
|
+
# that goes on to depend on it is abandoned rather than described wrongly.
|
|
207
|
+
# @param [String] dst The destination register.
|
|
208
|
+
# @param [String] mem The memory operand, as written.
|
|
209
|
+
# @param [Integer] size How many bytes the load reads.
|
|
210
|
+
# @return [void]
|
|
211
|
+
def load_value(dst, mem, size)
|
|
212
|
+
check_register!(dst)
|
|
213
|
+
|
|
214
|
+
value = read_value(arg_to_lambda(mem_operand(mem)))
|
|
215
|
+
registers[dst] = size == size_t ? value : clobbered_value
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# A store, tracked so a later load of the same slot reads it back, and
|
|
219
|
+
# requiring its target writable ({Processor#track_write}). A store narrower
|
|
220
|
+
# than a register leaves the rest of the slot holding whatever was there, so
|
|
221
|
+
# what the slot then holds is named as unknown rather than as the value
|
|
222
|
+
# stored -- the requirement that it be writable is what the candidate really
|
|
223
|
+
# establishes, and that is kept.
|
|
224
|
+
# @param [String] src The register holding the value stored.
|
|
225
|
+
# @param [String] mem The memory operand, as written.
|
|
226
|
+
# @param [Integer] size How many bytes the store writes.
|
|
227
|
+
# @return [void]
|
|
228
|
+
def store_value(src, mem, size)
|
|
229
|
+
check_register!(src)
|
|
230
|
+
|
|
231
|
+
dst_l = arg_to_lambda(mem_operand(mem)).ref!
|
|
232
|
+
track_write(dst_l, size == size_t ? registers[src] : clobbered_value)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# This arch writes a memory operand as an offset applied to one register,
|
|
236
|
+
# which the {Lambda} parser reads in its bracketed form. The offset is
|
|
237
|
+
# decimal, as objdump prints every operand but an upper immediate.
|
|
238
|
+
# @param [String] mem The operand, as written.
|
|
239
|
+
# @return [String]
|
|
240
|
+
# @example
|
|
241
|
+
# mem_operand('-1230(a5)') #=> '[a5-1230]'
|
|
242
|
+
# mem_operand('0(s1)') #=> '[s1+0]'
|
|
243
|
+
def mem_operand(mem)
|
|
244
|
+
m = mem.match(/\A(-?\d+)\((\w+)\)\z/)
|
|
245
|
+
raise_unsupported('memory operand', mem) if m.nil?
|
|
246
|
+
|
|
247
|
+
"[#{m[2]}#{format('%+d', Integer(m[1]))}]"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# The value +pc+ holds while the instruction at {@cur_addr} runs: this arch
|
|
251
|
+
# reads its own address, with no pipeline bias to add.
|
|
252
|
+
def pc_value
|
|
253
|
+
libc_base + @cur_addr
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
class << self
|
|
257
|
+
# RV64 is 64-bit.
|
|
258
|
+
# @return [Integer]
|
|
259
|
+
def bits
|
|
260
|
+
64
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
module OneGadget
|
|
4
4
|
module Emulators
|
|
5
5
|
# Arch-independent catalog of libc calls the emulator accepts without
|
|
6
|
-
# executing them (see {
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# executing them (see {COMMON} for what each requirement means, and
|
|
7
|
+
# {Processor#dispatch_safe_call} for how they are applied). These
|
|
8
|
+
# functions -- syscall wrappers and +posix_spawn+'s setup
|
|
9
9
|
# helpers -- have identical semantics on every architecture, so their
|
|
10
10
|
# requirements live here once instead of being copied into each arch's
|
|
11
11
|
# emulator, keeping the arches from drifting.
|
|
@@ -14,6 +14,12 @@ module OneGadget
|
|
|
14
14
|
# the specific +posix_spawnattr_setsigmask+/+setsigdefault+ keys precede the
|
|
15
15
|
# generic +posix_spawnattr_+ prefix.
|
|
16
16
|
module SafeCalls
|
|
17
|
+
# What each requirement asks of the caller:
|
|
18
|
+
# * +:global_var?+ -- nothing; it must already hold, or the candidate is aborted.
|
|
19
|
+
# * +:closed_fd+ -- nothing; the descriptor is recorded (see {Processor#note_closed_fd}).
|
|
20
|
+
# * +:null+, +:nullable_deref+ -- +<arg> == NULL+.
|
|
21
|
+
# * +:deref+ -- +readable: <arg>+.
|
|
22
|
+
# * +:writable+ -- +writable: <arg>+.
|
|
17
23
|
# @return [Hash{String => Hash{Integer => Symbol}}]
|
|
18
24
|
# Function name (or name prefix) => argument index => requirement.
|
|
19
25
|
COMMON = {
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'one_gadget/emulators/lambda'
|
|
4
|
+
|
|
5
|
+
module OneGadget
|
|
6
|
+
module Emulators
|
|
7
|
+
# The memory a candidate reads and writes. Every store is filed under the base
|
|
8
|
+
# its address is an offset from -- the stack pointer, the frame pointer, or any
|
|
9
|
+
# register holding a pointer the caller supplies -- so a later load of the same
|
|
10
|
+
# slot reads back what this candidate put there, and a load of an untouched one
|
|
11
|
+
# is answered as what the caller left. Mixed into {Processor}.
|
|
12
|
+
module TrackedMemory
|
|
13
|
+
# @return [Hash{Integer => OneGadget::Emulators::Lambda}] Memory written through +sp+.
|
|
14
|
+
def sp_based_stack = get_corresponding_stack(sp)
|
|
15
|
+
|
|
16
|
+
# @return [Hash{Integer => Lambda}, nil] Memory written through {Processor#bp}, or nil when the arch has none.
|
|
17
|
+
def bp_based_stack = bp && get_corresponding_stack(bp)
|
|
18
|
+
|
|
19
|
+
# Enable frame-pointer stack tracking with +bp+ as the frame register, so a
|
|
20
|
+
# gadget staging data at +[bp+imm]+ (e.g. an argv array off the frame
|
|
21
|
+
# pointer) is recovered instead of collapsing to a bare +writable:+. A nil
|
|
22
|
+
# +bp+ leaves the arch +sp+-only. Call from the arch initializer after +super+.
|
|
23
|
+
# @param [String, nil] bp The frame register's name, or nil for an arch with none.
|
|
24
|
+
# @return [void]
|
|
25
|
+
def setup_frame_pointer(bp)
|
|
26
|
+
@bp = bp
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Where +address+ lands in the memory this emulator tracks: the stack it
|
|
30
|
+
# falls in and its offset within it. A load or store passes the address it
|
|
31
|
+
# dereferences, i.e. its operand with that dereference peeled off.
|
|
32
|
+
# @param [Lambda, String] address An address.
|
|
33
|
+
# @return [(Hash{Integer => Lambda}?, Integer)] The stack, +nil+ if none
|
|
34
|
+
# tracks this address, and the offset to index it at.
|
|
35
|
+
# @example an offset from a register
|
|
36
|
+
# resolve_address(Lambda.parse('rsp+0x10')) #=> [sp_based_stack, 0x10]
|
|
37
|
+
# @example an offset from a pointer no register names
|
|
38
|
+
# resolve_address(Lambda.parse('[rbp-0x48]+0x8')) #=> [the "[rbp-0x48]" stack, 0x8]
|
|
39
|
+
def resolve_address(address)
|
|
40
|
+
base, offset = address_base(address)
|
|
41
|
+
[get_corresponding_stack(base), offset]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The memory +base+ addresses: what this candidate has written through it,
|
|
45
|
+
# keyed by offset. Every base gets one -- the stack pointer, the frame
|
|
46
|
+
# pointer, any other register, and a value no register names at all (a
|
|
47
|
+
# pointer the candidate derived and then built an array through).
|
|
48
|
+
#
|
|
49
|
+
# Keyed by how the base renders, which is what makes one store enough: a
|
|
50
|
+
# register that gets reassigned addresses somewhere else and renders
|
|
51
|
+
# differently, so it lands on a different key without any invalidation to
|
|
52
|
+
# arrange. Only a store overwriting what the base itself reads from would
|
|
53
|
+
# break that, which a candidate short enough to be a gadget doesn't do.
|
|
54
|
+
# @example (amd64) After +mov QWORD PTR [rsp+0x10], rdi+, keyed by offset.
|
|
55
|
+
# get_corresponding_stack('rsp') #=> { 0x10 => rdi }
|
|
56
|
+
# @param [String, Lambda] base A base, as {#resolve_address} yields it --
|
|
57
|
+
# not an offset expression, whose offset belongs in the key it indexes.
|
|
58
|
+
# @return [Hash{Integer => Lambda}, nil] nil when +base+ names nothing this
|
|
59
|
+
# emulator tracks memory for.
|
|
60
|
+
def get_corresponding_stack(base)
|
|
61
|
+
return nil unless base.is_a?(OneGadget::Emulators::Lambda) || registers.key?(base.to_s)
|
|
62
|
+
|
|
63
|
+
tracked_memory[base.to_s]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The values this candidate wrote through an address built from +text+, which
|
|
67
|
+
# the array reported at +text+ therefore holds -- wherever in it they landed.
|
|
68
|
+
# A reader told about that array has to be told about these too, or it is
|
|
69
|
+
# described as though the gadget had not written to it.
|
|
70
|
+
# @param [String] text How the address is named.
|
|
71
|
+
# @return [Array<String>] Each value written, rendered, without duplicates.
|
|
72
|
+
# @example (riscv64) a store of +s8+ through +((a5 << 0x3) + [sp+0xd0])+
|
|
73
|
+
# writes_through('[sp+0xd0]') #=> ['s8']
|
|
74
|
+
def writes_through(text)
|
|
75
|
+
return [] if text.empty?
|
|
76
|
+
|
|
77
|
+
derived_writes.filter_map do |base, values|
|
|
78
|
+
next unless operands_of(base).any? { |operand| operand.to_s == text }
|
|
79
|
+
|
|
80
|
+
# A literal is not the caller's to arrange -- it is already what it is,
|
|
81
|
+
# and the NULL such a loop writes to terminate the array is one.
|
|
82
|
+
values.grep_v(Integer).map(&:to_s)
|
|
83
|
+
end.flatten.uniq
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# Every base this candidate has written through, each mapped to the memory
|
|
89
|
+
# it addresses. See {#get_corresponding_stack}, which is how it is reached.
|
|
90
|
+
# @return [Hash{String => Hash{Integer => Lambda}}]
|
|
91
|
+
def tracked_memory
|
|
92
|
+
@tracked_memory ||= Hash.new { |memory, base| memory[base] = tracked_stack(base) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Split +address+ into the base it is offset from and that offset, in the
|
|
96
|
+
# forms {#get_corresponding_stack} and a tracked stack expect.
|
|
97
|
+
# @param [Lambda, String] address See {#resolve_address}.
|
|
98
|
+
# @return [(String, Lambda), Integer]
|
|
99
|
+
def address_base(address)
|
|
100
|
+
return [address, 0] unless address.is_a?(OneGadget::Emulators::Lambda)
|
|
101
|
+
# Still a dereference deep, so the whole thing names one value rather than
|
|
102
|
+
# an offset from anything: its own immediate is part of that name.
|
|
103
|
+
return [address, 0] if address.deref_count.positive?
|
|
104
|
+
# An operation's +obj+ is only the value it operates on, which addresses
|
|
105
|
+
# somewhere else entirely -- a candidate building an array through
|
|
106
|
+
# +(rsi & ~0xf)+ is not writing through +rsi+.
|
|
107
|
+
return [address.dup.tap { |base| base.immi = 0 }, address.immi] if address.operation?
|
|
108
|
+
|
|
109
|
+
[address.obj, address.immi]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# An always-on tracked stack keyed by offset: a Hash that lazily materialises
|
|
113
|
+
# +[reg+off]+ as a one-deref {Lambda}. Used for the +sp+- and {Processor#bp}-based stacks.
|
|
114
|
+
def tracked_stack(reg)
|
|
115
|
+
Hash.new do |h, k|
|
|
116
|
+
h[k] = OneGadget::Emulators::Lambda.new(reg).tap do |lmda|
|
|
117
|
+
lmda.immi = k
|
|
118
|
+
lmda.deref!
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# The value an instruction reads through +val+: what this candidate put at
|
|
124
|
+
# that address, when it is one the candidate has written, and the
|
|
125
|
+
# dereference itself otherwise (recording the read, see {#note_read}).
|
|
126
|
+
#
|
|
127
|
+
# A slot the gadget fills in reads back as what was put there, so a
|
|
128
|
+
# constraint on it names the value the caller has to arrange rather than
|
|
129
|
+
# whatever the slot held on entry -- which the gadget has already replaced.
|
|
130
|
+
# @param [Object] val The operand's value, as produced by {#arg_to_lambda}.
|
|
131
|
+
# @return [Object]
|
|
132
|
+
# @example (arm) +str r3, [sp, #4]+ then +ldr r0, [sp, #4]+ reads back r3
|
|
133
|
+
def read_value(val)
|
|
134
|
+
stored = stored_value(val)
|
|
135
|
+
return stored unless stored.nil?
|
|
136
|
+
|
|
137
|
+
note_read(val)
|
|
138
|
+
val
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# What this candidate stored at the address +val+ dereferences, or +nil+ if
|
|
142
|
+
# it stored nothing there. Only a single dereference of an address this
|
|
143
|
+
# emulator tracks names a slot it can answer for (see {#resolve_address}).
|
|
144
|
+
# @param [Object] val
|
|
145
|
+
# @return [Object, nil]
|
|
146
|
+
def stored_value(val)
|
|
147
|
+
return nil unless val.is_a?(OneGadget::Emulators::Lambda) && val.deref_count.positive?
|
|
148
|
+
|
|
149
|
+
stack, offset = resolve_address(val.dup.ref!)
|
|
150
|
+
stack&.key?(offset) ? stack[offset] : nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# The innermost base name of a (possibly nested or dereferenced) address
|
|
154
|
+
# lambda, following +obj+ through any nested lambdas.
|
|
155
|
+
# @param [OneGadget::Emulators::Lambda] lmda
|
|
156
|
+
# @return [String, nil] The root base name, or +nil+ for an absolute address.
|
|
157
|
+
# @example
|
|
158
|
+
# root_base(arg_to_lambda('[[$base+0x10]+0x8]')) #=> '$base'
|
|
159
|
+
# root_base(arg_to_lambda('x19+0xed8')) #=> 'x19'
|
|
160
|
+
def root_base(lmda)
|
|
161
|
+
obj = lmda.obj
|
|
162
|
+
obj = obj.obj while obj.is_a?(OneGadget::Emulators::Lambda)
|
|
163
|
+
obj
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Stores whose address is an operation rather than a base and a displacement
|
|
167
|
+
# -- a scaled index added to a pointer, say. Such a write lands somewhere in
|
|
168
|
+
# whatever that pointer addresses, at an offset only a value the caller
|
|
169
|
+
# supplies decides, so it is tracked under a base of its own that nothing
|
|
170
|
+
# reads back (see {#writes_through}).
|
|
171
|
+
# @return [Array<(Lambda, Array)>] Each address operation, and what went there.
|
|
172
|
+
def derived_writes
|
|
173
|
+
@derived_writes ||= []
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# The values an operation is built from, flattened out of its tree, so an
|
|
177
|
+
# operand is recognised as itself rather than as text inside a rendering
|
|
178
|
+
# (where +x3+ would be found in +x30+).
|
|
179
|
+
# @param [Lambda, Object] lmda An address, or one part of one.
|
|
180
|
+
# @return [Array] Its leaf operands, or +lmda+ itself when it is not an operation.
|
|
181
|
+
def operands_of(lmda)
|
|
182
|
+
return [lmda] unless lmda.is_a?(OneGadget::Emulators::Lambda) && lmda.operation?
|
|
183
|
+
|
|
184
|
+
operands_of(lmda.obj) + operands_of(lmda.rhs)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Track a store: write +values+ (one per word from +dst_l+) into the stack
|
|
188
|
+
# {#resolve_address} resolves +dst_l+ to, and require +dst_l+
|
|
189
|
+
# writable -- unless it is a pure +sp+ store. +sp+ is invariantly the
|
|
190
|
+
# writable stack; the frame pointer only conventionally is, so a store
|
|
191
|
+
# through it stays a real precondition (like amd64's +writable: rbp+imm+).
|
|
192
|
+
# @param [OneGadget::Emulators::Lambda] dst_l The destination, zero-deref.
|
|
193
|
+
# @param [Array<OneGadget::Emulators::Lambda, Integer>] values One per word.
|
|
194
|
+
# @return [void]
|
|
195
|
+
def track_write(dst_l, *values)
|
|
196
|
+
stack, offset = resolve_address(dst_l)
|
|
197
|
+
base, = address_base(dst_l)
|
|
198
|
+
derived_writes << [base, values] if base.is_a?(OneGadget::Emulators::Lambda) && base.operation?
|
|
199
|
+
values.each_with_index { |v, i| stack[offset + size_t * i] = v } if stack
|
|
200
|
+
add_writable(dst_l) unless stack.equal?(sp_based_stack)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Resolve +sp+- and (when tracked) {Processor#bp}-relative operands to their offset.
|
|
204
|
+
def eval_dict
|
|
205
|
+
bp ? { sp => 0, bp => 0 } : { sp => 0 }
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
@@ -10,6 +10,10 @@ module OneGadget
|
|
|
10
10
|
# Super class for amd64 and i386 processor.
|
|
11
11
|
class X86 < Processor
|
|
12
12
|
# Constructor for a x86 processor.
|
|
13
|
+
# @param [Array<String>] registers All the register names this architecture accepts.
|
|
14
|
+
# @param [String] sp The stack pointer's name.
|
|
15
|
+
# @param [String] bp The frame pointer's name.
|
|
16
|
+
# @param [String] pc The program counter's name.
|
|
13
17
|
def initialize(registers, sp, bp, pc)
|
|
14
18
|
super(registers, sp)
|
|
15
19
|
@pc = pc
|
|
@@ -30,8 +34,7 @@ module OneGadget
|
|
|
30
34
|
return handle_branch(mnem, cmd) != :fail if branch_mnem?(mnem)
|
|
31
35
|
|
|
32
36
|
inst, args = parse(cmd)
|
|
33
|
-
|
|
34
|
-
__send__(sym, *args) != :fail
|
|
37
|
+
__send__(inst.handler, *args) != :fail
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
# x86 conditional-jump mnemonics mapped to shared {Conditional::RELATION}
|
|
@@ -51,14 +54,11 @@ module OneGadget
|
|
|
51
54
|
COMPARES = { 'cmp' => :sub, 'test' => :and }.freeze
|
|
52
55
|
|
|
53
56
|
# A segment-prefixed operand reads thread-local storage, which isn't modelled
|
|
54
|
-
# (
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
# it -- so modelling this only produces gadgets that all but never apply.
|
|
60
|
-
# Worth revisiting if a libc is found reaching a terminal call under a
|
|
61
|
-
# condition that commonly holds, e.g. +errno != <some error>+.
|
|
57
|
+
# (the other arches reach it by instructions that are unsupported anyway).
|
|
58
|
+
# Every gadget observed behind one tests +errno == ENOEXEC+, which the caller
|
|
59
|
+
# would have had to arrange before entering, so modelling it would produce
|
|
60
|
+
# gadgets that all but never apply. Worth revisiting for a libc found
|
|
61
|
+
# reaching a terminal call under a condition that commonly holds.
|
|
62
62
|
# @example +cmp DWORD PTR fs:[r14], 0x8+ -- errno == ENOEXEC
|
|
63
63
|
SEGMENT_OPERAND = /\b(?:fs|gs|ds|es|ss|cs):/
|
|
64
64
|
|
|
@@ -125,6 +125,8 @@ module OneGadget
|
|
|
125
125
|
branch_on_compare(JCC[mnem], jump_target(cmd))
|
|
126
126
|
end
|
|
127
127
|
|
|
128
|
+
# The counter register a +jcxz+-family branch tests, at the width its
|
|
129
|
+
# mnemonic names.
|
|
128
130
|
def cx_reg(mnem)
|
|
129
131
|
{ 'jcxz' => 'cx', 'jecxz' => 'ecx', 'jrcxz' => 'rcx' }[mnem]
|
|
130
132
|
end
|
|
@@ -267,10 +269,17 @@ module OneGadget
|
|
|
267
269
|
registers[dst] = 0
|
|
268
270
|
end
|
|
269
271
|
|
|
272
|
+
# A {Lambda} knows how to add a number to itself, not the other way round,
|
|
273
|
+
# and addition commutes -- adding an unknown value to a known one is the
|
|
274
|
+
# same unknown value shifted (see {DataProcessing#offset_result}, which
|
|
275
|
+
# states the same rule for the architectures that go through it).
|
|
270
276
|
def inst_add(dst, src)
|
|
271
277
|
check_register!(dst)
|
|
272
278
|
|
|
273
|
-
registers[dst]
|
|
279
|
+
lhs = registers[dst]
|
|
280
|
+
rhs = read_value(arg_to_lambda(src))
|
|
281
|
+
lhs, rhs = rhs, lhs if lhs.is_a?(Integer)
|
|
282
|
+
registers[dst] = lhs + rhs
|
|
274
283
|
end
|
|
275
284
|
|
|
276
285
|
# +and dst, src+ both writes +dst+ and sets the flags a following branch
|
|
@@ -303,22 +312,20 @@ module OneGadget
|
|
|
303
312
|
registers[dst] -= src
|
|
304
313
|
end
|
|
305
314
|
|
|
306
|
-
# yap, nop
|
|
307
|
-
def inst_nop(*); end
|
|
308
|
-
|
|
309
315
|
# A marker for the branch predictor: it says a jump may land here and leaves
|
|
310
316
|
# every value alone.
|
|
311
317
|
def inst_endbr64(*); end
|
|
312
318
|
alias inst_endbr32 inst_endbr64
|
|
313
319
|
|
|
314
|
-
# Swap what two registers hold.
|
|
315
|
-
#
|
|
316
|
-
#
|
|
317
|
-
#
|
|
318
|
-
#
|
|
319
|
-
#
|
|
320
|
-
#
|
|
321
|
-
#
|
|
320
|
+
# Swap what two registers hold. Naming one place twice is nothing at all,
|
|
321
|
+
# whatever it names -- the multi-byte nop a compiler pads with.
|
|
322
|
+
# @raise [OneGadget::Error::UnsupportedInstructionArgumentError]
|
|
323
|
+
# For a memory operand, which is an exchange with memory and an atomic one,
|
|
324
|
+
# or a narrower view, which names part of a register where swapping whole
|
|
325
|
+
# values cannot reach.
|
|
326
|
+
# @example (amd64) A swap, then the padding.
|
|
327
|
+
# inst_xchg('rbx', 'rdi') #=> [rdi, rbx]
|
|
328
|
+
# inst_xchg('rax', 'rax') #=> nil
|
|
322
329
|
def inst_xchg(dst, src)
|
|
323
330
|
return if dst == src
|
|
324
331
|
raise Error::UnsupportedInstructionArgumentError, "xchg #{dst},#{src}" unless
|
|
@@ -334,6 +341,9 @@ module OneGadget
|
|
|
334
341
|
dispatch_safe_call(addr)
|
|
335
342
|
end
|
|
336
343
|
|
|
344
|
+
# A vector register holds one value per lane, so it becomes an array of
|
|
345
|
+
# them -- each lane named by the shift that reaches it -- where an ordinary
|
|
346
|
+
# register becomes a single {Lambda}.
|
|
337
347
|
def to_lambda(reg)
|
|
338
348
|
return super unless reg =~ /^xmm\d+$/
|
|
339
349
|
|
|
@@ -46,33 +46,6 @@ module OneGadget
|
|
|
46
46
|
def call_str
|
|
47
47
|
'bl'
|
|
48
48
|
end
|
|
49
|
-
|
|
50
|
-
def bin_sh_offset
|
|
51
|
-
@bin_sh_offset ||= str_offset('/bin/sh')
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def str_bin_sh?(str)
|
|
55
|
-
str.include?('$base') && str.include?(bin_sh_offset.to_s(16))
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Offset of the standalone "sh" string (\0-preceded and \0-terminated) that
|
|
59
|
-
# glibc passes as argv[0] in execl("/bin/sh", "sh", ...). Its distance from
|
|
60
|
-
# "/bin/sh" is build-specific, so locate it directly instead of guessing.
|
|
61
|
-
# +nil+ when the libc has no such string.
|
|
62
|
-
def sh_offset
|
|
63
|
-
return @sh_offset if defined?(@sh_offset)
|
|
64
|
-
|
|
65
|
-
idx = File.binread(file).index("\x00sh\x00")
|
|
66
|
-
@sh_offset = idx && idx + 1
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def str_sh?(str)
|
|
70
|
-
!sh_offset.nil? && str.include?('$base') && str.include?(sh_offset.to_s(16))
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def global_var?(str)
|
|
74
|
-
base_relative?(str, '$base')
|
|
75
|
-
end
|
|
76
49
|
end
|
|
77
50
|
end
|
|
78
51
|
end
|
|
@@ -12,30 +12,6 @@ module OneGadget
|
|
|
12
12
|
def emulator
|
|
13
13
|
OneGadget::Emulators::Amd64.new
|
|
14
14
|
end
|
|
15
|
-
|
|
16
|
-
# The branch-aware walker (see {Base#branch_aware_candidates}) already
|
|
17
|
-
# stitches +jmp+ targets, so only the filter remains.
|
|
18
|
-
def candidates
|
|
19
|
-
super do |candidate|
|
|
20
|
-
next true if candidate.include?('posix_spawn@')
|
|
21
|
-
next false unless candidate.include?(bin_sh_hex) # works in x86-64
|
|
22
|
-
next false unless candidate.lines.last.include?('execve') # only care execve
|
|
23
|
-
|
|
24
|
-
true
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def bin_sh_hex
|
|
29
|
-
@bin_sh_hex ||= str_offset('/bin/sh').to_s(16)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def str_bin_sh?(str)
|
|
33
|
-
str.include?('$base') && str.include?(bin_sh_hex)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def global_var?(str)
|
|
37
|
-
base_relative?(str, '$base')
|
|
38
|
-
end
|
|
39
15
|
end
|
|
40
16
|
end
|
|
41
17
|
end
|