Dies ist eine alte Version des Dokuments!
Obfuscation: ByteSwapping
Polymorphie
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: 1)
$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
auf0
- 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 RegisterRSP
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
gleich0
, 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 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 vorigen Beitrag übernehmen
Debug
Nach der Kompilierung des Injecters können wir mit dem Debug starten. Ich nutze hierbei x64dbg.
Wir drücken F9 (Ausführen) und landen im Entry Point der Anwendung. Von hier aus suchen wir die Funktion main()
Ist diese gefunden, markieren wir die Zeile und drücken F4 (Ausführen bis Auswahl) und springen mit F7 in die Funktion.
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.
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.
Ich habe an dieser Stelle wieder meine Calc-Payload benutzt, so dass am Ende calc.exe
ausgeführt wird.
Diskussion