Введение
Недавно ко мне в руки попал инструмент, который позволяет злоупотреблять функциями AppLocker, для того чтобы блокировать запуск AV/EDR-решений на хосте. Изучая принцип работы данного инструмента, я погрузился в основы работы самого компонента AppLocker. Изучая шаги его работы, я предположил, что сама его архитектура уязвима.
В этой статье я опишу результаты моего исследования архитектуры AppLocker, которое привело к обнаружению незащищенного канала между пользовательским режимом и ядром. Я покажу, как можно создать полную видимость применения политик для администратора, при этом фактически заблокировав передачу правил в драйвер ядра. Это позволяет любому исполняемому файлу успешно запускаться в обход ограничений политики. Также я покажу, каким образом можно подменить политику AppLocker, на лету используя примитив TOCTOU.
Эта статья будет полезна для прочтения начинающим vulnerability-research-специалистам в Windows.
AppLocker. Краткий разбор компонента
AppLocker является встроенным компонентом для контроля приложений в ОС Windows. С помощью этого компонента администраторы могут создавать различные политики пользования приложениями на системе. Запрещать или разрешать запуски исполняемых файлов, загрузку определенных DLL, запуск скриптов. Имеется также возможность опционально применять политики по различным критериям и группам пользователей.
Политики AppLocker
Посмотреть, какие классы политик существуют у AppLocker, можно в параметрах безопасности => Политики управления приложениями => AppLocker.
На что реагируют классы политик:
- Исполняемые правила — запуск .exe- и .com-файлов.
- Правила установщика Windows — запуск .msi-, .msp-, .mst-файлов.
- Правила сценариев — исполнение скриптов (.BAT, .JS, .PS1 и т. д.).
- Правила упакованных приложений — на установку чего-либо из appx-файлов.
В качестве примера рассмотрим применение политики запрета запуска определенного исполняемого файла. В данном случае мы запрещаем исполнение файла, который находится по пути %OSDRIVE%\me\TestProgram.exe
После применения правила при попытке запуска файла AppLocker предотвратит это и покажет алерт пользователю.
В правила можно вносить исключения, назначать их только для определенных пользователей или групп, а также применять множество различных функций, о которых можно почитать тут.
Далее рассмотрим, из каких компонентов состоит сам AppLocker.
Базовая архитектура AppLocker
AppLocker состоит из двух компонентов:
- UserMode Service — AppIDSvc.
- KernelMode Driver — AppID.
Служба AppIDSvc занимается тем, что управляет жизненным циклом драйвера, обеспечивает функцию обновления политик AppLocker.
Драйвер AppID применяет политики, которые забирает из UserMode и на уровне ядра ограничивает запуски процессов, загрузки DLL и т. д. Это происходит благодаря регистрации callback-функций с помощью PsSetLoadImageNotifyRoutine, PsSetCreateProcessNotifyRoutine по аналогии с тем, как это делает Sysmon, с тем исключением, что Sysmon просто собирает телеметрию и не имеет функционала реагирования. Ранее мы разбирали принцип его работы в одной из статей.
К примеру, при запуске процесса срабатывает зарегистрированная callback-функция и параметры процесса проверяются на соответствие политике AppLocker, которая хранится в драйвере AppID. В случае запрета — драйвер предотвращает запуск процесса на уровне ядра.
GhostLocker. Мотиватор ресерча
GhostLocker — утилита, которая автоматически генерирует политики AppLocker, которые блокируют запуск user-mode-компонентов AV/EDR и применяет их на системе.
На этом моменте можно возразить и сказать, что если блокируются исключительно user-mode-компоненты, то kernel-модули AV/EDR остаются в рабочем состоянии и продолжают свою работу на хосте.
Действительно, драйвера защитных решений продолжают свою работу. Однако практически все kernel-компоненты таких продуктов опираются на соответствующие user-mode-агенты, в которые они шлют свою телеметрию (события создания файлов, процессов, веток реестра и т. д.). Таким образом, применение политик GhostLocker делает эту телеметрию функционально неэффективной.
Вы не сможете построить корреляцию, расследовать инцидент или же произвести реагирование (если оно не зашито в логику драйвера), так как эти события просто не дойдут до вас.
Как GhostLocker эксплуатирует AppLocker
Ход работы GhostLocker следующий:
- Инициализируется массив имен процессов (EDR/AV), запуск которых нужно блокировать.
const wchar_t*
targetNames[] =
{
L"MpDefenderCoreService.exe",
L"MsMpEng.exe",
L"WinDefend.exe",
};
- Определяется тело PowerShell-скрипта, который создает политику AppLocker, генерирует шаблоны XML-правил и применяет их.
$dynamicBlockRules
+= '<FilePathRule Id="' + $id + '" Name="Block ' + $name + '" Description="Blocked by policy" UserOrGroupSid="S-1-1-0" Action="Deny">'
$dynamicBlockRules += '<Conditions><FilePathCondition Path="' + $exe + '" /></Conditions>'
$dynamicBlockRules += '</FilePathRule>'
- Программа делает снимок всех запущенных процессов и перебирает их. Имена процессов сравниваются с массивом из шага 1.
- Для каждого совпадения по PID запрашивается полный путь к исполняемому файлу через NtQuerySystemInformation. Полные пути подставляются в XML шаблоны AppLocker из скрипта в шаге 2.
- Сам скрипт запускается и через командлет Set-AppLockerPolicy применяет сгенерированные политики.
Set-AppLockerPolicy -XmlPolicy $tempPath -ErrorAction Stop gpupdate /force | Out-Null
После применения правил AppLocker не будет завершать уже запущенные процессы. Для этого потребуется дождаться перезагрузки хоста или самому ее инициировать. В таком случае AppLocker не даст запуститься агентам AV/EDR-решений.
Детектируем GhostLocker
Принцип работы инструмента успешно разобрали, настало время изучить, какие артефакты он оставляет на системе, для того чтобы детектировать эту технику, прежде чем агент EDR окажется в блоке.
Нам известно, что описание политик AppLocker представляет собой формат XML. Данные описания хранятся в реестре. Ветка: HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\SrpV2
В качестве примера будем рассматривать класс правил EXE.
У каждого класса политик есть отдельный GUID под свое правило, что мы и видим на скриншоте выше. Как раз в этих ветках хранится ключ Value, значение которого представляет из себя актуальное правило AppLocker в формате XML.
Используя Sysmon/Procmon, возможно посмотреть на артефакты записи в этот ключ при использовании GhostLocker.exe.
Можно заметить, что запись в ветку реестра осуществляет процесс svchost.exe, если поискать информацию о нем по PID, то станет ясно, что это хост-процесс службы групповой политики. То есть фактически правила AppLocker применяются именно групповой политикой вне зависимости от того, создаем мы их через оснастку или через Powershell командлет.
На этом моменте можно написать детектирующую логику на запись в ключ реестра \SOFTWARE\Policies\Microsoft\Windows\SrpV2 и поиск строк, которые указывают на блокировку процессов, которые связаны с работой конкретного AV/EDR-решения.
logsource:
category: registry_set
product: windows
detection:
ChangeAppLockerPolicyViaRegtistry:
RegKey|re: .*SOFTWARE\\Policies\\Microsoft\\Windows\\SrpV2\\Exe\\.*\\Value.*
PolicyhasDenyAction:
RegValue|re: .*Action\="Deny".*
PolicyDenyOneOfMostPopularAVEDRSolutions:
RegValue|contains:
— MsMpEng.exe #Windows Defender
— others...
condition: ChangeAppLockerPolicyViaRegtistry and PolicyhasDenyAction and PolicyDenyOneOfMostPopularAVEDRSolutions
Таким образом, можно детектировать работу GhostLocker. Однако в ходе исследования у меня появились вопросы по тому, как же на самом деле работает AppLocker.
Я знаю, что ничего не знаю
В какой-то момент своего исследования я осознал, что не понимаю, как на самом деле работает AppLocker. Поверхностно о принципе работы можно найти много информации, однако детальных разборов я не нашел.
Какие именно вопросы меня заинтересовали в первую очередь:
- В каком формате политики передаются драйверу?
- Каким образом драйвер узнает об изменении политики на хосте?
- Какой полный цикл обработки правила от создания его в UserMode до применения в драйвере?
Дальнейшее исследование дает ответы на поставленные вопросы
Как работает AppLocker в деталях
Началось исследование работы AppLocker с предположения. Имея на руках информацию об основных компонентах архитектуры, которые были обозначены выше (AppIDSvc, AppID), можно базово представить себе, как должен работать AppLocker.
- Служба AppIDSvc, работающая в пользовательском режиме, следит за изменениями в ветке реестра.
- При изменении политики AppLocker служба AppIDSvc забирает эту политику и приступает к валидации или преобразованиям в тот формат, который будет понятен драйверу.
- Служба AppIDSvc отправляет сигнал IOCTL-драйверу, в котором передает валидированную политику и сообщает о необходимости ее обновить.
- Драйвер AppID обновляет политику и применяет ее ко всем своим колбэкам.
Звучит красиво, ведь служба AppIDSvc находится под защитой Protect Process Light, что делает невозможным осуществление злоумышленниками атак типа Process Injection, для того чтобы повлиять на поведение драйвера. К примеру, чтобы не дать драйверу узнать об обновлении политик.
Однако дальнейшее исследование будет полностью опровергать представленное выше предположение :)
Что собой представляют правила AppLocker
Казалось бы, что ранее в статье мы уже рассматривали реестровые артефакты и XML представление правил AppLocker, однако драйвер AppID на самом деле не принимает правила в формате XML. Вместо этого драйвер ожидает на вход формат SDDL.
Если внимательно посмотреть на дополнительные артефакты, которые остаются после обновления политики AppLocker, мы можем увидеть наличие событий создания файлов: .exe, .dll, .msi, .script — по пути C:\Windows\System32\AppLocker\ с расширениями .New.
Таким образом, при обновлении политики процесс appidpolicyconverter.exe создает файлы, которые подозрительно похожи на классы тех самых правил, которые мы ранее видели в оснастке. (Исполняемые файлы, образы, MSI-пакеты, скрипты, Appx.)
Эти файлы с расширениями .New в дальнейшем меняют свое расширение на .AppLocker.
В итоге каждый раз после применения любой политики набор этих файлов обновляется.
Каждый из этих файлов представляет собой правила AppLocker, которые были преобразованы в бинарный формат с Magic: ApPF (4170 5046)
Исходя из логов, полученных выше, нетрудно предположить, какой процесс отвечает за преобразование из XML в SDDL
AppIDPolicyConverter, AppIDSvc, TaskScheduler
Итак, бинарь, который отвечает за преобразование политики из формата XML в формат SDDL, понятный драйверу AppID, находится по пути C:\Windows\System32\appidpolicyconverter.exe.
Сам AppIDPolicyconverter.exe запускается хост-процессом службы планировщика задач (Task Scheduler).
Следовательно, настало время найти задачу, которая отвечает за запуск appidpolicyconverter.exe. Описание задачи находится по пути C:\Windows\System32\Tasks\Microsoft\Windows\AppID\PolicyConverter. Все, что делает задача, — это запускает appidpolicyconverter.exe, при этом не имея никакого триггера (она не запускается автоматически по расписанию, событию, входу пользователя, загрузке и т. п.)
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.6" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<SecurityDescriptor>D:(A;;FA;;;BA)(A;;FA;;;SY)(A;CI;FA;;;LS)(A;CI;FA;;;S-1-5-80-2078495744-2416903469-4072184685-3943858305-976987417)</SecurityDescriptor>
<Source>$(@%systemroot%\system32\appidsvc.dll,-300)</Source>
<Author>$(@%systemroot%\system32\appidsvc.dll,-301)</Author>
<Description>$(@%systemroot%\system32\appidsvc.dll,-302)</Description>
<URI>Microsoft\Windows\AppID\PolicyConverter</URI>
</RegistrationInfo>
<Principals>
<Principal id="LocalSystem">
<UserId>S-1-5-18</UserId>
</Principal>
</Principals>
<Settings>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<MultipleInstancesPolicy>Queue</MultipleInstancesPolicy>
<StartWhenAvailable>true</StartWhenAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>
</Settings>
<Triggers />
<Actions Context="LocalSystem">
<Exec>
<Command>%windir%\system32\appidpolicyconverter.exe</Command>
</Exec>
</Actions>
</Task>
Значит, теперь пора найти того, кто эту задачу запускает. Главный подозреваемый — служба AppIDSvc. Проверим ее на наличие вызовов планировщика задач.
В ходе исследования работы службы AppIDSvc была найдена функция AiSvcpInitializeGpCallback(). Рассмотрим, как она работает.
- Создается объект события AiSvcpGroupPolicyChangeEvent с помощью функции CreateEventW().
- С помощью функций CreateThreadpoolWait() и SetThreadpoolWait() осуществляется регистрация callback-объекта в системном пуле потоков, который будет исполнять функцию AiSvcpGroupPolicyChangedCallback() при активации события AiSvcpGroupPolicyChangeEvent.
- Далее с помощью функций CreateThreadpoolTimer() и SetThreadpoolTimer() регистрируем callback AiSvcpConfigureAppID(), который thread pool вызовет по таймеру — примерно через 1 секунду.
__int64 AiSvcpInitializeGpCallback(void)
{
...........
AiSvcpGroupPolicyChangeEvent = CreateEventW(0LL, 0, 0, 0LL);
if ( AiSvcpGroupPolicyChangeEvent )
{
ThreadpoolWait = CreateThreadpoolWait(AiSvcpGroupPolicyChangedCallback, 0LL, 0LL);
AiSvcpGroupPolicyWaitObject = ThreadpoolWait;
if ( ThreadpoolWait )
{
SetThreadpoolWait(ThreadpoolWait, AiSvcpGroupPolicyChangeEvent, 0LL);
ThreadpoolTimer = CreateThreadpoolTimer(AiSvcpConfigureAppID, 0LL, 0LL);
AiSvcpGpNotifyTimer = ThreadpoolTimer;
if ( ThreadpoolTimer )
{
pftDueTime.QuadPart = -10000000LL;
SetThreadpoolTimer(ThreadpoolTimer, (PFILETIME)&pftDueTime, 0, 0);
return v0;
}
.....
}
На руках имеем две callback-функции, AiSvcpGroupPolicyChangedCallback() и AiSvcpConfigureAppID(), — рассмотрим, как они работают.
Работа AiSvcpGroupPolicyChangedCallback():
- С помощью RegOpenKeyExW открываем дескриптор ключа реестра SOFTWARE\Policies\Microsoft\Windows\SrpV2. Нам уже знаком этот ключ.
- Далее функция RegCreateKeyExW создает раздел реестра HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Srp\GP\.
- Функция RegQueryInfoKeyW запрашивает сведения о времени последнего изменения ветки реестра SOFTWARE\Policies\Microsoft\Windows\SrpV2.
LSTATUS
RegQueryInfoKeyW(
[in] HKEY hKey,
[out, optional] LPWSTR lpClass,
[in, out, optional] LPDWORD lpcchClass,
LPDWORD lpReserved,
[out, optional] LPDWORD lpcSubKeys,
[out, optional] LPDWORD lpcbMaxSubKeyLen,
[out, optional] LPDWORD lpcbMaxClassLen,
[out, optional] LPDWORD lpcValues,
[out, optional] LPDWORD lpcbMaxValueNameLen,
[out, optional] LPDWORD lpcbMaxValueLen,
[out, optional] LPDWORD lpcbSecurityDescriptor,
[out, optional] PFILETIME lpftLastWriteTime <------
);
- RegSetValueExW выставляет ключу реестра HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Srp\GP\LastGpNotifyTime значение времени последнего изменения политики AppLocker.
- Вызывается функция AiSvcpEnableRunTask(), которая создает COM-объект службы TaskScheduler, получает указатель на интерфейс ITaskService и вызывает цепочку методов из интерфейсов ITaskFolder и IRegisteredTask, которые в итоге приводят к запуску задачи PolicyConverter.
- С помощью SetThreadpoolWait() переводит Thread Pool снова в режим ожидания срабатывания события AiSvcpGroupPolicyChangeEvent
if...
hKey = 0LL;
gpKey = 0LL;
ftLastWriteTime = 0LL;
v4 = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"Software\\Policies\\Microsoft\\Windows\\SrpV2", 0, 0x20019u, &hKey);// #1
if...
v9 = RegCreateKeyExW( // #2
HKEY_LOCAL_MACHINE,
L"System\\CurrentControlSet\\Control\\Srp\\GP\\",
0,
0LL,
0,
0x20006u,
0LL,
&gpKey,
0LL);
if...
if ( !hKey )
{
ftLastWriteTime = (struct _FILETIME)1LL;
goto LABEL_19;
}
v4 = RegQueryInfoKeyW(hKey, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, 0LL, &ftLastWriteTime);// #3
if ( !v4 )
{
LABEL_19:
v11 = RegSetValueExW(gpKey, L"LastGpNotifyTime", 0, 0xBu, (const BYTE *)&ftLastWriteTime, 8u);// #4
if...
goto LABEL_23;
}
v7 = WPP_GLOBAL_Control;
if...
LABEL_23:
if ( gpKey )
RegCloseKey(gpKey);
if ( hKey )
RegCloseKey(hKey);
if ( _InterlockedCompareExchange((volatile signed __int32 *)&AiSvcpBootDelayFired, 0, 0) == 1 )
AiSvcpEnableRunTask(L"PolicyConverter", v5, 1);// #5
SetThreadpoolWait(AiSvcpGroupPolicyWaitObject, AiSvcpGroupPolicyChangeEvent, 0LL);// #6
Работа AiSvcpConfigureAppID():
- С помощью функции RegisterGPNotification регистрируется колбэк на изменение групповой политики. При изменении политики объект события AiSvcpGroupPolicyChangeEvent переходит в сигнальное состояние.
void __fastcall AiSvcpConfigureAppID(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_TIMER Timer)
{
DWORD LastError; // eax
__int64 v4; // r8
if...
if ( AiSvcpSrpEnabled )
{
if ( !RegisterGPNotification(AiSvcpGroupPolicyChangeEvent, 1) )
{
LastError = GetLastError();
if...
}
}
}
По итогу получаем следующую картину: служба AppIDSvc периодически проверяет, изменялась ли групповая политика на хосте, и если изменилась, то происходит вызов задачи PolicyConverter.
Задача PolicyConverter инициирует запуск appidpolicyconverter.exe, который преобразует политику AppLocker в формат SDDL и записывает ее по пути C:\Windows\System32\AppLocker в соответствующие файлы: (смотреть картинку «Набор правил AppLocker в формате SDDL»).
IOCTL. Как драйвер узнает, что нужно применить политику
После преобразования политики AppLocker в нужный формат драйверу каким-то образом нужно узнать о том, что она изменилась, и применить ее. Давайте посмотрим, как это происходит.
После выполнения всех необходимых преобразований AppIdPolicyConverter.exe получает дескриптор драйвера и отправляет ему IOCTL с кодом операции 0x229800.
DeviceIoControl(hObject, 0x229800u, 0LL, 0, 0LL, 0, &BytesReturned, 0LL)
Данный код операции в драйвере AppID отвечает за обновление политики AppLocker. Это можно посмотреть в реализации IRP Dispatcher (AipDeviceIoControlDispatch)
В ходе обновления драйвер читает файлы с расширением .AppLocker в папке C:\Windows\System32\AppLocker, валидирует их по характерному Magic и применяет их к своим колбэкам.
Цельная картина
Теперь можно собрать воедино все, что удалось узнать о принципе работы AppLocker.
- Служба AppIDSvc получает уведомление об изменении групповой политики на хосте.
- AppIDSvc, используя COM-интерфейс Task Scheduler, запрашивает выполнение задачи PolicyConverter.
- Служба Task Scheduler выполняет задачу PolicyConverter, в рамках которой запускается процесс C:\Windows\System32\AppIdpolicyConverter.exe.
- Процесс AppIdpolicyConverter.exe забирает из реестра XML-представление политики AppLocker.
- AppIdpolicyConverter.exe преобразует XML-формат в SDDL и записывает политики по пути C:\Windows\System32\AppLocker\.
- После успешной записи AppIdpolicyConverter.exe совершает IOCTL к драйверу с кодом операции 0x229800.
- Драйвер получает уведомление о необходимости обновления политики, после чего забирает политики из файлов в C:\Windows\System32\AppLocker\.
- Драйвер обновляет политику для своих колбэков.
Новая техника или уязвимость?
Получается ситуация, когда за главную операцию во всей цепочке (передача сигнала об изменении политики) отвечает не служба AppIDSvc, которая защищена PPL, а процесс appidpolicyconverter.exe, который не имеет такой защиты.
Помимо этого, похоже, что драйвер никак не проверяет, откуда и когда в папке C:\Windows\System32\AppLocker появились файлы политик.
В образовательных целях можно поэксплуатировать эти недостатки и попробовать выделить основные векторы атак на эту архитектуру.
Рассмотрим варианты эксплуатации архитектуры обновления политик AppLocker. Что потенциально можно сделать с теми знаниями, которые мы уже имеем?
Варианты:
- Помешать применению политик AppLocker.
- Подменить политики AppLocker на вредоносные. По аналогии с GhostLocker — блокировка AV/EDR-компонентов.
AppTrickster
Используя знания о цепочке применения политик, можно выделить две потенциальные цели для эксплуатации:
- C:\Windows\System32\appidpolicyconverter.exe.
- Хост-процесс службы планировщика задач.
Оба этих процесса не защищены PPL
Задача — каким-то образом уничтожить или перевести процесс appidpolicyconverter.exe в состояние ожидания, до того как произойдет вызов IOCTL драйвера AppID.
Проблема заключается в том, что процесс appidpolicyconverter.exe живет очень малое количество времени и поймать нужное окно достаточно тяжело.
Варианты поймать запуск процесса appidpolicyconverter.exe в UserMode:
- ETW — подключаемся к ETW провайдеру и ждем события запуска процесса. После чего переводим его по PID в debug mode, останавливая главный поток.
- WMI — подключаемся к пространству имен и, опрашивая его, ждем события выполнения процесса, аналогично по pid переводим его в debug mode.
- TaskScheduler — мы точно знаем, что служба планировщика задач будет запускать процесс appidpolicyconverter.exe. Возможно совершить инжект в службу планировщика и ждать этого события.
ETW
В варианте с ETW был использован провайдер Microsoft-Windows-Kernel-Process. GUID: 22fb2cd6-0e7b-422b-a0c7-2fad1fd0e716. Данный провайдер отвечает за мониторинг и логирование создания, завершения процессов и потоков, а также загрузки модулей в ядре операционной системы.
В этом провайдере есть события, по которым можно отслеживать запуски процессов. Пробуем перехватить запуск интересующего нас процесса и по его PID перевести его в debug-режим, заморозив основной поток с помощью функции DebugActiveProcess.
Однако этот эксперимент не увенчался успехом. События от провайдера доходили гораздо медленнее, чем процесс успевал полностью отработать и завершиться.
Ошибка с кодом 87 возникает как раз потому, что процесса на момент попытки его открытия уже не существует. Поэтому переходим к следующему варианту.
WMI
Идея заключается в том, чтобы подписаться на системное событие WMI __InstanceCreationEvent, которое генерируется при создании нового экземпляра класса Win32_Process, с очень маленьким интервалом опроса WITHIN, чтобы получать уведомление о запуске appidpolicyconverter.exe.
После получения уведомления по аналогии с ETW мы будем переводить процесс в Debug состояние с помощью обратного вызова.
В таком случае мы получаем состояние гонки. В тестовой среде примерно в 50 процентах случаев WMI callback успевает остановить appidpolicyconverter.exe до вызова IOCTL к драйверу и политика действительно не применяется.
Данный метод ненадежный и часто дает осечки. Идем дальше и ищем более стабильный способ задержки.
TaskScheduler
Вместо того чтобы угнаться за событием в real-time, можно попытаться узнать о намерении создать процесс службой планировщика задач в момент вызова задачи PolicyConverter и внедряться в поток выполнения в этот самый момент. Этот способ выглядит гораздо более стабильным, чем состояние гонки.
Для того чтобы осуществить задуманное, я воспользовался библиотекой Detours. Эта библиотека позволяет перехватывать вызовы Win32 API, модифицируя код функций в памяти процесса. С ее помощью можно, например, перехватить функцию CreateProcessW в процессе и, обнаружив попытку запуска appidpolicyconverter.exe, либо заблокировать ее, либо создать процесс в приостановленном состоянии, добавив флаги в параметр CreationFlags, что я и сделал в своем варианте.
Для того чтобы это осуществить, потребуется DLL-библиотека с модифицированной функцией CreateProcessW, а также инжектор, который ищет хост-процесс службы планировщика задач и осуществляет DLL-инъекцию detours-библиотеки.
После проведенных манипуляций можно увидеть стороннюю DLL в контексте процесса службы планировщика задач. Как раз эта DLL осуществляет перехват вызовов CreateProcessW и, в случае попытки запуска appidpolicyconverter.exe, запускает этот процесс сразу в приостановленном состоянии.
Таким образом, при внедрении такой DLL на хосте не будут применяться политики AppLocker вообще. Хотя в оснастке все будет выглядеть так, как будто политика успешно применилась. Ошибок работы службы также не возникнет. Злоумышленник может избежать блокировки нужных ему компонентов с помощью данной техники.
Какие есть артефакты эксплуатации:
- Запуск appidpolicyconverter.exe с нестандартными флагами. Но такую телеметрию может дать исключительно EDR и подобные ему инвазивные решения.
- Сама инъекция довольно шумная и можно отслеживать, к примеру, загрузку неподписанных DLL в планировщик задач.
AppLocker TOCTOU. Подмена политики на лету.
Переходим к самому интересному. Задача следующая: во время выполнения процедуры конвертации и отправки политики драйверу не просто не дать драйверу узнать о новой политике, а подложить свою, к примеру вредоносную, политику.
Звучит интересно. Но как это реализовать и ничего не сломать по пути? В данном случае можно использовать технику TOCTOU (Time of Check, Time of Use). Подробнее о том, каким образом атакующие используют ее, можно почитать здесь.
Вкратце идея TOCTOU-атаки состоит в том, чтобы воспользоваться промежутком между проверкой состояния данных или ресурса и их фактическим использованием. Если за это время злоумышленник успевает изменить данные или ресурс, программа использует уже непроверенное состояние.
Теперь нужно рассмотреть алгоритм работы применения политики и найти эти две временные точки: Time of Check, Time of Use
Ищем Time Of Check | Time Of Use
Посмотрев ближе на алгоритм работы преобразования политик AppLocker, можно заметить следующее:
Для каждого класса правил действия повторяются в одинаковой последовательности:
- открывается дескриптор файла с расширением .New;
- происходит запись преобразованных политик в файл с расширением .New;
- файл с расширением .New меняет расширение на .AppLocker. (это конечная стадия преобразования).
Сами классы правил также обрабатываются в одинаковом порядке:
- Appx;
- dll;
- exe;
- msi;
- script.
При этом IOCTL-вызов с кодом 0x229800 для обновления политики в драйвере происходит только единожды, после преобразования всех политик.
Следовательно, если мы хотим подменить правила класса exe, нам нужно остановить выполнение appidpolicyconverter.exe после успешной записи и переименования политики в Exe.AppLocker, но до вызова IOCTL.
Зная точный алгоритм записи правил, можно определить момент, в который мы хотим остановить выполнение appidpolicyconverter.exe. Это момент записи или получения дескриптора файла Script.AppLocker, так как это будет последнее действие перед отправкой IOCTL-сигнала.
В данном случае Time Of Check — момент успешной записи политики в файл Exe.AppLocker, а Time Of Use — отправка IOCTL. Между этими событиями есть временное окно, которым мы воспользуемся.
Реализация TOCTOU
Ключевую роль в использовании этой техники играют оппортунистические блокировки (OpLocks). Это механизм, позволяющий временно блокировать доступ к определенному файлу. В общем случае доступ к файлу может осуществляться для его выполнения, чтения, записи или удаления (или комбинации этих операций). С помощью оппортунистической блокировки мы можем заблокировать один или несколько из этих типов доступа. При этом при попытке доступа к такому заблокированному файлу поток процесса, который попытался получить доступ, будет остановлен до получения сигнала от блокирующего процесса.
Алгоритм работы TOCTOU в данном случае следующий:
- OpLock выставляется на файл C:\Windows\System32\AppLocker\Script.AppLocker. Это последний файл, в который будет записана политика.
HANDLE hLockFile = CreateFile(
L"C:\\Windows\\System32\\AppLocker\\Script.AppLocker",
GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE | GENERIC_ALL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL
);
if (hLockFile == INVALID_HANDLE_VALUE) {
printf("CreateFile failed. Error: %lu\n", GetLastError());
return 1;
}
printf("Open File success\n");
OVERLAPPED overlapped = { 0 };
HANDLE hevent = CreateEvent(NULL, FALSE, FALSE, NULL);
overlapped.hEvent = hevent;
REQUEST_OPLOCK_INPUT_BUFFER rOpInBuff = { 0 };
rOpInBuff.StructureLength = sizeof(REQUEST_OPLOCK_INPUT_BUFFER);
rOpInBuff.StructureVersion = REQUEST_OPLOCK_CURRENT_VERSION;
rOpInBuff.RequestedOplockLevel = OPLOCK_LEVEL_CACHE_READ | OPLOCK_LEVEL_CACHE_HANDLE;
rOpInBuff.Flags = REQUEST_OPLOCK_INPUT_FLAG_REQUEST;
REQUEST_OPLOCK_OUTPUT_BUFFER rOpOutBuff = { 0 };
rOpOutBuff.StructureLength = sizeof(rOpOutBuff);
rOpOutBuff.StructureVersion = REQUEST_OPLOCK_CURRENT_VERSION;
DeviceIoControl(hLockFile, FSCTL_REQUEST_OPLOCK, &rOpInBuff, sizeof(rOpInBuff), &rOpOutBuff, sizeof(rOpOutBuff), NULL, &overlapped);
- Во время применения политики appidpolicyconverter.exe успешно записывает политику в файл Exe.AppLocker, после чего доходит до Script.AppLocker. В этот момент поток appidpolicyconverter.exe замораживается и ждет сигнала от нашей блокировки, чтобы продолжить работу.
- Так как файл Exe.AppLocker уже существует и в него уже записана обновленная политика, мы открываем его дескриптор (с эксклюзивным доступом на запись — это гарантирует, что никто не запишет ничего в файл одновременно с нашей программой) и записываем в него свою, заранее подготовленную политику.
HANDLE
hTarget = CreateFile(L"C:\\Windows\\System32\\AppLocker\\Exe.AppLocker", GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hTarget == INVALID_HANDLE_VALUE) {
printf("Fail to get Target handle");
printf("%d", GetLastError());
CloseHandle(hLockFile);
return -1;
}
OVERLAPPED ovW = { 0 };
ovW.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
BOOL successWriteFile = WriteFile(hTarget, buffer, payloadfilesize.LowPart + 1, NULL, &ovW);
printf("WriteToFile..");
- Отпускаем OpLock. appidpolicyconverter.exe успешно отправляет IOCTL драйверу, после чего драйвер забирает ту политику, которую мы ему подложили в Exe.AppLocker
Что же делать защитникам? Если позволяет глубина телеметрии, необходимо отслеживать выставления блокировок на подозрительные файлы. Это касается не только конкретной реализации TOCTOU в AppTrickster, но и большинства актуальных эксплойтов, которые используют состояния гонки. К примеру, большинство zero-day-уязвимостей, которые выложил исследователь NightmareEclipse, используют данную технику для повышения привилегий.
Ответ Microsoft
О вариантах эксплуатации механизма обновлений политик AppLocker я сообщил в Microsoft в рамках репорта с классом Security Feature Bypass. Однако в ответе Microsoft не признала уязвимостью данный вектор по двум причинам.
- Для подобного рода инжектов и приостановок процесса требуются права администратора. В рамках политики принятия уязвимостей Microsoft считается, что последствия будут менее серьезными, поскольку злоумышленник, обладающий таким доступом, уже мог бы совершать более опасные действия.
- AppLocker не считается границей безопасности для обслуживания и стоит в одном ряду с обходом PPL Protection и способами UAC Bypass.
Выводы
В ходе исследования я не получил признания уязвимости от Microsoft, однако это не означает, что работа была бесполезной. Мне удалось найти новый вектор обхода AppLocker через механизм обновлений политик. С точки зрения bug bounty это неудачный кейс, но с исследовательской точки зрения — я получил более глубокое представление о том, как работает механизм AppLocker.
Отсюда следуют два вывода. Во-первых, внимательно знакомьтесь с требованиями вендора к обслуживанию уязвимостей: то, что исследователь считает обходом защиты, для Microsoft может не быть уязвимостью из-за требуемых привилегий, модели угроз или границ безопасности. Во-вторых, не нужно слепо доверять компонентам безопасности, даже если они официальные.




