Sie wirken gesund und schmackhaft wie Schneewittchens Apfel, aber unter ihrer Schale verbergen manche Programmeingaben einen hochgiftigen Kern. Speziell bei Cross-Site-Skripting, Formatstrings und Buffer-Grenzen müssen programmierende Admins der Versuchung widerstehen.
Die vorige Folge [1] behandelte bereits das Problem der Programmeingaben, die vorliegende konzentriert sich auf Spezialformen, denen Administratoren besonders häufig begegnen. Der Tenor von Folge 3 behält Gültigkeit: Der Entwickler prüfe alle Eingaben und ihre Zusammenhänge sorgfältig – sie gelten bis zum Beweis des Gegenteils als schuldig. Je komplexer die Eingaben, desto wichtiger ist diese Vorsichtsmaßnahme.
Ebenso tritt Whitelisting (nur bekannt gutartige Eingaben akzeptieren) gegenüber dem Blacklisting (nur als bösartig bekannte Eingaben zurückweisen) in den Vordergrund. Je komplizierter das Format der Eingabe, desto aufwändiger und fehleranfälliger ist das Erstellen einer umfassenden schwarzen Liste.
Bei schwierigen Problemen ist die Versuchung groß, die Eingabekontrolle auf nachgeschaltete Programme abzuwälzen (“Das werden die Jungs in der Entwicklung schon richtig machen!”). Dummerweise denken oft alle Beteiligten so und die Lücke bleibt offen. Jeder Admin und jeder Entwickler sollte sich gelegentlich ins Bewusstsein rufen, dass die eigenen Ausgaben immer Eingaben für andere Programme sind, die im Zweifelsfall genauso fehlerhaft wie die eigenen Werke arbeiten. Lieber eine Fehlerquelle mehrfach ausschließen, als sie zu übersehen (Defense in Depth, [1]).
Falle 1: Cross-Site-Skripting
Webseiten entwickeln zählt nicht unbedingt zu den klassischen Aufgaben von Administratoren. Gerade in kleinen und mittelgroßen Firmen werkeln sie dennoch oft an Intranetanwendungen oder am öffentlichen Netzauftritt und unterschätzen die Gefahren in diesem Umfeld. Dazu gehört die spezielle Form der Eingabe: Texte im HTML-Format (oder anderen Websprachen). Hinter der harmlosen Fassade verbirgt sich manchmal bösartiger Javascript-Code.
Das Besondere daran ist, dass das Ziel eines Angreifers nicht der Webserver ist, der das Dokument speichert, sondern der Browser des Besuchers, der es abruft. Beim Cross-Site-Skripting laufen die Skripte des Angreifers auf dem Rechner seines Opfers, während dessen Browser eine andere Präsenz besucht. Das Skript läuft im Kontext und mit den Rechten der besuchten Seite (Abbildung 1).
Ungastliches Gästebuch
In einem einfachen Gästebuch dürfen die Besucher selbst kurze Beiträge schreiben, die auf der Webseite des Gästebuchs erscheinen. Erlaubt der Betreiber beliebige Eingaben, kann ein Witzbold in seinen Beiträgen ein Skript verstecken, das ein fremdes Fenster im Browser des Besuchers öffnet:
Tolle Seite, weiter so!
<!-- <script>
window.open("http://debian.org/");
</script> -->
Das klingt noch harmlos, aber je nach Webseite stellt der Angreifer noch ganz andere Dinge damit an:
- Er verfälscht oder sabotiert den restlichen Inhalt einer
Seite, - stiehlt Cookies mit Session-IDs und gibt sich für den
Besitzer aus oder - bringt mit gefälschten Webseiten fremde Passwörter in
seinen Besitz.
Besonders riskant ist dies auf Seiten, auf denen es um Geld geht, etwa bei Banken oder Online-Casinos. Das Secure Programming Cookbook [3] widmet der Problematik ein ganzes Kapitel mit zahlreichen Beispielen.
Mangelnde Kontrolle von Benutzereingaben macht Cross-Site-Skripting möglich. Webentwickler übersehen das Problem oft, weil ihr Server gar nicht angegriffen wird. Er ist nur das Medium, das die Attacke an arglose Besucher weiterleitet, ohne selbst Schaden zu nehmen. Ähnlich verhält es sich beim Cross-Site-Authentication-Angriff, den ein Beitrag in der “Know-how”-Rubrik behandelt.

Abbildung 1: Der Angreifer schleust ein Skript in einen Forumsbeitrag ein (1), den der Server speichert (2). Beim Abrufen des Forums (3) setzt der Server den Rumpf der Webseite (4a) und die Einträge der Datenbank (4b) zusammen und liefert die Seite an den Besucher (5). Dessen Browser führt das Skript aus.
Gegenmaßnahmen
Cross-Site-Skripting ist zwar weit verbreitet, aber vergleichsweise leicht zu vereiteln. Eine sichere Anwendung entfernt aus den Eingaben allen Skriptcode oder weist zweifelhafte Texte gleich ganz zurück. Bei HTML finden sich Skriptanweisungen immer in Tags, also zwischen spitzen Klammern »<…>«. Die einfache Holzhammerlösung wäre, vor dem Weiterverarbeiten sämtliche Metazeichen in ihre ungefährliche HTML-Kodierung zu übersetzen (siehe Tabelle 1).
Aber wer den Anwendern etwas Markup erlauben will, kommt so nicht ans Ziel. Javascript-Code kann sich fast überall verbergen, etwa in den Tags »<img …>«, »<div …>« oder »<body …>«. In [2] finden sich zahlreiche weitere Beispiele. Deshalb kommt Blacklisting nicht in Betracht, aber Whitelisting verspricht Erfolg [1]: nur wenige bekanntermaßen gutartige HTML-Tags zulassen und den Rest als gefährlich zurückweisen, einschließlich aller Tags mit Attributen. Ungefährlich sind die Tags in Tabelle 2. Wem diese nicht ausreichen, der sollte das Problem an die Entwicklungsabteilung weitergeben oder die Literatur bemühen ([2], [3]).
|
Tabelle 1: |
|
|---|---|
|
Zeichen |
HTML-Kodierung |
|
< |
< |
|
> |
> |
|
& |
& |
|
” |
" |
Lösung für C und C++: Die Bibliothek Gateguardian [9] gibt dem Entwickler Mittel an die Hand, um diese Untiefen zu umschiffen. Die Funktion »inputg_escape_html()« codiert die HTML-Metazeichen »<>”&« mittels HTML-Escaping als »<«, »>«, »"« und »&«, lässt aber einige bekannt harmlose Tags wie »<h1>« oder »<br>« unverändert passieren. Auch skriptfreie Links wie »<a href=”http://Foo”>« bleiben unangetastet. Der Aufruf sieht so aus:
#include <inputguardian.c> [...] char *escaped; escaped = inputg_escape_html(eingabe);
Die Funktion gibt einen Zeiger auf das modifizierte HTML-Dokument zurück, der auf einen mit »malloc()« reservierten Speicher zeigt, den der Programmierer später mit »free(escaped)« freigeben muss. Einen Nullzeiger retourniert die Funktion nur, wenn sie keinen Speicher reservieren konnte.
|
Tabelle 2: |
|---|
|
abbr |
|
acronym |
|
b |
|
bdo |
|
big |
|
blink |
|
blockquote |
|
br |
|
center |
|
cite |
|
code |
|
dd |
|
del |
|
dfn |
|
dir |
|
dl |
|
dt |
|
em |
|
h1 |
|
h2 |
|
h3 |
|
h4 |
|
h5 |
|
h6 |
|
hr |
|
i |
|
ins |
|
kbd |
|
li |
|
menu |
|
nobr |
|
ol |
|
p |
|
plaintext |
|
pre |
|
q |
|
s |
|
samp |
|
small |
|
spacer |
|
strike |
|
strong |
|
sub |
|
sup |
|
tt |
|
u |
|
ul |
|
var |
Maskenball
Die Variante »inputg_escape_all_html()« verwandelt alle HTML-Metazeichen in ihre harmlose Web-Kodierung. Bei »inputg_escape_html_with_tag_table()« darf der Anwender die Liste der erlaubten Tags selbst bestimmen.
Achtung: Die Funktionen in Gateguardian stammen von »spc_escape_html()« aus dem Secure Programming Cookbook [4] ab. Im Original haben sich leider viele Bugs eingeschlichen. Als Ersatz für den fehlerhaften Code liefert der Autor ein Archiv [5] mit korrigierten Versionen. Seit Folge 3 sind neue Bugfixes hinzugekommen; wer dieses Archiv verwendet, sollte es neu herunterladen.
Lösung für die Shell: Am einfachsten ist ein kleines Wrapperprogramm, das die Funktion aus Gateguardian verwendet. Das Programm »escape-html« (Listing 1) ruft der Admin dann einfach mit »ESCAPED=`escape-html “$INPUT”`« aus seinen Shellskripten heraus auf.
Falle 2: E-Mail-Adressen
Administrative Skripte oder Programme erwarten oft Eingaben in Form von E-Mail-Adressen. Meist bestimmt der Admin die Adresse selbst, etwa wenn ihn ein nächtliches Skript detailliert über die gelaufenen Aktionen informieren soll. In anderen Fällen geben Benutzer Mailadressen vor, so etwa in einem Bugreport-Formular. Wer nicht gerade einen Mailclient oder gar Mailserver entwickelt, wertet die eingegebenen Adressen vermutlich nicht selbst aus. Trotzdem gilt “Cautela abundans non nocet” (zu viel Vorsicht schadet nicht).
|
Listing 1: |
|---|
01 #include "inputguardian.c"
02 #include <stdio.h>
03
04 int main(int argc, char **argv)
05 {
06 char *ret = inputg_escape_html(argv[1]);
07 if (ret != 0)
08 printf("%s", ret);
09 return !ret;
10 }
|

Abbildung 2: Die grobe Struktur des toten Briefkastens. Inetd lauscht auf Port 1111 und übergibt nach einer Anfrage die Kontrolle an »toter-briefkasten«. Dieses Programm schreibt die Miteilungen in die »/var/lib/tb« und liest sie auch daraus.
RFC 2822 [6] und seine Vorgänger legen fest, wie eine E-Mail-Adresse aussieht. Der neue Standard schreibt vor, dass konforme Programme bestimmte alte Formate weiterhin unterstützen. Im selbst gestrickten Formular darf der Admin guten Gewissens auf Extrawürste wie Routen in der Mailadresse verzichten (»<Route:Adresse>«). Laut RFC dürfen die Adressen sogar verschachtelte Kommentare enthalten.
Lösung für die Shell: Meist genügt ein grobschlächtiger Ansatz. Es kommt nur durch, was der Form »Name@Domain« genügt. Den erlaubten Zeichensatz für Name und Domäne schränkt der Admin nach Belieben ein und setzt seine Wünsche mit »egrep« durch:
echo "$ADDR" | egrep '^[a-zA-Z0-9_+-.]+U @[a-zA-Z0-9-]+(.[a-zA-Z0-9-]+)*$' || U exit 1
Wem dieser einfache Ansatz zu ungenau ist, findet in Jeffrey Friedls Regular-Expression-Buch [7] einen regulären Ausdruck, der beinahe das komplette RFC abdeckt. Nur verschachtelte Kommentare kennt er nicht. Auf [8] ist ein Perl-Programm zu finden, das die Regex verwendet, um E-Mail-Adressen zu verifizieren. Es lässt sich recht bequem verwenden: »perl email-opt.pl “$ADDR” >/dev/null || exit 1«
Lösung für C und C++: Die Gateguardian-Funktion »inputg_is_simple_email_address()« implementiert den Egrep-Holzhammertest in C:
#include <inputguardian.c> [...] int rc; rc = inputg_is_simple_email_address(addr) if (rc == 0) exit(1);
Das genauere Perl-Programm in C oder C++ nachzubilden ist nicht empfehlenswert, da es bei komplexen Parsing-Aufgaben leicht zu Fehlern und unbeabsichtigten Nebeneffekten kommt.
Toter Briefkasten
Eine andere Art von Eingabekontrollfehlern begeht das Programm »toter-briefkasten« in Listing 2, das Gelegenheitsprogrammierer Gerd als Netzwerkdienst konzipiert hat. Der Internet-Superserver »inetd« startet das Programm, dessen Standard-Ein- und -Ausgabe er mit dem anfragenden Client aus dem Netzwerk verbindet (Abbildung 2).
Agentin Annette hinterlegt eine geheime Botschaft mit einem Zugangscode im toten Briefkasten. Um eine Verbindung herzustellen, verwendet sie »telnet Server 1111« und gibt dann die Botschaft in der Form Code:Botschaft ein:
geheim:Vera ist eine Verräterin! NächsterU Code: x
Der Dienst akzeptiert einzeilige Botschaften mit höchstens 1000 Zeichen (Zeile 18 in Listing 2) und hängt sie an die Datei »/var/lib/tb« an (Zeilen 29 und 30). Danach beendet er sich und schließt damit die Verbindung (Zeile 44). Kontaktmann Klaus fragt später die Botschaften ab, indem er nur den Geheimcode eingibt:
geheim geheim:Vera ist eine Verräterin! NächsterU Code: x
Der Dienst durchforstet die Datei »/var/lib/tb« nach allen Zeilen, die mit dem richtigen Geheimcode beginnen, und gibt sie aus (Zeilen 35 bis 41). Natürlich liefe eine geheime Kommunikation im echten Agentenleben über einen verschlüsselten Kanal und nicht im Klartext ab. Im konkreten Fall spielt das aber wegen der verborgenen Fehler im Programm keine Rolle – und da stehen Formatstrings an erster Stelle.
|
Listing 2: Toter Briefkasten |
|---|
01 #include <stdio.h>
02 #include <stdlib.h>
03 #include <string.h>
04
05 FILE *f;
06
07 void oeffne_briefkasten(char *mode)
08 {
09 f = fopen("/var/lib/tb", mode);
10 if (f == NULL)
11 exit(1);
12 }
13
14 int main(void)
15 {
16 int len;
17 char eingabe[1000];
18 char zeile[1000];
19
20 if (!gets(eingabe))
21 return 1;
22 len = strlen(eingabe);
23 if (eingabe[len - 1] == 'r') {
24 eingabe[len - 1] = 0;
25 len = len - 1;
26 }
27 if (strchr(eingabe, ':')) {
28 /* "key:botschaft" -> speichern */
29 oeffne_briefkasten("a");
30 fprintf(f, "%sn", eingabe);
31 }
32 else {
33 /* "key" -> botschaften lesen */
34 oeffne_briefkasten("r");
35 while (fgets(zeile, 1000, f)) {
36 if (strncmp(zeile, eingabe, len)
37 == 0 && zeile[len] == ':') {
38 /* key stimmt -> anzeigen */
39 printf(zeile);
40 }
41 }
42 }
43
44 return 0;
45 }
|
Falle 3: Formatstrings
Gerd ist bei seinem toten Briefkasten der gleiche Fehler unterlaufen wie im Beispielprogramm Patch-fstring aus Folge 3 [1]. Relevant war dort die Programmzeile »fprintf(f, string);«. Die Lösung war damals nicht möglich, sie entspricht aber Gerds Problem. In seinem totem Briefkasten steckt der Bug in Zeile 39: Hier lautet der Code »printf(zeile);«. Auf den ersten Blick scheint Letzteres äquivalent zu »printf(“%s”, zeile);« zu sein. Beide Male gibt Printf den Inhalt der Variablen »zeile« aus. Allerdings interpretiert das Printf-Kommando den Formatstring (erstes Argument), aber der stammt vom Anwender, etwa dem Feind Fred. Gerd hat es versäumt, alle Eingaben auf ihre Sicherheit zu prüfen.
Hat Fred zum Beispiel den Code »x« in Erfahrung gebracht, verhindert er durch einen forcierten Programmabsturz, dass Annette unter diesem Code Daten für ihren Kontaktmann hinterlegen kann. Dazu schmuggelt er per Telnet folgende Zeile in das System ein:
x:%s%s%s%s%s%s
Später – Annette hat mittlerweile eine neue Botschaft deponiert – schaut Klaus im toten Briefkasten nach dem Eintrag zum Geheimcode »x«. Das Programm durchsucht »/var/lib/tb« nach Zeilen, die mit »x:« beginnen, stößt auf die von Fred eingeschmuggelte Gemeinheit und führt sinngemäß den folgenden Befehl aus: »printf(“x:%s%s%s%s%s%s”);«

Abbildung 5a: Die Textoberfläche Scbuilder setzt auf Libshellcode. Mit ihr ist es leicht, Shellcode für fast jeden Zweck zu erzeugen: Der Anwender wählt die Architektur, das Betriebssystem, den gewünschten Aufruf und einige Optionen.

Abbildung 5b: Den resultierenden Shellcode speichert Scbuilder auf Wunsch auch gleich als C-Programmcode. Kein Admin kann einen Overflow mehr mit der Ausrede akzeptieren, dass es zu schwer sei, die Lücke auszunutzen.
Chaos auf dem Stapel
Die Printf-Funktion versucht nun die Argumente der sechs »%s«-Anweisungen vom Stack zu lesen. Das Programm hat aber keine Werte übergeben, sodass dort mit hoher Wahrscheinlichkeit wenigstens eine ungültige Adresse liegt. Printf versucht von dieser Adresse einen String zu lesen und stürzt prompt mit einer Segmentation Violation ab. Das Programm kommt nicht mehr dazu, die Botschaften von Annette auszugeben – der Angriff war erfolgreich.
Neben der allgegenwärtigen Printf ist noch eine Reihe weiterer Funktionen aus den Systembibliotheken anfällig für Formatstring-Fehler. Die folgende Liste fasst gleichartige und ähnlich benannte Funktionen zusammen; das Sternchen dient als Wildcard:
- *printf()
- *scanf()
- v*printf()
- v*scanf()
- syslog()
Dazu kommen noch die BSD-spezifischen Funktionen:
- setproctitle()
- err*()
- verr*()
- warn*()
- vwarn*()
Generell erlauben es Formatstring-Fehler einem Angreifer, den Stack des Zielprogramms an nicht dafür vorgesehenen Stellen zu lesen, zu schreiben oder sogar seinen Inhalt auszuführen. Das nutzt er zum Beispiel für folgende Angriffe:
- Denial-of-Service (Absturz)
- Spionage (interne Programmdaten vom Stack lesen)
- Eine eigene Shell mit den Rechten des Opfers auf dem fremden
System starten [10]
Der Kasten “Spionage mit Formatstrings” führt vor, wie Fred einen solchen Angriff gegen den toten Briefkasten ausführt und damit fremde Botschaften liest, ohne deren Code zu kennen. Stattdessen könnte Fred auch gleich den ganzen Server unter seine Kontrolle bringen, indem er Code in den Puffer legt, der eine Shell startet. Dann verbiegt er gezielt die Rücksprungadresse von Printf dorthin. Wie das geht und weitere Erörterungen zum Thema beschreibt ein älterer Artikel [10].
Lösung für C und C++: Formatstring-Fehler sind zwar sehr gefährlich, sie lassen sich aber leicht vermeiden. Der Programmierer darf niemals Benutzereingaben in das Format-Argument stecken. Statt »printf(eingabe);« schreibt er konsequent das längere, aber sichere »printf(“%s”, eingabe);« (Listing 3, Zeile 45). In dieser verbesserten Version sind noch weitere Bugs behoben.
|
Listing 3: Toter Briefkasten |
|---|
01 #include <stdio.h>
02 #include <stdlib.h>
03 #include <string.h>
04
05 FILE *f;
06
07 void oeffne_briefkasten(char *mode)
08 {
09 f = fopen("/var/lib/tb", mode);
10 if (f == NULL)
11 exit(1);
12 }
13
14 int main(void)
15 {
16 int len;
17 char eingabe[1000];
18 char zeile[1000];
19
20 if (!fgets(eingabe, 1000, f))
21 return 1;
22 len = strlen(eingabe);
23 if (len > 0 && eingabe[len - 1] == 'r') {
24 eingabe[len - 1] = 0;
25 len = len - 1;
26 }
27 if (len > 0 && eingabe[len - 1] == 'n') {
28 eingabe[len - 1] = 0;
29 len = len - 1;
30 }
31 if (len == 0)
32 return 0;
33 if (strchr(eingabe, ':')) {
34 /* "key:botschaft" -> speichern */
35 oeffne_briefkasten("a");
36 fprintf(f, "%sn", eingabe);
37 }
38 else {
39 /* "key" -> botschaften lesen */
40 oeffne_briefkasten("r");
41 while (fgets(zeile, 1000, f)) {
42 if (strncmp(zeile, eingabe, len)
43 == 0 && zeile[len] == ':') {
44 /* key stimmt -> anzeigen */
45 printf("%s", zeile);
46 }
47 }
48 }
49
50 return 0;
51 }
|
Hilfe vom Compiler
Ein guter Tipp ist auch, beim GNU-C-Compiler immer mit der Option »-Wall« zu übersetzen – der Schalter aktiviert alle wichtigen Warnungen. Der Compiler prüft dann unter anderem, ob die Argumente eines Printf oder Scanf auch zum Format passen. Dann fällt bereits beim Übersetzen auf, wenn beispielsweise Argumente vertauscht sind:
int zahl;
char buf[99];
scanf("%d %20s", buf, &zahl);
Diesen fehlerhaften Code quittiert GCC (aufgerufen per »gcc -Wall -o foo foo.c«) mit folgenden Warnungen:
foo.c: In function `main': foo.c:8: warning: int format, differentU type arg (arg 2) foo.c:8: warning: char format, differentU type arg (arg 3)
Falle 4: Buffer-Overflows
Buffer Overflows sind eng verwandt mit Formatstring-Fehlern und länger bekannt als sie. Hier wie dort ist der Stack ein beliebtes, wenn auch nicht das einzige Ziel böswilliger Angreifer. Ein Buffer-Overflow tritt dann auf, wenn ein Programm Daten in einem Speicherbereich (Buffer) ablegt, der zu klein ist, um alles aufzunehmen. Dann überschreibt es Speicherzellen, denen eine andere Rolle zugewiesen ist.
Beispielsweise führt ein Überlauf der »eingabe«-Variablen in Listing 2 (Zeile 20) dazu, dass nacheinander »len«, der Framepointer und die Rücksprungadresse der überlangen Eingabe zum Opfer fallen (siehe Abbildung 3). Beim zufälligen Überlauf stürzt das Programm in der Regel beim Verlassen der Funktion ab, weil die Rücksprungadresse ins digitale Nirwana zeigt.
Viel unangenehmer wird es, wenn Fred im Buffer ausführbaren Code einschmuggelt. Meist startet der Code einfach eine Shell: »execve(“/bin/sh”, 0, 0);« und Fred übernimmt das angegriffene Benutzerkonto. Den so genannten Shellcode schreiben ist zwar nicht ganz einfach, aber neu erfinden muss ihn kein Möchtegern-Cracker. Dem ungeübten Angreifer kommen Bibliotheken wie Libshellcode [11] zur Hilfe. Mit dabei ist ein kleines Ncurses-GUI namens Scbuilder (Abbildungen 5a und 5b).
Knick in der Logik
Subtiler und kaum zu verhindern sind Versuche, die Programmlogik zu überlisten, indem Fred nur andere lokale Variablen überschreibt, die bei höheren Speicheradressen liegen. Im Beispiel wären das die Variable »len« oder Variablen der aufrufenden Funktion. In der harten Wirklichkeit haben sich findige Hacker auf diese Weise schon fremde Rechte angeeignet, indem sie etwa eine Variable überschrieben haben, die die UID enthielt, auf die das Programm später wechseln sollte. Mehr Details zu Buffer-Overflows geben [12] und [13].
|
Tabelle 3: Ein- und |
|
|---|---|
|
Falsch |
Richtig |
|
sprintf(buf, “%s”, str) |
sprintf(buf, “%99s”, str) odersnprintf(buf, 100, “%s”, |
|
scanf(“%s”, str) |
scanf(“%99s”, str) |
|
gets(buf) |
fgets(buf, 100, stdin) |
|
strcat(buf, str) |
strncat(buf, str, 99) |
|
strcpy(buf, str) |
strncpy(buf, str, 99); buf[99] = 0; |
Vorsicht mit fremden Daten
Mit im Spiel bei Speicherüberläufen ist immer Programmcode, der Daten liest, schreibt oder kopiert. In C stehen im Quelltext dann oft eine String-Funktion (»strcat()«, »strcpy()«…), eine Funktion zur formatierten Ein- und Ausgabe (»sprintf()«, »scanf()«…), eine Dateizugriffsfunktion (»fread()«, »gets()«…) oder wilde Zeigerarithmetik.
Ein Aufruf der »gets()«-Routine (Zeile einlesen; Listing 2, Zeile 20) ist fast immer falsch: Der Entwickler kann der Funktion nicht mitteilen, wie viel Speicher ihr zur Verfügung steht. Eine automatische Prüfung von Puffergrenzen sieht C nicht vor, folglich hat Gets den Buffer-Overflow schon eingebaut. Auch »scanf(input, “%s”, buffer)« ist nicht besser: Scanf speichert den gelesenen String im Buffer, egal wie groß der ist. Tabelle 3 sagt, wie es besser geht, und Zeile 20 in Listing 3 wendet dies an.
|
Listing 4: |
|---|
01 #include <limits.h>
02 #include <string.h>
03 #include <unistd.h>
04
05 int main(void)
06 {
07 char abs_path[PATH_MAX];
08
09 getcwd(abs_path, PATH_MAX);
10 strcat(abs_path, "/dateiname");
11 /* ... */
12
13 return 0;
14 }
|
Listing 4 zeigt einen weiteren recht häufigen Fehler, hier am Beispiel des Verarbeitens von Dateinamen. Der Aufruf von »getcwd()« in Zeile 9 speichert den Pfad des Arbeitsverzeichnisses in der Variablen »abs_path«. Bis hierher gibt es noch kein Problem, weil der Puffer groß genug ist. Allerdings prüft »strcat()« in Zeile 10 nicht mehr, ob der Puffer bereits voll ist, und schreibt möglicherweise über sein Ende hinaus.
Weitere Gefahr droht in diesem Beispiel eventuell, wenn der Pfad bereits länger ist als »PATH_MAX«. Diese Situation kann bei manchen Dateisystemen eintreten, wenn sie unbegrenzt lange Pfadnamen unterstützen. »getcwd()« kopiert dann zwar nur die angegebene Menge Zeichen, das ist aber auch ein Fehler, den das Programm erkennen (Rückgabewert »ERANGE«) müsste und angemessen darauf reagieren.
Platz da
Lösung für C und C++: Entweder der Programmierer sorgt für ausreichend großen, dynamisch reservierten Speicherplatz oder er beschränkt die Länge der Eingabe auf den vorhandenen Speicherplatz. Für einfache kleine Programme ist der zweite Ansatz oft besser, weil leichter umzusetzen. Tabelle 3 zeigt sichere Varianten einiger Funktionen der Standardbibliothek.
Lösung für C++: Der Entwickler verzichtet am besten auf die anfälligen Funktionen und verwendet möglichst die Stringklasse »std::string« und die Streamoperatoren »<<« und »>>«.
Keep it simple
Die Vielfalt und Komplexität der hier beschriebenen Probleme vermitteln einen Eindruck davon, wie schwer es sein kann, harmlose von vergifteten Eingaben zu trennen. Gerade deswegen drängt sich ein viel zitiertes Motto erfolgreicher Admins und Software-Entwickler auf: Keep it simple.
Um die Formate in allen Feinheiten zu verstehen, fehlt meist schon die Zeit. Gut beraten ist dann, wer sich auf eine grobe, leicht zu behandelnde Untermenge der erlaubten Eingaben beschränkt und alles andere abweist. Vielleicht landet so der vergiftete Apfel dort, wo er hingehört: im Sondermüll. (fjl)
|
Infos |
|---|
|
[1] Dominik Vogt, “Gesichtskontrolle – Sicheres Programmieren für Administratoren, Folge 3”: Linux-Magazin 07/05, S. 62 [2] David A. Wheeler, “Secure-Programs-HOWTO”: [http://www.dwheeler.com/secure-programs/] [3] Sverre H. Huseby, “Sicherheitsrisiko Web-Anwendung”: Dpunkt-Verlag, ISBN 3-89864-259-3 [4] Viega und Messier, “Secure Programming Cookbook”: O\’Reilly, ISBN 0-596-00394-3; [http://www.secureprogramming.com] [5] Sourcecode zum Secure Programming Cookbook: [http://www.dominikvogt.de/de/index.html#Links] [6] RFC 2822, “Internet Message Format”: [http://www.ietf.org/rfc/rfc2822.txt] [7] Jeffrey E. F. Friedl, “Reguläre Ausdrücke”: O\’Reilly, ISBN 3-89721-349-4; [http://www.oreilly.de/catalog/regex2ger/] [8] Beispielskripte zum Regex-Buch: [http://examples.oreilly.com/regex/] [9] Dominik Vogt, Gateguardian: [http://sourceforge.net/projects/gateguardian/] [10] Achim Leitner, “Kein Format – Sicherheitslücken durch den Format-String”: Linux-Magazin 08/01, S. 96 [11] Enrico Feresin, Libshellcode: [http://www.orkspace.net/software/libShellCode] [12] Achim Leitner, “Nicht ganz dicht – Sicherheitslücken in Programmen vermeiden”: Linux-Magazin 06/01, S. 30 [13] Aleph One, “Smashing The Stack For Fun And Profit”: Phrack Vol. 7, Issue 49, File 14; [http://www.phrack.org/show.php?p=49&a=14] |
|
Der Autor |
|---|
|
Dipl.-Math. Dominik Vogt ist langjähriger Software-Entwickler und Systemadministrator. Zurzeit arbeitet er als freiberuflicher EDV-Berater mit Schwerpunkt Softwaresicherheit. In seiner Freizeit werkelt er am Windowmanager Fvwm. |








