Mid Station

V8 Exploit

春节期间学习了v8引擎exploit相关的知识,挑了几道经典题目练手:

  • PlaidCTF2018:roll a d8
  • *CTF2019:OOB
  • GoogleCTF2018: Just-in-time

各路大神的writeup已经足够详细了,这里只记录一下解决v8题目比较关键的知识点。

首先比较关键的是一些基本对象的内存结构,v8题目的漏洞的落脚点基本都是数组越界,然后通过越界构造类型混淆,或者直接构造任意读写原语。因此数组相关结构体的布局将是把越界读写转换为任意读写的关键。这里采用asciiflow简单画了示意图。至于Tagged value和其他关键细节可以参考sakura事无巨细的博文v8 exploit.

JSObject

1
2
3
4
5
6
               JSObject
+-----------------------------+
0x00|kMapOffset* |(inherit from HeapObject)
0x08|kPropertiesOffest* |(inherit from JSReceiver)
0x10|kElementsOffset* |(inherit from JSObject)
+-----------------------------+

JSFunction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
               JSFunction
+-----------------------------+
0x00|kMapOffset* |
0x08|kPropertiesOffest* |
0x10|kElementsOffset* |
0x18|kPrototypeOrInitialMapOffset*|
0x20|kSharedFunctionInfoOffset* |
0x28|kContextOffset* |
0x30|kLiteralsOffset* +------+
0x38|kCodeEntryOffset* | |
0x40|kNextFunctionLinkOffset* | |
+-----------------------------+ |
|
+-----------------------------+<-----+
0x00|codeEntry |
|... |
0x50|(jit code) |(RWX before 6.7)
| |
| |
| |
+-----------------------------+

这里特别标记js函数jit code的偏移地址,在6.7版本之前jit code的内存是RWX权限,所以完成任意读写原语以后直接把自定义js函数的jit code改写成shellcode就完成get shell。

JSArray

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
              FixedArray
+-----------------------------+<----+
0x00|kMapOffset* | |
0x08|kLengthOffset | |
0x10|kElement[0] | |
0x18|kElement[1] | |
|... | |
| | |
| | |
+-----------------------------+ |
|
|
JSArray |
+-----------------------------+ |
0x00|kMapOffset* | |
0x08|kPropertiesOffest* | |
0x10|kElementsOffset* +-----+
0x18|kLengthOffset |
+-----------------------------+

JSArrayBuffer

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
             JSTypedArray                                                             JSTypedArray
+-----------------------------+ +-----------------------------+
0x00|kMapOffset* | 0x00|kMapOffset* |
0x08|kPropertiesOffest* | 0x08|kPropertiesOffest* |
0x10|kElementsOffset* | 0x10|kElementsOffset* +------+
0x18|kBufferOffset* +------+ 0x18|kBufferOffset* | |
0x20|kByteOffsetOffset | | 0x20|kByteOffsetOffset | |
0x28|kByteLengthOffset | | 0x28|kByteLengthOffset | |
0x30|kViewSize* | | 0x30|kViewSize* | |
0x38|kLengthOffset | | 0x38|kLengthOffset | |
+-----------------------------+ | +-----------------------------+ |
| |
| |
JSArrayBuffer | JSArrayBuffer |
+-----------------------------+------+ +-----------------------------+------+<-----+
0x00|kMapOffset* | 0x00|kMapOffset* | |
0x08|kPropertiesOffset* | 0x08|kPropertiesOffset* | |
0x10|kElementsOffset* | 0x10|kElementsOffset* +-------------+
0x18|kByteLengthOffset | 0x18|kByteLengthOffset |
0x20|kBackingStoreOffset* +------+ 0x20|Element 0 |
0x28|kBitFieldOffset | | 0x28|Element 1 |
+-----------------------------+ |change this to arw R/W |... |
| +-----------------------------+
heap chunk |
+-----------------------------+<-----+
| |
| |
| +
+----------------------------+

左边的布局是用以下语句生成出的内存布局,一般适用是ArrayBuffer空间比较大的情况:

1
2
var data_buf = new ArrayBuffer(24);
var data_view = new Float64Array(data_buf);

这种情况可以通过改写JSArrayBuffer->kBackingStoreOffset指针来完成任意读写。
但是有时会出现内存布局不满足越界读写范围的情况,这时可以尝试另一种声明方式:

1
var data_bf = new Float64Array(7);

这种方式会生成右边的内存布局,不同之处在于数值存放的对象通过inline的方式直接存放在JSArrayBuffer对象当中。利用方式是将JSArrayBuffer->kElementOffset指针改成addr - 0x20n | 1n(addr为目标内存地址),随后即可通过对Float64Array的操作来完成任意读写。

BigUint64 <-> Float64

另外一个js engine exploit需要常用到的小工具就是浮点数和64位整数之间的转换,之前都是用saleo exp中的Int64对象操作,后来看到Google CTF的官方exp里面用到了新的BigUint64支持,让这种转换变得更加简单直接了。

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
let conversion_buffer = new ArrayBuffer(8);
let float_view = new Float64Array(conversion_buffer);
let int_view = new BigUint64Array(conversion_buffer);

BigInt.prototype.hex = function() {
return '0x' + this.toString(16);
};

BigInt.prototype.i2f = function() {
int_view[0] = this;
return float_view[0];
}

BigInt.prototype.smi2f = function() {
int_view[0] = this << 32n;
return float_view[0];
}

Number.prototype.f2i = function() {
float_view[0] = this;
return int_view[0];
}

Number.prototype.f2smi = function() {
float_view[0] = this;
return int_view[0] >> 32n;
}

Number.prototype.i2f = function() {
return BigInt(this).i2f();
}

Number.prototype.smi2f = function() {
return BigInt(this).smi2f();
}

let debug = (x) => {
%DebugPrint(x);
}

本篇没有多少新贡献,仅是题目复现后的小小总结。
一拖又拖再拖的复工日期容易让人忘了初心,随便写点东西来振作精神好了。