Impressum Kontakt

Reverse Engineering für Neugierige

(balle)

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.