二進制漏洞挖掘之棧溢出-開啓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

略,NX棧不可執行,現在幾乎沒有不開啓NX保護的了

開啓NX(DEP),未開啓ASLR

其他保護未開啓,這個是棧溢出中最簡單的

 

未開啓ASLR

 

程序第一次運行:

 

程序第二次運行:

 

程序第三次運行:

 

從以上運行可以看到主程序基址和libc基址一直保持不變,主程序基址0x8048000,libc基址0x5f1000。

漏洞分析

此漏洞分析要完成幾個任務:

  1. 緩衝區距離返回地址之間的字節長度
  2. 確定利用方式

我們先解決第一個任務,首先看一下棧結構(對於函數調用存在約定如cdecl、stdcall、fastcall),我們這裏以cdecl調用約定的棧結構爲例:

 

上圖就是cdecl調用約定的棧結構,gcc編譯基本都採用這種調用約定。

現在我們藉助調試分析一下,首先在scanf(“%s”, buf)處下段,然後運行到斷點處,讓我們看看此時棧幀相關信息

 

從圖中我們可以看到當前棧幀保存的返回地址是0x607d28

當前棧相關信息

 

從圖中我們可以看到當前棧頂爲0xbfff250

buf相關信息

 

從圖中可以看出buf的地址時0xbffff260(0xbffff260-0xbffff270是該buf的緩衝區),目前緩衝區中數據爲0。

從以上圖中其實我們已經看出0xbfff27c處存儲的就是返回地址,buf距離0xbffff27c的長度爲28字節(0xbffff27c-0xbffff260),也就是從第29個字節開始的4個就是返回地址。我們來驗證一下,調試時輸入28個A

 

可以看出0xbffff260-0xbffff27b全部變成0x41(ASCII碼對應字符A),並且就連返回地址的起始兩位都被覆蓋成了00(小端,字符串結尾)。輸入32個A

 

從圖中我們可以看到返回地址已經完全被0x41414141覆蓋。

第一個任務已經解決完畢,讓我們看看第二個任務,由於程序開啓了nx保護,我們不能直接把shellcode寫在棧上來執行攻擊代碼。不過衆所周知libc是程序運行必須的,於是在漏洞利用時一般都是藉助現有的函數(特別是libc函數,如system exec等)來達到任意命令執行的目的。該程序並沒有顯示的調用這些函數,因此我們必須手動構造,這裏我以構造system(“/bin/sh”)爲例。

  1. 找出system的函數地址(這個比較簡單,程序沒用開啓ASLR, 只需找到system在文件中的地址即可,這個地址不變)
  2. 構造/bin/sh(直接構造可能有點困難),我們不直接構造,因爲在libc中包含/bin/sh字符串,直接利用a中提到的方法找到該字符串地址作爲參數傳遞給system即可。

 

構造後的棧結構如圖

實現exp

ret2libc

程序開啓了NX(棧不可執行,在棧上填充shellcode,並用shellcode地址覆蓋eip不能達到漏洞利用的目的),可以採用ret2libc方式繞過,讓執行流跳轉到libc函數中,這樣就滿足權限驗證條件,這裏藉助一個特別好用的工具pwntools來實現的exp

運行效果:

ROP

ROP(Return Oriented Programming)即面向返回地址編程,其主要思想是在棧緩衝區溢出的基礎上,通過利用程序中已有的小片段(gadgets)來改變某些寄存器或者變量的值,從而改變程序的執行流程,達到漏洞利用目的。

Rop技術是利用pop xxx、 ret類似的指令構造一個鏈來實現exp的,我們這裏介紹一下pop和ret指令。
Pop 操作數,操作數是寄存器,或者存儲器,不能是立即數

Pop xxx指令是從棧頂彈出數據並賦值給寄存器、存儲器xxx,如pop eax 就是從棧頂彈出數據並賦值給eax。

Ret指令從當前棧頂位置彈出返回執行指令的地址給EIP,等效指令是 pop eip。

讓我們用圖示展示一下ret2libc和rop技術細節:

從上圖可以看到ret2libc方式把返回地址直接覆蓋爲libc函數system地址,程序控制流被引導到libc函數system中。

下圖是ROP實現控制流變化圖示,它是否更適合於32位x86的linux操作系統?

我們先來解決一下圖中的兩個問題:

  1. 在這個場景中我們的gadget鏈的第一條指令是用pop xxx還是ret?

先忽略函數調用約定,gadget鏈的第一條指令是pop xxx還是ret取決於我們在圖中1處存放的是指令地址(函數地址)還是參數,ROP圖示1處存放的是system的地址,因此該gadget鏈第一條指令應該是指向ret的地址(pop eip),這樣程序控制流就被我們改變了。執行完ret指令後,system地址從棧上彈出。

2、/bin/sh怎麼傳遞給system函數?

在32位的linux系統下如果調用約定不是fastcall,被調用函數參數都是通過棧來傳遞的,如果調用約定是fastcall,被調用的函數的第一個和第二個參數將由EDX和ECX傳遞。其餘參數同cdecl約定,現在通過圖示對比一下二者區別:

上圖是通過棧傳遞參數的rop,從圖上可以看到這裏的rop鏈僅包含一條ret指令,rop棧填充字節數 – 4 = ret2libc棧填充字節數,並且返回地址被覆蓋成了ret指令的地址,system地址緊隨其後。

上圖是fastcall調用約定的rop,該rop就具備典型rop的模樣了,它是由pop和ret指令組合構成的。32位x86 linux系統fastcall調用約定規定頭兩個參數需要使用寄存器EDX、ECX傳遞,其餘參數使用棧。這樣就有很多組合如:

pop edx | ecx; pop ecx | edx;ret;或pop ecx | edx;ret; pop edx | ecx;ret;

或pop ecx | edx;pop xxx;ret; pop edx | ecx;pop xxx;ret;

需要按照ret對應棧上的地址是相應的指令地址,pop對應棧上的數據是相應的參數規則來佈置棧結構。上圖演示了第一種情況的棧佈局。

下面這幅圖演示了第二種rop鏈的棧佈置情況(後面的就不舉例了)。

需要注意64位x86 linux操作系統和32位是存在差異的,無特殊指定調用約定,32位gcc編譯都遵從cdecl調用約定即函數參數都通過棧傳遞。但是在64位系統函數參數傳遞遵從下面約定,當參數少於7個時,參數從左到右放入寄存器: rdi,  rsi,  rdx,  rcx,  r8,  r9。

當參數爲7個以上時,前6個與前面一樣,但後面的依次從“右向左”放入棧中,即和32位彙編一樣。

通過上面介紹32位x86 linux系統無特殊指定都遵從cdecl調用約定,因此函數參數仍然是存放在棧上,/bin/sh字符串的地址也就相應的存放在棧上,也就不存在pop xxx這樣的gadget。下圖是最終調用system的rop和棧佈局。

使用ROPgadget工具可以方便的幫我們搜尋我們關注的gadget,我的exp只需要ret,搜尋結果如下:

於是構造exp如下:

執行結果如下(從圖中可以看到我們輕易的繞過了NX保護):

我們在用ROPgadget工具測試一下:

從圖中我們並沒有發現pop edx或者pop ecx,所以即使使用了fastcall調用約定我們也不能輕易的構造rop鏈,而是需要精心設計如pop ebx; mov ebx,ecx; ret(略),32位x86的linux系統在不改變寄存器或者使用寄存器的情況,ret2libc比rop使用起來更方便。

在構造rop鏈的時候,有些時候你需要關注esp到ebp的距離是否足夠來容納你構造的超長rop鏈,如果鏈中存在leave指令要格外注意,它會引起esp變化。

 

 

 

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