详细分析谷歌紧急修复的 Chrome 0day(CVE-2021-21224)
编译:奇安信代码卫士
本文作者 iamelli0t 发布文章,详细分析了谷歌于今天修复的两个近期备受关注的两个漏洞Chrome CVE-2021-21220 和 CVE-2021-21224。如下节选编译了针对 CVE-2021-21224 漏洞的分析。
4月12日,Chromium 中的一个代码 commit 引发关注,它是针对 Chromium Javascript 引擎 v8 中某个漏洞的 bugfix。同时,该 bugfix 的回归测试用例 regress-1196683.js 也被提交。基于该回归测试用例,某研究员发布了exploit 样本。而由于 Chrome 发布管道的原因,直到4月13日该漏洞才被修复。
巧合的是,4月15日,v8 中某个 bugfix 的另外一个代码 commit 也包含在一个回归测试用例 regress-1195777.js 中。基于该测试用例,exploit 样本再次被暴露。由于最新的 Chrome 稳定版并未拉取该 bugfix commit,因此该样本仍然可在最新的 Chrome 渲染进程中遭利用。当易受攻击的 Chromium 浏览器在未启用沙箱 (-no-sandbox) 的情况下访问恶意链接时,该漏洞将被触发并造成远程代码执行。
该漏洞的 bugfix 如下:
该 commit 修复了一个整数会话节点生成错误,该节点用于在 SimplifiedLowering 阶段将64位整数转换为32位整数(截断)。提交前,如果当前节点的输出类型是 Signed32 或 Unsigned32,则生成TruncateInt64ToInt32 节点。提交后,如果当前节点的输出类型是 Unsigned32,则接下来需检查 use_info 的类型。只有当 use_info.type_check() == TypeCheckKind::kNone 时,才会生成 TruncateInt64ToInt32。
首先,通过 regress-1195777.js 分析该漏洞的根因:
(function() {
function foo(b) {
let x = -1;
if (b) x = 0xFFFFFFFF;
return -1 < Math.max(0, x, -1);
}
assertTrue(foo(true));
%PrepareFunctionForOptimization(foo);
assertTrue(foo(false));
%OptimizeFunctionOnNextCall(foo);
assertTrue(foo(true));
})();
触发 JIT 的函数 foo 中的关键代码是 return -1 < Math.max(0, x, -1) 。我们重点关注 TurboFan 关键阶段中的 Math.max(0, x, -1) 优化进程:
(1)TyperPhase
Math.max(0, x, -1) 对应于节点56 和节点58。节点58的输出是节点41
的输入:SpeculativeNumberLessThan (<)。
(2)TypedLoweringPhase
Math.max(0, x, -1) 中的两个常数参数 0,-1(节点54 和节点55)被常数节点32 和节点14 替换。
(3) SimplifiedLoweringPhase
原始的 NumberMax 节点56和节点58被 Int64LessThan + Select 节点替换。原始的节点41:SpeculativeNumberLessThan 被替换为 Int32LessThan。当处理 SpeculativeNumberLessThan 的输入节点时,由于输入节点 (Select) 的输出类型是 Unsigned32,则该漏洞被触发,且节点76: TruncateInt64ToInt32 的生成不正确。
Math.max(0, x, -1) 结果被截断为 Signed32。因此,当 Math.max(0, x, -1) 中的 x 是 Unsigned32 时,它会被 TruncateInt64ToInt32 截断为 Signed32。
最后,利用该漏洞,JIT 中异常值为1的变量x可通过如下代码获得(期望的值应该是0):
function foo(flag){
let x = -1;
if (flag){
x = 0xFFFFFFFF;
}
x = Math.sign(0 - Math.max(0, x, -1));
return x;
}
从根因分析来看,当 TurboFan 执行整数数据类型转换(扩展、阶段)时,都会触发CVE-2021-21220 和 CVE-2021-21224。利用这两个漏洞,可以获取 JIT 中异常值为1的变量 x。
根据遭在野利用的样本,利用步骤如下:
(1) 通过出错值为1的变量 x,创建一个数组,长度为1。
(2) 通过Array.prototype.shift() 获取长度为 0xFFFFFFFF 的界外数组。
关键代码如下:
var arr = new Array(x); // wrong: x = 1
arr.shift(); // oob
var cor = [1.8010758439469018e-226, 4.6672617056762661e-62, 1.1945305861211498e+103];
return [arr, cor];
变量 arr = new Array(x) 的 JIT 代码为:
Rdi 是 arr 的长度,其值为1。它通过指针压缩向左移动一位 (rdi+rdi),并存储在JSArray.length 属性 (+0xC) 中。
Arr.shift() 的 JIT 代码为:
Arr.shift() 后,arr 的长度直接由常数 0xFFFFFFFE 分配,优化过程如下:
(1) TyperPhase
数组长度分配操作主要由节点152和节点153组成。节点152计算 Array.length-1。节点153将结算结果保存在 Array.length (+0xC)中。
(2) LoadEliminationPhase
由于通过 Ignition 获取的x的值为0,因此常数折叠 (0-1=-1) 获得常数 0xFFFFFFFF。左移一位后,它是 0xFFFFFFFE,并存储在 Array.length (+0xC) 中。这样,获得长度为 0xFFFFFFFF 的界外数组。
获得界外数组后,接下来就是常见步骤了:
(3) 凭借该界外数组实现 addrof/fakeobj
(4) 构造一个虚假 JSArray,利用 addrof/fakeobj实现任意内存读取/写入原语
Exploit 样本中 arr 和 cor 的内存布局如下:
(a) 利用该漏洞获得长度为 0xFFFFFFFFF的arr(红框)
(b) 利用界外 arr 和 cor 实现 addrof/fakeobj(绿框)
(c) 利用界外 arr 修改 cor 的长度(黄框)
(d) 利用界外 cor,泄露映射和 cor 的属性(蓝框),伪造一个 JSArray并利用该虚假 JSArray 实现任意内存读/写原语。
(5) 借助 WebAssembly 执行 shellcode
最后,借助 WebAssembly 创建属性为 RWX 的内存页面。该 shellcode 被复制到内存页并在最后执行。
利用截图如下:
详细分析 Chrome V8 JIT 漏洞 CVE-2021-21220
https://iamelli0t.github.io/2021/04/20/Chromium-Issue-1196683-1195777.html
题图:Pixabay License
转载请注明“转自奇安信代码卫士 https://codesafe.qianxin.com”。
奇安信代码卫士 (codesafe)
国内首个专注于软件开发安全的
产品线。