棧介紹
在程序運行中用於保存函數調用信息和局部變量,程序的棧是從進程的虛擬地址空間的高地址向低地址增長的。
x86和x64傳參
- x86:函數參數在函數返回地址的上方。
- x64:前六個參數一次保存在rdi、rsi、rdx、rcx、r8、r9寄存器中,如果還有更多的參數的話才會保存在棧上。
x64中內存地址不能大於0x0007FFFFFFFFFFF,6字節長度,否則會拋出異常。
棧溢出原理
棧溢出指的是程序向棧中某個變量寫入的字節超過了這個變量本身所申請的字節數,因而導致與其相鄰的棧中的變量的值被改變。
棧溢出的基本前提:
- 程序必須向棧上寫入數據。
- 寫入的數據大小沒有被良好地控制。
利用步驟
首先尋找危險函數,常見的危險函數如下:
- 輸入:
- get,直接讀取一行,忽略'\x00'
- scanf
- vscanf
- 輸出:
- sprintf
- 字符串:
- strcpy,字符串復制,遇到'\x00'停止
- strcat,字符串拼接,遇到'\x00'停止
- bcopy
其次,計算我們所要操作的地址與我們所要覆蓋的地址的距離。
ida中的幾種索引:
- 相對於棧基地址的索引,可以直接通過查看ebp相對偏移獲得。
- 相對應棧指針的索引,一般需要進行調試,之后還是會轉換到第一種類型。
- 直接地址索引,就相當於直接給定了地址。
一般來說,有如下覆蓋需求:
- 覆蓋函數返回地址,這時候就直接看EBP即可。
- 覆蓋棧上某個變量的內容,這時候就需要更加精細的計算了。
- 覆蓋bss段某個變量的內容。
- 根據現實執行情況,覆蓋特定的變量或地址的內容。
之所以我們想要覆蓋某個地址,是因為我們想通過覆蓋地址的方法來直接或者間接地控制程序執行流程。
基本ROP
隨着NX保護的開啟,直接往棧上寫shellcode並控制執行的方式已經不可行了,於是提出了新的技術————ROP。其主要思想是在棧緩沖區溢出的基礎上,利用程序中已有的小片段(gadgets)來改變某些寄存器或者變量的值,從而控制程序的執行流程。
之所以稱之為ROP,是因為核心在於利用了指令集中的ret指令,改變了指令流的執行順序。ROP攻擊一般得滿足如下條件:
- 程序存在溢出,並且可以控制返回地址。
- 可以找到滿足條件的gadgets以及相應gadgets的地址。
如果gadgets每次的地址都是不固定的,那我們就需要想辦法動態獲取對應的地址。
ret2text
ret2text即控制程序執行程序本身已有的代碼(.text)。其實,這種攻擊犯法是一種籠統的描述。我們控制執行程序已有的代碼的時候也可以控制程序執行好幾段不相鄰的程序已有的代碼(也就是gadgets),這就是我們所要說的ROP。
ret2shellcode
ret2shellcode即控制程序執行shellcode代碼。一般來說,shellcode需要我們自己編寫。在棧溢出的基礎上,要想執行shellcode,需要對應的binary在運行時,shellcode所在的區域具有可執行權限。
ret2syscall
ret2syscalll,即控制程序執行系統調用,獲取shell。
ret2libc
ret2libc及控制程序執行libc中的函數,通常是返回至某個函數的plt處或者函數的具體位置(即函數對應的got表項的內容)。一般情況下,我們會選擇執行system("/bin/sh"),故而此時我們需要知道system函數的地址。
注意兩點:
- 可以通過覆蓋返回地址為pop來抬高棧,不需要再次調用漏洞函數。
- 在利用是通常通過泄露got表地址來泄露libc,由於libc的延遲綁定機制,我們需要泄露已經執行過的函數的地址。
中級ROP
中級ROP主要是使用了一些比較巧妙的Gadgets
ret2csu
在64位程序中,函數的前6個參數是通過寄存器傳遞的,但是大多數時候,我們很難找到每個寄存器對應的gadgets。這時候,我們可以利用x64下的_libc_csu_init中的gadgets。這個函數是用來對libc進行初始化操作的,而一般程序都會調用libc函數,所以這個函數一定會存在。
改進
並不是所有的程序漏洞都可以讓我們輸入足夠長字節。當允許輸入字節數較少時,有以下兩種方法:
(1) 提前控制RBX與RBP
這兩個寄存器的值主要是為了滿足cmp條件,並進行跳轉。如果我們可以提前控制這兩個數值,那么我們就可以減少16字節。
(2) 多次利用
gadgets是分為兩部分的,那么我們其實可以進行兩次調用來達到目的,以便於減少一次gadgets所需要的字節數。但這里的多次利用需要更嚴格的條件:
- 漏洞可以觸發
- 在兩次觸發之間,程序尚未修改r12-r15寄存器,這是因為要兩次調用。
當然,有時候我們也會遇到一次性可以讀入大量的字節,但是不允許漏洞再次利用的情況,這時候就需要我們一次性將所有的字節布置好,之后慢慢利用。
gadget
由於PC本身只是將程序的執行地址處的數據傳遞給CPU,而CPU則知識對傳遞來的數據進行解碼,只要解碼成功,就會進行執行。所以我們可以將源程序中一些地址進行偏移從而來獲取我們所想要的指令,只要可以確保程序不崩潰。
ret2reg
- 查看溢出函數返回時哪個寄存器的值指向溢出緩沖區空間。
- 然后通過call reg或者jmp reg指令,將EIP設置為該指令地址。
- reg所指向的空間上注入Shellcode(需要確保該空間是可以執行的,但通常都是在棧上)
BROP
BROP是沒有對應應用程序的源代碼或者二進制文件下,對程序進行攻擊,劫持程序流。
攻擊條件
- 源程序必須存在棧溢出漏洞,以便於攻擊者可以控制程序流程。
- 服務器端的進程在崩潰之后會重新啟動,並且重新啟動的進程地址與先前的地址一樣(這也就是說即使程序有ASLR保護,但是其只是在程序最初啟動的時候有效果)。
攻擊原理
大部分應用都會開啟ASLR、NX、Canary保護。分別看看BROP如何繞過這些保護,以及如何進行攻擊。
基本思路
- 判斷棧溢出長度,通過暴力枚舉。
- Stack Reading,獲取棧上的數據來泄露canaries,以及ebp和返回地址。
- Blind ROP,找到足夠多的gadgets來控制輸出函數的參數,並且對其進行調用,比如常見的write函數以及puts函數。
- Build the exploit,利用輸出函數來dump出程序以便於來找到更多的gadgets,從而可以寫出最后的exploit。
棧溢出長度
直接從1暴力枚舉即可,直到發現程序崩潰。
Stack Reading
棧布局
buffer|canary|saved fame pointer|saved returned address
buffer的長度我們可以通過暴力枚舉獲取。
其次,關於canary以及后面的變量,我們可以采取爆破的方法。
攻擊條件2表明了程序本身並不會應為crash有變化,所以每次的canary等值都是一樣的。所以我們可以按字節爆破。每個字節最多有256種可能,所以在32位的情況下,我們最多需要爆破1024次,64位最多爆破2048次。(其實canary的最低字節一般都是00,所以爆破次數更小)
Blind ROP
最朴素的執行write函數的方法就是構造系統調用。
pop rdi; ret # socket
pop rsi; ret # buffer
pop rdx; ret # length
pop rax; ret # write syscall number
syscall
但通常來說,想要找到一個syscall的地址基本可能,我們可以通過轉換為找write的方式來獲取。
通過前文我們知道,我們可以在libc_csu_init中通過偏移源碼找到
pop rsi pop rdi
pop r15 ret
ret
這樣我們就能獲取write函數調用的前兩個參數。
我們可以通過plt表來獲取write的地址。
write還有個參數length,一般來說程序中的rdx經常性會不是0,但是為了更好地控制程序輸出,我們仍然盡量可以控制這個值。但是,在程序
pop rdx; ret
這樣的指令幾乎沒有。那么,我們該如何控制rdx的值呢?字符串比較函數strcmp在執行的時候,rdx會被設置為將要被比較的字符串的長度,所以我們可以找到strcmp函數,從而來控制rdx。
那么接下來問題就簡單了,分為兩個步驟
一、尋找gadgets
由於尚未知道程序具體長什么樣,所以我們只能通過簡單的控制程序的返回地址為自己設置的值,從而來猜測相應的gadgets。當我們控制程序的返回地址時,一般有一下幾種情況:
- 程序直接崩潰
- 程序運行一段時間后崩潰
- 程序一直運行而並不崩潰
為了尋找合理的gadgets,我們可以分為一下兩步:
(1)尋找stop gadgets
stop gadgets一般指的是這樣一段代碼:當程序執行這段代碼時,程序會進入無限循環,這樣使得攻擊者能夠一直保持連接狀態。(其實stop gadget也並不一定得是上面的樣子,其根本目的在於告訴攻擊者,所測試的返回地址是一個gadgets)
之所以要尋找stop gadgets,是因為當我們猜到某個gadgets后,如果我們僅僅是將其布置在棧上,由於執行完這個gadget之后,程序還會跳到棧上的下一個地址。如果該地址是非法地址,那么程序就會crash。這樣的話,在攻擊者看來程序只是單純的crash了。因此,攻擊者就會認為在這個過程中並沒有執行到任何的useful gadget,從而放棄它。
但是如果我們布置了stop gadget,那么對於我們所要嘗試的每一個地址,如果它是一個gadget的話,那么程序就不會崩潰。接下來,就是去想辦法識別這些gadget。
(2)識別gadgets
那么,我們該如何識別這些gadgets呢?我們可以通過棧布局以及程序的行為來識別。為了更容易介紹,這里定義棧上的三種地址:
- probe,探針,也就是我們想要探測的代碼地址。一般來說,都是64位程序,可以直接從0x400000嘗試,如果不成功,有可能程序開啟了PIE保護,再不濟,就可能是程序是32位了。
- stop,不會使得程序崩潰的stop gadget的地址。
- trap,可以導致程序崩潰的地址。
我們可以通過在棧上擺放不同順序的Stop與Trap從而來識別出正在執行的指令。因為執行Stop意味着程序不會崩潰,執行Trap意味着程序會立即崩潰。
但是即使是這樣,我們仍然難以識別出正在執行的gadget到底是在對哪個寄存器進行操作。但如果遇到能通過
probe, trap, trap, trap, trap, trap, trap, stop, traps
這樣的gadgets,那么有很大可能這個gadgets就是brop gadgets,此外,這個gadgets通過錯位還可以生成pop rsp
等這樣的gadgets,可以使得成功內需崩潰也可以作為識別這個gadgets的標志。
此外根據我們之前學的ret2libc_csu_init可以指導該地址減去0x1a就會得到其上一個gadgets。可以供我們調用其他函數。
需要注意的是probe可能是一個stop gadget,我們得去檢查一下,怎么檢查呢?我們只需要讓后面的所有內容變為trap地址即可。因為如果是stop gadget的話,程序會正常執行,否則就會崩潰。
二、尋找plt
程序的plt表具有比較規整的結構,每個plt表項都是16字節。而且,在每個表項的6字節偏移處,是該表項對應的函數的解析路徑,即程序最初執行該函數的時候,會執行該路徑對函數的got地址進行解析。
此外,對於大多數plt調用來說,一般都不容易崩潰,即使時使用了比較奇怪的參數。所以說,如果我們發現了一系列長度為16的沒有使得程序崩潰的代碼段,那么我們有一定理由相信我們遇到了plt表。除此之外,我們還可以通過前后偏移6字節,來判斷我們是處於plt表項中間還是開頭。
(1)控制RDX
當我們找到plt表之后,我們如何確認strcmp的位置呢?需要提前說的是,並不是所有程序都會調用strcmp函數,哪我們就得利用其他方式來控制rdx的值了。這里給出程序中使用strcmp函數的情況。
之前,我們已經找到了brop的gadgets,所以我們可以控制函數的前兩個參數了。與此同時,我們定義一下兩種地址:
- readable,可讀的地址。
- bad,非法地址,不可訪問,比如說0x0。
那么我們如果控制傳遞的參數為這兩種地址的組合,會出現以下四種情況:
- strcmp(bad,bad)
- strcmp(bad,readable)
- strcmp(readable,bad)
- strcmp(readable,readable)
只有最后一種格式,程序才會正常執行。
那么我們該如何具體地去做呢?有一種比較直接的方法就是從頭到尾一次掃描每個plt表項,但是這個卻比較麻煩。我們可以選擇如下的一種方法:
- 利用plt表項的慢路徑
- 並且利用下一個表項的慢路徑的地址來覆蓋返回地址。
這樣,我們就不用來回控制相應的變量了。
當然,我們也可能碰巧找到strncmp或者strcasecmp函數,它們具有和strcmp一樣的效果。
(2)尋找輸出函數write@plt
當我們可以控制write函數的三個參數的時候,我們就可以再次遍歷所有的plt表,根據write函數將會輸出內容來找到對應的函數。需要注意的是,這里有個比較麻煩的地方在於我們需要找到文件描述符的值。一般情況下,我們有兩種方法找到這個值:
- 使用rop chain,同時使得每個rop對應的文件描述符不一樣。
- 同時打開多個連接,並且我們使用相對較高的數值來試一試。
需要注意的是:
- linux默認情況下,一個進程最多只能打開1024個文件描述符。
- posix標准么此申請的文件描述符數值總是當前最小可用數值。
(3)當然也可以puts@plt
尋找puts函數,我們自然需要控制rdi參數,在上面,我們已經找到了brop gadget。那么,我們根據brop gadget偏移9可以得到相應的gadgets(由ret2libc_csu_init中后續可得)。同時在程序還沒有開啟PIE保護的情況下,0x400000處為ELF文件的頭部,其內容為\x7fELF。所以我們可以根據這個來進行判斷。一般來說,其payload如下:
payload = 'A'*length +p64(pop_rdi_ret)+p64(0x400000)+p64(addr)+p64(stop_gadget)
到了這里,攻擊者已經可以控制輸出函數了,那么攻擊者就可以輸出.text段更多的內容以便來找到更多合適gadgets。同時,攻擊者還可以找到一些其它函數,如dup2或者execve函數。一般來說,攻擊者此時會去做如下事情:
- 將socket輸出重定向到到輸入輸出。
- 尋找"/bin/sh"的地址。一般來說,最好是找到一塊可寫的內存,利用write函數將這個字符串寫到相應的地址。
- 執行execve獲取shell,獲取execve不一定在plt表中,此時攻擊者就需要想辦法執行系統調用了。
高級ROP
高級ROP其實和一般的ROP基本一樣,其主要的區別在於它利用了一些更加低層的原理。
ret2_dl_runtime_resolve
原理
linux中是利用_dl_runtime_resolve(link_map_obj,reloc_index)來對動態鏈接的函數進行重定位的。如果我們可以控制相應的參數以及其對應地址的內容就可以控制解析的函數。具體利用方式如下:
-
控制程序執行dl_resolve函數
- 給定Link_map以及index兩個的參數。
- 當然我們可以直接定plt0對應的匯編代碼,這時,我們就需要一個index就足夠了。
-
控制index的大小,以便於指向自己所控制的區域,從而偽造一個指定的重定位表項。
-
偽造重定位表項,使得重定位表項所指的符號也在自己可以控制的范圍內。
-
偽造符號內容,使得符號對應的名稱也在自己可以控制的范圍內。
此外,這個攻擊成功的很必要條件:
- dl_resolve函數不會檢查對應的符號是否越界,它只會根據我們所定的數據來執行。
- dl_resolve函數最后的解析根本上依賴於所給定的字符串。
注意:
-
符號版本信息
- 最好使得 ndx = VERSYM[(reloc->r_info) >> 8] 的值為 0,以便於防止找不到的情況。
-
重定位表項
- r_offset 必須是可寫的,因為當解析完函數后,必須把相應函數的地址填入到對應的地址。
具體例子運用可參考ret2_dl_runtime_resolve繞過NX和ASLR的限制
SROP
signal機制
signal機制是類unix系統中進程之間相互傳遞信息的一種方法。一般,我們稱其為軟中斷信號,或者軟中斷。比如說,進程之間可以通過系統調用kill來發送軟中斷信號。一般來說,信號機制常見步驟如下圖所示:
- 內核向某個進程發送signal機制,該進程會暫時被掛起,進入內核態。
- 內核會為該進程保存相應的上下文,主要是將所有寄存器壓入棧中,以及壓入signal信息,以及指向sigreturn的系統調用地址。此時棧的結構如下:
ucontext
singinfo
sigreturn
我們稱ucontext以及siginfo這一段為Singal Frame。需要注意的是,這一部分是在用戶進程的地址空間的。之后會跳轉到注冊過的signal handler中處理相應的signal。因此,當signal handler執行完之后,就會執行sigreturn代碼。
對於Signal Frame來說,會因為架構的不同而有所區別,這里分別給出x86以及x64的sigcontext。
x86
struct sigcontext
{
unsigned short gs, __gsh;
unsigned short fs, __fsh;
unsigned short es, __esh;
unsigned short ds, __dsh;
unsigned long edi;
unsigned long esi;
unsigned long ebp;
unsigned long esp;
unsigned long ebx;
unsigned long edx;
unsigned long ecx;
unsigned long eax;
unsigned long trapno;
unsigned long err;
unsigned long eip;
unsigned short cs, __csh;
unsigned long eflags;
unsigned long esp_at_signal;
unsigned short ss, __ssh;
struct _fpstate * fpstate;
unsigned long oldmask;
unsigned long cr2;
};
x64
struct _fpstate
{
/* FPU environment matching the 64-bit FXSAVE layout. */
__uint16_t cwd;
__uint16_t swd;
__uint16_t ftw;
__uint16_t fop;
__uint64_t rip;
__uint64_t rdp;
__uint32_t mxcsr;
__uint32_t mxcr_mask;
struct _fpxreg _st[8];
struct _xmmreg _xmm[16];
__uint32_t padding[24];
};
struct sigcontext
{
__uint64_t r8;
__uint64_t r9;
__uint64_t r10;
__uint64_t r11;
__uint64_t r12;
__uint64_t r13;
__uint64_t r14;
__uint64_t r15;
__uint64_t rdi;
__uint64_t rsi;
__uint64_t rbp;
__uint64_t rbx;
__uint64_t rdx;
__uint64_t rax;
__uint64_t rcx;
__uint64_t rsp;
__uint64_t rip;
__uint64_t eflags;
unsigned short cs;
unsigned short gs;
unsigned short fs;
unsigned short __pad0;
__uint64_t err;
__uint64_t trapno;
__uint64_t oldmask;
__uint64_t cr2;
__extension__ union
{
struct _fpstate * fpstate;
__uint64_t __fpstate_word;
};
__uint64_t __reserved1 [8];
};
- signal handler返回后,內核執行sigreturn系統調用,為該進程恢復之前保存的上下文,其中包括將所有壓入的寄存器,重新pop回對應的寄存器,最后恢復進程的執行。其中,32位sigreturn的調用號為77,64位的系統調用號為15。
攻擊原理
內核在signal信號處理的過程中,主要做的工作就是為進程保存上下文,並且恢復上下文。這個主要的變動都在Signal Frame中。但是需要注意的是:
- Signal Frame被保存在用戶的地址空間中,所以用戶是可以讀寫的。
- 由於內核與信號處理程序無關,它並不會去記錄這個signal對應的Signal Frame,所以當執行sigreturn系統調用時,此時的Signal Frame並不一定是之前內核為用戶進程保存的Signal Frame。
舉兩個簡單的例子:
獲取shell
首先,我們假設攻擊者可以控制用戶進程的棧,那么它就可以偽造一個Signal Frame,如下所示:
當系統執行完sigreturn系統調用之后,會執行一系列的pop指令以便於恢復相應寄存器的值,當執行到rip時,就會將程序執行流指向syscall地址,根據相應寄存器的值,此時,便會得到一個shell。
system call chains
上面的例子只是單獨的獲得一個shell。有時候,我們可能會希望執行一系列的函數。我們只需要做兩處修改即可:
- 控制棧指針
- 把原來rip指向的syscall gadget換成syscall;ret gadget。
如下圖所示,這樣當每次syscall返回的時候,棧指針都會指向下一個Signal Frame。因此就可以執行一系列的sigreturn函數調用。
攻擊條件
我們在構造ROP攻擊的時候,需要滿足下面的條件:
-
可以通過棧溢出來控制棧的內容
-
需要知道相應的地址
- "/bin/sh"
- Signal Frame
- syscall
- sigreturn
-
需要有足夠大的空間來塞下整個sigal frame
值得一說的是,對於sigreturn系統調用來說,在64位系統中,sigreturn系統調用對應的系統調用號為15,只需要RAX=15,並且執行syscall即可實現抵用syscall調用。而RAX寄存器的值又可以通過控制某個函數的返回值來間接控制,比如說read函數的返回值為讀取的字節數。
ret2VDSO
VDSO(Virtual Dynamically-linked Shared Object),虛擬動態鏈接共享對象,所以它應該是虛擬的,與虛擬內存一致,在計算機中本身並不存在。具體來說,它是內核態的調用映射到用戶地址空間的庫。那么它為什么會存在呢?這是因為有些系統調用經常被用戶使用,這就會出現大量的用戶態和內核態切換的開銷。通過vdso,我們可以大量減少這樣的開銷,同時也可以使得我們的路徑更好。這里的路徑更好指的是,我們不需要使用傳統的int 0x80來進行系統調用,不同的處理起實現了不同的快速系統調用指令:
- intel是實現了sysenter,sysexit.
- amd實現了syscall,sysret
當不同的處理器架構實現了不同的指令時,自然就會出現兼容性問題,所以linux實現了vsycall接口,在底層會根據具體的結構來進行具體操作。而vsycall就實現在vdso中。
這里,我們順便來看一下vdso,在linux(kernel 2.6 or upper)中執行ldd /bin/sh,會發現有一個名字叫linux-vdso.so.1的動態文件,而系統中卻找不到它,它就是VDSO。
ki@ki-virtual-machine:/mnt/hgfs/gx16$ ldd /bin/sh
linux-vdso.so.1 => (0x00007ffea8bf3000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2448d63000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2449355000)
除了快速系統調用,glibc也提供了VDSO的支持,open(),read(),write(),gettimeofday()都可以直接使用VDSO中的實現。使得這些調用速度更快。內核新特性在不影響glibc的情況下也可以更快的部署。
這里我們以intel的處理器為例,進行簡單說明。
其中sysenter的參數傳遞方式與int 0x80一致,但我們可能需要自己布置好function prolog(32位為例)
push ebp
mov ebp,esp
此外,如果我們沒有提供functtion prolog的話,我們還需要一個可以進行棧遷移的gadgets,以便於可以改變棧的位置。
ROP Tricks
stack pivoting
stack pivoting,正如它所描述的,該技巧就是劫持棧指針指向攻擊者所能控制的內存處,然后再在相應的位置進行ROP。一般來說,我們可能在一下情況需要使用stack pivoting:
- 可以控制的棧溢出的字節數較少,難以構造較長的TOP鏈。
- 開啟了PIE保護,棧地址未知,我們可以將棧劫持到已知的區域。
- 其他漏洞難以利用,我們需要進行轉換,比如說將棧劫持到堆空間,從而在堆上寫rop以及進行對漏洞利用。
此外,利用stack pivoting有以下幾個要求
- 可以控制程序流。
- 可以控制sp指針。一般來說,控制棧指針會使用ROP,常見的控制棧指針的gadgets一般是
pop rsp/esp
當然,還會有一些其他的姿勢,比如說libc_csu_init中的gadgets,我們通過偏移就可以得到控制rsp指針。
上面是正常的,下面是偏移的。
gef➤ x/7i 0x000000000040061a
0x40061a <__libc_csu_init+90>: pop rbx
0x40061b <__libc_csu_init+91>: pop rbp
0x40061c <__libc_csu_init+92>: pop r12
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
gef➤ x/7i 0x000000000040061d
0x40061d <__libc_csu_init+93>: pop rsp
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
此外,還有更高級的fake frame。存在可以控制內容的內存,一般有如下:
- bss段。由於進程按頁分配內存,分配給bss段的內存大小至少一個頁(4K,0x1000大小)。然而一般bss段的內容用不了這么多的空間,並且bss段分配的內存頁擁有讀寫權限。
- heap。但是這個需要我們能夠泄露堆地址。
frame faking
正如這個技巧名字所說的那樣,這個技巧就是構造一個虛假的棧幀來控制程序額的執行流。
概括地講,我們在之前講的棧溢出不外乎兩種方式
- 控制程序EIP
- 控制程序EBP
其最終都是控制程序的執行流。在frame faking中,我們所利用的技巧便是同時控制EBP與EIP,這樣我們在控制程序執行流的同時,也改變了程序棧幀的位置。一般來說其payload如下:
buffer padding|fake ebp|leave ret addr|
這個原理介紹的文章很多,就不再做解釋。
在fake frame中,有一個需求就是,我們必須得有一塊可以寫的內存,並且我們還知道這塊內存的地址,這點與stack pivoting相似。
Stack smash
在程序加了canary保護之后,如果我們讀取的buffer覆蓋了對應的值時,程序就會報錯,而一般來說我們並不會關心報錯信息。而stack smash技巧則就是利用這點打印這一信息的程序來得到我們想要的內容。這是因為在程序啟動canary保護后,如果發現canary被修改的話,程序就會執行__stack_chk_fail函數來打印argv[0]指針所指向的字符串,正常情況下,這個指針指向了程序名。其代碼如下:
void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (2, "*** %s ***: %s terminated\n",
msg, __libc_argv[0] ?: "<unknown>");
}
所以說如果我們利用棧溢出覆蓋argv[0]為我們想要輸出的字符串的地址,那么在__fortify_fail函數中就會輸出我們的想要的信息。
棧上的partial overwrite
partial overwrite這種技巧在很多地方都適用,這里先以棧上的partial overwrite為例介紹。
我們知道,在開啟了隨機化(ASLR,PIE)后,無論高位的地址如何變化,低12位的頁內偏移始終是固定的,也就說如果我們能更改低位偏移,就可以在一定程度上控制程序的執行流,繞過PIE保護
內容來源
CTF Wiki Stack Introduction
CTF Wiki Stack Overflow Principle
CTF Wiki Basic ROP
CTF Wiki Intermediate ROP
CTF Wiki Advanced ROP
CTF Wiki ROP Tricks