Inhaltsverzeichnis

, , , , ,

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: 2)

$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üsselt01AF45C3
XOR Wert 1 15141550
verschlüsselt 14BB5093
XOR Wert 2 57565712
verschlüsselt 56F912D1

Der Code

Schritt 1: Python Encoder

Die entsprechende Python Funktion ist schnell erklärt:

    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
call_decoder:
	call decoder
	Shellcode: db 0x75,0x3d...0x75
decoder:
	pop rsi

Schritt 2.2: Decoder Schleife

decode_loop:
    test rcx, rcx
    jz Shellcode
    mov rdx, rsi
    sub dl, Shellcode
    test dl, 1
    jnz 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är0001001000110100

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
Ungerade Zahlen
odd_byte:
    xor byte [rsi], al 
Post-Processing
post_processing:
    inc rsi
    dec rcx
    jmp decode_loop

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
python shencode.py extract -i poly2.o -o poly2.raw --start-offset 100 --end-offset 404
python shencode.py formatout -i poly2.raw --syntax c
...
"\x48\x31\xc0...x67\x28\x75";

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.

Repository

Den kompletten Shellcode findet Ihr hier:


2)
Anmerkung: Gerade und ungerade bezieht sich auf das Offset, somit ist Byte 1 an Offset 0 gerade und Byte 2 ungerade