二進制漏洞挖掘之棧溢出-開啓NX開啓ASLR

二進制漏洞-棧溢出

github地址:https://github.com/ylcangel/exploits/tree/master/stack_overflow

測試平臺

系統:CentOS release 6.10 (Final)、32位

內核版本:Linux 2.6.32-754.10.1.el6.i686  i686 i386 GNU/Linux

gcc 版本: 4.4.7 20120313 (Red Hat 4.4.7-23) (GCC)

gdb版本:GNU gdb (GDB) Red Hat Enterprise Linux (7.2-92.el6)

libc版本:libc-2.12.so

漏洞原理

       在對棧緩衝區進行寫操作時(如memcpy),未對緩衝區大小進行判斷,導致寫入數據長度可能大於緩衝區長度。

通用利用方式

       寫入數據覆蓋返回地址,使返回地址指向惡意代碼起始地址。由於我是基於本地測試,也就是libc庫的版本已知,而基於遠程攻擊或不同版本的libc庫可能會存在差異。

漏洞測試程序

很明顯代碼在執行scanf時未對緩衝區大小進行判斷,存在棧溢出漏洞。

 

注意如無特殊說明,本文的exp都是基於該源碼編譯的二進制實現的。

所有測試均在linux環境下進行

開啓NX(DEP),開啓ASLR

上面例子採用Ret2libc方式實現poc,爲了防止基於該方式的攻擊,ASLR應運而生

開啓ASLR首先需要打開操作系統相應功能/proc/sys/kernel/randomize_va_space

目前randomize_va_space的值有三種,分別是[0,1,2]

0 - 表示關閉進程地址空間隨機化。

1 - 表示將mmap的基址,stack和vdso頁面隨機化。

2 - 表示在1的基礎上增加棧(heap)的隨機化。

開啓命令:echo 2 > /proc/sys/kernel/randomize_va_space

ASLR的開啓無法通過checksec來檢測,他的開啓與系統有關

 

ASLR只針對動態庫基址的中間位數進行隨機化,後三位並不會變

ASLR不會隨機化程序本身的基址

漏洞分析

現在我們來運行幾次看看(注意看主程序和libc的基址變化),第一次運行:

 

第二次運行(這幅圖圈錯位置了^-^):

 

第三次運行:

 

從運行結果可以看出libc的基址、棧、vdso已經隨機了,但主程序的基址卻保持不變。開啓ASLR後主程序以下部分並不會隨機化。

 

通過調試程序,我們得到了buf和返回地址之間距離同樣是28(同NX未開啓ASLR)

 

使用AAAA覆蓋返回地址(程序返回地址被覆蓋爲0x41414141)程序返回執行時崩潰,報段錯誤,再次證明我們得到的距離正確。

 

在執行之前的exp看一下效果,進程id爲2985

 

在看一下它的core dump,這裏報了段錯誤,執行0x62bf00處代碼錯誤。

 

實現exp

爆破

開啓ASLR後libc的基址隨機化了,固定system地址程序肯定會出錯(就如用之前的exp運行,除非它隨機化的基址恰好是0x5f1000),不過libc基址只有中間三位隨機化,這樣我們就可以採用爆破方式遍歷它任何一種可能(最大爆破次數4096)

Libc基址範圍0x1000-0xfff000

libc function address =   libc base address + function offset

system function offset = 0x62bf00 – 0x5f1000 = 0x3af00

bin_sh offset = 0x747b65 – 0x5f1000 = 0x156b65

理論上應該保證程序不崩潰來遍歷libc基址(肯定會在4096次中的一次爆破成功),不過讓程序不崩潰或者崩潰恢復到某個點(例如main函數)重新執行有點麻煩,我這裏偷懶一下,程序崩潰繼續下一個基址繼續爆破,多跑幾次總會命中。

 

執行結果

 

查看輸出的詳細日誌

 

Ret2plt

開啓ASLR後主程序的基址、plt、got、got.plt等地址並不會發生變化,如果主程序有主動調用了libc的system函數,那麼它會生成一個plt和got表項(間接指向函數地址),這樣我們就可以通過ret2plt來實現漏洞利用。

上面的c程序沒有主動調用system函數,我們設計一個程序,在源程序基礎上添加了一個函數shell0,該函數調用libc的system函數,注意我們這裏不會構造調用shell0的exp例子,添加shell0函數調用system僅僅是爲了在主程序中生成一個對應的plt表項,通過調用plt我們就能實現漏洞利用,而不必知道真正的libc的system函數的地址。

 

編譯程序後,程序會生成對應的重定位表項:

 

啓動調試器不運行程序(我們以printf爲例),查看printf(編譯時優化成了puts)plt表項,此時plt表項對應的got表項(0x8049810)指向plt表項的下一條指令(0x80483ce),當第一次調用printf時,其對應的函數地址將在動態鏈接器的幫助下得到解決。

 

接着我們運行程序到scanf處,printf函數已經被調用了,此時動態鏈接器也完成got表項的填充工作,通過調試我們可以清楚的看到got表項(0x8049810)對應的值已經由0x80483ce該變爲正確的puts函數地址(0x653aa0),後面再調用puts函數時就會直接調用對應函數,而不用在通過動態連接器,這也就是所謂的延遲加載或者懶加載。

當然我們並不關心got表項對應地址是否被填充爲正確的地址,我們只要找到plt表項的地址,然後調用該plt即可,動態鏈接器會幫我們填充並調用到正確的地址。

如下圖:

 

開啓ASLR後主程序的基址、plt、rodata是不會隨機化的,因此我們就可以構造讓plt覆蓋返回地址來達到利用的目的。

 

從上圖可以得知system的plt地址爲0x8048388,同時主程序存在command(ls -al)字符串,我們直接利用即可。

Exp如下:

 

運行結果如下(可以看出我們已經成功利用ret2plt技術完成漏洞攻擊了):

 

既然存在ret2plt,那有沒有ret2got呢?這個我不確定,我google也沒搜到相關信息,plt和got在內存中表現形式是不一樣的,plt對應的就是指令,可以直接調用執行,而got對應的是數據(指令地址),直接覆蓋返回地址肯定是不行的,需要進行轉換。

信息泄露

Libc各庫函數相對libc加載基址的偏移是固定的,是否開啓ASLR並不會影響它。如果能獲得libc中某個庫函數的地址,基於上述原理我們就能輕易算出libc加載的基址,這樣就可以輕易獲取任意函數地址甚至數據段特定數據的地址(代碼段緊挨着數據段,編譯時編譯器已經按4k頁對齊計算出了各個段數據相對基址的偏移,這使得你用基址+數據偏移就能得到想要的數據),因此我們需要泄露出某個庫函數在內存中的地址。可用於泄露內存信息的函數包括write、puts、printf,首選write函數,這個函數輸出長度是可控的,不會遇到\x00就截斷字符或末尾補充換行符\n(puts),printf會受到\x00影響。

同樣利用爆破時用過的公式:

libc function address =    libc base address + function offset

查看漏洞程序用了什麼庫函數,可以看到程序雖然調用的是printf,但編譯器卻把它替換成了puts,那我們exp就只能用puts來完成信息泄露了。

 

前面的exp都是構造一次payload來完成漏洞利用,這裏需要構造兩次(很榮幸我當初設計的程序比較簡單,繼續調用main函數不會崩潰),第一次是通過調用puts泄露puts在內存中的實際地址,第二次是調用通過計算得出的system函數來完成最終的漏洞利用。

第一次payload利用同上面,buf距離保留eip的距離是28字節,第二次距離不是28字節,這裏我們主要用gdb看一下第二次的情況:

從上圖我們可以看到並計算出buf距離保留eip的距離是20字節,我們需要驗證繼續覆蓋eip是否可以完成漏洞利用或者引起程序會崩潰。

很榮幸從下圖可以看到,即使是第二次覆蓋eip程序還是正常執行了,並且正確的執行到了system函數中(如果main函數帶有參數我想就不會那麼幸運了,可能會引起程序崩潰),

既然得到驗證,那就可以按照這個思路來實現exp了,它的核心思想是信息泄露或者說泄露內存信息(info leaks 、 memory leaks)。

如果程序中存在可以重複利用的信息泄露漏洞,那你還可以藉助pwntools的 DynELF工具來自動化的完成庫函數和庫函數地址的查找,當然你需要先實現一個leak函數,具體使用方法還請查看相應官方文檔。

我設計的程序中不存在可以重複利用的泄露信息的漏洞,於是需要自己特別構造。

Exp如下:

運行結果如下:

GOT覆蓋和解引用

Ret2plt成立的前提是主程序有調用對應的libc函數,這樣纔會在主程序中生成對應的plt樁,如果沒有這樣的plt樁怎麼辦,就如本文最開始的c程序,它並沒有顯示的調用system函數。如圖:

那我們還可以藉助got覆蓋或者解引用方式實現漏洞利用。

Got覆蓋(got hijack)

這個技巧幫助攻擊者,將特定Libc函數的GOT條目覆蓋爲另一個Libc函數的地址(在第一次調用之後)。在共享庫中,函數距離其基址的偏移永遠是固定的。所以,如果我們將兩個Libc函數的差值(puts和system)加到puts的GOT條目,我們就得到了system函數的地址。之後,調用puts就會調用system。

offset_diff    =    system_addr  -  puts_addr = 0x27ba0

GOT[puts]    =    GOT[puts]    +    offset_diff

利用ROP

測試函數中並沒有實現此功能的代碼,我們可以藉助ROP技術構造類似的功能,構造的ROP鏈大致和下面的樣子相似:

pop reg; ret;

add reg, 偏移;ret;(這裏目標操作數需要是存儲器地址,只有類似指令才能完成GOT覆蓋,GOT條目在內存中),當調用puts函數時就調用了system。

現在我們用工具搜索一下測試程序是否包含這些gadgets或者找到類似的gadgets構造出ROP鏈:

  1. 我們先看看是否存在滿足條件的pop指令的rop鏈,第一條鏈

第二條鏈,我們使ecx含有puts-system的偏移0x27ba0

第三條鏈,我們使ebp包含GOT[puts] – 0x5b042464 = 0x80497b8 - 0x5b042464 = -0x52FF8CAC(0xAD007354),然後執行add %ecx, got[puts]

構造的rop鏈對應的棧如下圖:

乍一看似乎很滿足條件,然而第二條鏈包含leave指令它會重新設置esp(等價於mov %ebp, %esp;),這會導致我們填充的棧不在我們控制之內,除非我們能讓leave之後的esp和之前的esp一致或在我們可控範圍內(再次搜索並沒有找到控制esp需要的片段,而對於棧劫持,我們沒有額外的可控區域來作爲新棧)。

我經過了大量的搜尋包括手動搜尋都沒有搜尋到比上面這三條鏈更具有說服意義的指令片段了,然而它的第二條鏈存在一點點缺陷導致構造ROP鏈失敗。

ROP+Info leak

看來直接構造ROP鏈來實現GOT覆蓋功能有困難,我們需要藉助別的技術來共同完成此功能並驗證這種方法的可行性。這裏我選擇了ROP+Info leak來完成最終exp。本質上我們只要覆蓋任一GOT表項爲system、execve這類的函數地址即可(這裏我們藉助read實現地址覆蓋)。具體過程這裏就不在介紹了,這裏使用rop僅僅是用來控制esp和主動調用puts函數,直接上exp代碼:

本exp第二次payload棧佈局:

通過gdb我們看一下是否實現了GOT覆蓋,從下圖我們不難看出已經將GOT[puts]成功覆蓋成了system函數地址。

運行結果:

從下圖可以看出GOT覆蓋是可行的,只是實現起來比較繁瑣和複雜,我僅僅用我的測試程序證明其可行性(我的測試程序不是特殊設計的,導致構造GOT覆蓋的exp顯得複雜),在某些場景下GOT覆蓋有其獨特的優越性,並且實現起來也簡單明瞭。

Got解引用

這個技巧類似於GOT覆蓋,但是這裏不會覆蓋特定    Libc函數的GOT條目,而是將它的值複製到寄存器中,並將偏移差加到寄存器的內容。因此,寄存器就含有所需的Libc函數地址。例如,GOT[puts]   包含puts的函數地址,將其複製到寄存器。兩個Libc函數(puts和system)的偏移差加到寄存器的內容。

現在跳到寄存器的值就調用了system。

offset_diff    =    system_addr  -  puts_addr

eax =    GOT[puts]

eax =    eax + offset_diff

同樣這裏也使用ROP技術構造該功能,構造的ROP鏈大致如下:

pop reg1; ret;#偏移

pop reg2; ret; #GOT[puts]

add reg2, reg1;call reg1;(直接調用system,或者push reg1;ret;)

讓我們逆着從add指令處搜尋一下:

從下圖可以看到包含add指令並直接調用call指令的指令片段存在兩處,分別用到寄存器edx和esi,我們再次搜尋一下測試程序看是否存在pop edx或pop esi的片段

搜尋如下:從圖中可以看到有搜尋到包含pop esi的指令片段,那麼我們只需要在找到pop eax指令片段就可以了。

搜尋pop eax,從下圖可以看到雖然搜尋到了包含pop eax的指令片段,但是該片段包含leave指令,上面已經提到該指令會重新設置esp,這樣會讓我們填充的棧不在控制範圍之內。

我經過了大量的搜尋沒有在測試程序中找到可以組合的rop鏈,但原理我已經講明白了,就不在特意去構造exp了。不過有個地方需要引起你的注意,在通過call指令調用函數時,你需要在棧上填充被調用函數的參數

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章