一步一步學 ROP 之 Linux_x86 篇

做者:蒸米@阿里聚安全html

ROP的全稱爲Return-oriented programming(返回導向編程),這是一種高級的內存攻擊技術能夠用來繞過現代操做系統的各類通用防護(好比內存不可執行和代碼簽名等)。雖然如今你們都在用64位的操做系統,可是想要紮實的學好ROP仍是得從基礎的x86系統開始,但看官請不要着急,在隨後的教程中咱們還會帶來linux_x64以及android (arm)方面的ROP利用方法,歡迎你們繼續學習。linux

小編備註:文中涉及代碼可在文章最後的github連接找到android

Control Flow Hijack 程序流劫持

比較常見的程序流劫持就是棧溢出,格式化字符串攻擊和堆溢出了。經過程序流劫持,攻擊者能夠控制PC指針從而執行目標代碼。爲了應對這種攻擊,系統防護者也提出了各類防護方法,最多見的方法有DEP(堆棧不可執行),ASLR(內存地址隨機化),Stack Protector(棧保護)等。可是若是上來就部署所有的防護,初學者可能會以爲無從下手,因此咱們先從最簡單的沒有任何保護的程序開始,隨後再一步步增長各類防護措施,接着再學習繞過的方法,按部就班。git

首先來看這個有明顯緩衝區溢出的程序:程序員

圖片描述

這裏咱們用github

#bash
gcc -fno-stack-protector -z execstack -o level1 level1.c

這個命令編譯程序。-fno-stack-protector和-z execstack這兩個參數會分別關掉DEP和Stack Protector。同時咱們在shell中執行:shell

圖片描述

這幾個指令。執行完後咱們就關掉整個linux系統的ASLR保護。編程

接下來咱們開始對目標程序進行分析。首先咱們先來肯定溢出點的位置,這裏我推薦使用pattern.py這個腳原本進行計算。咱們使用以下命令:安全

圖片描述

來生成一串測試用的150個字節的字符串:bash

圖片描述

隨後咱們使用 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

參考文獻

做者:蒸米@阿里聚安全,更多技術文章,請訪問阿里聚安全博客

相關文章
相關標籤/搜索