CTF必備技能丨Linux Pwn入門教程——棧溢出基礎

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

課程回顧>>Linux Pwn入門教程第一章:環境配置python

更多Pwn視頻課程:https://www.ichunqiu.com/courses/pwn?from=weixindocker

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

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

今天是Linux Pwn入門教程第二章:棧溢出基礎,閱讀用時約10分鐘。安全

函數的進入與返回bash

要想理解棧溢出,首先必須理解在彙編層面上的函數進入與返回。首先咱們用一個簡單執行一次回顯輸入的程序hello開始。用IDA加載hello,定位到main函數後咱們發現這個程序的邏輯十分簡單,調用函數hello獲取輸入,而後輸出「hello,」加上輸入的名字後退出。使用F5看反彙編後的C代碼能夠很是方便的看懂邏輯。服務器

 

咱們選中IDA-View窗口或者按Tab鍵切回到彙編窗口,在main函數的call hello一行下斷點,開啓32位的Docker環境,啓動調試服務器後直接按F9進行調試。數據結構

 

如圖,這是當前IDA的界面。在這張圖中咱們須要重點注意到的東西有棧窗口,EIP寄存器,EBP寄存器和ESP寄存器。函數

首先咱們能夠看到EIP寄存器始終指向下一條將要執行的指令,也就是說若是咱們能夠經過某種方式修改EIP寄存器的值,咱們就能夠控制整個程序的執行,從而「pwn」掉程序(要驗證這一點,咱們能夠在EIP後面的數字上點擊右鍵選擇Modify value.......把數值改爲080484DE而後F9繼續執行,從而跳過call hello一行)。

剩下的東西都和棧相關。顧名思義,棧就是一個數據結構中的棧結構,遵循先入後出的規則。這個棧的最小單位是函數棧幀,一個函數棧幀的結構如圖所示:

 

 

棧的生長方式是向低地址生長,也就是說這張圖的方向和IDA中棧窗口的方向是同樣的,越往上地址值越小。一樣的,新入棧的棧幀在IDA的窗口中會把原來的棧幀「壓」在下面。

ESP和EBP兩個寄存器負責標定當前棧幀的範圍。圖中標黑的部分即爲實際上ESP和EBP中間的最大區域(爲了方便講解,咱們把EIP和參數也列入一個函數的函數棧幀)。

圖中的局部變量和參數很好理解,但EBP和EIP又是什麼意思呢?咱們回到IDA調試窗口。按照程序的邏輯,接下來應該是執行call hello這行指令調用hello這個函數,函數執行完後回到下一行的mov eax, 0,其地址爲080484DE.而後咱們再把當前ESP和EBP的值記下來(受地址空間隨機化ASLR的影響,每臺電腦每次運行到此處的ESP和EBP值不必定相同),而後按F7進入hello函數。

 

如圖,執行完call hello這一行指令後發生了以下改變。由此咱們能夠得知call指令是能夠改變EIP「始終指向下一條指令地址」的行爲的,且call指令會把call下一條指令地址壓棧。咱們能夠理解爲call hello等價於push eip; mov eip, [hello]。因此咱們的第一個問題「棧幀中的EIP是什麼意思」的回答就是:棧幀中的EIP是call指令的下一條指令的地址,咱們繼續F8單步執行。

 

 

 

如圖,經過依次執行三條指令,程序爲hello函數開闢了新的棧幀,同時把原來的棧幀,即執行了call hello函數的main函數的棧幀的棧底EBP保存到棧中。繼續往下執行到read函數,而後隨便輸入一些比較有標誌性的內容,好比12345678,咱們就會發現存儲輸入的局部變量buf就在這片新開闢的棧幀中。

 

咱們已經接觸到了棧幀的開闢與被使用狀況,接下來咱們再經過調試繼續學習棧幀的銷燬。繼續F8到leave一行,此時咱們會發現棧幀再次回到了剛執行完sub esp, 18h的狀態。

執行完leave一行指令後棧幀被銷燬,總體狀態回到了call hello執行前的狀態。即leave指令至關於add esp, xxh; mov esp, ebp; pop ebp

 

 

再次F8,發現EIP指向了call hello的下一行指令,同時棧中保存的EIP值被彈出,棧頂地址+4. 即retn等同於pop eip

 

此時hello函數代碼執行完畢,控制流程返回到了調用hello函數的main函數中。

棧溢出實戰

經過上一節的調試,咱們大概理解了函數棧的初始化和銷燬過程。咱們發現隨着咱們的輸入變多,輸入的內容離棧上保存的EIP地址愈來愈近,那麼咱們可不能夠經過輸入修改掉棧上的EIP地址,從而在retn指令執行完後「pwn」掉程序呢?咱們按Ctrl+F2結束掉當前的調試,再試一次。爲了節約時間,這回咱們直接把斷點下在hello函數裏的call _read一行。

啓動調試,程序中斷後界面以下:

 

經過觀察read函數的參數和棧中的保存的EIP地址,咱們計算出二者的偏移是0x16個字節,也就是說輸入0x16=22個字節的數據,咱們的輸入就會和棧中的EIP「接上」,輸入22+4=26個字節,咱們的輸入就會覆蓋掉EIP。那麼咱們構造payload爲‘A’*22+‘B’*4

即AAAAAAAAAAAAAAAAAAAAAABBBB,根據咱們的推測,在EIP寄存器指向retn指令所在地址時,棧頂應該是‘BBBB’。即retn執行完以後,EIP裏的值將再也不是圖中框起來的080484DE,而是42424242(BBBB的ASCII值),按F8使IDA掛起,在docker環境中輸入payload:

 

棧中的EIP果真按照咱們的推測被修改爲42424242了。顯然,這是一個非法的內存地址,它所在的內存頁此時對咱們來講並無訪問權限,因此咱們運行完retn後程序將會報錯。

 

選擇OK,繼續F8而且選擇將錯誤傳遞給系統,這個進程接收到信號後將會結束,調試結束。咱們經過一個程序自己的bug構造了一個特殊輸入結束掉了它。

結合pwntools打造一個遠程代碼執行漏洞exp

經過上一節的內容,咱們已經能夠作到遠程使一個程序崩潰。不要小看這個成果。若是咱們能挖掘到安全軟件或者系統的漏洞從而使其崩潰,咱們就可讓某些保護失效,從而使後面的入侵更加輕鬆。固然,咱們也不該該知足於這個成果,若是能夠繼續擴大這個漏洞的利用面,製造一個著名的RCE(遠程代碼執行),隨心所欲,豈不是更好?

固然,CTF中的絕大部分pwn題也一樣須要經過暴露給玩家的一個IP地址和端口號的組合,經過對端口上運行的程序進行挖掘,使用挖掘到的漏洞使程序執行不應執行的代碼,從而獲取到flag,這也是咱們學習的目標。

爲了下降難度,我在編寫hello這個小程序的時候已經預先埋了一個後門——位於0804846B的名爲getShell的函數。

 

如圖,這個函數惟一的做用就是調用system("/bin/sh")打開一個bash shell,從而能夠執行shell命令與系統自己進行交互。

 

正常的程序流程並不會調用這個函數,因此咱們將會利用上一節中發現的漏洞劫持程序執行流程,從而執行getShell函數。

首先咱們把hello的IO轉發到10001端口上。

 

而後咱們從Docker環境中獲取其IP地址(個人是172.17.0.2,不一樣環境下可能不一樣)

而後在kali中啓動python,導入pwntools庫而且打開一個與Docker環境10001端口(即hello程序)的鏈接。

 

此時咱們能夠像上一篇文章同樣打開IDA進行附加調試,在這裏我就再也不次演示了。從上一節的分析咱們知道payload的組成應該是22個任意字符+地址。可是咱們要怎麼把16進制數表示的地址轉換成4個字節的字符串呢?

咱們能夠選用structs庫,固然pwntools提供了一個更方便的函數p32( )(即pack32位地址,一樣的還有unpack32位地址的u32( )以及不一樣位數的p16( ),p64( )等等),因此咱們的payload就是22*'A'+p32(0x0804846B)。

 

因爲讀取輸入的函數是read,咱們在輸入時不須要以回車做爲結束符(printf,getc,gets等則須要),咱們使用代碼io.send(payload)向程序發送payload。

 

因爲我在這裏沒有設置IDA附加調試,顯然程序也不會被斷點中斷,那麼這個時候hello回顯咱們的輸入以後應該成功地被payload劫持,跳轉到getShell函數上了。爲了與被pwn掉的hello進行交互,咱們使用io.interactive( )

 

能夠看到咱們已經成功地pwn掉了這個程序,取得了其所在環境的控制權。爲了增長一點氣氛,咱們在/home下面放了一個flag文件。讓咱們來看一下flag:

 

如圖,咱們成功的作出了第一個pwn題。爲了加深對棧溢出的理解,我選了幾個真實的CTF賽題做爲做業,注意不要將思惟固定在獲取shell上哦。

課後例題和練習題很是重要,小夥伴請務必下載練習。後臺回覆「課後練習題」便可得到練習文檔!

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

相關文章
相關標籤/搜索