ELF-Binaries editieren

Dieser Text befindet sich in der neusten Version auf http://www.eggdrop.ch/texts/binaries/.

18.01.05: Version 1.0

Inhalt

1. Einleitung

1.1. Um was gehts?

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.

1.2. Voraussetzungen

Die Voraussetzungen für diesen Text sind:

2. Ein einfaches Binary

2.1. Quellcode und Ziel dieses Kapitels

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;
}

2.2. Verändern des Programms mit Hilfe von Debugging-Symbolen

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

2.3. Verändern des Programms ohne die Debugging-Symbole

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

Creative Commons License

Dieses Werk gehört zu http://www.eggdrop.ch/ und ist unter einer Creative Commons Lizenz lizensiert.