二進制漏洞-棧溢出
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。
漏洞分析
此漏洞分析要完成幾個任務:
- 緩衝區距離返回地址之間的字節長度
- 確定利用方式
我們先解決第一個任務,首先看一下棧結構(對於函數調用存在約定如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”)爲例。
- 找出system的函數地址(這個比較簡單,程序沒用開啓ASLR, 只需找到system在文件中的地址即可,這個地址不變)
- 構造/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操作系統?
我們先來解決一下圖中的兩個問題:
- 在這個場景中我們的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變化。