文章首发于安全KER https://www.anquanke.com/post/id/235504
0x00 前言
之前一直在学习V8方面的漏洞,对于asm.js层的UAF漏洞还是第一次接触,本文将详细分析Chrome Issue 776677漏洞以及其利用方法。
0x01 前置知识
asm.js
asm.js不是一门新的语言,而是JavaScript的一个子集。由Mozilla于2013年提出,主要为了提升JS引擎执行代码的速度。通俗来说,同样是js代码,符合asm.js规范的代码对JS引擎更加友好,JS引擎在解释执行这些代码更加省心(例如不用进行变量类型推断了),也就更加快。
一旦 JavaScript 引擎发现运行的是 asm.js,就知道这是经过优化的代码,可以跳过语法分析这一步,直接转成汇编语言。另外,浏览器还会调用 WebGL 通过 GPU 执行 asm.js,即 asm.js 的执行引擎与普通的 JavaScript 脚本不同。这些都是 asm.js 运行较快的原因。据称,asm.js 在浏览器里的运行速度,大约是原生代码的50%左右。
在asm.js 中,变量只有两种数据类型。32位带符号整数和64位带符号浮点数
asm.js 的类型声明有固定写法,变量 | 0表示整数,+变量表示浮点数。如下
1 | var a = 1; |
在javascript中,区别是普通js还是asm.js的关键在于函数里是否使用"use asm";关键词修饰,在函数中,在"use asm";关键词之后的代码都属于asm.js,如下,由于module第一行使用了该关键字,那么函数后面的代码都将使用asm.js将代码翻译为汇编代码
1 | function module() { |
使用./d8 t.js -print-opt-code运行,可以看到,虽然没有多次循环触发JIT编译,但是仍然使用了turbofan进行编译
1 | kind = JS_TO_WASM_FUNCTION |
在asm.js中,不能直接调用javascript中的函数或者访问数据,如下会报错,找不到print函数
1 | function module() { |
想要在asm.js中调用js中的函数和对象,必须使用函数参数来间接传递地址进行访问,如下
1 | function module(stdlib) { |
通常,一个标准的asm.js函数模块参数应该有三个function module(stdlib,foreign,buffer) {},其中stdlib用来传递想要用到的一些函数,foreign用来传递一些变量,buffer用来共享内存。
1 | function module(stdlib,foreign,buffer) { |
如图,我们在asm.js里修改了ArrayBuffer的内容,然后在js层显示。
除了直接传入ArrayBuffer对象进行内存共享,还可以使用WebAssembly.Memory来构造
1 | var memory = new WebAssembly.Memory({initial:200}); |
其返回的buffer也是一个ArrayBuffer对象
1 | DebugPrint: 0x4cb18421: [JSArrayBuffer] in OldSpace |
其中initial:200代表申请200页大小的内存,每页为0x10000。使用WebAssembly.Memory的好处是其申请的内存空间可以扩展,如果想要扩充空间,·只需调用grow函数。而ArrayBuffer的话不能做到空间扩充。
1 | memory.grow(1); //扩充1页的内存 |
0x02 漏洞分析
patch分析
patch的地方比较多,经过分析,比较关键的地方是这两处
1 | index cecf460..24b9091 100644 |
1 | diff --git a/src/wasm/wasm-js.cc b/src/wasm/wasm-js.cc |
主要是为asm.js的memory设置了一个标记,不允许我们在memory传给asm.js的模块以后再调用memory.grow()函数。
其精简后的POC如下
1 | function module(stdlib,foreign,buffer) { |
与前面的示例差不多,我们仅仅是增加了一句memory.grow(1);,然后调用函数以后就出现了崩溃。
1 | root@ubuntu:~/Desktop/v8/out.gn/ia32.debug# ./d8 poc.js |
很明显是内存访问错误,使用gdb调试
1 | pwndbg> c |
查看该地址0xf1d99000的映射情况,可以发现这个地址不在映射之内
1 | ............. |
然而如果事先加了一个%DebugPrint将buffer打印的话,会发现
1 | DebugPrint: 0x52698935: [JSArrayBuffer] in OldSpace |
该不可访问地址实际就是ArrayBuffer的backing_store,那么可能的原因是memory.grow(1);操作使得ArrayBuffer的backing_store被释放掉了。我们在memory.grow(1);之后加一句%DebugPrint(buffer);,可以发现
1 | DebugPrint: 0x5c518941: [JSArrayBuffer] in OldSpace |
ArrayBuffer的一些指针确实被清空掉了,但是asm.js里访问时仍然使用了原来那个指针,这是一个UAF。
asm.js的UAF成因分析
在分析这个UAF之前,我们先大致了解一下asm.js的编译过程,一个比较关键的地方是位于src/wasm/module-compiler.cc文件中的InstanceBuilder::Build函数,第1792行开始,获取外部传入的ArrayBuffer对象,并设置一些属性
1 | 1792 if (!memory_.is_null()) { |
然后第1838行开始,进行一些检查,然后将ArrayBuffer对象设置到instance实例中。
1 | 1838 Address mem_start = nullptr; |
然后第1854行,使用WasmMemoryObject::New创建了一个memory_object,并将其设置到instance实例中
1 | In file: /home/sea/Desktop/v8/src/wasm/module-compiler.cc |
接下来为新创建的memory_object设置instance
1 | 1904 |
执行完后,该memory_object调用has_instances会返回true,说明已经为该memory_object设置好了实例
1 | pwndbg> p instance->memory_object()->has_instances() |
在分析完asm.js模块的编译过程以后,我们再来看一下JS层的grow函数。该函数的实现位于src/wasm/wasm-js.cc文件中的WebAssemblyMemoryGrow函数。第745行EXTRACT_THIS(receiver, WasmMemoryObject);获取到了js层的var memory = new WebAssembly.Memory({initial:200});这个对象。显然,在这里也表示为一个·WasmMemoryObject
1 | 740 i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate); |
然后这里获取到ArrayBuffer对象以及一些属性,并在旧的size基础上加上增量以后进行一些范围检查。
1 | In file: /home/sea/Desktop/v8/src/wasm/wasm-js.cc |
接下来调用WasmMemoryObject::Grow对ArrayBuffer的backing_store进行扩容,我们跟进该函数。
1 | ► 764 int32_t ret = i::WasmMemoryObject::Grow(i_isolate, receiver, |
在WasmMemoryObject::Grow函数里,通过一些检查以后,就调用GrowMemoryBuffer来分配一块更大的内存了。
1 | 476 uint32_t maximum_pages = FLAG_wasm_max_mem_pages; |
接下来来到这里,漏洞关键点来了
1 | ► 484 if (memory_object->has_instances()) { |
因为这里的memory_object来自与JS层的那个memory对象,而不是在InstanceBuilder::Build函数中创建的那个,因此memory_object->has_instances()返回的是false。
这意味着下面的代码不会执行
1 | if (memory_object->has_instances()) { |
由于SetInstanceMemory(isolate, instance, new_buffer);没有执行,导致wasm的实例里保存的那个buffer仍然是最开始那个buffer的地址,没有将new_buffer更新进去。此时程序继续执行,更新JS层的memory对象
1 | 494 memory_object->set_array_buffer(*new_buffer); |
接下来是对old_buffer进行处理
1 | 770 if (!old_buffer->is_shared()) { |
我们跟进DetachMemoryBuffer函数,这里将old_buffer的backing_store给释放掉了。
1 | In file: /home/sea/Desktop/v8/src/wasm/wasm-memory.cc |
可以看出,由于没有更新wasm实例里的ArrayBuffer对象,并且后面该ArrayBuffer被释放,由此导致了UAF的产生,原来backing_store的内存空间被unmmap掉了,使得其不再出现在映射表中。当我们再次访问这块内存时,便出现了段错误。
0x03 漏洞利用
概述
现在,我们制造出了一个UAF,但是backing_store的内存与一般对象的内存地址是分开的,不能指望将其他对象占位与此。与backing_store类似的是Array的Element,它们都属于大容量存储,因此它们之间很可能会分配到相邻的地址或者地址相差较小的位置。因此,我们可以考虑使用Heap Spray技术,在backing_store地址附近都布局下Array的Element对象,然后通过UAF去控制。由于64位内存空间太大,Heap Spray似乎无法成功将Element对象放到backing_store地址附近,因此这个漏洞目前仅在32位下成功利用。
何时进行 Heap Spray
我们需要寻找一个合适的时机进行Heap Spray,在asm.js模块的汇编代码中,我们注意到如下代码
1 | 0x255062c0 0 55 push ebp |
可以看到,因为我们在asm.js模块中声明x = x | 0;,所以会先调用ToNumber获取对象x的值,而ToNumber会调用对象内部的toString函数,因此,我们可以重写toString函数,并在toString函数里开始Heap Spray。
1 | //堆喷 |
使用gdb调试,然后断在崩溃处
1 | EAX 0x6666666a ('jfff') |
当前访问的地址是0xf4cb4000,查看其附近的内存
1 | ................. |
可以看到0xf4cb4000上方和下方很多0x85000大小的内存空间,这是正是堆喷到此处的一些Array对象以及Elements对象,我们在gdb中使用find命令查找紧挨着的一块内存里的关键字
1 | find /w4 0xf4d80000,0xf4e05000,0x9999999a |
结果如下,我们看到最近的一个数据位于0xf4d84108,可以看到该处正是某一个Array对象的Element,它与backing_store之间相差0xd0108,这个偏移并不是固定不变的,但是后12bit是固定不变的。
1 | 0xf4d84108 |
我们可以将所有的可能偏移都罗列出来,由于这些偏移在之前那个ArrayBuffer的length范围之内,也就是说该对象存在于那个UAF的空间里,于是我们可以利用UAF来改写Element,我们可以修改Element的length属性,这样,我们可以后续构造出一个oob数组,为了区别,我们还需要修改Element的第一个元素,这样方便我们找到到底是哪个Array的Element堆喷于此处。
为了方便罗列这些可能的偏移,并进行写,我们使用JS的模板来生成多种可能的偏移写语句,方便操作
1 | //距离不是固定的,因此需要将所有可能的距离都赋值,我们要修改Element的length和第一个元素 |
接下来就是搜索哪个被修改过的Array对象了
1 | //找到那个能够被我们UAF控制的Array |
同时,我们得继续寻找该Array对象Elements后方的Elements属于哪个Array,其中注意到,由于corrupted_array的elements的length被我们修改成了0xFFFFFFFF,因此在这里加大length,elements仍然不变,相当于构造了一个oob数组,这个特性也是新学到的。
1 | //寻找corrupted_array的后面是哪个Array对象的elements |
我们搜索next_array的目的是为了将next_array进行释放。这样方便我们进行第二轮Heap Spray,将其他对象布局到此处,然后用corrupted_array 进行控制。
1 | function gc() { |
这里,我们victim_array[next_array_idx] = null;使得该处对象失去了引用,然后通过申请大量的内存new ArrayBuffer(0x1000000);触发了垃圾回收器将next_array的内存回收,这样corrupted_array后方的内存就空闲了,然后我们堆喷多个HOLEY_ELEMENTS类型的Array,因为该Array存储着的是对象的指针,因此,我们结合corrupted_array和该处的Array,就可以构造addressOf原语和fakeObject原语。其构造布置比较简单,这里不再叙述。然后后续利用也比较容易了。
exp
1 | function gc() { |
0x04 感想
本次漏洞复现,学习了很多新知识,对于V8 UAF方面的漏洞还是第一次接触,结合Heap Spray也是第一次,收获比较大。
0x05 参考
asm.js:面向未来的开发
asm.js 和 Emscripten 入门教程
Issue 776677: Security: V8:Use After Free Leads to Remote Code Execution