Aus Linux-Magazin 10/2005

Workshop: Sicheres Programmieren für Administratoren - Folge 4

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.

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:
HTML-Metazeichen

 

Zeichen

HTML-Kodierung

<

<

>

>

&

&

&quot;

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 »<«, »>«, »&quot;« 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:
Skriptfreie HTML-Tags

 

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:
»escape-html.c«

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.

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
– so bitte nicht

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”);«

Spionage mit
Formatstrings

Um den Formatstring-Angriff zu verstehen, ist ein Blick auf den Stack von »toter-briefkasten« nötig (Abbildung 4). Eine genaue Darstellung ist auch in [10] und [12] zu finden. Im Bild stehen höhere Speicheradressen weiter unten, während der Stack nach oben wächst. Der Stackframe einer aufgerufenen Funktion liegt über dem der rufenden, also bei niedrigeren Adressen. Wichtig: Der Compiler legt die Argumente einer Funktion in aufsteigender Reihenfolge auf dem Stack ab. Printf erwartet also das zweite Argument bei der Adresse des ersten Arguments plus 4 (ein Wort hat auf Intel-Systemen 4 Byte).

Schritt 1: Den ungefähren Aufbau des Programms kennt Fred, aber die genaue Anordnung der Daten auf dem Stack ist ihm unbekannt. Sie hängt unter anderem von den Compileroptionen ab. Fred ermittelt zuerst den Abstand zwischen dem Printf-Parameter »format« und der Variablen »zeile«. Dazu schickt er an Port 1111 des Servers die Zeichenkette »a:aaaa%p%p%p%p« – »%p« gibt im Printf-Formatstring einen Zeiger aus. Anschließend fragt er die Botschaften zum Code »a« ab (Abbildung 3, erster Block).

Schritt 2: Fred beginnt mit wenigen »%p« und erhöht sukzessive auf acht Stück (Abbildung 3, zweiter Block). In der Ausgabe des letzten Zeigers findet er den Wert 0x61613a62, was den Zeichen »b:aa« am Anfang seiner Botschaft entspricht (»b« hat den Ascii-Hexcode 0x62, »:« ist 0x3a und »a« entspricht 0x61).

Schritt 3: Nun ergänzt Fred vorne zwei Leerzeichen (damit »aaaa« auf einer Wortgrenze liegt) und hinten ein »%s« (Abbildung 3, dritter Block). Bei einer Abfrage zeigt die Adresse für den »%s«-Parameter auf die Zeichenkette »aaaa«, in Hexadezimalschreibweise 0x61616161. Die Adresse ist ungültig, der Server produziert einen Coredump.

Abbildung 3: Fred knackt den toten Briefkasten mit Hilfe von »nc« (Netcat): Erst hinterlegt er eine Botschaft, die ihm später die Adressen verrät. Schritt 2: Im achten »%p« beginnt der Puffer »zeile«. Schritt 3: Das fertige Gerüst für den Angriffsstring. Schritt 4: Freds erste Näherung für die Zieladresse fördert bereits geheime Daten zutage. Den fertige Angriff zeigt Schritt 5.

Abbildung 3: Fred knackt den toten Briefkasten mit Hilfe von »nc« (Netcat): Erst hinterlegt er eine Botschaft, die ihm später die Adressen verrät. Schritt 2: Im achten »%p« beginnt der Puffer »zeile«. Schritt 3: Das fertige Gerüst für den Angriffsstring. Schritt 4: Freds erste Näherung für die Zieladresse fördert bereits geheime Daten zutage. Den fertige Angriff zeigt Schritt 5.

Schritt 4: Ersetzt Fred »aaaa« durch eine gültige Programmadresse, kann er damit jeden beliebigen Speicherbereich bis zum nächsten Nullzeichen ausgeben. Ein lohnendes Angriffsziel ist der Puffer, in dem das Programm die Zeilen aus der Datei liest, also die Variable »zeile« selbst. Fred konnte nicht vermeiden, einen Teil des Puffers mit seinem Angriffscode zu überschreiben, aber dahinter bleiben die Daten längerer Botschaften intakt. Um möglichst wenige Zeichen zu überschreiben, hat Fred in Schritt 1 mit vier »%p« begonnen und sich langsam vorgetastet.

Die absolute Adresse berechnet Fred durch gezieltes Raten und genaues Hingucken. Die Adresse 0xbffff350, die gleich hinter »format« auf dem Stack liegt (Abbildung 3, zweiter Block), sieht für seine Zwecke bereits viel versprechend aus. Durch Ausprobieren stellt sich heraus, dass dies die Adresse der Variablen »eingabe« ist, die noch vom letzten Funktionsaufruf übriggeblieben ist (»strncmp()«). Damit ergibt sich in erster Näherung für die Adresse von »zeile«:

0xbffff350  Adresse von eingabe
-      0x3e8  Länge von zeile (1000)
--------------------------------
  0xbfffef68

Fred kennt damit die ungefähre Adresse von »zeile«. Dazu addiert er noch die Länge seines Eingabestrings (26 Zeichen) und erhält 0xbfffef82. Diese Adresse setzt er statt des »aaaa« ein und landet einen Volltreffer (Abbildung 3, vierter Block). Er hat nun ein Mittel, um regelmäßig zumindest das Ende längerer Botschaften abzufangen. In diesem Beispiel ist ihm damit sogar ein Code für weitere Abfragen in die Hände gefallen.

Abbildung 4: Der Programmstack beim Aufruf von »printf()«. Achtung: Der Stack wächst nach oben, die Speicheradressen nehmen aber nach unten zu.

Abbildung 4: Der Programmstack beim Aufruf von »printf()«. Achtung: Der Stack wächst nach oben, die Speicheradressen nehmen aber nach unten zu.

Schritt 5: Abschließend optimiert Fred die Adresse. Er zählt sie einfach herunter, so lange er immer mehr Zeichen der Botschaft angezeigt bekommt (Abbildung 3, fünfter Block). Das klappt, bis er bei 0xbfffef7c angelangt.

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 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.

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
– verbessert

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
Ausgabe ohne Buffer-Overflow

 

Falsch

Richtig

sprintf(buf, “%s”, str)

sprintf(buf, “%99s”, str) odersnprintf(buf, 100, “%s”,
str)

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:
»path_max.c«

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.

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben