一个针对 CVE-2024-4947(V8 Maglev 类型混淆)的完整、自包含利用链,从最初的类型混淆触发一直到 任意代码执行。该 PoC 针对 非沙箱 的 d8 构建运行,并向 stdout 打印 CVE-2024-4947-PWNED 作为代码执行的证明。
⚠️ 这是一个针对 已修补、公开 漏洞的 研究 / 教育 用途 PoC。 它针对的是 V8 shell(
d8 --allow-natives-syntax)的开发构建 — 而非 真实的 Chrome 浏览器。参见 免责声明。
$ ./v8-build-nosandbox.sh # build the vulnerable d8 (WSL2 Ubuntu, ~10-20 min)
$ d8 --allow-natives-syntax --module exploit/exploit_rce.mjs
[engine] dblData0=0004f470 class=0
[inst] addr=0x001dc274 trusted_data(tagged)=0x00202bd5 td=0x00202bd4
[jt] jump_table_start = 0x00000967426cd000 (external code space)
[bridge] memory0_start -> jt, memory0_size -> huge (r/w reach jt+off)
[slot0] before: e9 3b 08 00 00
[shell] wrote 52 bytes @ jt+0x0100: VERIFIED
CVE-2024-4947-PWNED
$ echo $? # 0
该 52 字节的 shellcode 为 write(1, "CVE-2024-4947-PWNED\n", 20); exit_group(0)。
CVE-2024-4947 是 V8 Maglev JIT 编译器中的一个类型混淆漏洞,已在 Chrome 125.0.6422.60(提交 b3c01ac1e60a)中修复。它曾被 Lazarus APT 组织 在野 利用。当 Maglev 编译对 模块命名空间对象(JSModuleNamespace)的存储操作时,它使用了错误的 AccessInfo — 该存储被编译为一条普通的 mov [[obj + 4], rax],将受控值写入相邻对象的 map 字段,而不是走正常的属性存储路径。
利用过程如下:
NAME_DICTIONARY_TYPE (0xB2) map,这会改变 V8 存储对象哈希的位置。new WeakRef(...),使哈希写入落在 相邻对象的 length 槽位 → 越界 访问。由此我们获得经典的 V8 工具集:addrOf/fakeObj,进而实现任意 cage 内读写。
CVE-2024-4947 trigger (fake NAME_DICTIONARY map + WeakRef hash write)
└─► OOB write ─► corrupt doubleArray length
└─► in-cage 4/8-byte arbitrary R/W (the "engine")
└─► overwrite WasmTrustedInstanceData.memory0_start (+0x18)
& memory0_size (+0x20) → huge
└─► wasm load8_u / store8 = clean 64-bit R/W bridge
(no software bounds check in compiled code)
└─► read jump_table_start (+0x38) — external code space
└─► jump table region is RWX (this build)
└─► write shellcode into the slack (jt+0x100)
└─► repoint func0's `e9 rel32` slot at it
└─► call func0 → shellcode → RCE
| # | 阶段 | 详情 |
|---|---|---|
| 1 | 触发 | opt() 通过混淆的存储写入伪造的 map;new WeakRef(m) 使 corruptArray.length 越界。 |
| 2 | 引擎 | addrOf/fakeObj;破坏 doubleArray 的 length → 在任意 4 字节对齐的 cage 地址上进行任意 4 字节 R/W。 |
| 3 | 64 位桥 | WasmTrustedInstanceData.memory0_start (+0x18) 是编译后的 wasm i32.load8_u/i32.store8 使用的原始 64 位指针,且 没有软件边界检查(越界会命中守卫页 → SIGSEGV → wasm 陷阱)。将其重定向到任意地址,并将 memory0_size (+0x20) 设为巨大值 → 任意 64 位 R/W。 |
| 4 | 寻找代码空间 | jump_table_start (+0x38) 是指向外部代码空间(4 GB cage 之外)的原始 64 位指针。 |
| 5 | 写入 shellcode | 在此构建中,跳转表区域为 RWX(V8 在运行时修补条目):将 52 字节的 shellcode 写入 jt+0x100 处的 slack 空间。 |
| 6 | 重新指向槽位 | func0 的跳转表槽位是 5 字节的 e9 <rel32>(目标地址 = slot + 5 + rel32)。设置 rel32 → jt+0x100。 |
| 7 | 触发分发 | inst.exports.r(0) → JSToWasmWrapper 通过槽位 0 分发 → shellcode 执行。 |
大多数公开的 CVE-2024-4947 PoC 针对的是 沙箱化 的 d8 或真实的 Chrome 渲染进程(v8_enable_sandbox=true),并且止步于类型混淆 / OOB 原语。本 PoC 针对 无沙箱 的 d8,并将利用链一路推进到代码执行。重要的差异如下:
| 维度 | 常见公开 PoC 方案 | 本 PoC |
|---|---|---|
| 目标构建 | 沙箱化 d8 / Chrome 渲染进程 | v8_enable_sandbox=false 的 d8 — 受信指针为直接指针,外部代码空间开启 |
| 64 位 R/W 桥 | 覆写 JSTypedArray.external_pointer | 该路径在读取代码范围时会 崩溃(已通过实验验证);改为覆写 memory0_start 并使用原始 wasm 加载/存储 |
| 最终代码执行路径 | 代码空间为 W^X → JIT-spray + 间接重定向 | 跳转表区域为 RWX → 直接写入 shellcode + e9 rel32 槽位修补 |
| 跳转表槽位格式 | 文档通常假设为 movabs rax, imm64; jmp rax(12 字节) | 实测:5 字节 e9 <rel32>,目标 = slot + 5 + rel32 |
| 字段偏移 | 沙箱构建的布局 | 无沙箱 WasmTrustedInstanceData:jump_table_start@+0x38、memory0_start@+0x18、memory0_size@+0x20;WasmInstanceObject.trusted_data@+0x0c |
| 干净退出 | — | d8 是多线程的:必须使用 exit_group (231) 而非 exit (60),否则进程会挂起 |
完整的技术分析见 docs/walkthrough.md,其中包含每一行背后的实验发现。
.
├── exploit/
│ ├── Module.mjs # module namespace object corrupted by the trigger
│ ├── exploit_rce.mjs # the full chain (trigger → arbitrary R/W → RCE)
│ └── shellcode.S # assembly source for the 52-byte payload
├── build/
│ └── v8-build-nosandbox.sh # build the vulnerable no-sandbox d8 from V8 source
└── docs/
└── walkthrough.md # deep dive: bridge mechanics, layouts, gotchas
该 exploit 从自己的目录导入 Module.mjs(存在漏洞的模块命名空间对象),因此请将这两个文件放在一起(或调整 import 路径)。
git 以及约 10 GB 可用磁盘空间的 Linuxdepot_tools(git clone https://chromium.googlesource.com/chromium/tools/depot_tools)# 1. fetch V8 at the vulnerable tag (12.4.254.16 is the pre-fix release)
export PATH="$HOME/depot_tools:$PATH"
cd ~/v8w && fetch v8 && cd v8
git checkout 12.4.254.16 # or the commit just before b3c01ac1e60a
# 2. first build a normal release d8 (needed to seed args.gn), then:
./v8-build-nosandbox.sh # copies args.gn and appends v8_enable_sandbox = false
v8-build-nosandbox.sh 使用 ninja -C out.gn/x64.release_nosandbox -j6 d8。
out.gn/x64.release_nosandbox/d8 --allow-natives-syntax --module exploit/exploit_rce.mjs
预期输出以以下内容结尾:
CVE-2024-4947-PWNED
并且进程以状态 0 退出。
--allow-natives-syntax是%PrepareFunctionForOptimization/%OptimizeMaglevOnNextCall运行时调用所必需的。这就是为什么该 PoC 无法在真实 浏览器中运行——它按设计就是一个 d8 shell 概念验证。
对 V8 漏洞利用研究而言真正可复用(且不显而易见)的部分:
JSTypedArray.external_pointer 重定向在目标区域上失败时,改为重定向 WasmTrustedInstanceData.memory0_start。编译后的 wasm 字节加载/存储不进行软件边界检查;信号陷阱处理程序只转换守卫页故障,因此已映射但不可写的区域会崩溃而不会触发陷阱。try/catch 逐字节探测;wasm “越界”陷阱意味着守卫页(未映射),真正的 SIGSEGV 意味着已映射但为 RX。e9 rel32 槽位格式 — 5 字节相对跳转,而非许多文章所假设的 movabs 形式。已通过 --print-wasm-code 反汇编验证。本仓库 仅用于教育和防御性安全研究。它演示了针对 仅限开发 的 V8 构建利用 已修补 漏洞的过程。它 不是 针对现代 Chrome 的武器,无法绕过 V8 沙箱,并且需要非默认的 --allow-natives-syntax 标志。作者对任何滥用行为概不负责。
触发原语(伪造 NAME_DICTIONARY_TYPE map + WeakRef 哈希写入)遵循补丁发布后对在野 Lazarus 利用的公开分析——包括 Google 和 Exodus Intelligence 的 CVE-2024-4947 分析文章。后续阶段(跳转表探测、桥、槽位修补)是在本次工作中针对这一特定构建通过实验推导得出的。JIT-spray 后备方案的概念借鉴了公开的 V8 利用技术(例如 CVE-2024-5830 的 make_array 模式)。
MIT — 参见 LICENSE。