pwntools簡(jiǎn)單語(yǔ)法
作為最好用的pwn工具,簡(jiǎn)單記一下用法:
- 連接:本地process()、遠(yuǎn)程remote( , );對(duì)于remote函數(shù)可以接url并且指定端口
- 數(shù)據(jù)處理:主要是對(duì)整數(shù)進(jìn)行打包:p32、p64是打包為二進(jìn)制,u32、u64是解包為二進(jìn)制
- IO模塊:這個(gè)比較容易跟zio搞混,記住zio是read、write,pwn是recv、send
send(data): 發(fā)送數(shù)據(jù)
sendline(data) : 發(fā)送一行數(shù)據(jù),相當(dāng)于在末尾加\n
recv(numb=4096, timeout=default) : 給出接收字節(jié)數(shù),timeout指定超時(shí)
recvuntil(delims, drop=False) : 接收到delims的pattern
(以下可以看作until的特例)
recvline(keepends=True) : 接收到\n,keepends指定保留\n
recvall() : 接收到EOF
recvrepeat(timeout=default) : 接收到EOF或timeout
interactive() : 與shell交互
- ELF模塊:獲取基地址、獲取函數(shù)地址(基于符號(hào))、獲取函數(shù)got地址、獲取函數(shù)plt地址
e = ELF('/bin/cat')
print hex(e.address) # 文件裝載的基地址
0x400000
print hex(e.symbols['write']) # 函數(shù)地址
0x401680
print hex(e.got['write']) # GOT表的地址
0x60b070
print hex(e.plt['write']) # PLT的地址
0x401680
- 解題常用:
context.arch = 'amd64' //設(shè)置架構(gòu)
context.log_level = 'debug' //顯示log詳細(xì)信息
libc = ELF('./libc-2.24.so') //加載庫(kù)文件
Pwn基礎(chǔ)知識(shí)
ELF可重定位目標(biāo)文件的節(jié):
.bss(Block Strorage Start) 儲(chǔ)存未初始化的全局和c變量,和被初始化為0的全局和靜態(tài)變量
動(dòng)態(tài)鏈接共享庫(kù)
共享庫(kù)是致力于解決靜態(tài)庫(kù)缺陷(定期維護(hù)和更新,重新鏈接)的一個(gè)產(chǎn)物,共享庫(kù)也被稱為共享目標(biāo)(Shared Object),Linux中常用.so后綴來(lái)表示
動(dòng)態(tài)鏈接過(guò)程如下:

位置無(wú)關(guān)代碼(PIC)
共享庫(kù)的一個(gè)主要目的是允許多個(gè)正在運(yùn)行的進(jìn)程共享內(nèi)存中相同的庫(kù)代碼,節(jié)約內(nèi)存資源。若是事先分配專用的地址空間片,要求加載器總是在這個(gè)地址加載,這樣雖然簡(jiǎn)單,但對(duì)地址空間的使用效率不高,即使不用也要分配。除此之外難以管理,必須保證沒(méi)有片會(huì)重疊,并且當(dāng)庫(kù)修改了之后必須確認(rèn)已分配的片還適合它的大小,而修改之后更加難以管理,為了避免這些問(wèn)題,現(xiàn)代操作系統(tǒng)引入了位置無(wú)關(guān)代碼PIC(Position-Independent Code),使得可以把它們加載到內(nèi)存的任何位置而無(wú)需鏈接器修改。
- PIC數(shù)據(jù)引用
無(wú)論在內(nèi)存的何處加載一個(gè)目標(biāo)模塊(包括共享目標(biāo)模塊),數(shù)據(jù)段與代碼段的距離總保持不變,因此,代碼段中任何指令和數(shù)據(jù)段中任何變量之間的距離都是一個(gè)運(yùn)行時(shí)常量,與代碼段和數(shù)據(jù)段的絕對(duì)內(nèi)存位置無(wú)關(guān)。
編譯器利用這個(gè)事實(shí)生成對(duì)全局變量PIC的引用,它在數(shù)據(jù)段開(kāi)始的地方創(chuàng)建了一個(gè)表,叫做全局偏移量表(Global Offset Table,GOT)。在GOT中,每個(gè)被當(dāng)前目標(biāo)模塊引用的全局?jǐn)?shù)據(jù)(過(guò)程或全局變量)都有一個(gè)8字節(jié)條目(編譯器還會(huì)為GOT表中每個(gè)條目生成一個(gè)重定位記錄),加載時(shí)動(dòng)態(tài)鏈接器重定位GOT中每個(gè)條目,使得它們包含目標(biāo)的正確絕對(duì)地址。引用全局目標(biāo)的目標(biāo)模塊都有自己的GOT
libvector.so共享模塊的GOT,實(shí)現(xiàn)addcnt在內(nèi)存中+1

- PIC函數(shù)調(diào)用(重點(diǎn))
在ELF文件的動(dòng)態(tài)連接機(jī)制中,每一個(gè)外部定義的符號(hào)在全局偏移表 (Global Offset Table,GOT)中有相應(yīng)的條目,如果符號(hào)是函數(shù)則在過(guò)程連接表(Procedure Linkage Table,PLT)中也有相應(yīng)的條目,且一個(gè)PLT條目對(duì)應(yīng)一個(gè)GOT條目,原理如下:
假設(shè)程序調(diào)用一個(gè)由共享庫(kù)定義的函數(shù)。編譯器沒(méi)有辦法預(yù)測(cè)這個(gè)函數(shù)的運(yùn)行時(shí)地址,因?yàn)槎x它的共享模塊在運(yùn)行時(shí)可以加載到任意位置。正常的方法是為該引用生成一條重定位記錄,然后動(dòng)態(tài)鏈接器在程序加載的時(shí)候再解析它。不過(guò),因?yàn)樗枰溄悠餍薷恼{(diào)用模塊的代碼段,GUN使用延遲綁定(lazy binding)將過(guò)程地址的綁定推遲到第一次調(diào)用該過(guò)程時(shí)。
使用延遲綁定的動(dòng)機(jī)是對(duì)于一個(gè)像libc.so這樣的共享庫(kù)輸出的成百上千個(gè)函數(shù)中,一個(gè)典型的應(yīng)用程序只會(huì)使用其中很少的一部分,把函數(shù)地址的解析推遲到它實(shí)際被調(diào)用的地方,能避免動(dòng)態(tài)鏈接器在加載時(shí)進(jìn)行成百上千個(gè)其實(shí)并不需要的重定位。第一次調(diào)用過(guò)程的運(yùn)行時(shí)開(kāi)銷很大,但是其后的每次調(diào)用都只會(huì)花費(fèi)一條指令和一個(gè)間接的內(nèi)存引用。
延遲綁定是通過(guò)兩個(gè)數(shù)據(jù)結(jié)構(gòu)【GOT和過(guò)程鏈接表(Procedure Linkage Table,PLT)】之間的交互實(shí)現(xiàn),如果一個(gè)目標(biāo)模塊調(diào)用定義在共享庫(kù)的任何函數(shù),那么它就有GOT和PLT,GOT是數(shù)據(jù)段的一部分,PLT是代碼段的一部分

如圖,
PLT是一個(gè)數(shù)組,其中每個(gè)條目是16字節(jié)代碼。PLT[0]是個(gè)特殊條目,它跳轉(zhuǎn)到動(dòng)態(tài)鏈接器中。每個(gè)被可執(zhí)行程序調(diào)用的庫(kù)函數(shù)都有它自己的PLT條目。每個(gè)條目都負(fù)責(zé)調(diào)用一個(gè)具體的函數(shù)。PLT[1]調(diào)用系統(tǒng)啟動(dòng)函數(shù)(__libc_start_main),它初始化執(zhí)行環(huán)境,調(diào)用main 函數(shù)并處理其返回值。從PLT[2]開(kāi)始的條目調(diào)用用戶代碼調(diào)用的函數(shù),PLT[2]調(diào)用addvec,PLT[3]調(diào)用printf。
全局偏移量表(GOT)是一個(gè)數(shù)組,其中每個(gè)條目是8字節(jié)地址。和PLT聯(lián)合使用時(shí),GOT[O]和GOT[1]包含動(dòng)態(tài)鏈接器在解析函數(shù)地址時(shí)會(huì)使用的信息。GOT[2]是動(dòng)態(tài)鏈接器在ld-linux.so模塊中的人口點(diǎn)。其余的每個(gè)條目對(duì)應(yīng)于一個(gè)被調(diào)用的函數(shù),其地址需要在運(yùn)行時(shí)被解析。每個(gè)條目都有一個(gè)相匹配的PLT條目。例如,GOT[4]和PLT[2]對(duì)應(yīng)于addvec。初始時(shí),每個(gè)GOT條目都指向?qū)?yīng)PLT條目的第二條指令。
圖中步驟:
第1步:不直接調(diào)用addvec,程序調(diào)用進(jìn)入PLT[2],這是addvec的PLT條目。
第2步:第一條PLT指令通過(guò)GOT[4]進(jìn)行間接跳轉(zhuǎn)。因?yàn)槊總€(gè)GOT條目初始時(shí)都指向它對(duì)應(yīng)的PLT條目的第二條指令,這個(gè)間接跳轉(zhuǎn)只是簡(jiǎn)單地把控制傳送回PLT[2]中的下一條指令。
第3步:在把a(bǔ)ddvec 的ID(0x1)壓人棧中之后,PLT[2]跳轉(zhuǎn)到PLT[0]。
第4步:PLT[0]通過(guò)GOT[1]間接地把動(dòng)態(tài)鏈接器的一個(gè)參數(shù)壓人棧中,然后通過(guò)GOT[2]間接跳轉(zhuǎn)進(jìn)動(dòng)態(tài)鏈接器中。動(dòng)態(tài)鏈接器使用兩個(gè)棧條目來(lái)確定addvec的運(yùn)行時(shí)位置,用這個(gè)地址重寫(xiě)GOT[4],再把控制傳遞給addvec。
圖7-19b是后續(xù)再調(diào)用addvec時(shí)的控制流:
第1步:和前面一樣,控制傳遞到PLT[2]。
第2步:不過(guò)這次通過(guò)GOT[4]的間接跳轉(zhuǎn)會(huì)將控制直接轉(zhuǎn)移到addvec
腳本檢測(cè)與各類防護(hù)技術(shù)
首先使用checksec,

防護(hù)技術(shù):
RELRO:在Linux系統(tǒng)安全領(lǐng)域數(shù)據(jù)可以寫(xiě)的存儲(chǔ)區(qū)就會(huì)是攻擊的目標(biāo),尤其是存儲(chǔ)函數(shù)指針的區(qū)域,盡量減少可寫(xiě)的存儲(chǔ)區(qū)域可使安全系數(shù)提高。GCC, GNU linker以及Glibc-dynamic linker一起配合實(shí)現(xiàn)了一種叫做relro的技術(shù)Relocation Read Only, 重定向只讀,實(shí)現(xiàn)就是由linker指定binary的一塊經(jīng)過(guò)dynamic linker處理過(guò) relocation之后的區(qū)域?yàn)橹蛔x。(參考RELRO技術(shù)細(xì)節(jié))
Stack: 棧溢出檢查,用Canary金絲雀值是否變化來(lái)檢測(cè),Canary found表示開(kāi)啟。
金絲雀最早指的是礦工曾利用金絲雀來(lái)確認(rèn)是否有氣體泄漏,如果金絲雀因?yàn)闅怏w泄漏而中毒死亡,可以給礦工預(yù)警。這里是一種緩沖區(qū)溢出攻擊緩解手段:?jiǎn)⒂脳1Wo(hù)后,函數(shù)開(kāi)始執(zhí)行的時(shí)候會(huì)先往棧里插入cookie信息,當(dāng)函數(shù)真正返回的時(shí)候會(huì)驗(yàn)證cookie信息是否合法,如果不合法就停止程序運(yùn)行。攻擊者在覆蓋返回地址的時(shí)候往往也會(huì)將cookie信息給覆蓋掉,導(dǎo)致棧保護(hù)檢查失敗而阻止shellcode的執(zhí)行。在Linux將cookie信息稱為Canary。
NX: No Execute,棧不可執(zhí)行,也就是windows上的DEP。

分析緩沖區(qū)溢出攻擊,其根源在于現(xiàn)代計(jì)算機(jī)對(duì)數(shù)據(jù)和代碼沒(méi)有明確區(qū)分這一先天缺陷,就目前來(lái)看重新去設(shè)計(jì)計(jì)算機(jī)體系結(jié)構(gòu)基本上是不可能的,我們只能靠向前兼容的修補(bǔ)來(lái)減少溢出帶來(lái)的損害,DEP就是用來(lái)彌補(bǔ)計(jì)算機(jī)對(duì)數(shù)據(jù)和代碼混淆這一天然缺陷的。
DEP的基本原理是將數(shù)據(jù)所在內(nèi)存頁(yè)標(biāo)識(shí)為不可執(zhí)行,當(dāng)程序溢出成功轉(zhuǎn)入shellcode時(shí),程序會(huì)嘗試在數(shù)據(jù)頁(yè)面上執(zhí)行指令,此時(shí)CPU就會(huì)拋出異常,而不是去執(zhí)行惡意指令。DEP的主要作用是阻止數(shù)據(jù)頁(yè)(如默認(rèn)的堆頁(yè)、各種堆棧頁(yè)以及內(nèi)存池頁(yè))執(zhí)行代碼。硬件DEP需要CPU的支持,AMD和Intel都為此做了設(shè)計(jì),AMD稱之為No-Execute Page-Protection(NX),Intel稱之為Execute Disable Bit(XD)
Linux稱為 NX 與 DEP原理相同
PIE: position-independent executables, 位置無(wú)關(guān)的可執(zhí)行文件,也就是常說(shuō)的ASLR(Address space layout randomization) 地址隨機(jī)化,程序每次啟動(dòng)基址都隨機(jī)。
Linux虛擬內(nèi)存系統(tǒng)與動(dòng)態(tài)內(nèi)存的分配
12.01
Linux使用內(nèi)存分配器 ptmalloc2 – glibc
glibc是GNU發(fā)布的libc庫(kù),即c運(yùn)行庫(kù)。glibc是linux系統(tǒng)中最底層的api,幾乎其它任何運(yùn)行庫(kù)都會(huì)依賴于glibc。glibc除了封裝linux操作系統(tǒng)所提供的系統(tǒng)服務(wù)外,它本身也提供了許多其它一些必要功能服務(wù)的實(shí)現(xiàn)。
Linux進(jìn)程的虛擬內(nèi)存

Linux為每個(gè)進(jìn)程維護(hù)了一個(gè)單獨(dú)的虛擬地址空間,形式如圖所示。內(nèi)核虛擬內(nèi)存包含內(nèi)核中的代碼和數(shù)據(jù)結(jié)構(gòu)。內(nèi)核虛擬內(nèi)存的某些區(qū)域被映射到所有進(jìn)程共享的物理頁(yè)面。每個(gè)進(jìn)程共享內(nèi)核的代碼和全局?jǐn)?shù)據(jù)結(jié)構(gòu)。Linux將虛擬內(nèi)存組織成一些區(qū)域(也叫做段)的集合。一個(gè)區(qū)域(area 就已經(jīng)存在著的(已分配的)虛擬內(nèi)存的連續(xù)片(chunk),這些頁(yè)是以某種方式相關(guān)聯(lián)的。例如,代碼段、數(shù)據(jù)段、堆、共享庫(kù)段,以及用戶棧都是不同的區(qū)域。每個(gè)存在的虛擬頁(yè)面都保存在某個(gè)區(qū)域中,而不屬于某個(gè)區(qū)域的虛擬頁(yè)是不存在的,并且不能被進(jìn)程引用。區(qū)域允許虛擬地址空間有間隙。內(nèi)核不用記錄那些不存在的虛報(bào)頁(yè),而這樣的頁(yè)也不占用內(nèi)存、磁盤(pán)或者內(nèi)核本身中的任何額外資源。
系統(tǒng)內(nèi)存分布圖:

函數(shù)調(diào)用關(guān)系圖:

Linux進(jìn)程分配的方式: _brk()和_mmap()
如上圖,從操作系統(tǒng)角度來(lái)看,進(jìn)程分配內(nèi)存有兩種方式,分別由兩個(gè)系統(tǒng)調(diào)用完成:brk和mmap(不考慮共享內(nèi)存)
- brk是將數(shù)據(jù)段(.data)的最高地址指針_edata往高地址推;
- mmap是在進(jìn)程的虛擬地址空間中(堆和棧中間,稱為文件映射區(qū)域的地方)找一塊空閑的虛擬內(nèi)存。
這兩種方式分配的是虛擬內(nèi)存,沒(méi)有分配物理內(nèi)存。在第一次訪問(wèn)已分配的虛擬地址空間時(shí),發(fā)生缺頁(yè)中斷,操作系統(tǒng)負(fù)責(zé)分配物理內(nèi)存,然后建立虛擬內(nèi)存和物理內(nèi)存之間的映射關(guān)系
關(guān)于進(jìn)程缺頁(yè)中斷:當(dāng)MMU試圖翻譯某個(gè)虛擬地址時(shí),
ps -o majflt/minflt -C program //查看進(jìn)程發(fā)生缺頁(yè)中斷的次數(shù)
majflt代表major fault,中文叫大錯(cuò)誤,minflt代表minor fault,中文叫小錯(cuò)誤
當(dāng)一個(gè)進(jìn)程發(fā)生缺頁(yè)中斷的時(shí)候,進(jìn)程會(huì)陷入內(nèi)核態(tài),執(zhí)行以下操作:
- 檢查要訪問(wèn)的虛擬地址是否合法(是否在定義區(qū)域內(nèi)),檢查試圖進(jìn)行的內(nèi)存訪問(wèn)是否合法(進(jìn)程是否有這段內(nèi)存的權(quán)限)
- 查找/分配一個(gè)物理頁(yè)
- 填充物理頁(yè)內(nèi)容(讀取磁盤(pán),或者直接置0,或者什么也不干)
- 建立映射關(guān)系(虛擬地址到物理地址)
重新執(zhí)行發(fā)生缺頁(yè)中斷的那條指令,如果第3步,需要讀取磁盤(pán),那么這次缺頁(yè)中斷就是majflt,否則就是minflt

在標(biāo)準(zhǔn)C庫(kù)中,提供了malloc/free函數(shù)分配釋放內(nèi)存,這兩個(gè)函數(shù)底層是由brk,mmap,munmap這些系統(tǒng)調(diào)用實(shí)現(xiàn)的
動(dòng)態(tài)內(nèi)存分配:

需要額外虛擬內(nèi)存時(shí),用動(dòng)態(tài)內(nèi)存分配器(dynamic memory allocator)更方便,也有更好的可移植性。
動(dòng)態(tài)內(nèi)存分配器維護(hù)著一個(gè)進(jìn)程的虛擬內(nèi)存區(qū)域,稱為堆(heap)。系統(tǒng)之間細(xì)節(jié)不同,但是不失通用性,假設(shè)堆是一個(gè)請(qǐng)求二進(jìn)制零的區(qū)域,它緊接在未初始化的數(shù)據(jù)區(qū)域后開(kāi)始,并向上生長(zhǎng)(向更高的地址)。對(duì)于每個(gè)進(jìn)程,內(nèi)核維護(hù)著一個(gè)變量brk(“break"),它指向堆的頂部。分配器將堆視為一組不同大小的塊(block)的集合來(lái)維護(hù)。每個(gè)塊就是一個(gè)連續(xù)的虛擬內(nèi)存片(chunk),要么是已分配的,要么是空閑的。已分配的塊顯式地保留為供應(yīng)用程序使用??臻e塊可用來(lái)分配。空閑塊保持空閑,直到它顯式地被應(yīng)用所分配。一個(gè)已分配的塊保持已分配狀態(tài),直到它被釋放,這種釋放要么是應(yīng)用程序顯式執(zhí)行的,要么是內(nèi)存分配器自身隱式執(zhí)行的。
分配器有兩種基本風(fēng)格(顯式分配器,如c malloc free;隱式分配器,如java)。兩種風(fēng)格都要求應(yīng)用顯式地分配塊。它們的不同之處在于由哪個(gè)實(shí)體來(lái)負(fù)責(zé)釋放已分配的塊。
舉例說(shuō)明:
- malloc小于128k的內(nèi)存,使用brk分配內(nèi)存,將_edata往高地址推(只分配虛擬空間,不對(duì)應(yīng)物理內(nèi)存(因此沒(méi)有初始化),第一次讀/寫(xiě)數(shù)據(jù)時(shí),引起內(nèi)核缺頁(yè)中斷,內(nèi)核才分配對(duì)應(yīng)的物理內(nèi)存,然后虛擬地址空間建立映射關(guān)系),如下圖:

- malloc大于128k的內(nèi)存,使用mmap分配內(nèi)存,在堆和棧之間找一塊空閑內(nèi)存分配(對(duì)應(yīng)獨(dú)立內(nèi)存,而且初始化為0),如下圖:


- 進(jìn)程調(diào)用free(B)以后,如圖7所示:B對(duì)應(yīng)的虛擬內(nèi)存和物理內(nèi)存都沒(méi)有釋放,因?yàn)橹挥幸粋€(gè)_edata指針,如果往回推,那么D這塊內(nèi)存怎么處理?當(dāng)然,B這塊內(nèi)存,是可以重用的,如果這個(gè)時(shí)候再來(lái)一個(gè)40K的請(qǐng)求,那么malloc很可能就把B這塊內(nèi)存返回
- 進(jìn)程調(diào)用free(D)以后,如圖8所示:
當(dāng)最高地址空間的空閑內(nèi)存超過(guò)128K(可由M_TRIM_THRESHOLD選項(xiàng)調(diào)節(jié))時(shí),執(zhí)行內(nèi)存緊縮操作(trim)。在上一個(gè)步驟free的時(shí)候,發(fā)現(xiàn)最高地址空閑內(nèi)存超過(guò)128K,于是內(nèi)存緊縮,變成圖9所示。
Linux堆溢出漏洞利用之unlink
關(guān)于chunk: malloc文檔
eglibc-2.19/malloc/malloc.c:1094
/*
----------------------- Chunk representations -----------------------
*/
/*
This struct declaration is misleading (but accurate and necessary).
It declares a "view" into memory allowing access to necessary
fields at known offsets from a given base. See explanation below.
*/
// 一個(gè) 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;
};
/*
malloc_chunk details:
(The following includes lightly edited explanations by Colin Plumb.)
// chunk 的內(nèi)存管理采用邊界標(biāo)識(shí)的方法, 空閑 chunk 的 size 在該 chunk 的 size 字段和下一個(gè) chunk 的 pre_size 字段都有記錄
Chunks of memory are maintained using a `boundary tag' method as
described in e.g., Knuth or Standish. (See the paper by Paul
Wilson ftp://ftp.cs.utexas.edu/pub/garbage/allocsrv.ps for a
survey of such techniques.) Sizes of free chunks are stored both
in the front of each chunk and at the end. This makes
consolidating fragmented chunks into bigger chunks very fast. The
size fields also hold bits representing whether chunks are free or
in use.
An allocated chunk looks like this:
// 正在使用的 chunk 布局
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if allocated | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// 幾個(gè)術(shù)語(yǔ)規(guī)定, 'chunk' 就是整個(gè) chunk 開(kāi)頭, 'mem' 就是用戶數(shù)據(jù)的開(kāi)始, 'Nextchunk' 就是下一個(gè) chunk 的開(kāi)頭
Where "chunk" is the front of the chunk for the purpose of most of
the malloc code, but "mem" is the pointer that is returned to the
user. "Nextchunk" is the beginning of the next contiguous chunk.
// chunk 是雙字長(zhǎng)對(duì)齊
Chunks always begin on even word boundaries, so the mem portion
(which is returned to the user) is also on an even word boundary, and
thus at least double-word aligned.
// 空閑 chunk 被存放在雙向環(huán)鏈表
Free chunks are stored in circular doubly-linked lists, and look like this:
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' | Size of chunk, in bytes |P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Forward pointer to next chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Back pointer to previous chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Unused space (may be 0 bytes long) .
. .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' | Size of chunk, in bytes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// P 標(biāo)志位不能放在 size 字段的低位字節(jié), 用于表示前一個(gè) chunk 是否在被使用, 如果為 0, 表示前一個(gè) chunk 空閑, 同時(shí) pre_size 也表示前一個(gè)空閑 chunk 的大小, 可以用于找到前一個(gè) chunk 的地址, 方便合并空閑 chunk, 但 chunk 剛一開(kāi)始分配時(shí)默認(rèn) P 為 1. 如果 P 標(biāo)志位被設(shè)置, 也就無(wú)法獲取到前一個(gè) chunk 的 size, 也就拿不到前一個(gè) chunk 地址, 也就無(wú)法修改正在使用的 chunk, 但是這是無(wú)法修改前一個(gè) chunk, 但是可以通過(guò)本 chunk 的 size 獲得下一個(gè) chunk 的地址.
The P (PREV_INUSE) bit, stored in the unused low-order bit of the
chunk size (which is always a multiple of two words), is an in-use
bit for the *previous* chunk. If that bit is *clear*, then the
word before the current chunk size contains the previous chunk
size, and can be used to find the front of the previous chunk.
The very first chunk allocated always has this bit set,
preventing access to non-existent (or non-owned) memory. If
prev_inuse is set for any given chunk, then you CANNOT determine
the size of the previous chunk, and might even get a memory
addressing fault when trying to do so.
Note that the `foot' of the current chunk is actually represented
as the prev_size of the NEXT chunk. This makes it easier to
deal with alignments etc but can be very confusing when trying
to extend or adapt this code.
The two exceptions to all this are
// 這里的 the trailing size 是指下一個(gè) chunk 的 pre_size, 因?yàn)?top 位于最高地址, 不存在相鄰的下一個(gè) chunk, 同時(shí)這里也解答了上面關(guān)于 top 什么時(shí)候重新填滿
1. The special chunk `top' doesn't bother using the
trailing size field since there is no next contiguous chunk
that would have to index off it. After initialization, `top'
is forced to always exist. If it would become less than
MINSIZE bytes long, it is replenished.
2. Chunks allocated via mmap, which have the second-lowest-order
bit M (IS_MMAPPED) set in their size fields. Because they are
allocated one-by-one, each must contain its own trailing size field.
*/
P(PREV_INUSE)標(biāo)志位表示前一個(gè) chunk 是否在使用, 0 為沒(méi)有在使用.
prev_size 表示前一個(gè) chunk 的大小, 僅在 P (PREV_INUSE) 為 0 時(shí)有效, 也就是前一個(gè) chunk 為空閑狀態(tài).
size 表示該整個(gè) chunk 大小, 并非 malloc 返回值.
fd, bk, fd_nextsize, fd_nextsize 是對(duì)于空閑 chunk 而言, 對(duì)于正在使用的 chunk, 從當(dāng)前位置開(kāi)始就是 malloc 返回給用戶可用的空間.
fd, bk 組成了 Bins 的雙向環(huán)鏈表
對(duì)于空閑的 chunk 空間布局, 見(jiàn)上, 是環(huán)形雙向鏈表. 存放在空閑 chunk 容器中.
關(guān)于 chunk 有一些操作, 判斷前一個(gè)是否在使用, 判斷下一個(gè) chunk 是否正在使用, 是不是 mmap 分配的, 以及對(duì)標(biāo)志位 P 等的操作, 可以參考 glibc/malloc/malloc.c:1206 中 Physical chunk operations 一小節(jié)

如圖,如果輸入值的大小比f(wàn)irst變量的字節(jié)更大,那么輸入的數(shù)據(jù)就有可能覆蓋掉下一個(gè)chunk的chunk header——這可以導(dǎo)致任意代碼執(zhí)行
基本知識(shí)介紹:
unlink攻擊技術(shù)就是利用glibc malloc的內(nèi)存回收機(jī)制,欺騙glibc malloc 來(lái) unlink 第二個(gè)塊。unlink free的 GOT 條目會(huì)使其被 shellcode 地址覆蓋。free被漏洞程序調(diào)用時(shí),shellcode 就會(huì)執(zhí)行。
顯然,核心就是glibc malloc的free機(jī)制
一旦涉及到free內(nèi)存,那么就意味著有新的chunk由allocated狀態(tài)變成了free狀態(tài),此時(shí)glibc malloc就需要進(jìn)行合并操作——向前以及(或)向后合并。這里所謂向前向后的概念如下:將previous free chunk合并到當(dāng)前free chunk,叫做向后合并;將后面的free chunk合并到當(dāng)前free chunk,叫做向前合并
/*malloc.c int_free函數(shù)中*/
/*這里p指向當(dāng)前malloc_chunk結(jié)構(gòu)體,bck和fwd分別為當(dāng)前chunk的向后和向前一個(gè)free chunk*/
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
//修改指向當(dāng)前chunk的指針,指向前一個(gè)chunk。
p = chunk_at_offset(p, -((long) prevsize));
unlink(p, bck, fwd);
}
//相關(guān)函數(shù)說(shuō)明:
/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s)))
/*unlink操作的實(shí)質(zhì)就是:將P所指向的chunk從雙向鏈表中移除,這里BK與FD用作臨時(shí)變量*/
#define unlink(P, BK, FD) { \
FD = P->fd; \
BK = P->bk; \
FD->bk = BK; \
BK->fd = FD; \
...
}
整理一下:
首先檢測(cè)前一個(gè)chunk是否為free,這可以通過(guò)檢測(cè)當(dāng)前free chunk的PREV_INUSE(P)比特位知曉。當(dāng)前chunk(first chunk)的前一個(gè)chunk是allocated的,因?yàn)樵谀J(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)鏈表中移除
向前合并:
……
/*這里p指向當(dāng)前chunk*/
nextchunk = chunk_at_offset(p, size);
……
nextsize = chunksize(nextchunk);
……
if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize); //判斷nextchunk是否為free chunk
/* consolidate forward */
if (!nextinuse) { //next chunk為free chunk
unlink(nextchunk, bck, fwd); //將nextchunk從鏈表中移除
size += nextsize; // p還是指向當(dāng)前chunk只是當(dāng)前chunk的size擴(kuò)大了,這就是向前合并!
} else
clear_inuse_bit_at_offset(nextchunk, 0);
……
}
對(duì)抗技術(shù):
glibc malloc對(duì)相應(yīng)的安全機(jī)制進(jìn)行了加強(qiáng),具體而言,就是添加了如下幾條安全檢測(cè)機(jī)制。
- Double Free檢測(cè)
該機(jī)制不允許釋放一個(gè)已經(jīng)處于free狀態(tài)的chunk。因此,當(dāng)攻擊者將second chunk的size設(shè)置為-4的時(shí)候,就意味著該size的PREV_INUSE位為0,也就是說(shuō)second chunk之前的first chunk(我們需要free的chunk)已經(jīng)處于free狀態(tài),那么這時(shí)候再free(first)的話,就會(huì)報(bào)出double free錯(cuò)誤。相關(guān)代碼如下:
/* Or whether the block is actually not marked used. */
if (__glibc_unlikely (!prev_inuse(nextchunk)))
{
errstr = "double free or corruption (!prev)";
goto errout;
}
- next size非法檢測(cè)
該機(jī)制檢測(cè)next size是否在8到當(dāng)前arena的整個(gè)系統(tǒng)內(nèi)存大小之間。因此當(dāng)檢測(cè)到next size為-4的時(shí)候,就會(huì)報(bào)出invalid next size錯(cuò)誤。相關(guān)代碼如下:
nextsize = chunksize(nextchunk);
if (__builtin_expect (nextchunk->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (nextsize >= av->system_mem, 0)){
errstr = "free(): invalid next size (normal)";
goto errout;
}
- 雙鏈表沖突檢測(cè)
該機(jī)制會(huì)在執(zhí)行unlink操作的時(shí)候檢測(cè)鏈表中前一個(gè)chunk的fd與后一個(gè)chunk的bk是否都指向當(dāng)前需要unlink的chunk。這樣攻擊者就無(wú)法替換second chunk的fd與fd了。相關(guān)代碼如下:
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr (check_action, "corrupted double-linked list", P);
以上-----------------------------