Siebdruck
Reverse Engineering für Neugierige
Hier mal ein kleines Tutorial zum Thema Reverse Engineering unter GNU/Linux auf einer x86 Maschine. Das Paper analysiert ein einfaches Binary, dass ich geschrieben habe und das Ihr hier downloaden könnt.
First things first.
Zuerst mal muss ich darauf hinweisen, dass Reverse Engineering von fremder Software in der EU und in den USA illegal ist, sofern es nicht vom Urheber erlaubt wird. In den USA ist sogar die Verbreitung von Software und Artikeln über das Reverse Engineering Dank dem DMCA verboten. Zum Glück konnte dies beim Euro-DMCA verhindert werden.
Reverse Engineering wird bei weitem nicht nur dazu verwendet um Software zu cracken. Dieses Tutorial verwendet solch ein Beispiel, weil es sehr anschaulich und interessant ist, aber Reverse Engineering wird ebenfalls dazu benutzt um Malware wie Trojaner und Viren zu analysieren und Gegenmassnahmen zu ergreifen, um Spyware aufzudecken oder um Kopierschutzverfahren unter akademischen Gesichtspunkten auf ihre Wirksam- und Brechbarkeit zu analyiseren.
Man schiesst sich also derbe in den eigenen Fuss, wenn man Reverse Engineering komplett verbietet, denn Reverse Engineering gehört zu den digitalen Selbstverteidigungstechniken.
Aber weiter im Text. Ich sollte vielleicht erstmal sagen, was Dir dieses Tutorial nicht erzählt. Ich bringe Dir hier weder bei wie ein Betriebsystem oder Kernel funktioniert, wie Computer generell arbeiten, wie man Assembler liest / programmiert, noch wie ein Compilevorgang abläuft. Wenn Du mehr zu diesen Themen erfahren möchtest, lege ich Dir nahe das Buch Programming from the ground up zu lesen: http://savannah.nongnu.org/projects/pgubook. Meiner Meinung nach eines der besten Computerbücher überhaupt! Vielen Dank an den Autor Jonathan Bartlett und die Co-Autoren an dieser Stelle.
Gut. Also ich setze für die oben genannten Themen Grundkenntnisse voraus. Wenn Du diese nicht hast, lohnt es sich dennoch weiter zu lesen, weil so kannst Du wenigstens schon mal sehen wie man generell ein Binary analysiert und in diesem Fall eine Passwortroutine knackt. Aber um das Vorgehen wirklich zu verstehen, ist es notwendig wenigstens das eben genannte Buch mindestens einmal zu lesen!
Jetzt aber los! Du hast Dir das Binary runter geladen und ein Linux System mit installierten gcc, bin-utils, ltrace, strace und einem Hexeditor (z.B. khexedit), sowie Bier, Kaffee oder ein anderes Getränk Deiner Wahl geschnappt, also einfach mal fallen lassen.
Zuerst wollen wir wissen was das da für nen Binary ist.
file check-password
check-password: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.0, dynamically linked (uses shared libs), not stripped
Das Binary ist im ELF Format, 32 Bit für x86 Prozessoren. Soweit so gut. ELF ist das Standardformat für moderne Unix(-like) Betriebsysteme, auf Windows wäre das Equivalent PE für dieses EXE Zeugs.
Schön zu wissen ist, dass es dynamische Bibliotheken verwendet, d.h. nicht die gesamte Funktionalität ist im Programm selbst enthalten, sondern zusätzlich in anderen Programmen, die geladen werden und "not stripped" heisst, dass die Symboltablle im ELF Header nicht entfernt wurde und wir leicht raus bekommen können welche Funktionen das Programm konkret verwendet.
Mit dem Befehl strip check-password
hätten wir diese Symboltablle (gleich mehr dazu) entfernt.
Das solltest Du, wenn, dann aber erst nach dem Tutorial ausprobieren.
Ok. Da wir dynamische Bibliotheken verwenden, lass ma kucken welche.
ldd check-password
In der Ausgabe ist folgendes interessant:
libcrypt.so.1 => /lib/tls/libcrypt.so.1 (0x4001d000)
libcrypt sagt uns, dass das Programm die crypt() Funktion für die Verschlüsselung verwendet, d.h. das zu erratende Passwort ist höchstwahrscheinlich nicht im Klartext im Binary zu finden. Egal. Wir probieren es trotzdem, weil es ist gut zu wissen wie das geht.
strings check-password
Du siehst u.a. welche Libs und welche Funktionen verwendet werden UND diesen String hier
djjk/aWmbQkO6
.
Das sieht wie das verschlüsselte Passwort aus.
Bei dummen Binarys findest du so schon was du suchst.
Mit dem String da könntest du die Standardmethoden Dictionary- oder Bruteforce Attack starten, um das Passwort
zu cracken, aber... es geht schöner, viel schöner! ;)
strace ./check-password abc
Damit kucken wir was das Programm für Kernel Funktionen verwendet. Sieht super kompliziert aus und fast alles aus der Ausgabe ist die normale Lade ein Programm und führe es aus-Prozedur, die uns nicht weiter interessiert. Z.B. siehst du fast am Ende der Ausgabe sowas:
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\220s\0"..., 512) = 512
Da fängt der Kernel erst an das Binary zu lesen! Hm, irgendwie bringt uns diese Ausgabe nicht weiter. Keine spannenden Funktionen. Aber man kann dadurch z.b. entdecken, dass das Programm verdeckt einen Socket aufmacht, um etwas über das Netz zu senden / empfangen o.ä
Der Vollständigkeit halber ruf mal ltrace ./check-password abc
auf. Damit siehst Du die verwendeten
Funktionen aus dynamischen Libs.
strncpy(0xbffffcac, "abc", 20) = 0xbffffcac
crypt("abc", "dj") = "djouNTUyl4Hfg"
Aha! Zuerst wird unsere Eingabe in einen Buffer kopiert und dann mit crypt verschlüsselt (Salt ist "dj") und am
Ende kommt djouNTUyl4Hfg
raus. Das Ergebnis tut aber nicht... Tjo, macht nix, schaun wir einfach mal
weiter.
Ich hab Dir eben versprochen zu erklären was diese ELF Symboltabelle ist. Holen wir das mal kurz nach.
Jede dynamische Lib und jede Funktion in solch einer Lib hat eine Adresse. Einfaches Beispiel. Bei Dir im Umkreis
hast Du einen Spezialisten für Malerarbeiten, einen Typen, der fetten Sound produziert, eine Freundin, die
coole Bilder malt, usw.
Wenn Du von denen was willst, rufst Du sie an. Die Telefonnummer ist die Adresse und die ELF Symboltabelle ist nichts
anderes als das Telefonbuch Deines Binarys.
Lass uns das mal anschauen, indem wir nm check-password
in die Console hauen.
Die Telefonnummern unserer Freunde beginnen immer mit 0804 quasi die Ortsvorwahl für "lokale Angelegenheiten". Diese Nummern stehen genau so in dem Binary drin. D.h. es ruft sie wirklich an! Nehmen wir beispielsweise die Funktion check, die ich selbst geschrieben habe:
08048478 T check
...und schaun wir ob das Binary sie anruft: (Die Adressen können bei Dir auf Deinem Computer variieren, achte darauf!)
objdump -d check-password | grep 08048478
08048478 <check>:
Bingo! Ok was haben wir hier gemacht? objdump -d check-password
erlaubt es einem das Binary zu disassemblen, d.h. Du bekommst den Assembler Code angezeigt, den Dein Prozessor ausführt, wenn Du dieses Programm lädst. Assembler Code ist nichts anderes, als die wirklichen 0en und 1en in Worte übersetzt oder anders herum, wenn Du in Assembler programmierst ist der quasi letzte Schritt Deine Kommandos in Zahlen zu übersetzen.
Doch was genau heisst jetzt hier eigentlich 08048478
?
Das Programm denkt ihm gehört die Welt, d.h. es denkt es hätte den kompletten Arbeitsspeicher des ganzen
Computers nur für sich alleine und könnte damit machen was es wolle.
In Wirklichkeit ist dies die virtuelle(!) Adresse an der das Programm die Funktion für "check" findet.
Was ist, wenn wir eine Funktion suchen, die nicht in dem Binary selbst, sondern in einer Libs vorhanden ist?
Nehmen wir crypt!
08049804 b completed.4463
U crypt@@GLIBC_2.0
objdump -d check-password |grep 08049804
Hm. Gibt nix. Wie kann das sein? Hat wohl ne Weiterleitung eingerichtet, d.h. wir rufen jemanden an, der in einem anderen Programm wohnt. Ok. Nächster Versuch. Suchen wir die Nummer in der Lib:
objdump -d /usr/lib/i686/cmov/libcrypto.so.0.9.6|grep 08049804
Nix. Ok. Ich gebe zu ich habe eben ein wenig vereinfacht. Es gibt neben der Symboltabelle noch andere
Telefonbücher in Deinem Binary nämlich die .plt und die .got Sektion, aber die brauchen wir hier jetzt
nicht. Falls Du Dir trotzdem mal anschauen möchtest, was da so drin steht, dann geht das mit
objdump -d -j .plt check-password
bzw. objdump -d -j .got check-password
. Das sind grob
gesagt Weiterleitungen (Sprungtabellen) für externe Programmfunktionen und der Linker kümmert sich darum,
dass das Programm die Funktionen findet.
Doch bevor das hier zu kompliziert wird zurück zu unserem Problem. Wir wollen die Passwortabfrage von diesem
Binary knacken und die ist lokal und somit interessieren uns externe Angelegenheiten grad nicht weiter.
So jetzt geht es wirklich ans Eingemachte. Schnappen wir uns einen Debugger / Disassembler und zerlegen das Binary in seine Assemblereinzelteile!
gdb ./check-password
(gdb) disas main
Das disassembled die Main (Haupt)-Funktion des Programms, was zur Folge hat, dass Du ziemlich viele komische Befehle auf Deinem Bildschirm siehst und ganz viele Hexzahlen. Stör Dich grad ma nicht weiter dran und suche die check Funktion.
0x0804853a <main\4372>: call 0x8048478 <check>
Da isse. Und danach steht folgendes:
0x08048542 <main\4380>: mov %eax,0xfffffffc(%ebp)
0x08048545 <main\4383>: cmpl $0x1,0xfffffffc(%ebp)
Rückgabewerte von Funktionen stehen immer im EAX Register in Deinem Prozessor und dieser Rückgabewert wird anschließend mit 1 verglichen.
0x08048549 <main\4387>: jne 0x8048565 <main\43115>
JNE heisst Jump not Equal also wenn keine 1 gefunden wurde, rufe 0x8048565
auf (was auch immer da
geschieht). Wenn doch 1, dann latsch einfach weiter. Schaun wir mal ob wir eine 0 oder eine 1 sein wollen.
0x0804854e <main\4392>: push $0x80486bc
0x08048553 <main\4397>: call 0x8048370 <_init\4356>
Hier wird irgendwas auf den Stack geschmissen und eine weitere (externe) Funktion aufgerufen. Wir wissen wenn wir was
falsches eingeben, wird auf dem Bildschirm "Wrong password!" ausgegeben. Kucken wir mal was an der Adresse
0x80486bc
steht und interpretieren es als ASCII String.
(gdb) x/s 0x80486bc
0x80486bc <_IO_stdin_used\4340<: "Wrong password!"
Volltreffer! Das ist also das, was wir nicht haben wollen. Wir möchten eine 0 im Register EAX stehen haben nachdem die check Funktion ausgeführt wurde.
Ok. Setzen wir einen Breakpoint auf die Zeile, in der der Wert aus EAX zum Vergleichen ins RAM kopiert wird, das war diese Zeile hier
0x08048542 <main\4380>: mov %eax,0xfffffffc(%ebp)
(gdb) break *0x08048542
Wenn wir das Programm jetzt mit dem Parameter password starten, dann hält es genau an dieser Stelle an (bevor es diese ausführt).
(gdb) run password
Mal kucken was in EAX drin steht.
(gdb) print $eax
$4 = 1
Gut. Wenn das Programm jetzt weiter läuft und wir alles richtig verstanden haben, dann zeigt es uns "Wrong password!". Probieren wir's aus.
(gdb) continue
Continuing.
Wrong password!
Passt. Also nochmal neustarten...
(gdb) run password
Starting program: /home/dj/wargames/reveng/check-password password
Breakpoint 3, 0x08048542 in main ()
...und diesmal hätten wir gerne eine 0 in EAX.
(gdb) set $eax = 0
(gdb) print $eax
$5 = 0
Sehr schön. Weiter.
(gdb) continue
Continuing.
Accepted password!
sh: /bin/csh: No such file or directory
Tada! Damit hätten wir die Passwortabfrage geknackt. Das Programm versucht anschliessend uns eine C Shell zu geben, die auf diesem Computer allerdings nicht installiert ist.
Ich find es irgendwie ein wenig lästig immer in den Debugger zu wechseln und jedes Mal mir diesen Assembler Code anzutun und irgendwelche Werte in Registern zu überschreiben, um diese Passwortroutine zu umgehen. Das muss noch schöner funktionieren!
Schnell das Binary disassemblen.
objdump -d check-password|less
Und die Stelle suchen an der der Rückgabewert mit 1 verglichen wird.
8048545: 83 7d fc 01 cmpl $0x1,0xfffffffc(%ebp)
Meistens produziert unsere Passworteingabe eine 0. Wie schön wäre das Leben, wenn das Programm statt auf
eine 1 auf eine 0 überprüfen würde... Der Befehl cmpl $0x1,0xfffffffc(%ebp)
sieht in
Hexzahlen so aus 83 7d fc 01
. Wir erinnern uns Computer kennen nichts anderes als Zahlen und zwar
binäre Zahlen und der letzte Schritt vom Assemblercode zum Binary ist die Befehle in Zahlen zu übersetzen.
Binäre Zahlen sehen ziemlich unhandlich aus, deshalb zeigt man sie besser in Hex an. Netterweise ist in
83 7d fc 01
eine 01
enthalten.
Nehmen wir also einen beliebigen Hexeditor, ich empfehle khexedit, und suchen nach genau diesen Zahlen in dem Binary.
0000:0540 c4 10 89 45 fc 83 7d fc 01 75 1a 83 ec 0c 68 bc
Da haben wir's. Jetzt ändern wir die 1 in eine 0...
0000:0540 c4 10 89 45 fc 83 7d fc 00 75 1a 83 ec 0c 68 bc
...speichern und ab dafür!
./check-password hfakjfhak
Accepted password!
sh: /bin/csh: No such file or directory
Mit dem Ergebnis kann ich leben :)
Nur der Vollständigkeit halber es gibt noch weitere Möglichkeiten sich die Passwortabfrage vom Hals zu
schaffen. Die eine ist die Rückgabeparameter zu manipulieren, was wir gerade getan haben, eine andere wäre
die entsprechende Stelle im Binary, an der die check Funktion aufgerufen wird einfach mit 0x90 Werten (NOP = No operation)
zu überschreiben oder man ändert die Adresse, an die das Binary bei einer falschen Eingabe springt einfach
in die "richtige" Adresse oder man ersetzt den verschlüsselten Passwortstring djjk/aWmbQkO6
durch
einen anderen wie djouNTUyl4Hfg
. Für diesen String kennen wir das Klartextpasswort
abc
.
Diese Übungsaufgaben bleiben dem interessierten Leser überlassen.