Format-String-Fehler waren vor mehr als zehn Jahren weit verbreitet. Heute sind sie weitestgehend aus den Schwachstellen-Statistiken verschwunden. Dies liegt vor allem daran, dass Programmierer wesentliche mehr darauf achten, solche Fehler zu vermeiden. Ein weiterer Grund für die Besserung besteht in ausgefeilten Compilern, die versuchen, etwaige Format-String-Attacken trotz falscher Programmierung auszuschließen.
Eine dieser Schutzmechanismen ist die Option “FORTIFY_SOURCE” der Glibc-Bibliothek. Erst kürzlich wurde eine schon länger bekannte Schwachstelle in diesem Schutzmechanismus korrigiert. Diese Sicherheitslücke erlaubte es Angreifern, Format-String-Fehler trotz “FORTIFY_SOURCE” auszunutzen.
In seiner einfachsten Form sieht ein Format-String-Fehler in C-Code wie folgt aus:
printf(userinput);
Dabei ist “userinput” ein vom Benutzer spezifizierter String. Für diese Zeichenkette kann ein Angreifer einen beliebigen Format-String angeben, beispielsweise:
userinput="%08x.%08x.%08x.%08x.%08x"
Mit diesem String wird das Programm Speicher des Stacks auslesen und anzeigen. Eine korrekte Implementierung sähe so aus:
printf("%s", userinput);
Grundsätzlich haben sich zwei Hauptformen von Format-String-Attacken etabliert: Das Auslesen von beliebigem Speicher und das Beschreiben von beliebigem Speicher. Ersteres wird häufig mit “%s” realisiert. Die Idee hierbei ist, dass “%s” den Speicher einer vorgegeben Speicheradresse ausliest. Dazu bringt der Angreifer im Format-String sowohl die Ziel-Adresse als auch “%s” unter. Da der Format-String selbst auf dem Stack liegt, kann er das ganze so aufsetzen, dass “%s” die im Format-String angegeben Adresse verwendet und den Speicher von dort liest.
Das Beschreiben von Speicher wurde sehr häufig über den Format-String-Identifier “%n” realisiert, denn dieser verhält sich anders als alle anderen Identifier: Er gibt nichts aus sondern führt dazu, dass die Zahl der geschrieben Bytes an eine bestimmte Adresse geschrieben wird. Ein Angreifer kann dies ausnutzen, um Speicherbereiche zu überschreiben und so Programmcode auszuführen. Offensichtlich ist die zweite Angriffsform deutlich kritischer, da sie im schlimmsten Fall das Ausführen von Befehlen mit Root-Rechten ermöglicht. In der Tat waren um das Jahr 2000 herum zahlreiche Server-Applikation anfällig für vergleichbare Schwachstellen. Ein positiver Aspekt von Format-String-Fehlern ist, dass sie im Programmcode gewöhnlich recht leicht zu finden sind. Das erklärt auch, warum diese Angriffsform heute fast ausgestorben ist.
Nach den ersten veröffentlichten Exploits um das Jahre 2000 wurden im Laufe der Zeit die Format-String-Attacken immer ausgefeilter. Die Angreifer machten sich weitere Features von Format-Strings zu Nutze, um ihre Exploit-Codes zu vereinfachen. Eine dieser Techniken ist Direct Parameter Access. Dieses Format-String-Feature ermöglicht eine flexiblere Handhabung von Format-Strings wie in folgendem Beispiel:
printf("%1$s %2$s", "String1", "String2"); // gibt "String 1 String 2" aus
printf("%2$s %1$s", "String1", "String2"); // gibt "String 2 String 1" aus
Der Anwender hat hierbei mehr Kontrolle darüber, welche Daten an welcher Stelle im String ausgegeben werden sollen, da er durch die “%d$s”-Notation direkt die Variable spezifizieren kann.
Unzählige Format-String-Attacken machten sich Programmierfehler im Zusammenhang mit “printf()” und dessen Verwandten zu Nutze. Da all diese Funktionen in der Glibc implementiert sind, lag es nahe, einen allgemeinen Schutz in die Bibliothek einzubauen, der auch bei falscher Programmierung schlimmere Attacken verhindert. Das haben die Entwickler mit dem “FORTIFY_SOURCE”-Patch für die Glibc realisiert. Zweck der “FORTIFY_SOURCE”-Option ist es, sowohl Speicherschreib-Attacken via “%n” als auch Angriffe via Direct Parameter Access zu verhindern. Der Schutz wird aktiviert, sobald ein Programm mit “-D_FORTIFY_SOURCE=2” übersetzt wird. Wer sich den Assembler-Code der erzeugten Binärdatei ansieht, wird feststellen, dass Funktionen wie “printf()” ersetzt wurden, beispielsweise durch “printf_chk()”. Diese Funktion ist in “libc/debug/printf_chk.c” implementiert:
/* Write formatted output to stdout from the format string FORMAT. */
int ___printf_chk (int flag, const char *format, ...)
{ va_list ap; int done; _IO_acquire_lock_clear_flags2 (stdout); if (flag > 0) stdout->_flags2 |= _IO_FLAGS2_FORTIFY; va_start (ap, format); done = vfprintf (stdout, format, ap); va_end (ap); if (flag > 0) stdout->_flags2 &= ~_IO_FLAGS2_FORTIFY; _IO_release_lock (stdout); return done;
}
Beim Aufruf von “printf()” im Benutzerprogramm ist “flag=1” gesetzt, und damit wird das Flag “_IO_FLAGS2_FORTIFY” für den “FILE”-Stream-Zeiger (beispielsweise Stdout) aktiviert. Dieses Flag signalisiert für alle weiteren Funktionen, die auf dieser “FILE”-Struktur arbeiten, dass sie Extrakontrollen durchführen sollen, um Angriffs-Exploits zu vermeiden.
Um Schreib-Attacken mit “%n” zu verhindern, sind in “libc/stdio-common/vfprintf.c” verschiedene Kontrollmechanismen eingebaut, die bei gesetztem Flag “_IO_FLAGS2_FORTIFY” aktiv werden. Diese führen zu einem Programmabbruch, sobald dort festgestellt wird, dass die “%n” Adresse in einem beschreibbarem Speicherbereich wie Stack, BSS, DATA oder dem Heap liegt. Auch wenn hiermit immer noch Denial-of-Service-Attacken möglich sind (denn das Programm wird ja beendet), so werden doch gefährliche Speichermanipulationen, die zum Ausführen von Code führen können, vereitelt. Dies stellt einen zentralen Schutz gegen schreibende Format-String-Attacken dar.
Eine zweite Schutzfunktion wurde ebenfalls in “libc/stdio-common/vfprintf.c” eingebaut, um Attacken auf Basis von Direct Parameter Access zu verhindern:
/* Determine the number of arguments the format string consumes. */
nargs = MAX (nargs, max_ref_arg);
/* Allocate memory for the argument descriptions. */
args_type = alloca (nargs * sizeof (int));
memset (args_type, s->_flags2 & _IO_FLAGS2_FORTIFY ? '\xff' : '\0', nargs * sizeof (int));
args_value = alloca (nargs * sizeof (union printf_arg));
args_size = alloca (nargs * sizeof (int));
..
for (cnt = 0; cnt < nargs; ++cnt)
..
switch (args_type[cnt])
..
case -1: /* Error case. Not all parameters appear in N$ format strings. We have no way to determine their type. */ assert (s->_flags2 & _IO_FLAGS2_FORTIFY); __libc_fatal ("*** invalid %N$ use detected ***\n");
}
“arg_types[]” speichert hier den Typ eines Format-String-Arguments, beispielsweise “PA_INT” (dies ist in einem C enum als 0 definiert) für eine Integer-Variable. Dieser zweite Kontroll-Code hat zur Folge, dass alle “arg_type[]”-Einträge bei gesetztem Flag “_IO_FLAGS2_FORTIFY” zunächst auf -1 gesetzt werden. Dadurch sind einzelne Direct Parameter Access Format-Strings der Art “%4$x” nicht mehr möglich, sondern nur solche, bei denen keine Lücken auftreten, “%4$x %2$x %1$x %3$x” wäre beispielsweise in Ordnung. Damit sind viele Exploits, die auf Direct Parameter Access beruhen, nicht mehr anwendbar. Beide Schutzmechanismen kombiniert machen es praktisch unmöglich, Format-String-Attacken zum Ausführen von Programmcode oder Beschreiben von Speicher auszunutzen.
Noch ein Trick
Wie in einem Phrack-Artikel von November 2010 von Captain Planet dargelegt gibt es aber einen Trick, um beide Kontrollen zu umgehen. Das Hauptproblem beim Ausführen von Format-String-Attacken ist die Tatsache, dass “FORTIFY_SOURCE” das Schreiben auf beliebigen Speicher via “%n” unmöglich macht.
Eine Möglichkeit, dies zu umgehen, besteht darin, das Flag “IO_FLAGS2_FORTIFY” des FILE-Stream-Zeigers zu deaktivieren. Das Flag selbst ist ein 4-Byte-Wert im Speicher. Der Angreifer müsste also irgendwie in der Lage sein, diese 4 Byte gezielt mit Nullen zu überschreiben und damit alle Bitflags auf Null zu setzen. Ein aufmerksamer Leser wird festellen, dass damit die gesamte Bitmask des Flags zerstört ist. Dies macht aber nichts, da in den meisten Fällen tatsächlich nur das “_IO_FLAGS2_FORTIFY”-Flag gesetzt ist und keine weiteren.
Die entsprechende Attacke ist ein wenig trickreich, aber der Schlüssel zum Erfolg basiert auf folgenden Zeilen in der “vfprintf()”-Implementierung:
/* Fill in the types of all the arguments. */
for (cnt = 0; cnt < nspecs; ++cnt)
{ /* If the width is determined by an argument this is an int. */ if (specs[cnt].width_arg != -1) args_type[specs[cnt].width_arg] = PA_INT;
...
enum
{ /* C type: */ PA_INT, /* int */
Hier werden die verschiedenen Typen für die Argumente gesetzt. Einen Sonderfall stellen hierbei Format-Strings mit variabler Breite dar, die von dem If-Fall abgehandelt werden. An dieser Stelle wird dann “args_type[specs[cnt].width_arg] = PA_INT” gesetzt, was wegen “PA_INT=0” bedeutet, dass hier 4 Byte auf Null gesetzt werden. Jetzt muss der Angreifer es irgendwie anstellen, “specs[cnt].width_arg]” auf den Flag-Eintrag in der FILE-Struktur zeigen zu lassen. Dies gelingt durch einen Integer-Overflow für die “nargs”-Variable. Damit ist ein zentraler Format-String-Schutz von “FORTIFY_SOURCE” umgangen, denn ohne das gesetzte Flag kann ein Angreifer via “%n” auf beschreibbaren Speicher zugreifen. Im Phrack-Artikel finden sich auch einige Beispiel-Exploits, die dies demonstrieren.
Obwohl der Phrack-Artikel mehr als ein Jahr alt ist, wurde der Fehler in der Glibc erst Anfang März korrigiert.

