Die Challenge “FluxArchiv (Part 1)” befasst sich mit einer Archivierungssoftware. Damit können Archive erstellt und mit Passwörtern versehen werden. Außerdem erhalten wir die Information, dass ein Archiv mit einem sechsstelligen Passwort versehen wurde, dass nur Großbuchstaben und Zahlen beinhaltet. Zusätzlich zur Beschreibung erhalten wir diese Dateien.
Dabei handelt es sich um ein 64bit Linux-Binary (die Archivierungssoftware) und ein damit verschlüsseltes Archiv. Die Software führen wir direkt einmal aus.
ruport@lambda:~$ ./archiv ################################################################################ FluxArchiv - solved security since 2007! Written by sqall - leading expert in social-kernel-web-reverse-engineering. ################################################################################ Unknown or invalid command. Usage: ./archiv <command> <archiv> <password> <file> commands: -l <archiv> <password> - lists all files in the archiv. -a <archiv> <password> <file> - adds a file to the archiv (when archiv does not exist create a new archiv). -x <archiv> <password> <filename> - extracts the given filename from the archiv. -d <archiv> <password> <filename> - delete the given filename from the archiv.
Es können mehrere Operationen durchgeführt werden. Wir probieren testweise das Anzeigen der beinhalteten Dateien des Archives.
rup0rt@lambda:~$ ./archiv -l FluxArchiv.arc rup0rt
################################################################################
FluxArchiv - solved security since 2007!
Written by sqall - leading expert in social-kernel-web-reverse-engineering.
################################################################################
Given password is not correct.
Jeder Versuch auf diese Weise an das Archiv heran zu kommen, wir unterbunden. Wenden wir uns also dem Reversing zu und starten den GDB um das Binary genauer zu analysieren.
(gdb) disas main Dump of assembler code for function main: [...] 0x00402be6 <+129>: call 0x402a01 <checkHashOfPassword> [...] 0x00402c31 <+204>: call 0x401b9a <encryptDecryptData> [...] 0x00402c55 <+240>: call 0x402a4e <verifyArchiv> [...]
Da das Programm freundlicherweise mit Debugging-Symbolen kompiliert wurde, können wir auf alle Namen von Funktionen und Variablen zugreifen. Beim Betrachten der main-Funktion erkennen wir wiederholt die Aufrufe der Funktionen “checkHashOfPassword”, “encryptDecryptData” und “verifyArchiv”. Diese sehen wir uns nun genauer an.
(gdb) disas checkHashOfPassword Dump of assembler code for function checkHashOfPassword: 0x00402a01 <+0>: push rbp 0x00402a02 <+1>: mov rbp,rsp 0x00402a05 <+4>: sub rsp,0x70 0x00402a09 <+8>: mov QWORD PTR [rbp-0x68],rdi 0x00402a0d <+12>: lea rax,[rbp-0x60] 0x00402a11 <+16>: mov rdi,rax 0x00402a14 <+19>: call 0x400bf0 <SHA1_Init@plt> 0x00402a19 <+24>: mov rax,QWORD PTR [rbp-0x68] 0x00402a1d <+28>: mov rdi,rax 0x00402a20 <+31>: call 0x400c00 <strlen@plt> 0x00402a25 <+36>: mov rdx,rax 0x00402a28 <+39>: mov rcx,QWORD PTR [rbp-0x68] 0x00402a2c <+43>: lea rax,[rbp-0x60] 0x00402a30 <+47>: mov rsi,rcx 0x00402a33 <+50>: mov rdi,rax 0x00402a36 <+53>: call 0x400c90 <SHA1_Update@plt> 0x00402a3b <+58>: lea rax,[rbp-0x60] 0x00402a3f <+62>: mov rsi,rax 0x00402a42 <+65>: mov edi,0x6045a0 0x00402a47 <+70>: call 0x400c50 <SHA1_Final@plt> 0x00402a4c <+75>: leave 0x00402a4d <+76>: ret End of assembler dump.
Die Funktion “checkHashOfPassword” tut nichts weiter, als von einem Übergabeparameter den SHA1-Hash zu erstellen. Dazu werden die Funktionen SHA1_Init, SHA1_Update und SHA1_Final verwendet. Nach Ausführung der Funktion sollte sich der SHA1-Hash an der Speicherstelle 0x6045a0 befinden. Das überprüfen wird sofort.
(gdb) break *0x00402a4c Breakpoint 1 at 0x402a4c (gdb) run -l FluxArchiv.arc rup0rt [...] Breakpoint 1, 0x0000000000402a4c in checkHashOfPassword () (gdb) x/20xb 0x6045a0 0x6045a0 <hash_of_password>: 0x19 0xc4 0xe1 0xdf 0x38 0x59 0x96 0x55 0x6045a8 <hash_of_password+8>: 0x5a 0xa1 0x40 0xbd 0x46 0x49 0xe7 0x70 0x6045b0 <hash_of_password+16>: 0x7e 0xa0 0xb2 0x9d
Beim Versuch, das Archiv mit dem Passwort “rup0rt” zu entpacken, finden wir also tatsächlich an der erwarteten Speicherstelle, den SHA1-Hash von “rup0rt” – nämlich “19c4e1df385996555aa140bd4649e7707ea0b29d”. Sehen wir uns nun an, was anschließend in der Funktion “encryptDecryptData” geschieht.
(gdb) disas encryptDecryptData Dump of assembler code for function encryptDecryptData: 0x00401b9a <+0>: push rbp 0x00401b9b <+1>: mov rbp,rsp 0x00401b9e <+4>: sub rsp,0x20 0x00401ba2 <+8>: mov QWORD PTR [rbp-0x18],rdi 0x00401ba6 <+12>: mov rax,QWORD PTR [rbp-0x18] 0x00401baa <+16>: mov edx,0x0 0x00401baf <+21>: mov esi,0x0 0x00401bb4 <+26>: mov rdi,rax 0x00401bb7 <+29>: call 0x400ba0 <fseek@plt> 0x00401bbc <+34>: mov rdx,QWORD PTR [rbp-0x18] 0x00401bc0 <+38>: lea rax,[rbp-0x10] 0x00401bc4 <+42>: mov rcx,rdx 0x00401bc7 <+45>: mov edx,0xc 0x00401bcc <+50>: mov esi,0x1 0x00401bd1 <+55>: mov rdi,rax 0x00401bd4 <+58>: call 0x400c60 <fread@plt> 0x00401bd9 <+63>: movzx edx,BYTE PTR [rbp-0x10] 0x00401bdd <+67>: movzx eax,BYTE PTR [rip+0x1d9c] # 0x403980 <MAGIC_VALUE> 0x00401be4 <+74>: cmp dl,al 0x00401be6 <+76>: jne 0x401ca0 <encryptDecryptData+262> 0x00401bec <+82>: movzx edx,BYTE PTR [rbp-0xf] [...]
Hier werden aus dem zu entpackenden Archiv zunächst 0x0c (12) Bytes ausgelesen (Zeilen 12-18). Anschließend werden diese 12 Bytes nacheinander mit einem statischen Wert verglichen (Zeile 21). Es handelt sich demnach hierbei um eine Funktion, die den Kopf des Archives überprüft. Den erwarteten Wert können wir direkt auslesen:
(gdb) print (char[12]) MAGIC_VALUE
$20 = "FluXArChiV13"
Aha, jedes FluxArchiv muss also mit den Bytes “FluXArChiV13” beginnen. Bevor wir das genauer in der Datei überprüfen, sehen wir uns noch die letzte Funktion “verifyArchiv” an.
(gdb) disas verifyArchiv Dump of assembler code for function verifyArchiv: 0x00402a4e <+0>: push rbp 0x00402a4f <+1>: mov rbp,rsp 0x00402a52 <+4>: sub rsp,0xd0 0x00402a59 <+11>: mov QWORD PTR [rbp-0xc8],rdi 0x00402a60 <+18>: mov rax,QWORD PTR [rbp-0xc8] 0x00402a67 <+25>: mov edx,0x0 0x00402a6c <+30>: mov esi,0xc 0x00402a71 <+35>: mov rdi,rax 0x00402a74 <+38>: call 0x400ba0 <fseek@plt> 0x00402a79 <+43>: mov rdx,QWORD PTR [rbp-0xc8] 0x00402a80 <+50>: lea rax,[rbp-0x20] 0x00402a84 <+54>: mov rcx,rdx 0x00402a87 <+57>: mov edx,0x14 0x00402a8c <+62>: mov esi,0x1 0x00402a91 <+67>: mov rdi,rax 0x00402a94 <+70>: call 0x400c60 <fread@plt> 0x00402a99 <+75>: mov DWORD PTR [rbp-0x4],0x0 0x00402aa0 <+82>: jmp 0x402ae6 <verifyArchiv+152>
Hier wird zunächst der Dateiheader des Archives übersprungen (Zeilen 6-11), also der Zeiger hinter “FluXArChiV13” gesetzt. Anschließend werden 0x14 (20) weitere Bytes aus der Datei gelesen (Zeilen 12-18) und an der Position $RBP-0x20 abgelegt.
0x00402aa2 <+84>: mov edx,DWORD PTR [rbp-0x4] 0x00402aa5 <+87>: mov eax,edx 0x00402aa7 <+89>: shl eax,0x3 0x00402aaa <+92>: mov ecx,eax 0x00402aac <+94>: sub ecx,edx 0x00402aae <+96>: mov edx,0x66666667 0x00402ab3 <+101>: mov eax,ecx 0x00402ab5 <+103>: imul edx 0x00402ab7 <+105>: sar edx,0x3 0x00402aba <+108>: mov eax,ecx 0x00402abc <+110>: sar eax,0x1f 0x00402abf <+113>: sub edx,eax 0x00402ac1 <+115>: mov eax,edx 0x00402ac3 <+117>: shl eax,0x2 0x00402ac6 <+120>: add eax,edx 0x00402ac8 <+122>: shl eax,0x2 0x00402acb <+125>: mov edx,ecx 0x00402acd <+127>: sub edx,eax 0x00402acf <+129>: movsxd rax,edx 0x00402ad2 <+132>: movzx edx,BYTE PTR [rax+0x6045a0] 0x00402ad9 <+139>: mov eax,DWORD PTR [rbp-0x4] 0x00402adc <+142>: cdqe 0x00402ade <+144>: mov BYTE PTR [rbp+rax*1-0x60],dl 0x00402ae2 <+148>: add DWORD PTR [rbp-0x4],0x1 0x00402ae6 <+152>: cmp DWORD PTR [rbp-0x4],0x13 0x00402aea <+156>: jle 0x402aa2 <verifyArchiv+84>
Anschließend wird der SHA1-Hash unseres Passwortes byteweise von Adresse 0x6045a0 eingelesen (Zeile 20). Dabei jedoch nicht sequenziell von vorn, sondern abhängig von RAX. Kurz gesagt, wird der Hash verwürfelt. Die Bytes werden dann in den Speicher zurück geschrieben (Zeile 23). Wie genau der Hash gelesen wird, sehen wir im Live-Debugging deutlicher:
(gdb) display / $rax (gdb) break *0x0000000000402ade Breakpoint 2 at 0x402ade (gdb) run -l FluxArchiv.arc rup0rt [...] Breakpoint 3, 0x0000000000402ad2 in verifyArchiv () 2: $rax = 0 (gdb) c 2: $rax = 7 (gdb) c 2: $rax = 14 (gdb) c 2: $rax = 1 [...]
Die Bytes des Hashes werden demnach durch die Reihe 0, 7, 14, 1, 8, 15, … bestimmt, was der Funktion i * 7 mod 20 (für i von 0 bis 20) entspricht. Nach Abarbeitung der Schleife sollte der SHA1-Hash von “rup0rt” also verwürfelt sein. Das prüfen wir mit dem Debugger.
(gdb) b *0x0000000000402aec Breakpoint 5 at 0x402aec (gdb) run -l FluxArchiv.arc rup0rt [...] Breakpoint 5, 0x0000000000402aec in verifyArchiv () (gdb) x/20xb $rbp-0x60 0x7fffffffe1c0: 0x19 0x55 0xe7 0xc4 0x5a 0x70 0xe1 0xa1 0x7fffffffe1c8: 0x7e 0xdf 0x40 0xa0 0x38 0xbd 0xb2 0x59 0x7fffffffe1d0: 0x46 0x9d 0x96 0x49
Diese Darstellung entspricht genau der verwürfelten Darstellung des SHA1-Hashes von “rup0rt” (19c4e1df385996555aa140bd4649e7707ea0b29d). Sehen wir uns nun an, was anschließend weiter in der Funktion verifyArchiv() geschieht:
0x00402aec <+158>: lea rax,[rbp-0xc0] 0x00402af3 <+165>: mov rdi,rax 0x00402af6 <+168>: call 0x400bf0 <SHA1_Init@plt> 0x00402afb <+173>: lea rcx,[rbp-0x60] 0x00402aff <+177>: lea rax,[rbp-0xc0] 0x00402b06 <+184>: mov edx,0x14 0x00402b0b <+189>: mov rsi,rcx 0x00402b0e <+192>: mov rdi,rax 0x00402b11 <+195>: call 0x400c90 <SHA1_Update@plt> 0x00402b16 <+200>: lea rdx,[rbp-0xc0] 0x00402b1d <+207>: lea rax,[rbp-0x40] 0x00402b21 <+211>: mov rsi,rdx 0x00402b24 <+214>: mov rdi,rax 0x00402b27 <+217>: call 0x400c50 <SHA1_Final@plt> 0x00402b2c <+222>: mov DWORD PTR [rbp-0x4],0x0 0x00402b33 <+229>: jmp 0x402b58 <verifyArchiv+266> 0x00402b35 <+231>: mov eax,DWORD PTR [rbp-0x4] 0x00402b38 <+234>: cdqe 0x00402b3a <+236>: movzx edx,BYTE PTR [rbp+rax*1-0x40] 0x00402b3f <+241>: mov eax,DWORD PTR [rbp-0x4] 0x00402b42 <+244>: cdqe 0x00402b44 <+246>: movzx eax,BYTE PTR [rbp+rax*1-0x20] 0x00402b49 <+251>: cmp dl,al 0x00402b4b <+253>: je 0x402b54 <verifyArchiv+262> 0x00402b4d <+255>: mov eax,0x0 0x00402b52 <+260>: jmp 0x402b63 <verifyArchiv+277> 0x00402b54 <+262>: add DWORD PTR [rbp-0x4],0x1 0x00402b58 <+266>: cmp DWORD PTR [rbp-0x4],0x13 0x00402b5c <+270>: jle 0x402b35 <verifyArchiv+231> 0x00402b5e <+272>: mov eax,0x1
In den Zeilen 1-14 wird erneut ein SHA1-Hash erstellt, diesmal über den verwürfelten Hash unseres Passwortes. Im Anschluss wird das Ergebnis mit den aus der Datei gelesenen 20 Bytes (hinter dem Header) byteweise verglichen (Zeile 23). Wenn dieser Vergleich fehltschlägt, wird die Funktion abgebrochen und mit der Ausgabe “not correct” beendet.
Zusammengefasst tut das Programm also Folgendes:
- SHA1-Hash vom eingegebenen Passwort erstellen
- Header des Archives prüfen (FluXArChiV13)
- SHA1-Hash vom Passwort verwürfeln und neuen SHA1-Hash davon erstellen
- 20 Bytes hinter dem Header aus dem Archiv auslesen und mit dem neuen SHA1-Hash des Passwortes vergleichen
Um nun an das korrekte Passwort zu gelangen, müssen wir also die 20 betreffenden Bytes aus dem Archiv extrahieren und dann ein Passwort per Brute Force finden, das doppelt gehasht und verwüfelt dieselben 20 Bytes ergibt. Zunächst sehen wir uns also das Archiv im Hex-Editor an:
Neben dem erwarteten Datei-Header sehen wir die 20 Bytes, die das von uns gesuchte Passwort nach der beschriebenen Berechnung ergeben muss, nämlich “372942DF2712824505D8171F4F0BCB14153D39BA”.
Nach der Beschreibung der Challenge ist das Passwort sechs Zeichen lang und beinhaltet nur Großbuchstaben und Zahlen. Wir entwickeln also ein Python-Skript, dass den SHA1-Hash angreift und das Passwort per Brute Force bestimmt.
#!/usr/bin/env python import itertools import string import hashlib import sys start=int(sys.argv[1])*36**5 step=36**5 alpha = string.uppercase+string.digits keys = itertools.product(alpha,repeat=6) for i,key in enumerate(itertools.islice(keys,start,start+step)): key="".join(key) hash=hashlib.sha1(key) hash=hash.digest() str ="" str+=hash[0] str+=hash[7] str+=hash[14] str+=hash[1] str+=hash[8] str+=hash[15] str+=hash[2] str+=hash[9] str+=hash[16] str+=hash[3] str+=hash[10] str+=hash[17] str+=hash[4] str+=hash[11] str+=hash[18] str+=hash[5] str+=hash[12] str+=hash[19] str+=hash[6] str+=hash[13] newhash=hashlib.sha1(str).hexdigest() if (i+1)%10000 == 0: print key; if newhash == "372942DF2712824505D8171F4F0BCB14153D39BA".lower(): print "KEY:",key exit(0) print "DONE"
Um die Berechnung bequem verteilen und damit beschleunigen zu können, nimmt das Skript einen Parameter, der das Zeichen bestimmt, mit dem das Cracking begonnen werden soll. Die Ausführung des Crackers liefert für das 15. Zeichen folgende Ausgabe:
rup0rt@lambda:~/HackLu2013$ ./archiv.py 15
PAAHZ1
PAAPPT
[...]
PWFTPD
PWF1E5
KEY: PWF41L
Das Passwort für das Archiv lautet also “PWF41L”. Auch wenn es sich dabei bereits um die eigentliche Flagge handelt, bekommen wir von den FluxFingers noch den verdienten Lohn für unsere Arbeit in Form von Bild und Ton aus dem Archiv!
Die Lösung lautet “PWF41L“.