Aus Linux-Magazin 10/2005

Workshop: Sicheres Programmieren für Administratoren - Folge 4 (Seite 4)

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:

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
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben