HWS二進位制筆記
CTF相關
-
常見的二進位制程式漏洞
- 棧溢位,堆溢位,UAF,這個UAF比較經常被問到
-
上述漏洞怎麼利用
一、棧溢位
1.1 基礎棧結構
進入函式時:
- CALLXXX
- PUSH retadddr(call的下一條指令,目的:知道怎麼回來)
- JMP XXX
- 進入到XXX
- PUSH RBP(儲存棧基址,好恢復)
- MOV RBP,RSP(棧底抬上去)
- SUB RSP,XXh(抬高棧頂給區域性函式預留空間)
退出函式時:
- leave
- mov rsp,rbp(把rsp弄回來)
- pop rbp(把rbp弄回來)
- ret
- pop rip(這個時候RIP就被retaddr的值取代了)
1.2 棧溢位
發生棧溢位的基本前提:
- 程式必須向棧上寫入資料
- 寫入的資料大小沒有被良好地控制
1.3 主要分類
1.3.1 對抗DEP/NX保護技術
-
ret2text
- 即控制程式執行程式本身已有的的程式碼 (.text),我們控制執行程式已有的程式碼的時候也可以控制程式執行好幾段不相鄰的程式已有的程式碼 (也就是 gadgets),這就是我們所要說的 ROP
-
ret2shellcode
- 即控制程式執行 shellcode 程式碼。shellcode 指的是用於完成某個功能的彙編程式碼,常見的功能主要是獲取目標系統的 shell
- shellcode指的是用於完成某個功能的彙編程式碼,常見的功能主要是獲取目標系統的shell。在棧溢位的基礎上,shellcode所在的區域具有可執行許可權
-
ROP
- 利用程式中已經有的小片段(gadgets),控制程式的執行流,以ret結尾的指令,方便連續地控制程式地的執行流
- 滿足條件:
- 程式存在溢位,並且可以控制返回地址
- 可以找到滿足條件的gadgets以及相應gadgets的地址(如果不固定,就需要想辦法動態獲取對應的地址)
- 過程:
- 精心構造棧結構
- 利用返回地址ret的跳轉特點
- 不在棧中或bss段執行程式碼,而是在程式的可執行段尋找可以執行的小元件(gadget)
- 把小元件串起來,構造而成的就叫ROP鏈
-
ret2syscall
- ret2syscall,即控制程式執行系統呼叫,獲取 shell
-
ret2libc
- 即控制函式的執行 libc 中的函式,通常是返回至某個函式的 plt 處或者函式的具體位置 (即函式對應的 got 表項的內容)。一般情況下,我們會選擇執行 system("/bin/sh"),故而此時我們需要知道 system 函式的地址
- libc中應有盡有
- 然而ASLR/PIE技術使得程式基地址和libc基地址每次載入的都不一樣
- 延遲繫結機制
- 原理:
- 因為動態連結庫的載入機制是lazy原則(got表),即用時載入,第二次就不用載入
- 注意:只能洩露已經執行過一次的函式的libc地址
- 利用思路:
- 洩露GOT表中某個函式的libc地址
- 在libc中找到system,“bin/sh”這個函式的相對偏移
- 得到system的地址和“bin/sh”的地址
- 構造ROP鏈,成功利用
- 具體過程:
- 在執行了一次某函式之後,GOT表中就會把一個函式在程式中的終極偏移存起來
- 終極偏移=libc基址(每次載入都不一樣)+庫內函式相對偏移
- System=libc基址+system在庫中的相對偏移
- 如果開啟了PIE
- 結合其他漏洞進行先行洩露
1.3.2 對抗Canary保護技術
-
洩露Canary的值
- 洩露:fs:28h內的值(IDA靜態分析)
-
覆寫副本值
- 需要進行位置的爆破
- mapped段和glibc的偏移是固定的
-
劫持stack_chk_fail
-
可以修改全域性偏移表(GOT)中儲存的_stack_chk_fail函式地址,便可以在觸發Canary檢查失敗時,跳轉到指定的地址繼續執行
-
stack smashing
- 當Canary被覆蓋之後,會call到_stack_chk_fail列印argv[0]這個指標指向的字串,預設是程式的名字
- 如果我們把它覆蓋為其他的地址時,它就會把其他記憶體地址的資訊給列印出來
-
逐位元組爆破(BROP)
-
攻擊條件:
- 遠端程式必須先存在一個已知得stack overflow的漏洞,而且攻擊者知道如何觸發這個漏洞
- 服務程式在crash之後會重新復活,並且復活的程式不會被re-rand(意味著雖然有ASLR保護,但是復活的程式和之前的程式的地址隨機化與Canary是一樣的)。這個需求其實是合理的,因為當前像Nginx,MySQL,Apache、OpenSSH,Samba等伺服器應用都是符合這種特性的
-
核心就是想辦法洩露程式的更多資訊
- 由於我們不知道被攻擊程式的記憶體佈局,所以首先要做的事情就是通過某種方法從遠端伺服器dump出該程式的記憶體到本地
write(int sock, void *buf, int len)
-
BROP基本思路
-
判斷棧溢位的長度
- 直接暴力列舉,因為Canary被覆蓋或者返回地址被覆蓋,會導致程式Crash
-
逐位元組爆破Canary(如果沒有開,就跳過這一步)
- 一個一個位元組順序地進行嘗試來還原出真實的Canary
-
尋找stop gadget
-
尋找useful gadget(尤其是Brop gadget)
-
尋找可用的PLT表項
-
利用PLT表中的puts(或者是write)函式,配合useful gadget,來遠端dump資訊
-
尋找stop gadget
- 但目前為止,已經得到了合適的Canary來繞開stack canary的保護,但如果我們把返回地址覆蓋成某些我們隨意選取的記憶體地址的話,程式有很大可能性會crash
- 存在另外一種情況,即該return address指向了一塊程式碼區域,當程式的執行流跳到那段區域之後,程式並不會crash,而是進入了無限迴圈,這時程式僅僅是hang在了那裡,攻擊者能夠一直保持連線狀態。於是,我們把這種型別的gadget,稱位stop gadget,可以不斷爆破覆蓋返回地址嘗試stop gadget
-
-
對於Windows可以計算出來
- Canary = __security_cookie ^ ebp
-
1.3.3 New
-
棧溢位長度不夠:棧劫持
- 如果可以在.bss段等已知位置進行寫入,就可以提前進行棧佈局。通過覆蓋棧上儲存的saved rbp和saced rip,將棧進行劫持
- leave
- mov rsp,rbp(把rsp弄回來)
- pop rbp(把rbp弄回來)
- ret
- pop rip(這個時候rip就被retaddr的值取代了)
-
SROP
-
SROP的全稱是Sigreturn Oriented Programming。在這裡‘sigreturn’是一個系統呼叫,它在unix系統發生signal的時候會被間接地呼叫
-
signal機制
- signal機制是類unix系統中程式之間相互傳遞資訊地一種方法。一般,我們也稱其為軟中斷訊號,或者軟中斷。比如說,程式之間可以通過系統呼叫kill來傳送軟中斷訊號
- 核心會為該程式儲存對應的上下文,主要是將所有暫存器壓入棧中,以及壓入signal資訊,以及指向sigreturn的系統呼叫地址。此時棧的結構,我們稱ucontext以及siginfo這一段為Signal Frame。需要注意的是,這一部分是在使用者程式的地址空間的。之後會跳轉到註冊過的sinal handler中處理相應的signal。因此,當signal handler執行完之後,就會執行sigreturr程式碼
- 仔細回顧一下核心在signal訊號處理的過程中的工作,我們可以發現,核心主要做的工作就是為程式儲存上下文,並且恢復上下文。這個主要的變動都在Signal Frame中。但是徐婭注意的是:
- Signal Frame被儲存在使用者的地址空間中,所以使用者是可以讀寫的
- 由於核心與訊號處理程式無關,它並不會去記錄這個signal對應的Signal Frame,所以當執行sigreturn系統呼叫時,此時的Signal Frame並不一定是之前核心為使用者程式儲存的Signal Frame
-
利用思路:
-
如果系統執行一系列函式,只需要做兩處修改即可
- 控制棧指標
- 把原來RIP指向的syscall gadget換成syscall;ret gadget
-
在構造ROP攻擊時,需要滿足下面的條件:
-
可以通過棧溢位來控制棧的內容
-
需要知道相應的地址
-
“/bin/sh”
-
Signal Frame
-
syscall
-
sigreturn
-
-
需要有足夠大的空間塞下整個sigal frame
-
-
-
二、堆溢位
2.1 基礎
- malloc
- malloc(size_t n)
- 當n = 0時,返回當前系統允許的堆的最小記憶體塊
- 當n為負數時,由於在大多數系統中,size_t是無符號數(這一點非常重要),所以程式就會申請很大的記憶體空間,但通常來說都會失敗,因為系統沒有那麼多的記憶體可以分配
- free
- free(void* p)
- 當p為空指標時,函式不執行任何操作
- 當p已經釋放之後,再次釋放會出現亂七八糟的效果,這其實就是double free
- 除了被禁用(mallopt)的情況下,當釋放很大的記憶體空間時,程式會將這些記憶體空間還給系統,以便於減少程式所使用的空間
- 記憶體分配後的系統呼叫
- 在前面提到的函式中,無論是malloc還是free函式,我們動態申請和釋放記憶體時,都經常會使用,但是它們並不是真正與系統互動的函式,這些函式背後的系統呼叫主要是(s)brk函式以及mmap、munmap函式
2.2 堆相關資料結構
堆的操作相當的複雜,那麼在glibc內部必然也有精心設計的資料結構來管理它。與堆相應的資料結構主要分為:
- 巨集觀結構,包含堆的巨集觀資訊,可以通過這些資料結構索引堆的基本資訊
- 微觀結構,用於具體處理堆的分配與回收中的記憶體塊
2.2.1 微觀結構 malloc_chunk
在程式的執行過程中,我們稱由malloc申請的記憶體為chunk。這塊記憶體在ptmalloc內部用malloc_chunk結構體來表示。當程式申請的chunk被free後,會被加入到相應的空閒管理列表中
非常有意思的是,無論一個chunk的大小如何,處於分配狀態還是釋放狀態,它們都使用一個統一的結構,雖然它們使用了同一個資料結構,但是根據是否被釋放,它們的表現形式會有所不同:
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_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;
};
- prev_size
- 如果該chunk的物理相鄰的前一地址chunk(兩個指標的地址差值為前一個chunk的大小)是空閒的話,那該欄位記錄的是前一個chunk的大小(包括chunk頭)。否則,該欄位可以用來儲存物理相鄰的前一個chunk的資料。這裡的前一chunk指的是較低地址的chunk
- size
- 該chunk的大小,必須是2*SIZE_SZ的整數倍。如果申請的記憶體大小不是2*SIZE_SZ的整數倍,會被轉換滿足大小的最小的2*SIZE_SZ的倍數。32位系統中,SIZE_SZ是4;64位系統中,SIZE_SZ是8。該欄位的低三個位元位對chunk的大小沒有影響,它們從高到低分別表示:
- NO_MAIN_ARENA
- 記錄當前chunk是否屬於主執行緒,1表示不屬於,0表示屬於
- IS_MAPPED
- 記錄當前chunk是否由mmap分配的
- PREV_INUSE
- 記錄當前一個chunk塊是否被分配。一般來說,堆中第一個被分配的記憶體塊的size欄位的P位都會被置為1,以便於防止訪問前面的非法記憶體。當一個chunk的size的P位為0時,我們能通過prev_size欄位來獲取上一個chunk的大小以及地址。這也方便進行chunk間的合併
- NO_MAIN_ARENA
- 該chunk的大小,必須是2*SIZE_SZ的整數倍。如果申請的記憶體大小不是2*SIZE_SZ的整數倍,會被轉換滿足大小的最小的2*SIZE_SZ的倍數。32位系統中,SIZE_SZ是4;64位系統中,SIZE_SZ是8。該欄位的低三個位元位對chunk的大小沒有影響,它們從高到低分別表示:
- fd、bk
- chunk處於分配時,從fd欄位開始是使用者的資料。chunk空閒時,會被新增到對應的空閒管理連結串列中,其欄位的含義如下:
- fd:指向下一個(非物理相鄰)空閒的chunk
- bk:指向上一個(非物理相鄰)空閒的chunk
- 通過fd和bk可以將空閒的chunk塊加入到空閒的chunk塊連結串列中進行統一管理
- chunk處於分配時,從fd欄位開始是使用者的資料。chunk空閒時,會被新增到對應的空閒管理連結串列中,其欄位的含義如下:
- fd_nextsize、bk_nextsize,也是隻有chunk空閒的時候才使用,不過其用於較大的chunk(large chunk)
- fd_nextsize:指向前一個與當前chunk大小不同的第一個空閒塊,不包含bin的頭指標
- bk_nextsize:指向後一個與當前chunk大小不同的第一個空閒塊,不包含bin的頭指標
- 一半空閒的large chunk在fd的遍歷順序中,按照由大到小的順序排列,這樣做可以避免在尋找合適的chunk時挨個遍歷
2.2.1.2 已分配的chunk
一個已經分配的chunk的樣子如下,我們稱前兩個欄位稱為chunk header,後面的部分稱位user data。每次malloc申請內得到記憶體指標,其實指向user data的起始處
當一個chunk處於使用狀態時,它的下一個chunk的prev_size域無效,所以下一個chunk的該部分也可以被當前chunk使用,這就是chunk中的空間複用
An allocated chunk looks like this:
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2.2.1.2 已釋放的chunk
被釋放的chunk被記錄在連結串列中(可能是迴圈雙向連結串列,可能是單向連結串列),具體結構如下:
Free chunks are stored in circular doubly-linked lists, and look like this:
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' | Size of chunk, in bytes |A|0|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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|0|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2.2.2 bin
我們曾經說過,使用者釋放掉的chunk不會馬上歸還給系統,ptmalloc會統一管理heap和mmap對映區域中的空閒的chunk。當使用者再一次請求分配記憶體時,ptmalloc分配器會試圖在空閒的chunk中挑選一塊合適的給使用者。這樣可以避免頻繁的系統呼叫,降低記憶體分配時的開銷
在具體的實現中,ptmalloc採用分箱式方法對空閒的chunk進行管理。首先,它會根據空閒的chunk的大小,以及使用狀態將chunk初步分為4類:
- fast bins
- small bins
- large bins
- unsorted bin
每類中仍然有更細的劃分,相似大小的chun會用雙向連結串列連結起來,也就是說,每類bin的內部仍然會有多個互不相關的連結串列來儲存不同大小的chunk
對於small bins、large bins、unsorted bin來說,ptmalloc將它們維護在同一個陣列中,這些bin對應的資料結構在malloc_state中
- unsorted bin,字如其面,這裡面的chunk沒有進行排序,儲存的chunk比較雜
- 索引從2到63的bin稱位small bin,同一個small bin連結串列中的大小相同。兩個相鄰索引的small bin連結串列中的chunk大小相差的位元組數為2個機器字長,即32位相差8個位元組,64位相差16位元組
- small bins後面的bin被稱作large bins。large bins中的每一個bin都包含一定範圍內的chunk,其中的chunk按fd指標的順序從大到小排列。相同大小的chunk同樣按照最近使用順序排列
- ptmalloc為了提供分配的速度,會把一些小的chunk先放到fast bins的容器內。而且,fastbin容器中的chunk的使用標記總是被置位的,所以不滿足上面的原則
2.2.3 巨集觀結構arena
struct malloc_state
{
/* Serialize access. */
__libc_lock_define (, mutex);
/* Flags (formerly in max_fast). */
int flags;
/* Set if the fastbin chunks contain recently inserted free blocks. */
/* Note this is a bool but not all targets support atomics on booleans. */
int have_fastchunks;
/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;
/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];
/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];
/* Linked list */
struct malloc_state *next;
/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
struct malloc_state *next_free;
/* Number of threads attached to this arena. 0 if the arena is on
the free list. Access to this field is serialized by
free_list_lock in arena.c. */
INTERNAL_SIZE_T attached_threads;
/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
-
__libc_lock_define (, mutex);
- 該變數用於控制程式序列訪問同一個分配區,當一個執行緒獲取了分配區之後,其它執行緒要想訪問該分配區,就必須等待該執行緒完成後才能夠使用
-
flags
- flags記錄了分配區的一些標誌,不如bit 0記錄了分配區是否有fastbin chunk,bit 1標識分配區是否能返回連續的虛擬地址空間
-
fastbinsY[NFASTBINS]
- 存放每個fast chunk連結串列頭部分指標
-
top
- 指向分配區的top chunk
-
last_remainder
- 最新的chunk分割之後剩下的那部分
-
bins
- 用於儲存unstored bin,small bins和large bins的chunk連結串列
-
binmap
- ptmalloc用一個bit來標識某一個bin中是否包含空閒chunk
2.3 Use After Free
簡單地說,Use After Free就是其字面所表達的意思,當一個記憶體塊被釋放之後再次被使用。但是其實這裡有以下幾種情況:
- 記憶體塊被釋放後,其對應的指標被設定為NULL,然後再次使用,自然程式會崩潰
- 記憶體塊被釋放後,其對應的指標沒有被設定為NULL,然後在它下一次被使用之前,沒有程式碼對這塊記憶體塊進行修改,那麼程式很有可能可以正常運轉
- 記憶體塊被釋放後,其對應的指標沒有被設定為NULL,但是在它下一次使用之前,有程式碼塊對這塊記憶體進行了修改,那麼當程式再次使用這塊記憶體時,就很有可能出現奇怪的問題
- 而我們一般所指的Use After Free漏洞主要是後兩種。此外,我們一般稱被釋放後沒有被設定為NULL的記憶體指標為dangling pointer
2.4 Fastbin Attack
Fastbin Attack是一類漏洞的利用方法,是指所有基於fastbin機制的漏洞利用方法,這類利用的前提是:
- 存在堆溢位、use-after-free等能控制chunk內容的漏洞
- 漏洞發生於fastbin型別的chunk中
如果細分的話,可以做如下的分類:
- Fastbin Double Free
- House of Spirit
- Alloc to Stack
- Arbitrary Alloc
其中,前兩種主要漏洞側重於利用free函式釋放真的chunk或偽造的chunk,然後再次申請chunk進行攻擊,後兩種側重於故意修改fd指標,直接利用malloc申請指定位置chunk進行攻擊
fastbin attack存在的原因在於fastbin是使用單連結串列來維護釋放的堆塊的,並且由fastbin管理的chunk即使被釋放,其next_chunk的prev_inuse位也不會被清空。我們來看一下fastbin是怎樣管理空閒chunk的
int main(){
void *chunk1, *chunk2, *chunk3;
chunk1 = malloc(0x30);
chunk2 = malloc(0x30);
chunk3 = malloc(0x30);
//進行釋放
free(chunk1);
free(chunk2);
free(chunk3);
return 0;
}
程式執行完後的記憶體情況:
0x602000: 0x0000000000000000 0x0000000000000041 <== chunk1
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000041
0x602050: 0x0000000000602000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000041
0x602090: 0x0000000000602040 0x0000000000000000
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000020f41
2.4.1 Fastbin Double Free
Fastbin Double Free是指fastbin的chunk可以被多次釋放,因此可以再fastbin連結串列中存在多次。這樣導致的後果是多次分配可以從fastbin連結串列中取出同一個堆塊,相當於多個指標指向同一個堆塊,結合推塊的資料內容可以實現類似於型別混淆(type confused)的效果
Fastbin Double Free能夠成功利用主要有兩部分的原因:
- fastbin的堆塊釋放後next_chunk的pre_inuse位不會被清空
- fastbin在執行free的時候僅驗證了main_arena直接指向的塊,即連結串列指標頭部的塊。對於連結串列後面的塊,並沒有進行驗證
/* Another simple check: make sure the top of the bin is not the record we are going to add(i.e.,double free).*/
if(_builtin_expect(old == p, 0)){
errstr = "double free or corruption(fasttop)";
goto errout;
}
簡單繞過:
int main(void){
void *chunk1, *chunk2, *chunk3;
chunk1 = malloc(0x30);
chunk2 = malloc(0x30);
//進行釋放
free(chunk1);
free(chunk2);
free(chunk1);
return 0;
}
2.5 Unlink
我們在利用unlink所造成的漏洞時,其實就是對進行unlink chunk進行記憶體佈局,然後藉助unlink操作來達成修改指標的效果
我們先來回顧一下unlink的目的與過程,其目的是把一個雙向連結串列中的空閒塊拿出來(例如free時和目前物理相鄰的free chunk進行合併)。其基本的過程是:
2.5.1 古老的unlink
假設有空間連續的兩個chunk(Q,Nextchunk),其中Q處於使用狀態、Nextchunk處於釋放狀態。那麼如果我們通過某種方式(比如溢位)將Nextchunk的fd和bk指標修改為指定的值。則當我們free(Q)時:
- glibc判斷這個塊是small chunk
- 判斷向前合併,發現前一個chunk處於使用狀態,不需要前向合併
- 判斷後向合併,發現後一個chunk處於空閒狀態,需要合併
- 繼而對Nextchunk採取unlink操作
那麼unlink具體執行的效果是什麼樣子呢?我們可以來分析一下(這裡P即為Nextchunk):
- FD = P->fd = target addr -12
- BK = P->bk = expect value
- FD->bk - BK,即*(target addr - 12 + 12) = BK = expect value
- BK->fd = FD,即*(expect value + 8) = FD = target addr -12
最後實現的效果就是:*(target addr) = expect value,可以將惡意程式碼存放在expect value處,將target addr裡面存的值為expect value的記憶體地址
看起來我們似乎可以通過 unlink 直接實現任意地址讀寫的目的,但是我們還是需要確保 expect value +8 地址具有可寫的許可權。
比如說我們將 target addr 設定為某個 got 表項,那麼當程式呼叫對應的 libc 函式時,就會直接執行我們設定的值(expect value)處的程式碼。需要注意的是,expect value+8 處的值被破壞了,需要想辦法繞過。
對於musl libc就是利用的上述雙向連結串列演算法,沒有任何檢查
2.5.2 當前的unlink(glibc)
// fd bk
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
此時
- FD->bk = target addr - 12 + 12=target_addr
- BK->fd = expect value + 8
相當於我們是釋放Q,來觸發unlink Nextchunk,然後利用unlink Nextchunk實現任意地址寫
那麼我們上面所利用的修改 GOT 表項的方法就可能不可用了。但是我們可以通過偽造的方式繞過這個機制,相當於抽象虛擬了兩個chunk,一個是fakeFD當作Nexchunk的下一個chunk用來存放偽造的bk指標,一個是fakeBK當作Nexchunk的上一個chunk用來存放偽造的fd指標
首先我們通過覆蓋,將 nextchunk 的 FD 指標指向了 fakeFD,將 nextchunk 的 BK 指標指向了 fakeBK 。那麼為了通過驗證,我們需要
fakeFD -> bk == P
<=>*(fakeFD + 12) == P
fakeBK -> fd == P
<=>*(fakeBK + 8) == P
當滿足上述兩式時,可以進入 Unlink 的環節,進行如下操作:
fakeFD -> bk = fakeBK
<=>*(fakeFD + 12) = fakeBK
fakeBK -> fd = fakeFD
<=>*(fakeBK + 8) = fakeFD
如果讓 fakeFD + 12 和 fakeBK + 8 指向同一個指向 P 的指標,那麼:
*P = P - 8
*P = P - 12
利用思路:
- 條件
- UAF ,可修改 free 狀態下 smallbin 或是 unsorted bin 的 fd 和 bk 指標
- 已知位置存在一個指標指向可進行 UAF 的 chunk
- 效果
使得已指向 UAF chunk 的指標 ptr 變為 ptr - 0x18
- 思路
設指向可 UAF chunk 的指標的地址為 ptr
- 修改 fd 為 ptr - 0x18
- 修改 bk 為 ptr - 0x10
- 觸發 unlink
ptr 處的指標會變為 ptr - 0x18
2.6 堆中的Off-By-One
嚴格來說Off-By-One漏洞是一種特殊的溢位漏洞,off-By-One指程式向緩衝區中寫入時,寫入的位元組數超過了這個緩衝區本身所申請的位元組數並且只越界了一個位元組
off-by-one是指單位元組緩衝區溢位,這種漏洞的產生往往與邊界驗證不嚴和字串操作有關,當然也不排除寫入的size正好就只多了一個位元組的情況。其中邊界驗證不嚴通常包括:
- 使用迴圈語句向堆塊中寫入資料時,迴圈的次數設定錯誤(這在 C 語言初學者中很常見)導致多寫入了一個位元組。
- 字串操作不合適
一般來說,單位元組溢位被認為是難以利用的,但是因為 Linux 的堆管理機制 ptmalloc 驗證的鬆散性,基於 Linux 堆的 off-by-one 漏洞利用起來並不複雜,並且威力強大。 此外,需要說明的一點是 off-by-one 是可以基於各種緩衝區的,比如棧、bss 段等等,但是堆上(heap based) 的 off-by-one 是 CTF 中比較常見的。我們這裡僅討論堆上的 off-by-one 情況
2.7 Unsorted Bin Attack
- 當一個較大的chunk被分割成兩半後,如果剩下的部分大於MINSIZE,就會被放到Unsorted Bin中
- 釋放一個不屬於fastbin的chunk,並且該chunk不和top chunk緊鄰時,該chunk會被首先放到unsorted bin中
- 當進行malloc_consolidate時,可能會把合併後的chunk放到unsorted bin中,如果不是和top chunk近鄰的話
基本使用情況 :
- Unsorted Bin 在使用的過程中,採用的遍歷順序是 FIFO,即插入的時候插入到 unsorted bin 的頭部,取出的時候從連結串列尾獲取。
- 在程式 malloc 時,如果在 fastbin,small bin 中找不到對應大小的 chunk,就會嘗試從 Unsorted Bin 中尋找 chunk。如果取出來的 chunk 大小剛好滿足,就會直接返回給使用者,否則就會把這些 chunk 分別插入到對應的 bin 中
在 glibc/malloc/malloc.c 中的 _int_malloc
有這麼一段程式碼,當將一個 unsorted bin 取出的時候,會將 bck->fd
的位置寫入本 Unsorted Bin 的位置:
/* remove from unsorted list */
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
換而言之,如果我們控制了 bk 的值,我們就能將 unsorted_chunks (av)
寫到任意地址
2.8 House of Orange
house of orange利用方法來自於 Hitcon CTF 2016 中的一道同名題目。由於這種利用方法在此前的 CTF 題目中沒有出現過,因此之後出現的一系列衍生題目的利用方法我們稱之為 House of Orange
首先需要知道目標漏洞是堆上的漏洞但是特殊之處在於題目中不存在free函式或其他釋放堆塊的函式,我們一般知道想要利用堆漏洞,需要對堆塊進行malloc和free操作,但是在House of Orange利用中無法使用free函式,因此House of Orange核心就是通過漏洞利用來獲得free的效果
假設目前的top chunk已經不滿足malloc的分配需求,首先我們在程式中的malloc呼叫會執行到_int_malloc函式中,在_int_malloc函式中,會依次檢驗fastbin、small bins、unsorted bin、large bins是否可以滿足分配要求,假如都不能滿足使用者申請堆記憶體的要求,需要執行sysmalloc來向系統申請更多的空間。但是對於堆來說有mmap和brk兩種分配方式,我們需要讓堆以brk的形式擴充,之後原有的top chunk會被置於unsorted bin中
有以下要求:
- 偽造的size必須要對齊到記憶體頁
- size要大於MINISIZE(0x10)
- size要小於之後申請的chunksize + MINISIZE(0x10)
- size的prev inuse位必須為1
2.9 tcache
在2.26及以後的glibc版本中加入了tcache機制管理長度較小的堆塊,其優先順序很高,會先於全部的bin來處理。每個連結串列的個數是一定的,當快取連結串列裝滿時,分配方式就與之前版本的malloc相同
- 單連結串列結構
- LIFO分配策略
- 申請堆塊時不檢查size欄位
- 不檢查double free
可以看出來tcache相當於一種弱化的fastbin,fastbin上的所有攻擊方法都可以在tcache中實現,並且攻擊者無需偽造堆塊,利用起來更加容易
2.10 IO_FILE Related
2.10.1 FILE結構體概述
FILE 在 Linux 系統的標準 IO 庫中是用於描述檔案的結構,稱為檔案流。 FILE 結構在程式執行 fopen 等函式時會進行建立,並分配在堆中。我們常定義一個指向 FILE 結構的指標來接收這個返回值
FILE 結構定義在 libio.h 中,如下所示
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
程式中的 FILE 結構會通過_chain 域彼此連線形成一個連結串列,連結串列頭部用全域性變數_IO_list_all 表示,通過這個值我們可以遍歷所有的 FILE 結構。
在標準 I/O 庫中,每個程式啟動時有三個檔案流是自動開啟的:stdin、stdout、stderr。因此在初始狀態下,_IO_list_all 指向了一個有這些檔案流構成的連結串列,但是需要注意的是這三個檔案流位於 libc.so 的資料段。而我們使用 fopen 建立的檔案流是分配在堆記憶體上的
我們可以在 libc.so 中找到 stdin\stdout\stderr 等符號,這些符號是指向 FILE 結構的指標,真正結構的符號是
- _IO_2_1_stderr_
- _IO_2_1_stdout_
- _IO_2_1_stdin_
但是事實上_IO_FILE 結構外包裹著另一種結構_IO_FILE_plus,其中包含了一個重要的指標 vtable 指向了一系列函式指標
在 libc2.23 版本下,32 位的 vtable 偏移為 0x94,64 位偏移為 0xd8
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}
vtable 是 IO_jump_t 型別的指標,IO_jump_t 中儲存了一些函式指標,在後面我們會看到在一系列標準 IO 函式中會呼叫這些函式指標
void * funcs[] = {
1 NULL, // "extra word"
2 NULL, // DUMMY
3 exit, // finish
4 NULL, // overflow
5 NULL, // underflow
6 NULL, // uflow
7 NULL, // pbackfail
8 NULL, // xsputn #printf
9 NULL, // xsgetn
10 NULL, // seekoff
11 NULL, // seekpos
12 NULL, // setbuf
13 NULL, // sync
14 NULL, // doallocate
15 NULL, // read
16 NULL, // write
17 NULL, // seek
18 pwn, // close
19 NULL, // stat
20 NULL, // showmanyc
21 NULL, // imbue
};
2.10.2 偽造vtable劫持程式流程
前面我們介紹了 Linux 中檔案流的特性(FILE),我們可以得知 Linux 中的一些常見的 IO 操作函式都需要經過 FILE 結構進行處理。尤其是_IO_FILE_plus 結構中存在 vtable,一些函式會取出 vtable 中的指標進行呼叫
因此偽造 vtable 劫持程式流程的中心思想就是針對_IO_FILE_plus 的 vtable 動手腳,通過把 vtable 指向我們控制的記憶體,並在其中佈置函式指標來實現
因此 vtable 劫持分為兩種:
- 一種是直接改寫 vtable 中的函式指標,通過任意地址寫就可以實現
- 另一種是覆蓋 vtable 的指標指向我們控制的記憶體,然後在其中佈置函式指標
在2.24版本的glibc中,全新加入了針對_IO_FILE_plus的vtable的劫持的檢測措施,glibc會在呼叫虛擬函式之前首先檢查vtable地址的合法性。首先會驗證vtable是否位於_IO_vtable段中,如果滿足條件就執行,否則會呼叫_IO_vtable_check做進一步檢查
洩露flags資訊,覆蓋flags洩露資訊,用這個方法leak的核心就是_IO_IS_APPENDING這個flag值,將這個flag搞成1之後,就可以通過修改_IO_buf_base來完成leak
而在賽題中,只需要爆破半個位元組就可以拿到stdout的地址,然後從stdout開頭開始覆蓋,改掉flag之後再低位覆蓋_IO_buf_base就可以完成leak,中間的三個變數在輸出的過程中都不怎麼用得到,直接蓋成0就行了
2.11 Glibc-2.29新特性
2.11.1 tcache新增防護機制
typedef struct tcache_entry{
struct tcache entry *next;
struct tcache perthread_struct *key;
}tcache_entry;
/* This test succeeds on double free. However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
if (__glibc_unlikely (e->key == tcache))
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = tmp->next)
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}
這裡會對tcache
連結串列上的所有chunk進行對比,檢測是否有重複,這讓原本在glibc-2.27
和glibc-2.28
肆虐的tcache double free
攻擊很難實施,但是鑑於tcache
的特性,tcache
的利用還是要比其他的bins
方便很多
2.11.2 unsorted bin
原本這裡僅僅有size檢查。
bck = victim->bk;
size = chunksize (victim);
mchunkptr next = chunk_at_offset (victim, size);
if (__glibc_unlikely (size <= 2 * SIZE_SZ)
|| __glibc_unlikely (size > av->system_mem))
malloc_printerr ("malloc(): invalid size (unsorted)");
if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ)
|| __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
malloc_printerr ("malloc(): invalid next size (unsorted)");
if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size))
malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)");
if (__glibc_unlikely (bck->fd != victim)
|| __glibc_unlikely (victim->fd != unsorted_chunks (av)))
malloc_printerr ("malloc(): unsorted double linked list corrupted");
if (__glibc_unlikely (prev_inuse (next)))
malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)");
然後在glibc-2.29
中,增加了如下檢查:
- 對下一個相鄰chunk的size檢查
- 對下一個相鄰chunk的
prev_size
進行檢查 - 檢查
unsorted bin
雙向連結串列的完整性,對unsorted bin attack
可以說是很致命的檢查 - 對下一個chunk的
prev_inuse
位進行檢查
這麼多的檢查,將使得unsorted bin
更難利用。
下面這個檢查早在glibc-2.28
就有了,這裡提一下,主要是防護unsorted bin attack
2.11.3 top chunk
對於top chunk
增加了size
檢查,遏制了House of Force
攻擊。
victim = av->top;
size = chunksize (victim);
if (__glibc_unlikely (size > av->system_mem))
malloc_printerr ("malloc(): corrupted top size");
三、Linux核心
- Linux保護機制更多,限制更多
- SMEP
- 即Supervisor Mode Execution Protection(管理模式執行保護)。如果處理器處於ring0模式,並試圖執行有user資料的記憶體時,就會觸發一個頁錯誤,用來保護核心使其不允許執行使用者空間程式碼
3.1 ret2user
簡單舉例:
- 由核心棧溢位
- 沒有開啟KALSR、Canary、SMEP
驅動通過_copy_from_user
將使用者輸入的資料讀入到了核心棧中的buffer,由於沒有限制長度,所以是簡單的棧溢位,類似ret2shellcode(沒開SMEP)
-
ret2usr 攻擊利用了 使用者空間的程式不能訪問核心空間,但核心空間能訪問使用者空間 這個特性來定向核心程式碼或資料流指向使用者控制元件,以
ring 0
特權執行使用者空間程式碼完成提權等操作 -
溢位後,直接將返回地址覆寫為使用者態呼叫
commit_creds(prepare_kernel_cred(0));
的函式的地址進行提權,然後使用iretq指令從核心態返回到使用者態,從而get shell -
所以還需要偽造好之後執行iretq指令會彈回到暫存器中的值,這裡有兩種偽造的思路:
- 在存放全域性變數的bss段上進行偽造,並修改rsp指標進行棧劫持
- 直接在核心棧上進行偽造
-
從核心態拿到使用者態的shell
3.2 核心ROP
再舉一例:
- 有核心棧溢位
- 開啟了SMEP
- 沒有開啟KALSR、Canary
系統根據cr4暫存器的值判斷是否開啟了smep,然而rc4暫存器可以使用mov指令進行修改,這提供了兩種思路:
- 利用ROP,直接執行
commit_creds(prepare_kernel_cred(0));
,然後iret返回使用者空間 - 利用ROP設定cr4暫存器的值,關閉smep,然後進行ret2user攻擊
qwd2018_core
3.3 Double_fetch
Double Fetch 從漏洞原理上屬於條件競爭漏洞,是一種核心態與使用者態之間的資料訪問競爭
在 Linux 等現代作業系統中,虛擬記憶體地址通常被劃分為核心空間和使用者空間。核心空間負責執行核心程式碼、驅動模組程式碼等,許可權較高。而使用者空間執行使用者程式碼,並通過系統呼叫進入核心完成相關功能。通常情況下,使用者空間向核心傳遞資料時,核心先通過通過 copy_from_user 等拷貝函式將使用者資料拷貝至核心空間進行校驗及相關處理,但在輸入資料較為複雜時,核心可能只引用其指標,而將資料暫時儲存在使用者空間進行後續處理。此時,該資料存在被其他惡意執行緒篡改風險,造成核心驗證通過資料與實際使用資料不一致,導致核心程式碼執行異常
3.4 UAF
核心的UAF和使用者態的差不多,不同的是核心使用的是slab/slub分配器來管理堆塊
四、格式化字串漏洞利用技術
格式化字串漏洞(format string)主要是printf函式家族的問題。printf、fprintf、sprintf、snprintf等格式化字串函式可以接受可變數量的引數,並將第一個引數作為格式化字串,根據其來解析之後的引數
重要特性:
- printf()函式的引數個數不固定
- printf()函式的引數分成兩個部分(第一個引數中的格式化字串的數量決定了後面引數的數量)
Printf()的棧操作:
五、Linux保護手段
【面試原題】:上述漏洞有什麼防護措施?
常見的保護機制:
- Canary
- Fortify
- NX/DEP
- PIE/ASLR
- RELRO
5.1 NX保護
作用:
將資料(堆、棧)所在記憶體頁標識為不可執行,當程式溢位成功轉入shellcode時,程式會嘗試在資料頁面上執行指令,此時CPU就會丟擲異常,而不是去執行惡意指令
編譯選項:
- 關閉:-z execstack
- 開啟:-z noexecstack
5.2 PIE保護
作用:
使得程式地址空間分佈隨機化,增加ROP等利用的難度(因為地址都是不確定的了)
編譯選項:
- 關閉:-no-pie
- 開啟:-pie -fPIC
5.3 Canary保護
作用:
函式開始執行的時候會先往棧裡插入Canary的值,當函式真正返回的時候會驗證Canary值是否合法,如果不合法就停止程式執行。可以防止棧溢位覆蓋返回地址
編譯選項:
- 關閉:-fno-stack-protector
- 啟用(只為區域性變數中含有char的函式插入保護程式碼):-fstack-protector
- 啟用(為所有函式插入保護程式碼):-fstack-protector-all
5.4 Fortify保護
作用:
主要用來防止格式化字串漏洞。包含%n的格式化字串不能位於程式記憶體中的可寫地址。當使用位置引數時,必須使用範圍內的所有引數,如果要使用%7$x,必須同時使用1$,2$,3$,4$,5$,6$
編譯選項:
- 關閉:-D_FORTIFY_SOURCE = 0
- 開啟:-D_FORTIFY_SOURCE = 2
5.5 RELRO保護
作用:
設定符號重定向表為只讀並在程式啟動時就解析並繫結所有動態符號,從而減少對GOT(Global Offset Table)表攻擊
編譯選項:
- 開啟(部分):-z lazy
- 開啟(完全):-z now
相關文章
- 二進位制與二進位制運算
- 進位制詳解:二進位制、八進位制和十六進位制
- JavaScript 二進位制、八進位制與十六進位制JavaScript
- 二進位制
- (二進位制)
- 十進位制——二 (八、十六 )進位制
- 二進位制,八進位制,十進位制,十六進位制的相互轉換
- KITTI-二進位制點雲資料集使用筆記筆記
- 【進位制轉換】二進位制、十六進位制、十進位制、八進位制對應關係
- 二進位制、十進位制與十六進位制相互轉化
- java中二進位制、八進位制、十進位制、十六進位制的轉換Java
- 二進位制,八進位制,十進位制,十六進位制之間的轉換
- 計算機基礎進位制轉換(二進位制、八進位制、十進位制、十六進位制)計算機
- 二進位制轉十進位制快速方法
- JAVA 二進位制,八進位制,十六進位制,十進位制間進行相互轉換Java
- 什麼是二進位制?二進位制如何轉換?
- 04 二進位制
- Cocoapods 二進位制
- leetcode -- 二進位制LeetCode
- 十進位制轉二進位制推導(草稿)
- JavaScript十進位制轉換為二進位制JavaScript
- [計算機基礎] 計算機進位制轉換:二進位制、八進位制、十進位制、十六進位制計算機
- 進位制之間的轉換之“十六進位制 轉 十進位制 轉 二進位制 方案”
- 一看就懂二進位制、八進位制、十六進位制數轉換十進位制
- 整數轉化成八進位制、十六進位制、二進位制,以及轉回
- 進位制與二進位制及相關轉換
- 二進位制或序列
- 二進位制陣列陣列
- 僅做筆記用:base64字串轉換為十六進位制形式表示的二進位制資料筆記字串
- 二進位制檔案記憶體對映記憶體
- 折騰筆記[1]-打包ffmpeg-cli到單個wasm二進位制筆記ASM
- 3416:【例72.1】 二進位制轉化為十進位制
- 遞迴函式實現十進位制正整數轉換為二進位制,八進位制,十六進位制遞迴函式
- java二進位制運算十進位制(精確運算)Java
- Python 十進位制轉換為二進位制 高位補零Python
- Solidity語言學習筆記————45、應用二進位制介面(ABI)說明Solid筆記
- 【MySQL解惑筆記】Mysql5.7.x無法開啟二進位制日誌MySql筆記
- 輸出二進位制數