czwartek, 27 lutego 2025

picoCTF - Forky

W tym poście chciałbym opisać rozwiązanie zadania Forky z działu Reverse Engineering picoCTF. 


Do zadania dołączony jest skompilowany kod. W celu dekompilacji można posłużyć się narzędziem jak cutter lub decompiler online.

Poniżej fragment decompilacji w assemblerze z programu cutter:

  1. doNothing(void *arg_4h);
  2. ; var int32_t var_8h @ stack - 0x8
  3. ; arg void *arg_4h @ stack + 0x4
  4. 0x0000054d      push    ebp
  5. 0x0000054e      mov     ebp, esp
  6. 0x00000550      sub     esp, 0x10
  7. 0x00000553      call    __x86.get_pc_thunk.ax ; sym.__x86.get_pc_thunk.ax
  8. 0x00000558      add     eax, 0x1a7c
  9. 0x0000055d      mov     eax, dword [arg_4h]
  10. 0x00000560      mov     dword [var_8h], eax
  11. 0x00000563      nop
  12. 0x00000564      leave
  13. 0x00000565      ret
  14. int main(int argc, char **argv, char **envp);
  15. ; var int prot @ stack - 0x1c
  16. ; var int flags @ stack - 0x18
  17. ; var void *var_14h @ stack - 0x14
  18. ; var int32_t var_10h @ stack - 0x10
  19. ; arg int argc @ stack + 0x4
  20. 0x00000566      lea     ecx, [argc]
  21. 0x0000056a      and     esp, 0xfffffff0
  22. 0x0000056d      push    dword [ecx - 4]
  23. 0x00000570      push    ebp
  24. 0x00000571      mov     ebp, esp
  25. 0x00000573      push    ebx
  26. 0x00000574      push    ecx
  27. 0x00000575      sub     esp, 0x10
  28. 0x00000578      call    __x86.get_pc_thunk.bx ; sym.__x86.get_pc_thunk.bx
  29. 0x0000057d      add     ebx, 0x1a57
  30. 0x00000583      mov     dword [prot], 3
  31. 0x0000058a      mov     dword [flags], 0x21 ; '!'
  32. 0x00000591      sub     esp, 8
  33. 0x00000594      push    0          ; size_t offset
  34. 0x00000596      push    0xffffffffffffffff ; int fd
  35. 0x00000598      push    dword [flags] ; int flags
  36. 0x0000059b      push    dword [prot] ; int prot
  37. 0x0000059e      push    4          ; size_t length
  38. 0x000005a0      push    0          ; void *addr
  39. 0x000005a2      call    mmap       ; sym.imp.mmap ; void *mmap(void *addr, size_t length, int prot, int flags, int fd, size_t offset)
  40. 0x000005a7      add     esp, 0x20
  41. 0x000005aa      mov     dword [var_14h], eax
  42. 0x000005ad      mov     eax, dword [var_14h]
  43. 0x000005b0      mov     dword [eax], 0x3b9aca00
  44. 0x000005b6      call    fork       ; sym.imp.fork
  45. 0x000005bb      call    fork       ; sym.imp.fork
  46. 0x000005c0      call    fork       ; sym.imp.fork
  47. 0x000005c5      call    fork       ; sym.imp.fork
  48. 0x000005ca      mov     eax, dword [var_14h]
  49. 0x000005cd      mov     eax, dword [eax]
  50. 0x000005cf      lea     edx, [eax + 0x499602d2]
  51. 0x000005d5      mov     eax, dword [var_14h]
  52. 0x000005d8      mov     dword [eax], edx
  53. 0x000005da      mov     eax, dword [var_14h]
  54. 0x000005dd      mov     eax, dword [eax]
  55. 0x000005df      sub     esp, 0xc
  56. 0x000005e2      push    eax        ; int32_t arg_4h
  57. 0x000005e3      call    doNothing  ; sym.doNothing
  58. 0x000005e8      add     esp, 0x10
  59. 0x000005eb      mov     eax, 0
  60. 0x000005f0      lea     esp, [var_10h]
  61. 0x000005f3      pop     ecx
  62. 0x000005f4      pop     ebx
  63. 0x000005f5      pop     ebp
  64. 0x000005f6      lea     esp, [ecx - 4]
  65. 0x000005f9      ret

W assemblerze dosyć ciężko to przeanalizować. Natomiast widać, że czterokrotnie następuje wywołanie funckcji fork (stworzenie nowych procesów). Po stworzeniu tych procesów przeprowadzane są operacje na zmiennych i dopiero na samym końcu wywoływana jest funkcja doNothing. 

Zdekompilowany fragment kodu narzędziem online. W trochę bardziej czytelnej formie:

  1. undefined4 main(undefined1 param_1)
  2. {
  3.   int *piVar1;
  4.  
  5.   piVar1 = (int *)mmap((void *)0x0,4,3,0x21,-1,0);
  6.   *piVar1 = 1000000000;
  7.   fork();
  8.   fork();
  9.   fork();
  10.   fork();
  11.   *piVar1 = *piVar1 + 0x499602d2;
  12.   doNothing(*piVar1);
  13.   return 0;
  14. }
  15.  
  16. void doNothing(undefined4 param_1)
  17. {
  18.   __x86.get_pc_thunk.ax();
  19.   return;
  20. }

W zadaniu należy znaleść wartość wprowadzoną do zmiennej doNothing(). Teoretycznie powinno być 1000000000 + 0x499602D2. Natomiast pojawia się cztery wywołania funkcji fork(). Funkcja fork powoduje powstanie kolejnego egzemplarza procesu. Zmienia się tylko id. W związku z tym, że fork jest wywołane czterokrotnie to końcowa ilość procesów wynosi 2^4 = 16. 

Działa to tak:

Na początku, po uruchomieniu programu, istnieje jeden proces. Pierwsze wywołanie fork() powoduje utworzenie nowego procesu, co daje łącznie 2 procesy. Drugie wywołanie fork() sprawia, że każdy z tych dwóch procesów tworzy kolejny, zwiększając ich liczbę do 4. Trzecie wywołanie fork() ponownie podwaja liczbę procesów, co skutkuje 8 aktywnymi procesami. Czwarte wywołanie fork() powoduje, że każdy z 8 istniejących procesów tworzy nowy, co daje łącznie 16 procesów.

Czyli nastąpi szesnastokrotne dodanie wartości 0x499602D2 do wartości 1000000000.

Do obliczeń można wykorzystać program w C:

  1. #include <stdio.h>
  2. #include <stdint.h>
  3.  
  4. int main()
  5. {
  6.     int32_t val = (1000000000 + (16 * 1234567890));
  7.     printf("wartosc %d", val);
  8.  
  9.     return 0;
  10. }

Otrzymuje wynik -721750240, ponieważ następuje przypisanie wartości do zmiennej int32. Jest on zgodny z oczekiwaną wartością jako flaga.