ethical.blue Magazine

// Cybersecurity clarified.

Kod powłoki dla Windows x86-64 (x64)

30.05.2022   Dawid Farbaniec
...
Typowe programy komputerowe są zależne od odwołań do zasobów takich jak np. funkcje interfejsu systemowego (API) czy po prostu dane potrzebne aplikacji (napisy, liczby itd.). Natomiast kod relokowalny powinien być self–contained, czyli tłumacząc opisowo: zawierać w sobie to co jest mu potrzebne do działania. Dzięki temu, gdy zostanie umieszczony gdziekolwiek w pamięci komputera, to nadal będzie działał poprawnie, a nawet miał możliwość przeniesienia swoich fragmentów w inne miejsca (ang. (self–)relocation).

Odwołania zewnętrzne są utrudnieniem

Instrukcje procesora, które nie odwołują się do funkcji API (przykładowe rozkazy: MOV, ADD, DEC, XOR itp.) mogą być bez problemu wykonywane w dowolnym miejscu w pamięci.

Jednak w natywnych aplikacjach dla systemu Windows to funkcje WinAPI udostępniają główne funkcjonalności programiście. Utworzenie pliku, odczytanie pliku, połączenie sieciowe, wyświetlenie okna i wiele innych możliwości daje systemowe API. W typowych programach wywołanie funkcji WinAPI następuje (w uproszczeniu) poprzez podanie jej nazwy (i argumentów jeśli przyjmuje), która to nazwa zamieniana jest na adres (wartość liczbowa określająca miejsce funkcji w bibliotece systemowej) i wywoływana.

Kod relokowalny wstawiony do jakiegoś miejsca w pamięci musi sobie w nietypowy sposób poradzić z wywoływaniem funkcji API systemu Windows. W zwykłym programie w języku Asembler wywołanie wykonuje się poprzez rozkaz CALL podając mu nazwę funkcji i wymagane argumenty poprzez odpowiednie rejestry/stos.

W kodzie relokowalnym opisywanym tutaj podanie nazwy funkcji jest niemożliwe. Adresów funkcji nie można wpisać do kodu, bo są one zmienne. Standardowe pobranie adresu danej funkcji przez GetProcAddress jest również niemożliwe, ponieważ trzeba najpierw znać adres funkcji GetProcAddress.

Konwencja wywoływania funkcji Microsoft x64

Wywołanie funkcji w Asemblerze x64 nazywane też wywołaniem podprogramu przenosi sterowanie do innego miejsca w kodzie. Gdy blok kodu określany funkcją się wykona, to następuje powrót, który jest możliwy poprzez odłożony wcześniej na stosie programu adres powrotny. Funkcje wywołuje się instrukcją procesora CALL. To ona odkłada na stos wspomniany wcześniej adres powrotny i przekazuje kontrolę do wywoływanego podprogramu.

Nie ma jednego uniwersalnego sposobu na wywołanie funkcji. Zależne jest to od architektury, a to jak działa wywołanie i związane z nim operacje określają konwencje wywołania (ang. calling conventions).

Przypomnijmy sobie, że w Asemblerze MASM32 dla architektury x86-32 korzysta się z konwencji stdcall, która jest domyślna dla API systemu Windows. Wyczyszczenie stosu programu jest obowiązkiem funkcji, która jest wywoływana (ang. callee), czyli programista korzystający z takiej funkcji ma spokój z czyszczeniem stosu. Argumenty (nazywane też parametrami) przekazywane są poprzez stos „od końca”, czyli od prawej do lewej strony. Jeśli funkcja zwraca jakiś rezultat, to znajdzie się on w rejestrze akumulatora EAX. Niektóre funkcje, gdy wynik jest większy niż 32-bity zwracają wynik w parze rejestrów EDX:EAX. W konwencji stdcall, jeśli chcemy (w naszej funkcji) modyfikować wartości rejestrów ESI, EDI, EBP i EBX, to powinniśmy zachować ich wartości np. na stosie, a następnie je przywrócić przed powrotem do Windows.

Wywołanie funkcji CreateFile w Visual C++
hFile = CreateFile(szFileName, 

           GENERIC_WRITE,
           0, 0,
           CREATE_NEW,
           FILE_ATTRIBUTE_NORMAL, 0);
  
Wywołanie funkcji CreateFile w Asemblerze MASM32 (konwencja stdcall)
push 0 ;bez szablonu atrybutów

push FILE_ATTRIBUTE_NORMAL ;atrybuty pliku
push CREATE_NEW ;utwórz nowy plik
push 0 ;atrybuty bezpieczeństwa domyślne
push 0 ;sharing mode domyślny
push GENERIC_WRITE ;otwarcie do zapisu
push offset szFileName ;nazwa tworzonego pliku
call CreateFileA ;wywołanie funkcji WinAPI
mov hFile, eax

Asembler MASM x64 dla architektury x86-64 (w skrócie x64) korzysta z konwencji wywoływania funkcji nazwanej Microsoft x64. Wyczyszczenie stosu programu jest obowiązkiem funkcji wywołującej (ang. caller). Argumentów nie przekazuje się tylko przez stos, ale przez wybrane rejestry takie jak: R9, R8, RDX, RCX. Ze stosu korzysta się, gdy argumentów jest więcej niż cztery i odkłada się je „od końca”, czyli od prawej do lewej strony. Rezultat funkcji, jeśli ma rozmiar mniejszy niż 64-bity, zwracany jest w rejestrze akumulatora RAX. W konwencji Microsoft x64, jeśli chcemy (w naszej funkcji) modyfikować wartości rejestrów RBP, RBX, RDI, RSI, RSP, R12, R13, R14 i R15, to powinniśmy zachować ich wartości np. na stosie, a następnie je przywrócić przed powrotem do Windows. Należy również pamiętać o wyrównaniu stosu do okrągłych 16 bajtów. Ilość miejsca rezerwowanego na stosie wraz z adresem powrotnym powinna być podzielna przez 16 bez reszty.

Wywołanie funkcji CreateFile w MASM x64 (konwencja Microsoft x64)
sub rsp, 38h ;alokacja miejsca na stosie

mov qword ptr [rsp+30h], 0 ;bez szablonu atrybutów
mov qword ptr [rsp+28h], FILE_ATTRIBUTE_NORMAL ;atrybuty pliku
mov qword ptr [rsp+20h], CREATE_NEW ;utwórz nowy plik
mov r9, 0 ;atrybuty bezpieczeństwa domyślne
mov r8, 0 ;sharing mode domyślny
mov rdx, GENERIC_WRITE ;otwarcie do zapisu
mov rcx, offset szFileName ;nazwa tworzonego pliku
call CreateFileA ;wywołanie funkcji WinAPI
mov hFile, rax
add rsp, 38h ;zwolnienie miejsca na stosie

Thread Environment Block (TEB)

Punktem zaczepienia do pobrania adresów funkcji WinAPI w tworzonym kodzie relokowalnym jest zdobycie adresu bazowego systemowej biblioteki kernel32.dll. Pierwszym krokiem jest poznanie bloku TEB (nazywanego też TIB).

Thread Environment Block to struktura danych, która zawiera informacje o aktualnie wykonywanym wątku. Dostęp do niej można uzyskać poprzez rejestr segmentowy GS w trybie 64-bitowym oraz rejestr FS w trybie 32-bitowym.

Istotną sprawą jest, że pomimo lekkiego owiania tajemnicą tej struktury (jest to wewnętrzna struktura systemowa), to możliwe jest zdobycie jej budowy bez dokonywania jakiejś inżynierii wstecznej (RCE). W pliku nagłówkowym winternl.h dla Visual C++ w środowisku Visual Studio można znaleźć definicję tej struktury:
//plik winternl.h

typedef struct _TEB {
    PVOID Reserved1[12];
    PPEB ProcessEnvironmentBlock;
    PVOID Reserved2[399];
    BYTE Reserved3[1952];
    PVOID TlsSlots[64];
    BYTE Reserved4[8];
    PVOID Reserved5[26];
    PVOID ReservedForOle;  // Windows 2000 only
    PVOID Reserved6[4];
    PVOID TlsExpansionSlots;
} TEB, *PTEB;
Należy pamiętać, że są to wewnętrzne API i struktury, które mogą ulec zmianie. O czym informuje początek komentarza w pliku winternl.h:
Quote:
winternl.h -- This module defines the internal NT APIs and data structures that are intended for the use only by internal core Windows components.  These APIs and data structures may change at any time. These APIs and data structures are subject to changes from one Windows release to another Windows release.  To maintain the compatiblity of your application, avoid using these APIs and data structures.

Jako podsumowanie tego podrozdziału muszę zaznaczyć w jaki sposób poznanie tej struktury przybliża nas do celu.

Otóż poprzez strukturę TEB możliwe jest uzyskanie dostępu do innej struktury: PEB.

Process Environment Block (PEB)

Blok PEB podobnie jak wcześniej opisany TEB jest używaną wewnętrznie przez system strukturą danych.

Pobranie adresu bazowego struktury PEB w Asemblerze MASM x64 można wykonać np. tak:
mov rax, gs:[30h] ;RAX = PEB structure
czy poprzez użycie rozkazu procesora rdgsbase:
rdgsbase rax ;RAX = PEB structure
Dodatkowo wspomnę, że w Visual C++ adres PEB można pobrać funkcją wewnętrzną (ang. intrinsic):
//x86-64 (64-bit)

PVOID peb_x64 = (PVOID) __readgsqword(0x30);
//x86 (32-bit)
PVOID peb_x86 = (PVOID) __readfsdword(0x18);

W pliku nagłówkowym winternl.h dla Visual C++ można znaleźć definicję tej struktury:
//plik winternl.h

typedef struct _PEB {
    BYTE Reserved1[2];
    BYTE BeingDebugged;
    BYTE Reserved2[1];
    PVOID Reserved3[2];
    PPEB_LDR_DATA Ldr;
    PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
    PVOID Reserved4[3];
    PVOID AtlThunkSListPtr;
    PVOID Reserved5;
    ULONG Reserved6;
    PVOID Reserved7;
    ULONG Reserved8;
    ULONG AtlThunkSListPtr32;
    PVOID Reserved9[45];
    BYTE Reserved10[96];
    PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
    BYTE Reserved11[128];
    PVOID Reserved12[1];
    ULONG SessionId;
} PEB, *PPEB;

W strukturze PEB w kodzie powyżej należy zwrócić uwagę na pole Ldr (od Loader). Jest to struktura _PEB_LDR_DATA, która w pliku nagłówkowym winternl.h prezentuje się następująco:
typedef struct _PEB_LDR_DATA {

    BYTE Reserved1[8];
    PVOID Reserved2[3];
    LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;
W powyższej strukturze (_PEB_LDR_DATA) znajduje się pole InMemoryOrderModuleList, które jest listą modułów, którą należy przeszukać, aby uzyskać adres bazowy kernel32.dll.

Struktura _LIST_ENTRY prezentuje element wyżej wspomnianej listy, a jego definicja w winnt.h wygląda następująco:
typedef struct _LIST_ENTRY {

    struct _LIST_ENTRY *Flink;
    struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

Adres bazowy modułu kernel32.dll

Po zerknięcie na rozmiar w bajtach poszczególnych pól wyżej opisanych struktur możliwe jest przemieszczanie się po nich. Całość polega na wpisaniu odpowiednich wartości przesunięć (ang. offset) oraz dereferencji pamięci poprzez operator ptr (dla wartości 64-bitowych qword ptr).

;Znajdź adres kernel32.dll

;poprzez przeszukanie struktury
;ProcessEnvironmentBlock (PEB)

;R10 = TEB.ProcessEnvironmentBlock
mov r10, gs:[60h]

;R10 = ProcessEnvironmentBlock->Ldr
mov r10, qword ptr [r10 + 18h]

;R11 = Ldr->InMemoryOrderModuleList
mov r11, qword ptr [r10 + 20h]

;R10 = InMemoryOrderModuleList.Flink
mov r10, qword ptr [r11]

;R11 = InMemoryOrderModuleList.Flink->Flink
mov r11, qword ptr [r10]

;R10 = InMemoryOrderModuleList.Flink->Flink->Flink
;R10 = kernel32.dll base address!
mov r10, qword ptr [r11 + 20h]
Powyższy kod w Asemblerze MASM64 zwraca w rejestrze R10 adres bazowy modułu kernel32.dll. Po uzyskaniu dostępu do listy InMemoryOrderModuleList następuje przejście do jej trzeciego elementu. Jest tak, gdyż to właśnie trzeci moduł to szukany kernel32.dll. Pierwszy moduł to uruchomiony plik wykonywalny, a drugi to ntdll.dll.

Poniżej przedstawiono dodatkowo aplikację, która znaleziony w strukturze PEB adres bazowy modułu kernel32.dll porównuje z adresem pobranym zwykłą metodą tj. przez wywołanie funkcji LoadLibrary. Oczywiście to sprawdzenie jest tylko w celach debugowania/nauki i tego fragmentu nie będzie w tworzonym shellcode.

; Get kernel32.dll base address

; through Process Environment Block
; ethical.blue 2019

extrn ExitProcess : proc
extrn LoadLibraryA : proc
extrn MessageBoxA : proc

.code
Main proc
;Znajdź adres kernel32.dll
;poprzez przeszukanie struktury
;ProcessEnvironmentBlock (PEB)

;R10 = TEB.ProcessEnvironmentBlock
mov r10, gs:[60h]

;R10 = ProcessEnvironmentBlock->Ldr
mov r10, qword ptr [r10 + 18h]

;R11 = Ldr->InMemoryOrderModuleList
mov r11, qword ptr [r10 + 20h]

;R10 = InMemoryOrderModuleList.Flink
mov r10, qword ptr [r11]

;R11 = InMemoryOrderModuleList.Flink->Flink
mov r11, qword ptr [r10]

;R10 = InMemoryOrderModuleList.Flink->Flink->Flink
;R10 = kernel32.dll base address!
mov r10, qword ptr [r11 + 20h]

;Poniżej sprawdzenie poprawności,
;czyli porównanie znalezionego
;adresu bazowego kernel32.dll
;z adresem pobranym przez LoadLibraryA

;zachowaj znaleziony
;adres kernel32.dll
;na stosie
push r10

;pobierz adres kernel32.dll
;w sposób zwykły
sub rsp, 28h
mov rcx, offset kernel32dll
call LoadLibraryA
add rsp, 28h

;zdejmij adres kernel32.dll
;ze stosu programu
pop rcx

;porównaj adresy
cmp rcx, rax
jne _bad

        _good:
mov rdx, offset szFound
jmp _msgbox

        _bad:
mov rdx, offset szNotFound

;wyświetl stosowny komunikat
        _msgbox:
sub rsp, 28h
xor r9, r9
xor r8, r8
;rdx ustawione wcześniej
xor rcx, rcx
call MessageBoxA
add rsp, 28h

        _exit:
sub rsp, 28h
xor rcx, rcx
call ExitProcess

kernel32dll db "kernel32.dll", 0
szFound db "kernel32.dll address found.", 0
szNotFound db "kernel32.dll address NOT found.", 0
Main endp
end

Przykładowy szablon kodu powłoki (shellcode)

Tak oto udało się stworzyć szablon pozwalający na uproszczone pisanie kodów powłoki typu shellcode. Samouczek krok po kroku jak zbudować kod: Projekt w języku Asembler x64 dla Visual Studio 2022
;☣️ Custom payload template ☣️

;
;✔️ In R15 register you have GetProcAddress.
;✔️ In RDI register you have LoadLibraryA.
;
;📖 Windows API is at your disposal. 📖
;
;How to build executable (cmd.exe):
;ml64.exe prog1.asm /link /entry:Main /subsystem:windows

.code
    Main proc
        nop
        nop
        nop
        
        ;✂️--- CUT HERE ---✂️
        
        push rbp
        push rbx
        push rdi
        push rsi
        push rsp
        push r12
        push r13
        push r14
        push r15
        
        mov r10, gs:[60h]
        mov r10, qword ptr [r10 + 18h]
        mov r11, qword ptr [r10 + 20h]
        mov r10, qword ptr [r11]
        mov r11, qword ptr [r10]
        mov rbx, qword ptr [r11 + 20h]
        mov r9d, dword ptr [rbx + 3Ch]
        add r9, rbx
        add r9, 18h + 70h
        mov r11d, dword ptr [r9]
        lea r8, qword ptr [rbx + r11]
        mov ecx, dword ptr [r8 + 18h]
        mov r12d, dword ptr [r8 + 20h]
        add r12, rbx
        
        _search_loop:
        lea r10, qword ptr [r12 + rcx * sizeof dword]
        mov edi, dword ptr [r10]
        add rdi, rbx
        lea rsi, qword ptr [szGetProcAddress]
        
        _compare_str:
        cmpsb
        jne _function_not_found
        mov al, byte ptr [rsi]
        test al, al
        
        jz _function_found
        jmp _compare_str
        
        _function_not_found:
        loop _search_loop
        jmp _exit
        
        _function_found:
        mov r10d, dword ptr [r8 + 24h]
        add r10, rbx
        mov cx, word ptr [r10 + rcx * sizeof word]
        mov r10d, dword ptr [r8 + 1Ch]
        add r10, rbx
        mov eax, dword ptr [r10 + rcx * sizeof dword]
        add rax, rbx
        mov r15, rax
        
        ;GetProcAddress("kernel32.dll", "LoadLibraryA");
        mov rcx, "Ayra"
        push rcx
        mov rcx, "rbiLdaoL"
        push rcx
        mov rdx, rsp
        mov rcx, rbx
        sub rsp, 30h
        call rax ;call GetProcAddress
        add rsp, 30h + 10h
        ;RDI = LoadLibraryA
        mov rdi, rax
        
        ;In R15 register you have GetProcAddress.
        ;In RDI register you have LoadLibraryA.
        ;(...)
        ;Your function calls HERE.
        ;(...)
        ;Windows API is at your disposal.
        
        pop r15
        pop r14
        pop r13
        pop r12
        pop rsp
        pop rsi
        pop rdi
        pop rbx
        pop rbp
        
        jmp _exit
            szGetProcAddress db "GetProcAddress", 0
        _exit:
        ret
        
        ;✂️--- CUT HERE ---✂️
        
        nop
        nop
        nop
    Main endp
end

Edukacyjny shellcode/payload generator

Quote:
Edukacyjna aplikacja typu generator shellcode dla architektury Windows x64. Dzięki temu programowi lepiej poznają Państwo zagadnienia związane z tworzeniem kodu relokowalnego/wstrzykiwalnego. Aplikacja zawiera cztery wbudowane, nieszkodliwe ładunki w postaci kodów źródłowych w języku Asembler (MASM x64). Posiada też szablon do łatwiejszego zaprogramowania własnego ładunku oraz generator, który zaciemnia wprowadzone bajty, aby nie były jawnie zakodowane w pliku. Program nie zawiera plików wykonywalnych ładunków, ani złośliwego kodu.









Pobierz z witryny Microsoft: https://apps.microsoft.com/store/detail/ethical7/9NWG8CDLW3T6

Wykaz literatury

Advanced Micro Devices Inc., 2017 – AMD64 Architecture Programmer's Manual
Intel Corporation, 2019 – Intel 64 and IA-32 Architectures Software Developer's Manual
https://docs.microsoft.com/en-us/cpp/assembler/masm/masm-for-x64-ml64-exe [access: 2020-07-28]
https://docs.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb [access: 2020-07-28]