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:







