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