ROP的全稱為Return-oriented programming(返回導向編程),這是一種高級的記憶體攻擊技術可以用來繞過現代操作系統的各種通用防禦(比如記憶體不可執行和代碼簽名等)。雖然現在大家都在用64位的操作系統,但是想要扎實的學好ROP還是得從基礎的x86系統開始,但看官請不要著急。 ...
一步一步學ROP之linux_x86篇
作者:蒸米@阿裡聚安全
一、序
ROP的全稱為Return-oriented programming(返回導向編程),這是一種高級的記憶體攻擊技術可以用來繞過現代操作系統的各種通用防禦(比如記憶體不可執行和代碼簽名等)。雖然現在大家都在用64位的操作系統,但是想要扎實的學好ROP還是得從基礎的x86系統開始,但看官請不要著急,在隨後的教程中我們還會帶來linux_x64以及android (arm)方面的ROP利用方法,歡迎大家繼續學習。
小編備註:文中涉及代碼可在文章最後的github鏈接找到。
二、Control Flow Hijack 程式流劫持
比較常見的程式流劫持就是棧溢出,格式化字元串攻擊和堆溢出了。通過程式流劫持,攻擊者可以控制PC指針從而執行目標代碼。為了應對這種攻擊,系統防禦者也提出了各種防禦方法,最常見的方法有DEP(堆棧不可執行),ASLR(記憶體地址隨機化),Stack Protector(棧保護)等。但是如果上來就部署全部的防禦,初學者可能會覺得無從下手,所以我們先從最簡單的沒有任何保護的程式開始,隨後再一步步增加各種防禦措施,接著再學習繞過的方法,循序漸進。
首先來看這個有明顯緩衝區溢出的程式:
這裡我們用
#bash
gcc -fno-stack-protector -z execstack -o level1 level1.c
這個命令編譯程式。-fno-stack-protector和-z execstack這兩個參數會分別關掉DEP和Stack Protector。同時我們在shell中執行:
這幾個指令。執行完後我們就關掉整個linux系統的ASLR保護。
接下來我們開始對目標程式進行分析。首先我們先來確定溢出點的位置,這裡我推薦使用pattern.py這個腳本來進行計算。我們使用如下命令:
來生成一串測試用的150個位元組的字元串:
隨後我們使用gdb ./level1調試程式。
我們可以得到記憶體出錯的地址為0x37654136。隨後我們使用命令:
就可以非常容易的計算出PC返回值的覆蓋點為140個位元組。我們只要構造一個”A”*140+ret字元串,就可以讓pc執行ret地址上的代碼了。
接下來我們需要一段shellcode,可以用msf生成,或者自己反編譯一下。
這裡我們使用一段最簡單的執行execve ("/bin/sh")命令的語句作為shellcode。
溢出點有了,shellcode有了,下一步就是控制PC跳轉到shellcode的地址上:
[shellcode][“AAAAAAAAAAAAAA”….][ret]
^------------------------------------------------|
對初學者來說這個shellcode地址的位置其實是一個坑。因為正常的思維是使用gdb調試目標程式,然後查看記憶體來確定shellcode的位置。但當你真的執行exp的時候你會發現shellcode壓根就不在這個地址上!這是為什麼呢?原因是gdb的調試環境會影響buf在記憶體中的位置,雖然我們關閉了ASLR,但這隻能保證buf的地址在gdb的調試環境中不變,但當我們直接執行./level1的時候,buf的位置會固定在別的地址上。怎麼解決這個問題呢?
最簡單的方法就是開啟core dump這個功能。
開啟之後,當出現記憶體錯誤的時候,系統會生成一個core dump文件在tmp目錄下。然後我們再用gdb查看這個core文件就可以獲取到buf真正的地址了。
因為溢出點是140個位元組,再加上4個位元組的ret地址,我們可以計算出buffer的地址為$esp-144。通過gdb的命令 “x/10s $esp-144”,我們可以得到buf的地址為0xbffff290。
OK,現在溢出點,shellcode和返回值地址都有了,可以開始寫exp了。寫exp的話,我強烈推薦pwntools這個工具,因為它可以非常方便的做到本地調試和遠程攻擊的轉換。本地測試成功後只需要簡單的修改一條語句就可以馬上進行遠程攻擊。
最終本地測試代碼如下:
執行exp:
接下來我們把這個目標程式作為一個服務綁定到伺服器的某個埠上,這裡我們可以使用socat這個工具來完成,命令如下:
隨後這個程式的IO就被重定向到10001這個埠上了,並且可以使用 nc 127.0.0.1 10001來訪問我們的目標程式服務了。
因為現在目標程式是跑在socat的環境中,exp腳本除了要把p = process('./level1')換成p = remote('127.0.0.1',10001) 之外,ret的地址還會發生改變。解決方法還是採用生成core dump的方案,然後用gdb調試core文件獲取返回地址。然後我們就可以使用exp進行遠程溢出啦!
三、Ret2libc – Bypass DEP 通過ret2libc繞過DEP防護
現在我們把DEP打開,依然關閉stack protector和ASLR。編譯方法如下:
這時候我們如果使用level1的exp來進行測試的話,系統會拒絕執行我們的shellcode。如果你通過sudo cat /proc/[pid]/maps查看,你會發現level1的stack是rwx的,但是level2的stack卻是rw的。
level1: bffdf000-c0000000 rw-p 00000000 00:00 0 [stack]
level2: bffdf000-c0000000 rwxp 00000000 00:00 0 [stack]
那麼如何執行shellcode呢?我們知道level2調用了libc.so,並且libc.so里保存了大量可利用的函數,我們如果可以讓程式執行system(“/bin/sh”)的話,也可以獲取到shell。既然思路有了,那麼接下來的問題就是如何得到system()這個函數的地址以及”/bin/sh”這個字元串的地址。
如果關掉了ASLR的話,system()函數在記憶體中的地址是不會變化的,並且libc.so中也包含”/bin/sh”這個字元串,並且這個字元串的地址也是固定的。那麼接下來我們就來找一下這個函數的地址。這時候我們可以使用gdb進行調試。然後通過print和find命令來查找system和”/bin/sh”字元串的地址。
我們首先在main函數上下一個斷點,然後執行程式,這樣的話程式會載入libc.so到記憶體中,然後我們就可以通過”print system”這個命令來獲取system函數在記憶體中的位置,隨後我們可以通過” print __libc_start_main”這個命令來獲取libc.so在記憶體中的起始位置,接下來我們可以通過find命令來查找”/bin/sh”這個字元串。這樣我們就得到了system的地址0xb7e5f460以及"/bin/sh"的地址0xb7f81ff8。下麵我們開始寫exp:
要註意的是system()後面跟的是執行完system函數後要返回地址,接下來才是”/bin/sh”字元串的地址。因為我們執行完後也不打算乾別的什麼事,所以我們就隨便寫了一個0xdeadbeef作為返回地址。下麵我們測試一下exp:
OK。測試成功。
四、ROP– Bypass DEP and ASLR 通過ROP繞過DEP和ASLR防護
接下來我們打開ASLR保護。
現在我們再回頭測試一下level2的exp,發現已經不好用了。
如果你通過sudo cat /proc/[pid]/maps或者ldd查看,你會發現level2的libc.so地址每次都是變化的。
那麼如何解決地址隨機化的問題呢?思路是:我們需要先泄漏出libc.so某些函數在記憶體中的地址,然後再利用泄漏出的函數地址根據偏移量計算出system()函數和/bin/sh字元串在記憶體中的地址,然後再執行我們的ret2libc的shellcode。既然棧,libc,heap的地址都是隨機的。我們怎麼才能泄露出libc.so的地址呢?方法還是有的,因為程式本身在記憶體中的地址並不是隨機的,如圖所示:
Linux記憶體隨機化分佈圖
所以我們只要把返回值設置到程式本身就可執行我們期望的指令了。首先我們利用objdump來查看可以利用的plt函數和函數對應的got表:
我們發現除了程式本身的實現的函數之外,我們還可以使用read@plt()和write@plt()函數。但因為程式本身並沒有調用system()函數,所以我們並不能直接調用system()來獲取shell。但其實我們有write@plt()函數就夠了,因為我們可以通過write@plt ()函數把write()函數在記憶體中的地址也就是write.got給列印出來。既然write()函數實現是在libc.so當中,那我們調用的write@plt()函數為什麼也能實現write()功能呢? 這是因為linux採用了延時綁定技術,當我們調用write@plit()的時候,系統會將真正的write()函數地址link到got表的write.got中,然後write@plit()會根據write.got 跳轉到真正的write()函數上去。(如果還是搞不清楚的話,推薦閱讀《程式員的自我修養 - 鏈接、裝載與庫》這本書)
因為system()函數和write()在libc.so中的offset(相對地址)是不變的,所以如果我們得到了write()的地址並且擁有目標伺服器上的libc.so就可以計算出system()在記憶體中的地址了。然後我們再將pc指針return回vulnerable_function()函數,就可以進行ret2libc溢出攻擊,並且這一次我們知道了system()在記憶體中的地址,就可以調用system()函數來獲取我們的shell了。
使用ldd命令可以查看目標程式調用的so庫。隨後我們把libc.so拷貝到當前目錄,因為我們的exp需要這個so文件來計算相對地址:
最後exp如下:
接著我們使用socat把level2綁定到10003埠:
最後執行我們的exp:
五、小結
本章簡單介紹了ROP攻擊的基本原理,由於篇幅原因,我們會在隨後的文章中會介紹更多的攻擊技巧:如何利用工具尋找gadgets,如何在不知道對方libc.so版本的情況下計算offset;如何繞過Stack Protector等。歡迎大家到時繼續學習。另外本文提到的所有源代碼和工具都可以從我的github下載:https://github.com/zhengmin1989/ROP_STEP_BY_STEP
六、參考文獻
- The geometry of innocent flesh on the bone: return-into-libc without function calls (on the x86)
- picoCTF 2013: https://github.com/picoCTF/2013-Problems
- Smashing The Stack For Fun And Profit: http://phrack.org/issues/49/14.html
- 程式員的自我修養
- ROP輕鬆談
作者:蒸米@阿裡聚安全,更多技術文章,請訪問阿裡聚安全博客