Dieser Text befindet sich in der neusten Version auf http://www.eggdrop.ch/texts/binaries/.
18.01.05: Version 1.0
In diesem Text geht es um das Bearbeiten von ELF-Binaries. ELF steht für "Executable and Linking Format" und ist das momentan verbreitete Binary-Format auf UNIX-Betriebssystemen (z. B. auf Linux). Das Ziel ist es, Änderungen an solchen Binaries vornehmen zu können, ohne dass man im Besitze des jeweiligen Quellcodes ist. Dieser Text ist hauptsächlich für Text- und Lernzwecke gedacht.
Die Voraussetzungen für diesen Text sind:
Unser erstes Programm nimmt eine Zahl entgegen und addiert 5 dazu, z. B.:
% ./add 7 12
Wir werden das Binary so umändern, dass das Programm nicht mehr addiert, sondern subtrahiert.
Den Quellcode könnte ich mir sparen, hier ist er für die faulen Leute trotzdem:
#include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { int number; if (argc != 2) return 1; number = atoi(argv[1]) + 5; printf("%d\n", number); return 0; }
Wir kompilieren das Programm nun mit dem Compiler-Switch -ggdb, damit Debugging-Symbole aktiviert sind, und starten danach den Debugger gdb. Mit den Debugging-Symbolen weiss gdb dann beispielsweise, welcher C-Code gerade ausgeführt wird.
% gcc -o add add.c -ggdb -Wall % gdb add [...] (gdb)
Wir setzen einen Breakpoint an den Anfang der main()-Funktion und starten das Programm mit z. B. der Zahl 7:
(gdb) break main Breakpoint 1 at 0x80483d4: file add.c, line 8. (gdb) run 7 Starting program: /home/tom/binaries/add 7 Breakpoint 1, main (argc=2, argv=0xbfffea24) at add.c:8 8 if (argc != 2) return 1;
Mit next gehen wir einen Befehl (bzw. eine Codezeile) weiter. gdb zeigt dabei immer schön die ganze Codezeile an:
(gdb) next 10 number = atoi(argv[1]) + 5;
Dies ist die zu verändernde Zeile. Weiter geht's:
(gdb) print $eip $1 = (void *) 0x80483e3 (gdb) next 12 printf("%d\n", number); (gdb) print $eip $2 = (void *) 0x80483f9
Mit print $eip lassen wir uns hier das Register eip anzeigen. eip ist der Instruction-Pointer und zeigt auf die Speicherstelle der nächsten auszuführenden Anweisung. Gehen wir nun nochmals eine Zeile weiter mit next und zeigen uns den eip erneut an, haben wir zwei Adressen. Zwischen diesen Speicheradressen befindet sich der Maschinencode von number = atoi(argv[1]) + 5;.
Jetzt disassemblieren wir:
(gdb) disas 0x80483e3 0x80483f9 Dump of assembler code from 0x80483e3 to 0x80483f9: 0x080483e3 <main+31>: mov 0xc(%ebp),%eax 0x080483e6 <main+34>: add $0x4,%eax 0x080483e9 <main+37>: mov (%eax),%eax 0x080483eb <main+39>: mov %eax,(%esp) 0x080483ee <main+42>: call 0x80482e4 <_init+72> 0x080483f3 <main+47>: add $0x5,%eax 0x080483f6 <main+50>: mov %eax,0xfffffffc(%ebp) End of assembler dump. (gdb)
Das ganze ist deshalb so lang, weil zuerst noch die atoi()-Funktion aufgerufen wird (mit call). Die Anweisung, die uns interessiert ist die danach folgende add-Anweisung, die den Wert des Registers eax um fünf erhöht.
Jetzt nehmen wir die add-Anweisung genauer unter die Lupe. Wir wollen sehen, wie sie binär aussieht. Das geschieht mit dem x-Befehl von gdb. Die Syntax ist dabei: x/Format Adresse. Bei Format geben wir dabei die Länge und das Format des Speicherdumps an. Wir wollen drei Hex-Bytes, da die Anweisung 3 Bytes lang ist (die add-Anweisung fängt ja bei main+47 an, die nächste bei main+50, Differenz ist drei). das Format lautet hier also 3xb (genaueres zum Format siehe unter help x):
(gdb) x/3xb 0x080483f3 0x80483f3 <main+47>: 0x83 0xc0 0x05
Wir müssen jetzt bei diesen 3 Bytes den Befehl add durch den Befehl sub ersetzen, damit die Zahl subtrahiert wird. Dazu müssen wir herausfinden, welches von diesen 3 Bytes das add ist, und welche Hexzahl denn sub entspricht. Am einfachsten disassemblieren wir diese drei Bytes mit ndisasm, ersetzen das add durch sub und assemblieren es wieder mit nasm. Das Disassemblieren erfolgt deshalb, weil nasm eine andere Syntax hat als gdb.
% echo -ne '\x83\xc0\x05' > bin % ndisasm bin 00000000 83C005 add ax,byte +0x5 % echo 'sub ax,byte +0x5' > asm % nasm -o bin asm % ndisasm bin 00000000 83E805 sub ax,byte +0x5
Aha! Das mittlere Byte wurde von 0xc0 durch 0xe8 ersetzt.
Wir müssen jetzt wissen, wo wir das Byte in dem Binary ersetzen wollen. 0x80483f3 war ja nur die Speicheradresse der Anweisung und nicht der Offset in dem Binary.
Dazu untersuchen wir zuerst den Header des Binarys:
% readelf -h add | grep Entry Entry point address: 0x8048300
Der Entrypoint ist die Speicheradresse, an die die erste auszuführende Anweisung geladen wird. Subtrahieren wir jetzt die Adresse von der add-Anweisung (0x80483f3) vom Entrypoint (0x8048300) erhalten wir den Offset der Anweisung vom Entrypoint aus:
% bc obase=16 ibase=16 80483F3-8048300 F3
Hinweis: Beim Rechnen mit bc muss man die Hex-Buchstaben immer gross schreiben, sonst erhält man einen parse error.
Jetzt suchen wir den Datei-Offset zum .text-Segment, was ebenfalls mit readelf geht. Im .text-Segment sind die Anweisungen des Programms nacheinander gespeichert:
% readelf -S add | egrep '(text|Off)' [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [12] .text PROGBITS 08048300 000300 000210 00 AX 0 0 16
Der Offset beträgt also 0x300 (Spalte Off). Addieren wir den Offset der Anweisung (0xf3) dazu, erhalten wir 0x3f3. An dieser Stelle in der Datei befindet sich die zu verändernde Anweisung. Sollten wir noch im gdb drinn sein, gehen wir jetzt dort raus, weil wir die Datei dann nicht bearbeiten können. Ich verändere Binäre Dateien am liebsten mit hexedit:
% hexedit add
Mit Ctrl+G können wir den Offset 3F3 angeben und wir landen bei der richtigen Adresse:
New position ? 0x3f3 000003F0 FE FF FF 83 C0 05 89 45 FC 8B 45 FC 89 44 24 04 .......E..E..D$. --- add --0x3F3/0x4D39--------------------------------------------------
Schön sehen wir den Bytecode 83 C0 05. Wir schieben den Cursor noch um ein Byte nach rechts und schreiben E8 drüber:
000003F0 FE FF FF 83 E8 05 89 45 FC 8B 45 FC 89 44 24 04 .......E..E..D$. -** add --0x3F5/0x4D39--------------------------------------------------
Mit Ctrl+W speichern wir und mit Ctrl+X verlassen wir hexedit.
Jetzt kommt der grosse Test, ob's geklappt hat:
% ./add 7 2
Diesmal kompilieren wir unser Programm ohne die Debugging-Symbole und mit höchster Optimierung (was das ganze noch mehr erschwert) und entfernen alle restlichen Symbole nachträglich noch mit strip:
% gcc -o add add.c -Wall -O3 % strip add
Wir versuchen diesmal wieder einen Breakpoint in gdb zu setzen:
% gdb add [...] (gdb) break main Function "main" not defined. Make breakpoint pending on future shared library load? (y or [n]) n (gdb)
Da alle Symbole gestrippt wurden, ist die Adresse der main()-Funktion nicht bekannt (und main() wird auch nicht nachgeladen, also beantworten wir die Nachfrage mit "n").
% readelf -h add | grep Entry Entry point address: 0x8048300
Die main()-Funktion beginnt leider nicht genau bei 0x8048300, weil die libc vorher noch etwas Code reinschiebt. Wir setzen einen Breakpoint auf die printf()-Funktion, damit wir eine ungefähre Ahnung haben, wo die main()-Funktion sich befindet. Wenn ich mit -O3 kompiliere, wird ein Breakpoint auf die atoi()-Funktion ignoriert, weil gcc anscheinend sein eigenes Builtin dann einfügt.
(gdb) break printf Function "printf" not defined. Make breakpoint pending on future shared library load? (y or [n]) y Breakpoint 1 (printf) pending. (gdb)
Die Warnung beim Setzen des Breakpoints kam deshalb, weil wir das Programm dynamisch gelinkt haben, und die printf()-Funktion somit erst bei der Ausführung nachgeladen wird. Würden wir die libc statisch dazulinken (und strippen), könnten wir dies nicht tun, weil dann überhaupt keine Symbole mehr vorhanden wären.
Jetzt führen wir das Programm aus und schauen uns mit dem Befehl bt die Stack-Frames an:
(gdb) run 7 Starting program: /home/tom/binaries/add 7 (no debugging symbols found) (no debugging symbols found) (no debugging symbols found) Breakpoint 2 at 0x400766a6 Pending breakpoint "printf" resolved Breakpoint 2, 0x400766a6 in printf () from /lib/tls/libc.so.6 (gdb) bt #0 0x400766a6 in printf () from /lib/tls/libc.so.6 #1 0x0804841e in ?? () #2 0x08048544 in _IO_stdin_used () #3 0x0000000c in ?? () #4 0x0000000a in ?? () #5 0x00000000 in ?? () #6 0x400164a0 in ?? () from /lib/ld-linux.so.2 #7 0x08048490 in ?? () #8 0xbfffec48 in ?? () #9 0x4003e904 in __libc_start_main () from /lib/tls/libc.so.6 #10 0x4003e904 in __libc_start_main () from /lib/tls/libc.so.6 #11 0x08048321 in ?? () (gdb)
Die printf()-Funktion wurde also durch eine unbekannte Funktion (muss main() sein) ausgeführt und zwar von der Adresse 0x0804841e.
Wir disassemblieren jetzt von der Adresse 0x8048300 (Entrypoint) bis zu 0x0804841e (Befehl unmittelbar nach dem Aufruf von printf()) und schauen uns dann vor allem den hinteren Teil an:
(gdb) disas 0x08048300 0x0804841e Dump of assembler code from 0x8048300 to 0x804841e: 0x08048300: xor %ebp,%ebp 0x08048302: pop %esi 0x08048303: mov %esp,%ecx 0x08048305: and $0xfffffff0,%esp 0x08048308: push %eax 0x08048309: push %esp 0x0804830a: push %edx 0x0804830b: push $0x8048490 0x08048310: push $0x8048430 0x08048315: push %ecx 0x08048316: push %esi 0x08048317: push $0x80483d0 0x0804831c: call 0x80482e0 0x08048321: hlt 0x08048322: nop 0x08048323: nop 0x08048324: push %ebp 0x08048325: mov %esp,%ebp 0x08048327: push %ebx 0x08048328: call 0x804832d 0x0804832d: pop %ebx 0x0804832e: add $0x1307,%ebx 0x08048334: push %eax 0x08048335: mov 0x18(%ebx),%eax 0x0804833b: test %eax,%eax 0x0804833d: je 0x8048341 0x0804833f: call *%eax 0x08048341: mov 0xfffffffc(%ebp),%ebx 0x08048344: leave 0x08048345: ret 0x08048346: nop 0x08048347: nop 0x08048348: nop 0x08048349: nop 0x0804834a: nop 0x0804834b: nop 0x0804834c: nop 0x0804834d: nop 0x0804834e: nop 0x0804834f: nop 0x08048350: push %ebp 0x08048351: mov %esp,%ebp 0x08048353: sub $0x8,%esp 0x08048356: cmpb $0x0,0x8049650 0x0804835d: jne 0x804838c 0x0804835f: mov 0x8049550,%eax 0x08048364: mov (%eax),%edx 0x08048366: test %edx,%edx 0x08048368: je 0x8048385 0x0804836a: lea 0x0(%esi),%esi 0x08048370: add $0x4,%eax 0x08048373: mov %eax,0x8049550 0x08048378: call *%edx 0x0804837a: mov 0x8049550,%eax 0x0804837f: mov (%eax),%edx 0x08048381: test %edx,%edx 0x08048383: jne 0x8048370 0x08048385: movb $0x1,0x8049650 0x0804838c: leave 0x0804838d: ret 0x0804838e: mov %esi,%esi 0x08048390: push %ebp 0x08048391: mov %esp,%ebp 0x08048393: sub $0x8,%esp 0x08048396: mov 0x8049630,%eax 0x0804839b: test %eax,%eax 0x0804839d: je 0x80483c0 0x0804839f: mov $0x0,%eax 0x080483a4: test %eax,%eax 0x080483a6: je 0x80483c0 0x080483a8: movl $0x8049630,(%esp) 0x080483af: call 0x0 0x080483b4: lea 0x0(%esi),%esi 0x080483ba: lea 0x0(%edi),%edi 0x080483c0: mov %ebp,%esp 0x080483c2: pop %ebp 0x080483c3: ret 0x080483c4: nop 0x080483c5: nop 0x080483c6: nop 0x080483c7: nop 0x080483c8: nop 0x080483c9: nop 0x080483ca: nop 0x080483cb: nop 0x080483cc: nop 0x080483cd: nop 0x080483ce: nop 0x080483cf: nop 0x080483d0: push %ebp Beginn von main(), Stack wird eingerichtet 0x080483d1: mov %esp,%ebp 0x080483d3: sub $0x18,%esp 0x080483d6: and $0xfffffff0,%esp 0x080483d9: cmpl $0x2,0x8(%ebp) Wenn argc 2 ist ... 0x080483dd: je 0x80483e8 ... springe zu 0x080483e8, ---. 0x080483df: mov $0x1,%eax 1 nach eax schieben | 0x080483e4: mov %ebp,%esp | 0x080483e6: pop %ebp | 0x080483e7: ret return mit Wert von eax | 0x080483e8: mov 0xc(%ebp),%edx <---' 0x080483eb: xor %ecx,%ecx 0x080483ed: mov 0x4(%edx),%eax 0x080483f0: mov %ecx,0xc(%esp) 0x080483f4: mov $0xa,%edx 0x080483f9: xor %ecx,%ecx 0x080483fb: mov %edx,0x8(%esp) 0x080483ff: mov %ecx,0x4(%esp) 0x08048403: mov %eax,(%esp) 0x08048406: call 0x80482d0 atoi() ausführen 0x0804840b: movl $0x8048544,(%esp) 0x08048412: add $0x5,%eax 5 addieren 0x08048415: mov %eax,0x4(%esp) 0x08048419: call 0x80482f0A printf() ausführen End of assembler dump. (gdb)
Die main()-Funktion fängt anscheinend nach dem letzten nop (= no operation) an. Schauen wir genau hin, sehen wir auch die gesuchte add-Anweisung unter der Adresse 0x08048412. Ab jetzt gehts weiter wie im vorherigen Abschnitt.
Zurück zu www.eggdrop.ch | Back to www.eggdrop.ch
Letzte Änderung: 18.01.05
Dieses Werk gehört zu http://www.eggdrop.ch/ und ist unter einer Creative Commons Lizenz lizensiert.