Linux pwn入門教程(1)——棧溢位基礎

張悠悠66發表於2018-06-29

作者:Tangerine@SAINTSEC

原文來自:https://bbs.ichunqiu.com/thread-42241-1-1.html

0×00 函式的進入與返回

要想理解棧溢位,首先必須理解在彙編層面上的函式進入與返回。首先我們用一個簡單執行一次回顯輸入的程式hello開始。用IDA載入hello,定位到main函式後我們發現這個程式的邏輯十分簡單,呼叫函式hello獲取輸入,然後輸出“hello,”加上輸入的名字後退出。使用F5看反彙編後的C程式碼可以非常方便的看懂邏輯。
image.png我們選中IDA-View視窗或者按Tab鍵切回到彙編視窗,在main函式的call hello一行下斷點,開啟32位的docker環境,啟動除錯伺服器後直接按F9進行除錯。

如圖,這是當前IDA的介面。在這張圖中我們需要重點注意到的東西有棧視窗,EIP暫存器,EBP暫存器和ESP暫存器。

首先我們可以看到EIP暫存器始終指向下一條將要執行的指令,也就是說如果我們可以通過某種方式修改EIP暫存器的值,我們就可以控制整個程式的執行,從而”pwn”掉程式(要驗證這一點,我們可以在EIP後面的數字上點選右鍵選擇Modify value.......把數值改成080484DE然後F9繼續執行,從而跳過call hello一行)。

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

image.png

區域性變數1
…….
……
…….
區域性變數m
區域性變數n
EBP
EIP
引數1
…….
引數n

棧的生長方式是向低地址生長,也就是說這張圖的方向和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單步執行。

image.pngimage.pngimage.png如圖,通過依次執行三條指令,程式為hello函式開闢了新的棧幀,同時把原來的棧幀,即執行了call hello函式的main函式的棧幀的棧底EBP儲存到棧中。繼續往下執行到read函式,然後隨便輸入一些比較有標誌性的內容,比如12345678,我們就會發現儲存輸入的區域性變數buf就在這片新開闢的棧幀中。

image.png

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

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


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

image.png0×01 棧溢位實戰

通過上一節的除錯,我們大概理解了函式棧的初始化和銷燬過程。我們發現隨著我們的輸入變多,輸入的內容離棧上儲存的EIP地址越來越近,那麼我們可不可以通過輸入修改掉棧上的EIP地址,從而在retn指令執行完後“pwn”掉程式呢?我們按Ctrl+F2結束掉當前的除錯,再試一次。為了節約時間,這回我們直接把斷點下在hello函式裡的call _read一行。

啟動除錯,程式中斷後介面如下

image.png通過觀察read函式的引數和棧中的儲存的EIP地址,我們計算出兩者的偏移是0×16個位元組,也就是說輸入0×16=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

image.png棧中的EIP果然按照我們的推測被修改成42424242了。顯然,這是一個非法的記憶體地址,它所在的記憶體頁此時對我們來說並沒有訪問許可權,所以我們執行完retn後程式將會報錯。

image.png選擇OK,繼續F8並且選擇將錯誤傳遞給系統,這個程式接收到訊號後將會結束,除錯結束。我們通過一個程式本身的bug構造了一個特殊輸入結束掉了它。

0×02 結合pwntools打造一個遠端程式碼執行漏洞exp

通過上一節的內容,我們已經可以做到遠端使一個程式崩潰。不要小看這個成果。如果我們能挖掘到安全軟體或者系統的漏洞從而使其崩潰,我們就可以讓某些保護失效,從而使後面的入侵更加輕鬆。當然,我們也不應該滿足於這個成果,如果可以繼續擴大這個漏洞的利用面,製造一個著名的RCE(遠端程式碼執行),為所欲為,豈不是更好?當然,CTF中的絕大部分pwn題也同樣需要通過暴露給玩家的一個IP地址和埠號的組合,通過對埠上執行的程式進行挖掘,使用挖掘到的漏洞使程式執行不該執行的程式碼,從而獲取到flag,這也是我們學習的目標。

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

image.png如圖,這個函式唯一的作用就是呼叫system(“/bin/sh”)開啟一個bash shell,從而可以執行shell命令與系統本身進行互動

image.png

正常的程式流程並不會呼叫這個函式,所以我們將會利用上一節中發現的漏洞劫持程式執行流程,從而執行getShell函式。

首先我們把hello的IO轉發到10001埠上

image.png

然後我們從docker環境中獲取其ip地址(我的是172.17.0.2,不同環境下可能不同)

然後在kali中啟動python,匯入pwntools庫並且開啟一個與docker環境10001埠(即hello程式)的連線

image.png此時我們可以像上一篇文章一樣開啟IDA進行附加除錯,在這裡我就不再次演示了。從上一節的分析我們知道payload的組成應該是22個任意字元+地址。但是我們要怎麼把16進位制數表示的地址轉換成4個位元組的字串呢?我們可以選用structs庫,當然pwntools提供了一個更方便的函式p32()(即pack32位地址,同樣的還有unpack32位地址的u32()以及不同位數的p16(),p64()等等),所以我們的payload就是22*'A'+p32(0x0804846B)

image.png由於讀取輸入的函式是read,我們在輸入時不需要以回車作為結束符(printf,getc,gets等則需要),我們使用程式碼io.send(payload)向程式傳送payload

image.png

由於我在這裡沒有設定IDA附加除錯,顯然程式也不會被斷點中斷,那麼這個時候hello回顯我們的輸入之後應該成功地被payload劫持,跳轉到getShell函式上了。為了與被pwn掉的hello進行互動,我們使用io.interactive()

image.png可以看到我們已經成功地pwn掉了這個程式,取得了其所在環境的控制權。為了增加一點氣氛,我們在/home下面放了一個flag檔案。讓我們來看一下flag是啥

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

有問題大家可以留言哦也歡迎大家到春秋論壇中來耍一耍  >>>點選跳轉

相關文章