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

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Введение

Мы анализировали существующую уязвимость V8 CVE-2023–2033. Как только мы воспользовались этой ошибкой, нам не составило труда получить типичные примитивы эксплойта, такие как addrof, чтение и запись в кучЕ V8. Проблема в том, что нам нужно выйти из песочницы V8, чтобы добиться выполнения кода.

Однажды нам довелось прочитать твит от @zh1x1an1221. Ему удалось взломать хром, промодемонстрировав запуск калькулятора, воспользовавшись CVE-2023–3079, и еще одной распространенной уязвимостью, он смог обойти «песочницу». В твите он упомянул коммит, связанный с песочницей, который он использовал для выхода из песочницы. Похоже, что коммит поместил в «песочницу» необработанный указатель в объекте WebAssembly, который был использован для обхода «песочницы» V8. На этот коммит стоило обратить внимание, поскольку необработанные указатели в куче V8 всегда были источниками выхода из песочницы V8.

В этой статье мы поделимся подробностями того, как мы достигли произвольных примитивов записи и выполнения кода, используя необработанный указатель в объекте WasmIndirectFunctionTable. CVE-2023-2033 мы рассматривать не будем, так как о ней уже много подробных описаний. Ниже будет краткий анализ исправлений, связанных с обходом песочницы.

Описание

Чтобы понять обход песочницы V8 в этой статье, нам нужно понять три концепции WebAssembly: модуль, экземпляр и таблица. Модуль — это набор кода WebAssembly без сохранения состояния, экземпляр которого мы можем создать с помощью JavaScript. Мы можем думать об этом как о двоичном файле (например, ELF), поскольку мы можем создавать процессы из двоичного файла. Экземпляр — это исполняемый объект с сохранением состояния, созданный из модуля. Как и модули других языков программирования, модуль WebAssembly может содержать экспортированные функции WebAssembly, к которым мы можем получить доступ с помощью JavaScript.

Таблица — самое важное понятие в этом посте. Это массив функций, к которым мы можем получить доступ через индексы таблицы. Записи в таблице доступны как для чтения, так и для записи динамически с помощью кода WebAssembly или API-интерфейсов JavaScript.

Когда мы создаем экземпляр модуля, экземпляр может импортировать функции JavaScript и таблицы WebAssembly. Ниже приведен пример кода WebAssembly. Он импортирует функцию JavaScript и таблицу WebAssembly ( jstimes и tbl). Затем он определяет две функции $f42, $f83 которые используются для инициализации импортированной таблицы. Наконец, он определяет две экспортируемые функции times2 и pwn.

Код: Скопировать в буфер обмена
Код:
(module
  ;; The common type we use throughout the sample.
  (type $int2int (func (param i32) (result i32)))

  ;; Import a function named jstimes3 from the environment and call it
  ;; $jstimes3 here.
  (import "env" "jstimes3" (func $jstimes3 (type $int2int)))

  (import "js" "tbl" (table 2 funcref))
  (func $f42 (result i32) i32.const 42)
  (func $f83 (result i32) i32.const 83)
  (elem (i32.const 0) $f42 $f83)

  (func (export "times2") (type $int2int) (i32.const 16))
  (func (export "pwn") (type $int2int) (i32.const 16) (call $jstimes3))
)

Мы можем импортировать приведенный выше код WebAssembly в JavaScript с помощью следующего кода.

JavaScript: Скопировать в буфер обмена
Код:
const tbl = new WebAssembly.Table({
    initial: 2,
    element: "anyfunc"
});
const importObject = {
    env: {
        jstimes3: (n) => 3 * n,
    },
    js: { tbl }
};
var code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 10, 2, 96, 1, 127, 1, 127, 96, 0, 1, 127, 2, 27, 2, 3, 101, 110, 118, 8, 106, 115, 116, 105, 109, 101, 115, 51, 0, 0, 2, 106, 115, 3, 116, 98, 108, 1, 112, 0, 2, 3, 5, 4, 1, 1, 0, 0, 7, 16, 2, 6, 116, 105, 109, 101, 115, 50, 0, 3, 3, 112, 119, 110, 0, 4, 9, 8, 1, 0, 65, 0, 11, 2, 1, 2, 10, 24, 4, 4, 0, 65, 42, 11, 5, 0, 65, 211, 0, 11, 4, 0, 65, 16, 11, 6, 0, 65, 16, 16, 0, 11]);
var module = new WebAssembly.Module(code);
var instance = new WebAssembly.Instance(module, importObject);
var times2 = instance.exports.times2;

%DebugPrint(instance);

В V8 экземпляр и таблица WebAssembly реализованы как WasmInstanceObjectи WasmTableObject. Когда экземпляр импортирует таблицу, импортированная таблица сохраняется в поле tables файла WasmInstanceObject. Затем WasmIndirectFunctionTable выделяется и сохраняется в поле indirect_function_tables файла WasmInstanceObject. WasmIndirectFunctionTable имеет поле targets, содержащее указатели на функции WasmTableObject. Импортированные функции JavaScript сохраняются в поле imported_function_targets файла WasmInstanceObject. Итак, из приведенного выше кода WebAssembly и JavaScript структура выглядит следующим образом:

1707040233222.png


Расположение памяти среди объектов Wasm


Использование WasmIndirectFunctionTable для получения примитива произвольной записи

Когда мы создаем дамп памяти объекта WasmIndirectFunctionTable, мы видим, что targets это необработанный указатель, указывающий на область памяти за пределами песочницы V8.

DebugPrint: 0x239d001a43ed: [WasmInstanceObject] in OldSpace
- map: 0x239d001997a5 <Map[224](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x239d001a35d1 <Object map = 0x239d001a43c5>
- elements: 0x239d00000219 <FixedArray[0]> [HOLEY_ELEMENTS]
- module_object: 0x239d00042991 <Module map = 0x239d00199379>
- exports_object: 0x239d00042af1 <Object map = 0x239d001a4661>
- native_context: 0x239d00183c2d <NativeContext[282]>
- tables: 0x239d00042a91 <FixedArray[1]>
- indirect_function_tables: 0x239d00042a9d <FixedArray[1]
- ...

0x239d00042a9d: [FixedArray]
- map: 0x239d00000089 <Map(FIXED_ARRAY_TYPE)>
- length: 1
0: 0x239d00042ab9 <WasmIndirectFunctionTable>
0x239d00042ab9: [WasmIndirectFunctionTable]
- map: 0x239d00001599 <Map[32](WASM_INDIRECT_FUNCTION_TABLE_TYPE)>
- size: 2
- sig_ids: 0x562ebe531150
- targets: 0x562ebe531170
- managed_native_allocations: 0x239d00042ad9 <Foreign>
- refs: 0x239d00042aa9 <FixedArray[2]>
Нажмите, чтобы раскрыть...

pwndbg> x/8gx 0x239d00042ab8
0x239d00042ab8: 0x0000000200001599 0x0000562ebe531150
0x239d00042ac8: 0x0000562ebe531170 <-- targets
0x239d00042ad8: 0x00008ba00000036d 0x0000000400000089
0x239d00042ae8: 0x00000000001a43ed 0x00000219001a4661
pwndbg> x/4gx 0x562ebe531170
0x562ebe531170: 0x00003bc1b5892000 0x00003bc1b5892005 <-- $f42, $f83
0x562ebe531180: 0x0000000000000020 0x0000000000000081
Нажмите, чтобы раскрыть...

Когда мы ищем коды доступа к указателю targets, мы можем найти следующую функцию:

C++: Скопировать в буфер обмена
Код:
void WasmIndirectFunctionTable::Set(uint32_t index, int sig_id,
                                    Address call_target, Object ref) {
  sig_ids()[index] = sig_id;
  targets()[index] = call_target;
  refs().set(index, ref);
}

WasmIndirectFunctionTable::Set записывает в область памяти call_target, на которую указывает targets. Поскольку targets в песочнице V8 это необработанный указатель, мы можем добиться произвольного примитива записи, изменив указатель с помощью наших примитивов чтения/записи в песочнице. Теперь вопрос в том, можем ли мы установить произвольное значение по нашему выбору для call_target. Итак, мы проанализировали, как мы можем достичь WasmIndirectFunctionTable::Set и откуда берется это значение call_target.

Маршрут к WasmIndirectFunctionTable::Set начинается от WasmTableObject::Set. Это реализация WebAssembly.Table.prototype.set() JavaScript API. Сначала он вызывает WasmTableObject::SetFunctionTableEntry.

C++: Скопировать в буфер обмена
Код:
void WasmTableObject::Set(Isolate* isolate, Handle<WasmTableObject> table,
                          uint32_t index, Handle<Object> entry) {
  // ...
  switch (table->type().heap_representation()) {
    // ...
    default:
      DCHECK(!table->instance().IsUndefined());
      if (WasmInstanceObject::cast(table->instance())
              .module()
              ->has_signature(table->type().ref_index())) {
        SetFunctionTableEntry(isolate, table, entries, entry_index, entry);
        return;
      }
      entries->set(entry_index, *entry);
      return;
  }
}

WasmTableObject::SetFunctionTableEntry проверяет, имеет ли передаваемая функция WasmTableObject::Set тип WasmExportedFunction. Если да, он получает родительский объект WasmInstanceObject экспортируемой функции. Затем он загружает индекс экспортированной функции, и этот индекс используется для получения указателя на объект wasm::WasmFunction, который находится в файле WasmInstanceObject. Со всеми значениями он вызывает WasmTableObject::UpdateDispatchTables.

C++: Скопировать в буфер обмена
Код:
void WasmTableObject::SetFunctionTableEntry(Isolate* isolate,
                                            Handle<WasmTableObject> table,
                                            Handle<FixedArray> entries,
                                            int entry_index,
                                            Handle<Object> entry) {
  // ...
  Handle<Object> external = WasmInternalFunction::GetOrCreateExternal(
      Handle<WasmInternalFunction>::cast(entry));

  if (WasmExportedFunction::IsWasmExportedFunction(*external)) {
    auto exported_function = Handle<WasmExportedFunction>::cast(external);
    Handle<WasmInstanceObject> target_instance(exported_function->instance(),
                                               isolate);
    int func_index = exported_function->function_index();
    auto* wasm_function = &target_instance->module()->functions[func_index];
    UpdateDispatchTables(isolate, *table, entry_index, wasm_function,
                         *target_instance);
  }
  // ...
}

WasmTableObject::UpdateDispatchTables выполняет итерацию по таблицам диспетчеризации внутри таблицы и обновляет соответствующие значения для каждой записи WasmIndirectFunctionTable, вызывая метод WasmIndirectFunctionTable::Set.

Здесь мы видим, что call_target переданное значение — WasmIndirectFunctionTable::Set это возвращаемое значение WasmInstanceObject::GetCallTarget.

C++: Скопировать в буфер обмена
Код:
void WasmTableObject::UpdateDispatchTables(Isolate* isolate,
                                           WasmTableObject table,
                                           int entry_index,
                                           const wasm::WasmFunction* func,
                                           WasmInstanceObject target_instance) {
  DisallowGarbageCollection no_gc;

  // We simply need to update the IFTs for each instance that imports
  // this table.
  FixedArray dispatch_tables = table.dispatch_tables();
  DCHECK_EQ(0, dispatch_tables.length() % kDispatchTableNumElements);

  // ...
  Address call_target = target_instance.GetCallTarget(func->func_index);

  int original_sig_id = func->sig_index;

  for (int i = 0, len = dispatch_tables.length(); i < len;
       i += kDispatchTableNumElements) {
    int table_index =
        Smi::cast(dispatch_tables.get(i + kDispatchTableIndexOffset)).value();
    WasmInstanceObject instance = WasmInstanceObject::cast(
        dispatch_tables.get(i + kDispatchTableInstanceOffset));
    int sig_id = target_instance.module()
                     ->isorecursive_canonical_type_ids[original_sig_id];
    WasmIndirectFunctionTable ift = WasmIndirectFunctionTable::cast(
        instance.indirect_function_tables().get(table_index));
    ift.Set(entry_index, sig_id, call_target, call_ref);
  }
}

WasmInstanceObject::GetCallTarget возвращает фактический адрес (т. е. указатель кода функции) функции WebAssembly, индекс которой в экземпляре равен func_index. Параметр func_index может быть либо импортированной функцией, либо экспортированной функцией. Если функция является импортированной функцией, цель вызова будет получена из imported_function_targets. Поскольку мы уже проверили, что это func_indexfrom WasmExportedFunction, возвращаемое значение будет from jump_table_start() + ....

C++: Скопировать в буфер обмена
Код:
Address WasmInstanceObject::GetCallTarget(uint32_t func_index) {
  wasm::NativeModule* native_module = module_object().native_module();
  if (func_index < native_module->num_imported_functions()) {
    return imported_function_targets().get(func_index);
  }
  return jump_table_start() +
         JumpTableOffset(native_module->module(), func_index);
}

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

DebugPrint: 0x3ed3001a4f89: [WasmInstanceObject] в OldSpace
...
- import_function_targets: 0x3ed300042cd9 <ByteArray[8]>
...
- jump_table_start: 0x10553c7e7000
...
Нажмите, чтобы раскрыть...

Таким образом, мы должны сделать чтобы WasmInstanceObject::GetCallTarget исполняла if (func_index < ...), чтобы сделать возвращаемое значение контролируемым.

native_module->num_imported_functions() равно 1 из нашего кода Wasm ( (import "env" "jstimes3" (func $jstimes3 (type $int2int)))).

func_index читается из объекта WasmExportedFunctionData, который находится в песочнице V8. Поэтому, если мы установим function_index в нулевое значение экспортированной функции Wasm и вызовем WasmInstanceObject::GetCallTarget, то функция возьмет ветвь if и вернет значение в imported_function_targets.

DebugPrint: 0x2bc001a4505: [Function] in OldSpace
- map: 0x02bc00193751 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x02bc00184299 <JSFunction (sfi = 0x2bc001460a5)>
- elements: 0x02bc00000219 <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x02bc001a44e1 <SharedFunctionInfo js-to-wasm:i:i>
- ...
0x2bc001a44e1: [SharedFunctionInfo] in OldSpace
- map: 0x02bc00000d75 <Map[36](SHARED_FUNCTION_INFO_TYPE)>
- name: 0x02bc00002775 <String[1]: #3>
- kind: NormalFunction
- syntax kind: AnonymousExpression
- function_map_index: 206
- formal_parameter_count: 1
- expected_nof_properties: 0
- language_mode: sloppy
- data: 0x02bc001a44b5 <Other heap object (WASM_EXPORTED_FUNCTION_DATA_TYPE)>
- ...
0x2bc001a44b5: [WasmExportedFunctionData] in OldSpace
- map: 0x02bc00001ea9 <Map[44](WASM_EXPORTED_FUNCTION_DATA_TYPE)>
- internal: 0x02bc001a449d <Other heap object (WASM_INTERNAL_FUNCTION_TYPE)>
- wrapper_code: 0x02bc0002bb9d <Code BUILTIN GenericJSToWasmWrapper>
- js_promise_flags: 0
- instance: 0x02bc001a4381 <Instance map = 0x2bc001997a5>
- function_index: 3
- ...
Нажмите, чтобы раскрыть...

Ниже приведены обобщенные шаги для получения произвольного примитива записи:

1. Создайте таблицу WebAssembly и экземпляр WebAssembly, который импортирует таблицу.
— Модуль WebAssembly должен импортировать хотя бы одну функцию JavaScript, чтобы значение native_module->num_imported_functions() было ненулевым.
2. Перезапишите указатель targets в файле WasmIndirectFunctionTable на произвольный адрес WasmInstanceObject
- Этот указатель будет указателем where (Write-What-Where) произвольного примитива записи.
3. Установите значение function_index экспортированной функции WebAssembly на ноль.
4. Перезапишите содержимое, на которое указывает imported_function_targets произвольным значением.
- Это значение будет значением what (Write-What-Where) произвольного примитива записи.
5. Вызов WebAssembly.Table.prototype.set().
- Этот вызов запишет what в файл where. (WWH)

1707040380493.png


V8 Crash - invalid write access


Примитив произвольной записи для выполнения кода

Импортированные функции при создании экземпляра модуля WebAssembly сохраняются в imported_function_targets в файле WasmInstanceObject.

imported_function_targets содержит точки входа кода импортированных функций.

Указатели представляют собой необработанные указатели с разрешениями RWX.

DebugPrint: 0x418001a4fa1: [WasmInstanceObject] in OldSpace
- ...
- imported_function_targets: 0x041800042cd9 <ByteArray[8]>
- ...
Нажмите, чтобы раскрыть...

pwndbg> x/8gx 0x041800042cd8
0x41800042cd8: 0x000000100000095d 0x00003cef5608b700
0x41800042ce8: 0x0000000200000089 0x00000089001a5081
0x41800042cf8: 0x000000000000000a 0x0000000000000000
0x41800042d08: 0x001a5169001a50bd 0x00000006000000d9
pwndbg> vmmap 0x00003cef5608b700
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x41b80c80000 0x52000000000 ---p 1047f380000 0 [anon_41b80c80]
► 0x3cef5608b000 0x3cef5608c000 rwxp 1000 0 [anon_3cef5608b] +0x700
Нажмите, чтобы раскрыть...

Таким образом, с помощью произвольного примитива записи мы можем скопировать наш шелл-код в память rwx и выполнить его с помощью экспортированной функции Wasm, которая вызывает перезаписанную импортированную функцию.

0x3bdb0004cce5: [WasmIndirectFunctionTable]
- map: 0x3bdb00001589 <Map[20](WASM_INDIRECT_FUNCTION_TABLE_TYPE)>
- size: 2
- sig_ids: 0x3bdb0004ccc5 <ByteArray[8]>
- targets: 0x3bdb0004ccd5 <ExternalPointerArray[2]>
- refs: 0x3bdb0004ccb5 <FixedArray[2]>
Нажмите, чтобы раскрыть...

Патчи

Патчи для обхода песочницы выполняются в два этапа.

Первый патч превратил указатель targets в указатель в куче (сжатый по указателю), так что указатель нельзя использовать для получения произвольного примитива записи. Мы заметили, что этот коммит был помечен тем же номером проблемы, что и CVE-2023-2033. Это означает, что реальный эксплойт, доступный автору отчета о проблеме, мог использовать ту же технику эксплойта.

Точки входа в код targets также были уязвимы, поэтому второй патч превратил объект targets в код ExternalPointerArray, содержащий закодированные указатели ( ExternalPointer) вместо необработанных указателей. Этот патч не позволял злоумышленникам изменять указатели кода в target.

0x3bdb0004cce5: [WasmIndirectFunctionTable]
- map: 0x3bdb00001589 <Map[20](WASM_INDIRECT_FUNCTION_TABLE_TYPE)>
- size: 2
- sig_ids: 0x3bdb0004ccc5 <ByteArray[8]>
- targets: 0x3bdb0004ccd5 <ExternalPointerArray[2]>
- refs: 0x3bdb0004ccb5 <FixedArray[2]>
Нажмите, чтобы раскрыть...

Ниже приводится график, связанный с CVE-2023–2033 и исправлениями обхода песочницы.

21 июля 2023 г.: выпущен второй патч для обхода песочницы.
14 апреля 2023 г.: выпущен первый патч для обхода песочницы.
12 апреля 2023 г.: CVE-2023–2033 пропатчено.
11 апреля 2023 г.: сообщение о проблеме CVE-2023–2033.

Рекомендации

v8.dev/blog/pointer-compression
developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Module
developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Instance
developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Table
developer.mozilla.org/en-US/docs/WebAssembly/Exported_functions
x.com/zh1x1an1221/status/1694573285563056201?s=20
bugs.chromium.org/p/chromium/issues/detail?id=1432210
developer.mozilla.org/en-US/docs/WebAssembly/Understanding_the_text_format

Переведено специально для XSS.IS
Автор перевода: yashechka
Источник: https://blog.theori.io/a-deep-dive-...ique-used-in-in-the-wild-exploit-d5dcf30681d4
 
Сверху Снизу