Zu dieser Challenge gibt es keine weitere Aufgabenstellung, da der Name bereits selbsterklärend ist – “Crack It!”. Alles was wir erhalten, ist eine ZIP-Datei mit einem Binary.
rup0rt@lambda:~/ATAST2012$ file crackit
crackit: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.15, BuildID[sha1]=0x1eb85f9048b1d8dddfbc3898826aaddfe63a55ab, not stripped
… einem Linux-32bit-Programm um genau zu sein. Dieses führen wir, wie gewohnt, erst einmal aus um deren Funktionsweise überblicken zu können.
rup0rt@linux:~/ATAST2012$ ./crackit Usage : ./crackit <password> rup0rt@linux:~/ATAST2012$ ./crackit 12345 hahaha wrong password :p
Wie bei der vorherigen Challenge auch wird also ebenso ein Passwort benötigt um die Abfrage des Programmes passieren und somit die Lösung zur Aufgabe (Crackt It!) erhalten zu können. Erneut beginnen wir mit einer Betrachtung des Assembler-Codes durch Verwendung von “objdump”, wobei folgender Abschnitt direkt auffällt:
8048620: c7 44 24 0c 00 00 00 mov DWORD PTR [esp+0xc],0x0 8048628: c7 44 24 08 00 00 00 mov DWORD PTR [esp+0x8],0x0 8048630: c7 44 24 04 00 00 00 mov DWORD PTR [esp+0x4],0x0 8048638: c7 04 24 00 00 00 00 mov DWORD PTR [esp],0x0 804863f: e8 c4 fd ff ff call 8048408 <ptrace@plt> 8048644: 85 c0 test eax,eax 8048646: 79 16 jns 804865e <main+0x6d>
Das Programm ruft die Funktion “ptrace()” auf (Zeile 5), prüft anschließend deren Rückgabewert (Zeile 6) und springt, davon abhängig, weiter in den Quellcode (Zeile 7). Mit “ptrace()” können Prozesse debuggt und gesteuert werden. Dies Funktioniert jedoch nur für einen Prozess. Sollten wir also bei Programmstart selbst einen Debugger verwenden, wird “ptrace()” fehlschlagen und das Programm beendet.
Hierbei handelt es sich hierbei also um eine Anti-Debugging-Methode, die verhindern soll, dass wir mit Programmen wie “strace” oder dem GDB das Programm bei der Ausführung beobachten. Um diesen Schutz zu umgehen verändern wir einfach den bedingten Sprung (Zeile 7) in einen unbedingten Sprung unter Verwendung eines Hex-Editors.
Dazu suchen wir in der Datei nach dem Opcodes 0x79 0x16 (JNS 16) (siehe Zeile 7), welche wir bei Position 0x646 im Binary finden. Das JNS (0x79) ersetzen wir nun mit dem Opcode für JMP (0xEB) und speichern die Datei wieder ab. Nun sollte es uns möglich sein, das Programm selbst unter Verwendung eines Debuggers zu starten.
Als Nächstes sehen wir uns den Assembler-Quellcode des Binarys weiter an:
804865e: b8 85 85 04 08 mov eax,0x8048585 8048663: 8b 00 mov eax,DWORD PTR [eax] 8048665: 25 ff 00 00 00 and eax,0xff 804866a: 3d cc 00 00 00 cmp eax,0xcc 804866f: 75 0c jne 804867d <main+0x8c> 8048671: c7 04 24 01 00 00 00 mov DWORD PTR [esp],0x1 8048678: e8 fb fd ff ff call 8048478 <exit@plt> 804867d: b8 85 85 04 08 mov eax,0x8048585 8048682: 83 c0 22 add eax,0x22 8048685: 8b 00 mov eax,DWORD PTR [eax] 8048687: 25 ff 00 00 00 and eax,0xff 804868c: 3d cc 00 00 00 cmp eax,0xcc 8048691: 75 0c jne 804869f <main+0xae> 8048693: c7 04 24 01 00 00 00 mov DWORD PTR [esp],0x1 804869a: e8 d9 fd ff ff call 8048478 <exit@plt> 804869f: b8 85 85 04 08 mov eax,0x8048585 80486a4: 83 c0 35 add eax,0x35 80486a7: 8b 00 mov eax,DWORD PTR [eax] 80486a9: 25 ff 00 00 00 and eax,0xff 80486ae: 3d cc 00 00 00 cmp eax,0xcc 80486b3: 75 0c jne 80486c1 <main+0xd0> 80486b5: c7 04 24 01 00 00 00 mov DWORD PTR [esp],0x1 80486bc: e8 b7 fd ff ff call 8048478 <exit@plt> 80486c1: b8 85 85 04 08 mov eax,0x8048585 80486c6: 83 c0 47 add eax,0x47 80486c9: 8b 00 mov eax,DWORD PTR [eax] 80486cb: 25 ff 00 00 00 and eax,0xff 80486d0: 3d cc 00 00 00 cmp eax,0xcc 80486d5: 75 0c jne 80486e3 <main+0xf2> 80486d7: c7 04 24 01 00 00 00 mov DWORD PTR [esp],0x1 80486de: e8 95 fd ff ff call 8048478 <exit@plt>
Nach der “ptrace()”-Funktion prüft das Programm an vier Stellen im Quellcode (ausgehend von 0x8048585) ob sich ein 0xcc (INT) an den entsprechenden Positionen befindet. INT (Interrupt) wird von Debuggern in Form von Breakpoints verwendet, um den Programmfluss zu unterbrechen und die Kontrolle so an den Debugger zurück zu geben.
Durch das Prüfen auf 0xcc versucht das Programm also festzustellen ob an bestimmten Schlüsselpositionen Haltepunkte von einem Debugger gesetzt wurden. Hierbei handelt es sich somit um eine weitere Anti-Debugging-Methode. Damit das Programm von uns vollkommen uneingeschränkt untersucht werden kann, werden wir auch diesen Schutzmechanismus entfernen.
Dazu werden wir den gesamten Block mit NOPs (No Operation = 0x90) ersetzen. Wir suchen also nach dem Beginn des Blockes 0xb8 0x85 0x85 (Zeile 1) und Ersetzen alle Zeichen bis zum Ende des Blockes 0xfd 0xff 0xff (Zeile 34) mit 0x90.
Nun, nachdem alle Anti-Debugging-Mechanismen entfernt sind, kann das Programm, wie gewohnt mit dem GDB gestartet werden. Aus dem Programmfluss unterhalb unserer NOPs erkennen wir:
(gdb) disas main 0x080486e3 <+242>: mov eax,DWORD PTR [ebp+0xc] 0x080486e6 <+245>: add eax,0x4 0x080486e9 <+248>: mov eax,DWORD PTR [eax] 0x080486eb <+250>: mov DWORD PTR [esp],eax 0x080486ee <+253>: call 0x8048585 <pwd> 0x080486f3 <+258>: test eax,eax 0x080486f5 <+260>: jne 0x8048705 <main+276>
Der Rückgabewert der <pwd>-Funktion ist entscheidend dafür, ob das Passwort akzeptiert wird, oder nicht. Die Berechnung und Überprüfung des Passwortes muss also innerhalb dieser Funktion erfolgen.
(gdb) disas pwd 0x0804859a <+21>: xor eax,eax 0x0804859c <+23>: mov eax,DWORD PTR [ebp-0x3c] 0x0804859f <+26>: mov DWORD PTR [esp],eax 0x080485a2 <+29>: call 0x8048428 <strlen@plt> 0x080485a7 <+34>: cmp eax,0xa 0x080485aa <+37>: jne 0x80485d9 <pwd+84> 0x080485ac <+39>: mov DWORD PTR [esp+0x4],0x14 0x080485b4 <+47>: lea eax,[ebp-0x34] 0x080485b7 <+50>: mov DWORD PTR [esp],eax 0x080485ba <+53>: call 0x8048544 <GeneratePassword> 0x080485bf <+58>: lea eax,[ebp-0x34] 0x080485c2 <+61>: mov DWORD PTR [esp+0x4],eax 0x080485c6 <+65>: mov eax,DWORD PTR [ebp-0x3c] 0x080485c9 <+68>: mov DWORD PTR [esp],eax 0x080485cc <+71>: call 0x8048468 <strcmp@plt>
Zunächst ermittelt das Programm die Länge des von uns eingegebenen Passwortes mit Hilfe der Funktion “strlen()”. Nur wenn die Länge unseres Passwortes gleich 0xa (10) ist, wird der Programmfluss fortgesetzt. Wir wissen also, dass wir einen zehn Zeichen langen Parameter bei Aufruf übergeben müssen.
Anschließend wird die Funktion “GeneratePassword” aufgerufen. In dieser Funktion muss das eigentliche Passwort generiert werden um es darauf hin, mit der von uns eingegebenen Zeichenkette vergleichen zu können. Wir können nun genau untersuchen, wie das Passwort erstellt wird, oder einfach die Position finden, an der die beiden Strings miteinander verglichen werden – letzteres ist natürlich die schnellere Lösung ;-).
Direkt nach dem Aufruf von “GeneratePassword” wird nämlich die “strcmp()”-Funktion verwendet. Hier muss also der Vergleich des generierten Passwortes mit der von uns eingegebenen Zeichenkette erfolgen. Wir setzen daher einen Haltepunkt mit GDB vor den Aufruf der “strcmp()”-Funktion und starten das Programm mit einem zehn Zeichen langen String.
(gdb) b *0x080485cc Breakpoint 1 at 0x80485cc (gdb) r 1234567890 Starting program: /home/creeq/ATAST2012/crackit3 1234567890 Breakpoint 1, 0x080485cc in pwd ()
Das Programm stoppt und wir befinden uns unmittelbar vor dem Aufruf von “strcmp()”. An dieser Stelle müssten also die beiden Zeichenketten, die verglichen werden sollen, bereits oben auf dem Stack liegen. Dies sehen wir uns nun an:
(gdb) x/8x $esp 0xbffff530: 0xbffff7d0 0xbffff554 0xb7fefc16 0xb7fffac0 0xbffff540: 0xb7fe0b58 0x00000001 0x00000001 0xbffff7d0 (gdb) x/s 0xbffff7d0 0xbffff7d0: "1234567890" (gdb) x/s 0xbffff554 0xbffff554: "zkhfspiyrw"
In den zwei Stack-Adressen finden wir zum einen die von uns eingegebene Zeichenkette “1234567890” und zum anderen tatsächlich das vom Programm erwartete Passwort!
Die Lösung lautet somit “zkhfspiyrw“.