作者:栈长@蚂蚁安全实验室
原文链接:https://mp.weixin.qq.com/s/bYhQlblYxQRHqTb3zOrTew

1、背景

Google于4 月 13 日发布了最新的 Chrome 安全通告,公告链接(https://chromereleases.googleblog.com/),其中修复了pwn2own中攻破 Chrome 所使用的一个严重的安全漏洞(CVE-2021-21220),该漏洞影响x64架构的 Chrome,可导致Chrome 渲染进程远程代码执行,并使用了巧妙的手段绕过了 Chrome 内部的各种缓释措施,目前Chrome最新版89.0.4389.128已修复。

在 Google 官方发布安全通告之前,4 月 12 号已有安全研究员公开了该漏洞的利用代码,该漏洞影响范围广,危害大,光年实验室第一时间对该漏洞进行了分析。

实际测试老版本的x64架构的Chrome或 Chromium 83、86、87、88 受此漏洞影响,存在漏洞的代码在 5 年前就被引入,最远可能影响至 Chrome 55 版本,下面是具体的漏洞分析详情:

2、漏洞分析

漏洞存在于 Chrome 的 JS 引擎的 JIT 编译器 Turbofan 当中,Instruction Selector阶段在处理ChangeInt32ToInt64节点时,会先检查 node 的 input 节点,如果 input 节点的操作码是 Load,那么会根据该 input节点的 LoadRepresentation 和 MachineRepresentation进行一些特殊的处理,如果判断该 input 节点的 MachineRepresentation 的类型是kWord32, 那么会根据 LoadRepresentation 是有符号的还是无符号的选择对应的指令,如果是有符号的选择X64Movsxlq,在x86指令集中是有符号扩展,如果是无符号的选择X64Movl, 在x86指令集中是无符号扩展。

漏洞的根源是V8 对ChangeInt32ToInt64的假设是该节点的输入必定被解释为一个有符号的Int32的值,所以无论 LoadRepresentation如何,都应该使用X64Movsxlq指令。

void InstructionSelector::VisitChangeInt32ToInt64(Node* node) {
    DCHECK_EQ(node->InputCount(), 1);
    Node* input = node->InputAt(0);
    if (input->opcode() == IrOpcode::kTruncateInt64ToInt32) {
        node->ReplaceInput(0, input->InputAt(0));
    }
    X64OperandGenerator g(this);
    Node* const value = node->InputAt(0);
    if (value->opcode() == IrOpcode::kLoad && CanCover(node, value)) {
        LoadRepresentation load_rep = LoadRepresentationOf(value->op());
        MachineRepresentation rep = load_rep.representation();
        InstructionCode opcode;
        switch (rep) {
        case MachineRepresentation::kBit:// Fall through.
        case MachineRepresentation::kWord8:
            opcode = load_rep.IsSigned() ? kX64Movsxbq : kX64Movzxbq;
            break;
        case MachineRepresentation::kWord16:
            opcode = load_rep.IsSigned() ? kX64Movsxwq : kX64Movzxwq;
            break;
        case MachineRepresentation::kWord32:
            opcode = load_rep.IsSigned() ? kX64Movsxlq : kX64Movl;
            break;
        default:
            UNREACHABLE();
}

触发漏洞的 poc 如下:

const arr = new Uint32Array([2 ** 31]);
function foo() {
return (arr[0] ^ 0) + 1;
}
console.log(foo()); //这一行输出-2147483647
for (let i = 0; i < 100000; i++)
foo();
console.log("after optimization");
console.log(foo());//这一行输出2147483649

同样一个函数在优化前和优化后返回的结果不一致。在优化前,arr[0] ^ 0的结果用十六进制表示是0x80000000, 对于异或运算,JS 引擎会将结果看做一个有符号的 Int32 的值,所以异或的结果是-2147483648, 加上1 以后变成-2147483647。

但是为什么优化后的结果不一致了呢,我们可以观察程序运行过程中生成的 Turbofan 的图。arr[0] ^ 0这个表达式被优化成了一个Load节点, 对应下图中的 #81 节点,而(arr[0] ^0) + 1这个加法运算被优化成了#58 ChangeInt32ToInt64节点和#50 Int64Add节点,如下图中黄色高亮部分所示,在图中可以看到,Load 节点的 MachineRepresentation 是Word32,而LoadRepresentation的类型是Uint32, 故而 Turbofan 选择了movl指令,也就是无符号扩展指令,导致 Load 节点的值会被无符号扩展为 64 位,然后和 1 相加,最后结果自然是2147483649。

用调试器调试也可以验证,在执行到mov ecx, DWORD PTR [rcx] 这一行时,rcx寄存器指向的值为0x80000000, 无符号扩展变成了2147483648, 最后加上 1 变成了2147483649。

3、止血修复方案

由于 Chrome 的沙箱机制,该漏洞仍需配合一个提权漏洞或沙箱漏洞才能在受害者的主机上执行任意代码,尚不能形成完整的攻击链路,但是鉴于市面上有些基于 Chromium 内核的应用关闭了沙箱功能,这些应用仍然有可能受到该漏洞的影响,产生实际的危害。

使用了 Chromium 内核的开发者可以参考此修复链接(https://chromium-review.googlesource.com/c/v8/v8/+/2820971)进行修复,在 Instruction Selector 阶段处理 ChangeInt32ToInt64节点时,如果MachineRepresentation 是kWorkd32,无论LoadRepresentation 是有符号的还是无符号的,都选择X64Movsxlq指令。或者将 Chromium 内核升级到最新版本。Chrome 用户则需尽快将 Chrome 升级到最新版本。

蚂蚁安全光年实验室:隶属于蚂蚁安全实验室。通过对基础软件及设备的安全研究,达到全球顶尖破解能力,致力于保障蚂蚁集团及行业金融级基础设施安全。因发现并报告行业系统漏洞,上百次获得Google、Apple等国际厂商致谢。

扫码关注蚂蚁安全实验室微信公众号,干货不断!


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1556/