Hack.Lu CTF 2013 – FluxArchiv (1)

Hack.lU 2013 CTF - FluxArchiv (Part 1) - Task Description

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:

  1. SHA1-Hash vom eingegebenen Passwort erstellen
  2. Header des Archives prüfen (FluXArChiV13)
  3. SHA1-Hash vom Passwort verwürfeln und neuen SHA1-Hash davon erstellen
  4. 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:

Hack.Lu 2013 CTF - FluxArchiv (Part 1) - Binary view of Archive

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!

Hack.Lu 2013 CTF - FluxArchiv (Part 1) - FluxFingers

Die Lösung lautet “PWF41L“.

Leave a Reply

Your email address will not be published. Required fields are marked *