Chrome 远程代码执行漏洞(CVE-2021-21220)
Chrome 远程代码执行漏洞(CVE-2021-21220)
前言
Google于4 月 13 日发布了最新的 Chrome 安全通告,公告链接(https://chromereleases.googleblog.com/),其中修复了pwn2own中攻破 Chrome 所使用的一个严重的安全漏洞(CVE-2021-21220),该漏洞影响x64架构的 Chrome,可导致Chrome 渲染进程远程代码执行,并使用了巧妙的手段绕过了 Chrome 内部的各种缓释措施,目前Chrome最新版89.0.4389.128已修复。
实际测试老版本的x64架构的Chrome或 Chromium 83、86、87、88 受此漏洞影响,存在漏洞的代码在 5 年前就被引入,最远可能影响至 Chrome 55 版本,下面是具体的漏洞分析详情:
环境搭建
v8编译环境搭建
漏洞版本commit
通过访问如下链接可以获取到对应版本的信息
方法一
https://omahaproxy.appspot.com
/Untitled_1.png)
但是该方法于2023年3月31日已经弃用了
方法二
从漏洞的issue链接https://bugs.chromium.org/p/chromium/issues/detail?id=821137
找到修复的commit链接https://chromium.googlesource.com/v8/v8.git/+/b5da57a06de8791693c248b7aafc734861a3785d ,可以看到漏洞信息、存在漏洞的上一个版本(parent)、diff修复信息和漏洞poc
方法三
可以去如下的网站获取对应的commit,也是https://omahaproxy.appspot.com的平替
https://chromiumdash.appspot.com/fetch_version?version=110.0.5481.61
v8环境搭建
最后得出对应的v8的commit为09ecd88ef275f6c66605218a0ffb72123ea3b5e1
漏洞分析
POC1
漏洞存在于 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指令。
1 | void InstructionSelector::VisitChangeInt32ToInt64(Node* node) { |
触发漏洞的 poc 如下:
1 | const arr = new Uint32Array([2 ** 31]); |
同样一个函数在优化前和优化后返回的结果不一致。在优化前,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。
/229594fb-115b-4eff-b615-7f26315e408a.png)
用调试器调试也可以验证,在执行到mov ecx, DWORD PTR [rcx] 这一行时,rcx寄存器指向的值为0x80000000, 无符号扩展变成了2147483648, 最后加上 1 变成了2147483649。
/7321efec-b7c8-446d-840e-c095e09852e3.png)
POC2
1 | var b = new Uint32Array([0x80000000]); |
上述PoC来源于:https://github.com/security-dbg/CVE-2021-21220/blob/main/exploit.js
因为我认为这个PoC更利于理解该漏洞。
在正常情况下,该函数的逻辑:
- b[0]为uint32类型的变量,其值为0x80000000。
- 异或了0以后,变成了int32类型,其值为-2147483648。
- 加上1以后,变成了-2147483647,赋值给了x。但是类型会被扩展成int64,因为js的变量是弱类型,如果x一开始的类型是int32,值为2147483647(0x7fffffff),那么x+1不会变成-1,而会变成。2147483648(0x80000000),因为int32被扩展成了int64。
- 然后使用math.abs函数计算绝对值,x值变为2147483647(0x7fffffff)。
- x - 0x7FFFFFFF = 0。
- 使用math.max函数计算x与0之间的最大值,为0。
- x - 1 = -1。
- 因为x=-1,所以x改为0。
- 新建了一个长度为0的数组。
- 因为长度为0,所以shitf无效,数组不变。
但是上述逻辑,经过JIT优化以后,就不一样了:
- b[0]为uint32类型的变量,其值为0x80000000。
- 将其转化成int64类型,其值为0x80000000。
- 加上1以后,变成了0x80000001。
- 然后使用math.abs函数计算绝对值,x值变为0x80000001。
- x - 0x7FFFFFFF = 2。
- 使用math.max函数计算x与0之间的最大值,为2。
- x - 1 = 1。
- 新建了一个长度为1的数组。
- shitf函数将数组的长度设置为-1,这就让我们得到了长度为-1的数组,通过该数据进行后续利用。
在JIT的优化过程中,存在两个问题:
1.将b[0]转化为int64,把符号去掉了,从Turbo流程图看,是通过ChangeInt32ToInt64来改变b[0]的变量类型,而在这个opcode实现的代码中:
1 | void InstructionSelector::VisitChangeInt32ToInt64(Node* node) { |
根据上面代码可以看出,如果b[0]是有符号的,那么将会使用kX64Movsxlq指令进行转换,如果是无符号的就会使用kX64Movl指令进行转换。
b[0]因为是一个uint32类型的变量,所以使用movl进行扩展大小,所以没有扩展其符号,导致出现了问题。
2.shift函数将数组长度设置为-1。
shift函数的正常逻辑是,判断数组的长度,如果其长度大于0,并且小于100,那么将会对长度的赋值进行优化,预测其长度,然后进行减1操作,直接写入数组的长度。
在JIT的预测当中,x的值为0,因为其预测是按照没有bug的情况进行预测的,但是实际情况x为1,这就导致实际情况的x通过了shitf的长度检查,然后却把x认为是0,从而-1,把数组的长度设置为了-1。
POC3
1 | function gc() { |
exp
1 | /* |



