- PVSM.RU - https://www.pvsm.ru -
Периодически, как правило во вторую среду месяца, можно услышать истории о том, что Windows после очередного обновления перестает загружаться, показывая синий экран смерти. В большинстве случаев причиной такой ситуации оказывается либо руткит, либо специфичное системное ПО, фривольно обращающееся со внутренними структурами ОС. Винят, конечно, все равно обновление, ведь «до него все работало». С таким отношением не удивительно, что «Майкрософт» не поощряет использование всего, что не документировано. В какой-то момент, а именно с релизом Windows Server 2003, MS заняла более активную позицию в вопросе борьбы с чудо-поделками сторонних разработчиков. Тогда появился механизм защиты целостности ядра — kernel patch protection, более известный как PatchGuard.
С самого начала он не позиционировался как механизм защиты от руткитов, поскольку руткиты работают в ядре с теми же привилегиями, а следовательно, PatchGuard может быть обезврежен. Это скорее фильтр, отсекающий ленивых разработчиков руткитов.
Самым популярным местом модифицирования ядра была таблица системных вызовов. При помощи модификации указателей на функции системных вызовов можно было легко их перехватывать, фильтровать, логировать и т. п. Причем этот патч был популярен как для руткитов, так и для антивирусного ПО. Другие интересные для патча объекты — таблицы дескрипторов (GDT, IDT). Через модифицирование глобальной таблицы дескрипторов можно было изменять атрибуты сегментов, создавая бекдоры для кода, а через таблицу дескрипторов прерываний можно было перехватывать… прерывания! Продвинутые же парни сплайсили непосредственно функции ядра.
Соответственно, первая версия PatchGuard защищала:
С развитием NT перерабатывалось множество компонентов ядра, в том числе и PatchGuard. На текущий момент уже сложно перечислить все, что защищается с его помощью:
Стоит отметить, что PatchGuard активно использует новую реализацию обработки исключений, введенной в x64-версиях Windows. Используется он как для обфускации самого PatchGuard, так и для проверки целостности защищаемых образов.
В предыдущих версиях Windows обработчик исключений использовал структуры данных прямо на стеке, что даже позволяло обходить stack cookies при эксплуатации уязвимостей. Основное изменение заключается в хранении специальной таблицы внутри исполняемого образа с записями для каждой отдельной его функции.
typedef struct _IMAGE_RUNTIME_FUNCTION_ENTRY {
uint32_t BeginAddress; // Начало функции
uint32_t EndAddress; // Конец функции
union {
uint32_t UnwindInfoAddress; // Указатель на данные, используемые для раскрутки стека и
uint32_t UnwindData; // обработки исключений
};
} _IMAGE_RUNTIME_FUNCTION_ENTRY, *_PIMAGE_RUNTIME_FUNCTION_ENTRY;
За счет того, что адрес начала и конца любой функции можно получить прямо в рантайме, задача подсчета контрольной суммы отдельно взятой функции становится тривиальной. Для сравнения — в x86-версиях контроль целостности образов невозможен из-за того, что непонятно, как определить границы отдельной функции, а образ целиком (или даже отдельные его секции) накрывать контрольной суммой нельзя, поскольку в том же ядре присутствуют функции, которые патчатся самим ядром на лету.
При загрузке ОС PatchGuard создает от 1 до 4 контекстов — структур данных, в которых хранятся копии используемых им функций, контрольные суммы защищаемых структур и ключи шифрования самого контекста. Эти контексты хранятся в неподкачиваемом пуле в зашифрованном виде. О проверке контекстов поговорим чуть позже.
Инициализируются контексты PatchGuard в фазе 1 загрузки ОС. Функция, непосредственно занимающаяся созданием контекста, не имеет публичного символа (будем называть ее KiInitializePatchGuardContext), но найти ее можно внутри функции KiFilterFiberContext. Мы нашли два места, в котором возможно создание контекста PatchGuard:
... -(call)-> Phase1InitializationDiscard -(call)-> KeInitAmd64SpecificState -(exception)-> KiFilterFiberContext
... -(call)-> Phase1InitializationDiscard -(call)-> sub_14071815C -(call)-> ExpLicenseWatchInitWorker -(call)-> KiFilterFiberContext
Первый вариант всегда создает хотя бы один контекст, в то время как второй только в 4% случаев. Также первый вариант примечателен тем, что вызывает функцию KiFilterFiberContext неявно, а именно через «вброс» исключения.
__int64 KeInitAmd64SpecificState()
{
signed int v0; // edx@2
__int64 result; // rax@2
// В безопасном режиме PatchGuard не работает
if ( !InitSafeBootMode )
{
v0 = __ROR4__(KdPitchDebugger | KdDebuggerNotPresent, 1);
// При отсутствии отладчика деление вызовет исключение (переполнение при делении на -1),
// обработчиком которого как раз будет KiFilterFiberContext
result = (v0 / ((KdPitchDebugger | KdDebuggerNotPresent) != 0 ? -1 : 17));
}
return result;
}
Функция sub_14071815C очевидно не имеет публичного символа, поскольку связана с проверкой лицензии ОС.
VOID ExpLicenseWatchInitWorker()
{
PVOID KiFilterParam;
NTSTATUS (*KiFilterFiberContext)(PVOID pFilterparam);
BOOLEAN ForgetAboutPG;
// KiServiceTablesLocked == KiFilterParam
KiFilterParam = KiInitialPcr.Prcb.HalReserved[1];
KiInitialPcr.Prcb.HalReserved[1] = NULL;
KiFilterFiberContext = KiInitialPcr.Prcb.HalReserved[0];
KiInitialPcr.Prcb.HalReserved[0] = NULL;
ForgetAboutPG = (InitSafeBootMode != 0) | (KUSER_SHARED_DATA.KdDebuggerEnabled >> 1);
// 96% случаев
if (__rdtsc() % 100 > 3)
ForgetAboutPG |= 1;
if (!ForgetAboutPG && KiFilterFiberContext(KiFilterParam) != 1)
KeBugCheckEx(SYSTEM_LICENSE_VIOLATION, 0x42424242, 0xC000026A, 0, 0);
}
Ниже приведен псевдокод функции KiFilterFiberContext, выбирающей способ проверки конкретного контекста и вызывающей функцию создания самого контекста.
BOOLEAN KiFilterFiberContext(PVOID pKiFilterParam)
{
BOOLEAN Result = TRUE;
DWORD64 dwDpcIdx1 = __rdtsc() % 13; // Выбор DPC, в которой будет осуществляться проверка
DWORD64 dwRand2 = __rdtsc() % 10; // 50 на 50, что создастся второй контекст
DWORD64 dwMethod1 = __rdtsc() % 6; // Выбор метода запуска проверки
AntiDebug();
Result = KiInitializePatchGuardContext(dwDpcIdx, dwMethod1, (dwRand2 < 6) + 1, pKiFilterParam, TRUE);
if (dwRand2 < 6)
{
DWORD64 dwDpcIdx2 = __rdtsc() % 13;
DWORD64 dwMethod2 = __rdtsc() % 6;
do
{
dwMethod2 = __rdtsc() % 6;
}
while ((dwMethod1 != 0) && (dwMethod1 == dwMethod2));
Result = KiInitializePatchGuardContext(dwDpcIdx2, dwMethod2, 2, pKiFilterParam, FALSE);
}
AntiDebug();
return Result;
}
Функция, создающая контекст PatchGuard, обфусцирована настолько, что автоматические средства с ней не справляются, а исследователям внезапно становится неинтересно ее реверсить. В статике это полная каша, 10K+ строк декомпилированного «в лоб» кода (сама декомпиляция в IDA Pro занимает около 40 минут).

Все говорит об обширном использовании макросов:
Динамика тоже довольно непроста. Вот пара примеров из того, что рассыпано по коду.
cli
xor eax, eax
cmp byte ptr cs:KdDebuggerNotPresent, al
jnz short loc_140F3CFBD
jmp short loc_140F3CFBB
sti
cli
sidt fword ptr [rbp+320h]
lidt fword ptr [rbp+228h]
mov dr7, r13
lidt fword ptr [rbp+320h]
sti
Рассмотрим параметры функции KiInitializePatchGuardContext.
DPC, которые вызывают проверку через исключение внутри себя, «смотрят» — является ли параметр DeferredContext указателем на неканоническую память. Если указатель в порядке, DPC выполняет свою законную работу. Иначе DPC вызывает цепочку рекурсивных функций, приводящих в конечном итоге к исключению (из-за разыменовывания неканонического адреса) и исполнению его обработчика.
Проверка контекста состоит из двух этапов: сперва проверка структуры самого контекста, которая происходит на DPC-уровне, затем планируется work item, осуществляющий проверку защищаемых структур в системном потоке. Если проверка была удачной, старый контекст удаляется и вместо него создается новый, который будет запущен через случайный интервал времени. Если проверка не удалась, PatchGuard зачищает все свои следы, в том числе зануляя стек, и демонстрирует синий экран с кодом ошибки 0x109: CRITICAL_STRUCTURE_CORRUPTION.
Гифка с саморасшифровывающимся на первом этапе проверки контекстом:

Существует несколько подходов к обезвреживанию PatchGuard:
Нам понравился последний способ, поскольку он является самым «чистым»: ничего не нужно хукать и пачтить, необходимо просто заменить значение некоторых переменных.
Эти действия необходимо успеть совершить за 2 минуты по описанным выше причинам. Результат — проверка контекста никогда не будет запущена, а также не будет запланирована новая. PatchGuard не будет работать.
При осмотре KiFilterFiberContext из Windows 10 Technical Preview мы заметили небольшое изменение. Все старые методы планирования остались прежними. Однако появился новый, который пока что безусловно возвращает STATUS_HV_FEATURE_UNAVAILABLE. Немного покопавшись, мы обнаружили функцию KiSwInterruptDispatch, внутри которой явно идет расшифровка и вызов проверки контекста. Очевидно, что будет добавлена возможность осуществлять проверку контекстов по запросу гипервизора Hyper-V. От гипервизора при определенных условиях будет приходить синтетическое прерывание, обработчик которого будет проверять целостность ядра.
В статье мы старались не указывать имена конкретных функций не потому, что нам жалко. Все просто: имена функций, используемых для расшифровки и проверки контекстов, намеренно изменены разработчиками PatchGuard и меняются в разных версиях ОС.
Вот пример несоотвтествия названия функции тому, чем она действительно занимается. Это та самая функция, копия которой используется для саморасшифровки контекста.

Одно хорошо — все эти функции находятся рядом, так что начать можно с функции KiFilterFiberContext. Очевидно, они все лежат в одном файле исходного кода. Однако проверка целостности ядра не ограничивается одним PatchGuard. В различные части ядра вставлены макросы, осуществляющие проверку тех или иных структур. Каждое такое место приходится искать вручную. Пример:
... --> Phase1InitializationDiscard --> CcInitializeCacheManager --> CcInitializeBcbProfiler
С вероятностью 50% данная функция осуществляет подсчет контрольной суммы для произвольной функции ядра и планирует ее проверку каждые 2 минуты в DPC с функцией CcBcbProfiler.
Так что удачи в поисках! PatchGuard интересен именно тем, что его весело реверсить ;)
Ссылки в помощь:
Kernel patch protection: frequently asked questions [1]
Bypassing PatchGuard on Windows x64 [2]
PatchGuard Reloaded [3]
TSS blog: Patch-Guard 1 [4]
The Windows 8.1 Kernel Patch Protection [5]
Understanding and Defeating Windows 8.1 Kernel
Patch Protection
Microsoft Windows 8.1 Kernel Patch Protection Analysis & Attack Vectors [6]
Автор: HonoraryBoT
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/informatsionnaya-bezopasnost/78374
Ссылки в тексте:
[1] Kernel patch protection: frequently asked questions: http://msdn.microsoft.com/en-us/library/windows/hardware/dn613955(v=vs.85).aspx
[2] Bypassing PatchGuard on Windows x64: http://uninformed.org/index.cgi?v=3&a=3
[3] PatchGuard Reloaded: http://uninformed.org/index.cgi?v=8&a=5
[4] TSS blog: Patch-Guard 1: http://kitrap08.blogspot.ru/2012/07/patch-guard-1.html
[5] The Windows 8.1 Kernel Patch Protection: http://vrt-blog.snort.org/2014/08/the-windows-81-kernel-patch-protection.html
[6] Microsoft Windows 8.1 Kernel Patch Protection Analysis & Attack Vectors: http://blog.ptsecurity.com/2014/09/microsoft-windows-81-kernel-patch.html
[7] Источник: http://habrahabr.ru/post/246841/
Нажмите здесь для печати.