buuctf rip 詳細wp


buu上rip這道題作為pwn里面最簡單的棧題,意外的發現網上很多wp因為遠程環境的更新,需要維持堆棧平衡,所以原先老舊的wp在本地可以打通,但在遠程卻打不通,甚至很多人的blog就拿着原本可以打通的wp貼上去,自己都沒有實操一遍,我相信很多人都和我一樣因為這些wp對初期學習造成了不小的困擾,特別是初期自己底層知識什么都不懂的時候,看到一些甚至是在胡亂解釋的wp,就不知道該如何進行后續的學習了,所以今天我通過近兩天的摸索,寫一篇盡量正確的wp,因為我自己水平有限,一些細節的地方可能會有錯誤,但大的思路一定是對的,希望能給大家提供幫助,其中用到了別人ppt里現成的圖,本人僅用作學習交流,侵刪。

ps:因為這里是作為本人的學習筆記,所以前面會有大量的關於棧如何工作的基礎內容,不想看的可以直接跳過去看最后的wp

1.Stack(棧)的工作原理

1.1C語言內存分布

首先我們來看當一個c語言函數在執行的時候,操作系統是如何調度內存將數據存放並且完成相關函數操作的

 

在右邊的圖中,我們可以大致的了解,一個c程序被編譯成可執行文件執行時,他在內存中的存儲情況如該圖所示,這是一個內存空間,地址由底部逐漸升高,其中,最上層的kernel是操作系統的核心源碼,他是操作系統完成各項功能的關鍵,這一部分我們暫時不做深入的研究,在早期的學習中,我們關注的是Stack(棧),Heap(堆),BSS(靜態內存分配)。

其中Stack(棧)用於靜態分配中的存放局部變量,如局部變量t和ptr都被儲存在了棧中,而BSS存儲全局變量,Heap則負責存儲動態分配的內存空間,如c語言中的malloc/free分配內存時,就會分配到Heap區域。

而Heap與Stack中間的內存空間,則是共享的一片內存空間,Heap從低地址向高地址分配空間,Stack從高地址向低地址分配空間,從而完整高效的使用了這一片內存空間。

1.2棧中的內存分布與工作原理

好了,現在我們已經大致了解了c語言的內存分布,其中heap和Bss尤其是heap會在后面更深入的學習中使用,我也會在后續的wp中更新相關知識,今天這道題只需要用的棧的相關知識,現在我們來看,當函數調用時,棧的內存空間是如何分布的。

棧這個數據結構相信大家早就學過,首先我們需要了解一下棧中常用的3個寄存器,64位cpu對應rsp,rbp,rip三個寄存器。而32位cpu則對應esp,ebp,eip三個寄存器。然后我們了解一下棧幀的概念,一個棧幀就是保存一個函數的狀態,簡單來說就是一個函數所需要的棧空間,rsp/esp永遠指向棧幀的棧頂,rbp/ebp則永遠指向棧幀的棧底,rip/eip指向當前棧棧幀執行的命令。如圖中文字所,棧從高地址向低地址開辟內存空間,所以低地址的是棧頂,而棧底的第一個棧幀在這里存放着我們的主函數的父函數,所以main函數並不是最棧頂的函數,main上面還會在編譯過程中有一些庫函數,但是他們並不會產生棧幀,因為棧先進后出的特性,所以當在main函數中需要調用其他函數時,就開辟一個新的函數棧幀,並存儲上一個棧的棧底,當調用結束時,將現在的棧幀彈出,恢復到原來的main函數繼續執行完main函數,比如,當上面的代碼main函數調用到sum函數時,便會開辟一個新的棧幀,而sum函數所需要的參數,會被逆向存儲在父函數(在這里也就是main函數)的棧幀中

 

下面,我們來看每個棧幀的具體結構

 

 

上面的幾張圖,就是創建新棧幀的過程,當然,圖中所演示的是在32位cpu中的情況,也是就是寄存器與存儲字長有着細微的變化,但是差別並不是很大,並且,圖中的對於寄存器的各種操作都是在匯編代碼中具體實現的,這里我們並不贅述太多,相信大家都對簡單的匯編或多或少有些了解,圖中我們可以看到兩個相鄰的棧幀,子函數(callee's function state)棧幀的Return Address緊挨着父函數(caller's...state),而我們需要注意的是,Return Address是什么呢?在第二張圖中,很明確的告訴我們,在調用子函數時,我們將匯編中父函數的下一個匯編指令的地址,放入Return Address,這樣我們在子函數完成時,便可以將Return Address中的值彈入rip/eip中,這樣程序便會從上次調用的地方繼續完成父函數,而這一點,也就是我們實行棧溢出的關鍵,我們不妨想一想,如果我們能夠通過某種方式,操控Return Address的返回地址,那么是不是意味着,我們可以任意操控遠程的機器指向任何指令,也就是說我們只要可以篡改Return Address指向一個危險函數的地址,理論上,我們就可以通過危險函數干任何我們想干的事情。那么我們再來看看當子函數調用結束后,是如何刪除子函數的棧幀返回父函數的。

 

這里具體需要ppt中的匯編代碼輔助理解,但是匯編代碼的流程圖實在太多,改天有空我會發上去,大概就是esp先等於ebp,然后再pop ebp 將esp指向的地址的值賦給ebp,也就是此時的父函數的一個棧幀的棧底,於是ebp就回到了父函數的棧底,而因為pop以后esp自動加一指向Return Address(因為棧是從高地址指向低地址,所以是加一),然后再執行return指令,簡單的講就是pop eip,將esp指向的值彈入到eip中,前面我們說過Return Address里存放的是當前棧幀函數的父函數調用當前函數時下一個指令的地址,而eip又是當前要執行的指令地址的寄存器,於是這樣就會回到父函數繼續執行父函數的下一個指令。而此時pop以后esp再次加一,所以就回到了父函數棧幀的棧頂,ebp也在上一次pop ebp時就回到了父函數的棧底,而這樣一個過程可以理論上被無數次執行,所以用棧來實現函數調用及其的方便。

2.buuctf rip wp

2.1棧溢出的原理

那么上面我們學習了棧的基礎知識,我們便以buu上的這一道rip來看一下最簡單的棧溢出。

如果你認真的看了上面的棧的工作流程,那么你就會發現,實際上在一個函數調用完以后,就要刪除此時的棧幀並將Return Address的值返回rip/eip,Return Address的值也就是上面說的,父函數調用此函數(也就是他的子函數)時的下一個地址,通俗一點解釋就是從父函數跳轉到子函數時,父函數會從某個call指令斷開,跳轉到子函數,Return Address就是把他從斷開的地方接上,也就是斷開指令下一個指令的地址,而rip/eip將執行這一指令,並繼續完成父函數,那么我們只要設法將Return Address的值改變到一個危險函數的地址,我們就可以通過這個危險函數獲得系統的控制權。

那么我們怎么樣才可以改變Return Address的地址呢?以這道rip為例,下載rip給我們的elf文件,將其拖入ida pro,按f5將其反編譯成c語言偽代碼

 

我們可以看到,主要有一個main函數,還有一個fun函數

 

這個elf可執行文件,補充一下,elf是linux下的可執行文件,相當於windows中的exe文件,他的反編譯文件是由一個main函數和一個fun函數組成的,當我們用虛擬機在unbantu中正常執行他時,他只會執行main函數,因為fun函數並沒有被調用,而fun函數也就是我們上面說的危險函數,system是c語言下的一個可以執行shell命令的函數,目前你可以簡單理解為,執行了這個危險函數,我們就拿到了遠端服務器的shell,也就是相當於在windows下以管理員身份開啟cmd,那么我們就可以通過一系列后續指令控制遠端服務器,但在ctf中,我們只需要拿到shell以后獲得flag就算成功。

那么,回過頭來,我們再來看這個main函數,我們剛剛說過,函數的局部變量會存放在他的棧中,那么在main函數中,他char了一個s,也就是在main函數的棧幀中,划分了一個15字節的存儲空間,我們在unbantu中file一下這個文件:

 

我們可以發現,這是一個64位的elf文件,也就是說,每個存儲單元是8個字節(如果不知道的去學學計組),簡單的講就是一個字節是8位,因為他是64位,所以一個存儲單元就是8個字節,同理32位就是4個字節。(注意,這里我出了個小失誤,我們應該先file這個文件,看看他是64位還是32位,再選擇將其拖入64位的ida還是32位的ida)

然后我們可以先通過checksec查看保護機制(不知道沒關系,以后才會用到),因為這題是任何保護都沒有打開的,所以我們可以實現最簡單的棧溢出

 

接回上面的話題,我們開辟了一個15個字節的存儲空間,那么在棧幀中系統就會給我們分配一個15個字節的存儲空間,那么我們再注意一下我們是如何寫入這15個字節的數據的,沒錯,我們使用的是gets函數,相信大家在c語言中都學習過這個函數,我們在c語言的學習中知道,這個函數時可以無限制輸入數據的,但當時,我們並沒有意識到gets函數時危險的,現在,我們通過前面棧的工作原理的學習,我們發現,我們明明只分配了15個字節的內存空間,但是我們可以輸入無數個字節,那么這會導致什么問題呢?請大家自己回過頭再去看一看前面棧的結構圖!

此時,我們的s就在Local Variables,把他想象成一個水桶,我們如果可以一直往里面不停的倒水,那么這個水桶的水滿了,是不是就可以溢出到另一個水桶Caller's ebp里?再繼續倒水,那是不是就溢出到了Return Address里,那么原本不屬於Return Address的水由於其他水桶的溢出而進入了Return Address,也就是改變了Return Address的值。這個時候,我們前面所說的,通過改變Return Address的值來完成對危險函數的調用,是不是就可以實現了?

那么,最后的問題就是,我們該怎么確定溢出多少水呢?這個也是很簡單的問題,Local Variables也就是char s[]划定了15個字節的內存空間,那么我們需要知道這個內存空間在棧中的位置,就可以知道需要多少個字節才能到達

我們打開ida pro ,作為最簡單的棧題,我們在ida pro中的main函數在創建空間s時已經清晰的告訴了我們距離rbp的距離是Fh,這是16進制也就是15個字節,當然,這只是理想的情況,在復雜一些的情況中,開辟的內存地址顯然不會像這題一樣緊挨着上一個rbp,甚至有時出題人會故意在ida的靜態調試中告訴你錯誤的地址,這個時候你需要用pwndbg進行動態調試,這題通過動態調試也可以發現,二者的地址是一樣的。所以我們首先需要輸入15個字節到達rbp的位置。

然后Caller's ebp中存儲的是上一個函數的ebp的值,當然,我們這個圖的例子是32位的系統,而我們是64位的系統,所以ebp應該是rbp才對,是8個字節,那么我們還需要8個自己的數據把Caller's rbp的數據填滿(當然在本題中應該是rbp,因為是64位的系統),這樣我們就填滿了前兩個水桶,你可以理解為水即將溢出進入Return Address了,所以接下來我們輸入的值,將溢出進入Return Address,也就是說,這時我們還需要輸入危險函數,也就是fun函數的地址,查看一下ida pro發現fun函數的地址是0x401186,於是只需要再輸入0x401186這一地址值,該地址就被我們送到了Return Address,當這個函數調用結束后就會被送到rip,執行fun函數,從而控制shell。

2.2exp

from pwn import *

p = remote("node4.buuoj.cn",27296)

payload=b'A'15+b'B'8+p64(0x401186+1)

p.sendline(payload)

p.interactive()

作為最簡單的pwn題,當然是最簡單的exp,只要知道原理幾行代碼就可以搞定,我們發送了15個A用來填充s,再發送8個字節用來填充b,將地址打包位p64位的數據一起發送,就可以完成棧溢出,至於最后為什么要+1,我們可以發現,不加一我們在本地可以打通,但是卻打不通遠程,這也是我開頭說的,和以前payload不一樣的地方,原理我們是沒有錯的,這里+1是為了堆棧平衡,詳細可以看大佬的博客http://blog.eonew.cn/archives/958

因為現在已經是凌晨三點,再加上我對這里還有點模糊,也就不廢話了,總之我們學的知識是沒有錯的,這里需要堆棧平衡應該也是遠程buu的服務器更新以后linux環境發生了變化,加了新的要求,而不是因為其他原因,改成15個字節直接發送地址不加一也可以完成交互,但是並不是因為不需要覆蓋rbp,而是滿足堆棧平衡的一種另外一種方式,后續如果學明白了會更新。

總結

雖然是最簡單的pwn題,exp只有短短幾行代碼,但是想要完全掌握背后的知識,卻不是那么簡單,哪怕是已經學了兩天,今天在復盤棧的工作原理的時候,發現自己也會很多不熟練的地方,所以只有將基礎打牢,能在腦海中自動演示棧的工作原理,才能完成后續更加復雜的學習,這也是二進制安全難入門的地方之一,在此與各位師傅共勉。

這里面用的圖,全都是某個安全團隊大師傅講pwn入門公開課用的圖,因為一些問題不太方便公開,如果有人需要完整ppt的話,如果有疑問或者想要ppt再或者想要一起學習的師傅可以中私信聯系我。

其實大家看完這題以后一定和我剛學完一樣會有一個異或,那就是實際情況中,怎么可能會有人傻到將程序設置一個后門函數呢?實際情況中確實是不會有人這么傻的,因此這只是一個最簡單的棧溢出題,意在讓你了解棧工作的基本原理,屬於level 0,在后續更深入的學習中,我們會遇到更符合實際情況的題目,也會見招拆招,構造出更復雜的payload。

那么就是這樣,寫完已經凌晨三點了,還是那句話,很簡單的題目,但很多厲害的大佬都懶得詳細寫這些,可能我第一次寫博客寫的也不是很好,但希望對各位能有所幫助,就是這樣,睡覺了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM