Bei dieser Challenge (Reversing 400) wird uns wiederholt nur “wortlos” eine Datei übergeben. Wir sind also wieder völlig auf uns gestellt, herauszufinden, wie und wo in diesem Binary der Schlüssel zur Aufgabe versteckt ist.
Wie gewohnt beginnen wir mit einer Untersuchung des Dateityps mit dem Tool “find”:
rup0rt@lambda:~/CSAW2012$ file csaw2012reversing
csaw2012reversing: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x012c3cf67d5aa15a9985ea064958921dc600c367, not stripped
Dieses Mal handelt es sich also um ein Linux-Binary, das für ein 64bit-System kompiliert wurde. Es wird daher Zeit, die entsprechende Virtuelle Machine zu starten (hier ein Debian GNU Linux 6.0) um die Datei näher zu betrachten.
Wie gewöhnlich lässt uns die Neugier nicht im Stich und wir führen die Datei zu aller Erst ohne Nachzudenken einfach aus! Als Antwort erhalten wir:
Das Programm scheint uns hier den verschlüsselten Key zum Lösen der Challenge auszugeben, der uns jedoch in dieser Form überhaupt nichts nützt. Um zu prüfen, ob es sich wirklich um verschiedene Zeichen handelt – denn die Ausgabe wirkt durch die Fragezeichen recht merkwürdig – nutzen wir das Werkzeug “xxd”.
Es handelt sich also tatsächlich um einen 16 Byte langen, verschlüsselten String. Demnach muss nun ein Weg gefunden werden, diesen Key zu entschlüsseln!
Zunächst einmal bietet es sich an, sich den Assembler-Code, auf dem das Binary basiert, zu betrachten, in der Hoffnung, die Berechnung des Schlüssels oder vielleicht sogar den Schlüssel selbst vor der Verschlüsselung abgreifen zu können. Dazu verwenden wir das Tool “objdump”. Hier ein Auszug des disassemblierten Codes aus der Funktion “<main>”.
40069b: e8 29 ff ff ff callq 4005c9 <encrypt> 4006a0: b8 fc 07 40 00 mov $0x4007fc,%eax 4006a5: 48 8d 55 e0 lea -0x20(%rbp),%rdx 4006a9: 48 89 d6 mov %rdx,%rsi 4006ac: 48 89 c7 mov %rax,%rdi 4006af: b8 00 00 00 00 mov $0x0,%eax 4006b4: e8 e7 fd ff ff callq 4004a0 <printf@plt> 4006b9: bf ff ff ff ff mov $0xffffffff,%edi 4006be: e8 f1 fe ff ff callq 4005b4 <done> 4006c3: 48 8d 45 e0 lea -0x20(%rbp),%rax 4006c7: 48 89 c7 mov %rax,%rdi 4006ca: e8 24 ff ff ff callq 4005f3 <decrypt>
Das Programm ruft hier die Funktion “<encrypt>” auf, bevor anschließend der String mittels “<printf>” bei Speicheradresse 0x4004b4 auf dem Bildschirm ausgegeben wird. Bis dahin kennen wir das Programm bereits, da das genau das Verhalten ist, das wir beim Ausführen schon beobachten konnten.
Danach wird bei Speicheradresse 0x4006be ohne weitere Bedingung, die Funktion “<done>” aufgerufen, wobei die Abarbeitung des Codes mit der Funktion “<decrypt>” bei Speicheradresse 0x4006ca nicht fortgesetzt wird. Der entscheidende Schritt, den Schlüssel zu erhalten, liegt also darin, das Programm dazu zu bewegen, die Funktion “<decrypt>” dennoch aufzurufen!
Da die Abarbeitung hier nicht an Bedingungen geknüpft ist, können wir von “Außen” wenig machen. Der einfachste und schnellste Weg erscheint mir daher, das Binary direkt zu verändern und den Aufruf der Funktion “<done>” aus dem Programmcode zu entfernen. Dies sollte dazu führen, dass kein Wegsprung mehr erfolgt und die Funktion “<decrypt>” bei der sequentiellen Abarbeitung aufgerufen wird.
Die Manipulation der Opcodes kann hier ganz einfach mit einem normalen Hex-Editor erfolgen – ich habe “bless” verwendet.
Die gezielte Suche nach den Opcodes von “call <done>” (E8 F1 FE FF FF), die aus dem Objekt-Dump abgelesen werden können, führt zu einem Treffer im Binary. Dies muss also die Stelle sein, an der der von uns ungewollte Abzweig im Programmcode erfolgt, der den Aufruf der Funktion “<decrypt>” verhindert.
Diese fünf Bytes müssen demnach verändert und im besten Fall aus dem Binary entfernt werden. Um jedoch nicht ungewollt Sprünge zu verfälschen, Programmbereiche zu verändern und so das gesamte Programm unbrauchbar zu machen, entfernen wir die Bytes nicht einfach, sondern ersetzen sie mit dem Opcode “NOP” (No Operation), der vom Prozessor einfach übergangen wird.
Der NOP-Opcode wird durch die hexadezimale Darstellung 0x90 repräsentiert, was bedeutet, dass die fünf Byte lange Call-Instruktion an Position 0x6be des Binarys durch fünf NOP-Instruktionen ersetzt wird. Das Ergebnis sieht folgendermaßen aus:
Der Aufruf der “<done>”-Funktion sollte nun aus dem Programmcode getilgt sein und wir können das Binary mit unseren Änderungen abspeichern. Eine Erfolgskontrolle mit dem Tool “objdump” führt nun zu folgender Ausgabe:
40069b: e8 29 ff ff ff callq 4005c9 <encrypt> 4006a0: b8 fc 07 40 00 mov $0x4007fc,%eax 4006a5: 48 8d 55 e0 lea -0x20(%rbp),%rdx 4006a9: 48 89 d6 mov %rdx,%rsi 4006ac: 48 89 c7 mov %rax,%rdi 4006af: b8 00 00 00 00 mov $0x0,%eax 4006b4: e8 e7 fd ff ff callq 4004a0 <printf@plt> 4006b9: bf ff ff ff ff mov $0xffffffff,%edi 4006be: 90 nop 4006bf: 90 nop 4006c0: 90 nop 4006c1: 90 nop 4006c2: 90 nop 4006c3: 48 8d 45 e0 lea -0x20(%rbp),%rax 4006c7: 48 89 c7 mov %rax,%rdi 4006ca: e8 24 ff ff ff callq 4005f3 <decrypt>
Unsere Anpassungen werden vom Disassembler korrekt interpretiert, was vermuten lässt, dass dies auch der Prozessor bei Ausführung tun sollte. Wir starten also das veränderte Binary nochmals und bekommen diesmal folgende Ausgabe:
Alles verlief erfolgreich und es war uns mit sehr einfachen Maßnahmen möglich, den Schlüssel zu erhalten.
Das Ergebnis lautet “csawissohard__:(“.