文章首发于安全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