前言
雖然程式加載以及動態符號鏈接都已經很理解了,但是這伙卻被行程的記憶體映像給”糾纏"住,看著看著就一發不可收拾——很有趣,
下面一起來探究“緩沖區溢位和注入”問題(主要是關心程式的記憶體映像),
行程的記憶體映像
永遠的 Hello World,太熟悉了吧,
#include <stdio.h>
int main(void)
{
printf("Hello World\n");
return 0;
}
如果要用行內匯編(inline assembly)來寫呢?
1 /* shellcode.c */
2 void main()
3 {
4 __asm__ __volatile__("jmp forward;"
5 "backward:"
6 "popl %esi;"
7 "movl $4, %eax;"
8 "movl $2, %ebx;"
9 "movl %esi, %ecx;"
10 "movl $12, %edx;"
11 "int $0x80;" /* system call 1 */
12 "movl $1, %eax;"
13 "movl $0, %ebx;"
14 "int $0x80;" /* system call 2 */
15 "forward:"
16 "call backward;"
17 ".string \"Hello World\\n\";");
18 }
看起來很復雜,實際上就做了一個事情,往終端上寫了個 Hello World ,不過這個非常有意思,先簡單分析一下流程:
- 第 4 行指令的作用是跳轉到第 15 行(即
forward標記處),接著執行第 16 行, - 第 16 行呼叫
backward,跳轉到第 5 行,接著執行 6 到 14 行, - 第 6 行到第 11 行負責在終端列印出
Hello World字串(等一下詳細介紹), - 第 12 行到第 14 行退出程式(等一下詳細介紹),
為了更好的理解上面的代碼和后續的分析,先來介紹幾個比較重要的內容,
常用暫存器初識
X86 處理器平臺有三個常用暫存器:程式指令指標、程式堆疊指標與程式基指標:
| 暫存器 | 名稱 | 注釋 |
|---|---|---|
| EIP | 程式指令指標 | 通常指向下一條指令的位置 |
| ESP | 程式堆疊指標 | 通常指向當前堆疊的當前位置 |
| EBP | 程式基指標 | 通常指向函式使用的堆疊頂端 |
當然,上面都是擴展的暫存器,用于 32 位系統,對應的 16 系統為 ip,sp,bp ,
call,ret 指令的作用分析
-
call指令跳轉到某個位置,并在之前把下一條指令的地址(
EIP)入堆疊(為了方便”程式“回傳以后能夠接著執行),這樣的話就有:call backward ==> push eip jmp backward -
ret指令通常
call指令和ret是配合使用的,前者壓入跳轉前的下一條指令地址,后者彈出call指令壓入的那條指令,從而可以在函式呼叫結束以后接著執行后面的指令,ret ==> pop eip
通常在函式呼叫后,還需要恢復 esp 和 ebp,恢復 esp 即恢復當前堆疊指標,以便釋放呼叫函式時為存盤函式的區域變數而自動分配的空間;恢復 ebp 是從堆疊中彈出一個資料項(通常函式呼叫過后的第一條陳述句就是 push ebp),從而恢復當前的函式指標為函式呼叫者本身,這兩個動作可以通過一條 leave 指令完成,
這三個指令對我們后續的解釋會很有幫助,更多關于 Intel 的指令集,請參考:Intel 386 Manual, x86 Assembly Language FAQ:part1, part2, part3.
什么是系統呼叫(以 Linux 2.6.21 版本和 x86 平臺為例)
系統呼叫是用戶和內核之間的介面,用戶如果想寫程式,很多時候直接呼叫了 C 庫,并沒有關心系統呼叫,而實際上 C 庫也是基于系統呼叫的,這樣應用程式和內核之間就可以通過系統呼叫聯系起來,它們分別處于作業系統的用戶空間和內核空間(主要是記憶體地址空間的隔離),
用戶空間 應用程式(Applications)
| |
| C庫(如glibc)
| |
系統呼叫(System Calls,如sys_read, sys_write, sys_exit)
|
內核空間 內核(Kernel)
系統呼叫實際上也是一些函式,它們被定義在 arch/i386/kernel/sys_i386.c (老的在 arch/i386/kernel/sys.c)檔案中,并且通過一張系統呼叫表組織,該表在內核啟動時就已經加載了,這個表的入口在內核源代碼的 arch/i386/kernel/syscall_table.S 里頭(老的在 arch/i386/kernel/entry.S),這樣,如果想添加一個新的系統呼叫,修改上面兩個內核中的檔案,并重新編譯內核就可以,當然,如果要在應用程式中使用它們,還得把它寫到 include/asm/unistd.h 中,
如果要在 C 語言中使用某個系統呼叫,需要包含頭檔案 /usr/include/asm/unistd.h,里頭有各個系統呼叫的宣告以及系統呼叫號(對應于呼叫表的入口,即在呼叫表中的索引,為方便查找呼叫表而設立的),如果是自己定義的新系統呼叫,可能還要在開頭用宏 _syscall(type, name, type1, name1...)來宣告好引數,
如果要在匯編語言中使用,需要用到 int 0x80 呼叫,這個是系統呼叫的中斷入口,涉及到傳送引數的暫存器有這么幾個,eax 是系統呼叫號(可以到 /usr/include/asm-i386/unistd.h 或者直接到 arch/i386/kernel/syscall_table.S 查到),其他暫存器如 ebx,ecx,edx,esi,edi 一次存放系統呼叫的引數,而系統呼叫的回傳值存放在 eax 暫存器中,
下面我們就很容易解釋前面的 Shellcode.c 程式流程的 2,3 兩部分了,因為都用了 int 0x80 中斷,所以都用到了系統呼叫,
第 3 部分很簡單,用到的系統呼叫號是 1,通過查表(查 /usr/include/asm-i386/unistd.h 或 arch/i386/kernel/syscall_table.S)可以發現這里是 sys_exit 呼叫,再從 /usr/include/unistd.h 檔案看這個系統呼叫的宣告,發現引數 ebx 是程式退出狀態,
第 2 部分比較有趣,而且復雜一點,我們依次來看各個暫存器,首先根據 eax 為 4 確定(同樣查表)系統呼叫為 sys_write,而查看它的宣告(從 /usr/include/unistd.h),我們找到了引數依次為檔案描述符、字串指標和字串長度,
- 第一個引數是
ebx,正好是 2,即標準錯誤輸出,默認為終端, - 第二個引數是
ecx,而ecx的內容來自esi,esi來自剛彈出堆疊的值(見第 6 行popl %esi;),而之前剛好有call指令引起了最近一次壓堆疊操作,入堆疊的內容剛好是call指令的下一條指令的地址,即.string所在行的地址,這樣ecx剛好參考了Hello World\\n字串的地址, - 第三個引數是
edx,剛好是 12,即Hello World\\n字串的長度(包括一個空字符),這樣,Shellcode.c的執行流程就很清楚了,第 4,5,15,16 行指令的巧妙之處也就容易理解了(把.string存放在call指令之后,并用popl指令把eip彈出當作字串的入口),
什么是 ELF 檔案
這里的 ELF 不是“精靈”,而是 Executable and Linking Format 檔案,是 Linux 下用來做目標檔案、可執行檔案和共享庫的一種檔案格式,它有專門的標準,例如:X86 ELF format and ABI,中文版,
下面簡單描述 ELF 的格式,
ELF 檔案主要有三種,分別是:
- 可重定位的目標檔案,在編譯時用
gcc的-c引數時產生, - 可執行檔案,這類檔案就是我們后面要討論的可以執行的檔案,
- 共享庫,這里主要是動態共享庫,而靜態共享庫則是可重定位的目標檔案通過
ar命令組織的,
ELF 檔案的大體結構:
ELF Header #程式頭,有該檔案的Magic number(參考man magic),型別等
Program Header Table #對可執行檔案和共享庫有效,它描述下面各個節(section)組成的段
Section1
Section2
Section3
.....
Program Section Table #僅對可重定位目標檔案和靜態庫有效,用于描述各個Section的重定位資訊等,
對于可執行檔案,檔案最后的 Program Section Table (節區表)和一些非重定位的 Section,比如 .comment,.note.XXX.debug 等資訊都可以洗掉掉,不過如果用 strip,objcopy 等工具洗掉掉以后,就不可恢復了,因為這些資訊對程式的運行一般沒有任何用處,
ELF 檔案的主要節區(section)有 .data,.text,.bss,.interp 等,而主要段(segment)有 LOAD,INTERP 等,它們之間(節區和段)的主要對應關系如下:
| Section | 解釋 | 實體 |
|---|---|---|
| .data | 初始化的資料 | 比如 int a=10 |
| .bss | 未初始化的資料 | 比如 char sum[100]; 這個在程式執行之前,內核將初始化為 0 |
| .text | 程式代碼正文 | 即可執行指令集 |
| .interp | 描述程式需要的解釋器(動態連接和裝載程式) | 存有解釋器的全路徑,如 /lib/ld-linux.so |
而程式在執行以后,.data,.bss,.text 等一些節區會被 Program header table 映射到 LOAD 段,.interp 則被映射到了 INTERP 段,
對于 ELF 檔案的分析,建議使用 file,size,readelf,objdump,strip,objcopy,gdb,nm 等工具,
這里簡單地演示這幾個工具:
$ gcc -g -o shellcode shellcode.c #如果要用gdb除錯,編譯時加上-g是必須的
shellcode.c: In function ‘main’:
shellcode.c:3: warning: return type of ‘main’ is not ‘int’
f$ file shellcode #file命令查看檔案型別,想了解作業原理,可man magic,man file
shellcode: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
dynamically linked (uses shared libs), not stripped
$ readelf -l shellcode #列出ELF檔案前面的program head table,后面是它描
#述了各個段(segment)和節區(section)的關系,即各個段包含哪些節區,
Elf file type is EXEC (Executable file)
Entry point 0x8048280
There are 7 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x0044c 0x0044c R E 0x1000
LOAD 0x00044c 0x0804944c 0x0804944c 0x00100 0x00104 RW 0x1000
DYNAMIC 0x000460 0x08049460 0x08049460 0x000c8 0x000c8 RW 0x4
NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r
.rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag
06
$ size shellcode #可用size命令查看各個段(對應后面將分析的行程記憶體映像)的大小
text data bss dec hex filename
815 256 4 1075 433 shellcode
$ strip -R .note.ABI-tag shellcode #可用strip來給可執行檔案“減肥”,洗掉無用資訊
$ size shellcode #“減肥”后效果“明顯”,對于嵌入式系統應該有很大的作用
text data bss dec hex filename
783 256 4 1043 413 shellcode
$ objdump -s -j .interp shellcode #這個主要作業是反編譯,不過用來查看各個節區也很厲害
shellcode: file format elf32-i386
Contents of section .interp:
8048114 2f6c6962 2f6c642d 6c696e75 782e736f /lib/ld-linux.so
8048124 2e3200 .2.
補充:如果要洗掉可執行檔案的 Program Section Table,可以用 A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux 一文的作者寫的 elf kicker 工具鏈中的 sstrip 工具,
程式執行基本程序
在命令列下,敲入程式的名字或者是全路徑,然后按下回車就可以啟動程式,這個具體是怎么作業的呢?
首先要再認識一下我們的命令列,命令列是內核和用戶之間的介面,它本身也是一個程式,在 Linux 系統啟動以后會為每個終端用戶建立一個行程執行一個 Shell 解釋程式,這個程式解釋并執行用戶輸入的命令,以實作用戶和內核之間的介面,這類解釋程式有哪些呢?目前 Linux 下比較常用的有 /bin/bash ,那么該程式接收并執行命令的程序是怎么樣的呢?
先簡單描述一下這個程序:
- 讀取用戶由鍵盤輸入的命令列,
- 分析命令,以命令名作為檔案名,并將其它引數改為系統呼叫
execve內部處理所要求的形式, - 終端行程呼叫
fork建立一個子行程, - 終端行程本身用系統呼叫
wait4來等待子行程完成(如果是后臺命令,則不等待),當子行程運行時呼叫execve,子行程根據檔案名(即命令名)到目錄中查找有關檔案(這是命令解釋程式構成的檔案),將它調入記憶體,執行這個程式(解釋這條命令), - 如果命令末尾有
&號(后臺命令符號),則終端行程不用系統呼叫wait4等待,立即發提示符,讓用戶輸入下一個命令,轉 1),如果命令末尾沒有&號,則終端行程要一直等待,當子行程(即運行命令的行程)完成處理后終止,向父行程(終端行程)報告,此時終端行程醒來,在做必要的判別等作業后,終端行程發提示符,讓用戶輸入新的命令,重復上述處理程序,
現在用 strace 來跟蹤一下程式執行程序中用到的系統呼叫,
$ strace -f -o strace.out test
$ cat strace.out | grep \(.*\) | sed -e "s#[0-9]* \([a-zA-Z0-9_]*\)(.*).*#\1#g"
execve
brk
access
open
fstat64
mmap2
close
open
read
fstat64
mmap2
mmap2
mmap2
mmap2
close
mmap2
set_thread_area
mprotect
munmap
brk
brk
open
fstat64
mmap2
close
close
close
exit_group
相關的系統呼叫基本體現了上面的執行程序,需要注意的是,里頭還涉及到記憶體映射(mmap2)等,
下面再羅嗦一些比較有意思的內容,參考《深入理解 Linux 內核》的程式的執行(P681),
Linux 支持很多不同的可執行檔案格式,這些不同的格式是如何解釋的呢?平時我們在命令列下敲入一個命令就完了,也沒有去管這些細節,實際上 Linux 下有一個 struct linux_binfmt 結構來管理不同的可執行檔案型別,這個結構中有對應的可執行檔案的處理函式,大概的程序如下:
-
在用戶態執行了
execve后,引發int 0x80中斷,進入內核態,執行內核態的相應函式do_sys_execve,該函式又呼叫do_execve函式,do_execve函式讀入可執行檔案,檢查權限,如果沒問題,繼續讀入可執行檔案需要的相關資訊(struct linux_binprm描述的), -
接著執行
search_binary_handler,根據可執行檔案的型別(由上一步的最后確定),在linux_binfmt結構鏈表(formats,這個鏈表可以通過register_binfmt和unregister_binfmt注冊和洗掉某些可執行檔案的資訊,因此注冊新的可執行檔案成為可能,后面再介紹)上查找,找到相應的結構,然后執行相應的load_binary函式開始加載可執行檔案,在該鏈表的最后一個元素總是對解釋腳本(interpreted script)的可執行檔案格式進行描述的一個物件,這種格式只定義了load_binary方法,其相應的load_script函式檢查這種可執行檔案是否以兩個#!字符開始,如果是,這個函式就以另一個可執行檔案的路徑名作為引數解釋第一行的其余部分,并把腳本檔案名作為引數傳遞以執行這個腳本(實際上腳本程式把自身的內容當作一個引數傳遞給了解釋程式(如/bin/bash),而這個解釋程式通常在腳本檔案的開頭用#!標記,如果沒有標記,那么默認解釋程式為當前SHELL), -
對于
ELF型別檔案,其處理函式是load_elf_binary,它先讀入ELF檔案的頭部,根據頭部資訊讀入各種資料,再次掃描程式段描述表(Program Header Table),找到型別為PT_LOAD的段(即.text,.data,.bss等節區),將其映射(elf_map)到記憶體的固定地址上,如果沒有動態連接器的描述段,把回傳的入口地址設定成應用程式入口,完成這個功能的是start_thread,它不啟動一個執行緒,而只是用來修改了pt_regs中保存的PC等暫存器的值,使其指向加載的應用程式的入口,當內核操作結束,回傳用戶態時接著就執行應用程式本身了, -
如果應用程式使用了動態連接庫,內核除了加載指定的可執行檔案外,還要把控制權交給動態連接器(
ld-linux.so)以便處理動態連接的程式,內核搜尋段表(Program Header Table),找到標記為PT_INTERP段中所對應的動態連接器的名稱,并使用load_elf_interp加載其映像,并把回傳的入口地址設定成load_elf_interp的回傳值,即動態聯結器的入口,當execve系統呼叫退出時,動態連接器接著運行,它檢查應用程式對共享鏈接庫的依賴性,并在需要時對其加載,對程式的外部參考進行重定位(具體程序見《行程和行程的基本操作》),然后把控制權交給應用程式,從ELF檔案頭部中定義的程式進入點(用readelf -h可以出看到,Entry point address即是)開始執行,(不過對于非LIB_BIND_NOW的共享庫裝載是在有外部參考請求時才執行的),
對于內核態的函式呼叫程序,沒有辦法通過 strace(它只能跟蹤到系統呼叫層)來做的,因此要想跟蹤內核中各個系統呼叫的執行細節,需要用其他工具,比如可以通過 Ftrace 來跟蹤內核具體呼叫了哪些函式,當然,也可以通過 ctags/cscope/LXR 等工具分析內核的源代碼,
Linux 允許自己注冊我們自己定義的可執行格式,主要介面是 /procy/sys/fs/binfmt_misc/register,可以往里頭寫入特定格式的字串來實作,該字串格式如下: :name:type:offset:string:mask:interpreter:
name新格式的標示符type識別型別(M表示魔數,E表示擴展)offset魔數(magic number,請參考man magic和man file)在檔案中的啟始偏移量string以魔數或者以擴展名匹配的位元組序列mask用來屏蔽掉string的一些位interpreter程式解釋器的完整路徑名
Linux 下程式的記憶體映像
Linux 下是如何給行程分配記憶體(這里僅討論虛擬記憶體的分配)的呢?可以從 /proc/<pid>/maps 檔案中看到個大概,這里的 pid 是行程號,
/proc 下有一個檔案比較特殊,是 self,它鏈接到當前行程的行程號,例如:
$ ls /proc/self -l
lrwxrwxrwx 1 root root 64 2000-01-10 18:26 /proc/self -> 11291/
$ ls /proc/self -l
lrwxrwxrwx 1 root root 64 2000-01-10 18:26 /proc/self -> 11292/
看到沒?每次都不一樣,這樣我們通過 cat /proc/self/maps 就可以看到 cat 程式執行時的記憶體映像了,
$ cat -n /proc/self/maps
1 08048000-0804c000 r-xp 00000000 03:01 273716 /bin/cat
2 0804c000-0804d000 rw-p 00003000 03:01 273716 /bin/cat
3 0804d000-0806e000 rw-p 0804d000 00:00 0 [heap]
4 b7b90000-b7d90000 r--p 00000000 03:01 87528 /usr/lib/locale/locale-archive
5 b7d90000-b7d91000 rw-p b7d90000 00:00 0
6 b7d91000-b7ecd000 r-xp 00000000 03:01 466875 /lib/libc-2.5.so
7 b7ecd000-b7ece000 r--p 0013c000 03:01 466875 /lib/libc-2.5.so
8 b7ece000-b7ed0000 rw-p 0013d000 03:01 466875 /lib/libc-2.5.so
9 b7ed0000-b7ed4000 rw-p b7ed0000 00:00 0
10 b7eeb000-b7f06000 r-xp 00000000 03:01 402817 /lib/ld-2.5.so
11 b7f06000-b7f08000 rw-p 0001b000 03:01 402817 /lib/ld-2.5.so
12 bfbe3000-bfbf8000 rw-p bfbe3000 00:00 0 [stack]
13 ffffe000-fffff000 r-xp 00000000 00:00 0 [vdso]
編號是原檔案里頭沒有的,為了說明方便,用 -n 引數加上去的,我們從中可以得到如下資訊:
- 第 1,2 行對應的記憶體區是我們的程式(包括指令,資料等)
- 第 3 到 12 行對應的記憶體區是堆疊段,里頭也映像了程式參考的動態連接庫
- 第 13 行是內核空間
總結一下:
- 前兩部分是用戶空間,可以從
0x00000000到0xbfffffff(在測驗的2.6.21.5-smp上只到bfbf8000),而內核空間從0xC0000000到0xffffffff,分別是3G和1G,所以對于每一個行程來說,共占用4G的虛擬記憶體空間 - 從程式本身占用的記憶體,到堆疊段(動態獲取記憶體或者是函式運行程序中用來存盤區域變數、引數的空間,前者是
heap,后者是stack),再到內核空間,地址是從低到高的 - 堆疊頂并非
0xC0000000下的一個固定數值
結合相關資料,可以得到這么一個比較詳細的行程記憶體映像表(以 Linux 2.6.21.5-smp 為例):
| 地址 | 內核空間 | 描述 |
|---|---|---|
| 0xC0000000 | ||
| (program flie) 程式名 | execve 的第一個引數 | |
| (environment) 環境變數 | execve 的第三個引數,main 的第三個引數 | |
| (arguments) 引數 | execve 的第二個引數,main 的形參 | |
| (stack) 堆疊 | 自動變數以及每次函式呼叫時所需保存的資訊都 | |
| 存放在此,包括函式回傳地址、呼叫者的 | ||
| 環境資訊等,函式的引數,區域變數都存放在此 | ||
| (shared memory) 共享記憶體 | 共享記憶體的大概位置 | |
| ... | ||
| ... | ||
| (heap) 堆 | 主要在這里進行動態存盤分配,比如 malloc,new 等, | |
| ... | ||
| .bss (uninitilized data) | 沒有初始化的資料(全域變數哦) | |
| .data (initilized global data) | 已經初始化的全域資料(全域變數) | |
| .text (Executable Instructions) | 通常是可執行指令 | |
| 0x08048000 | ||
| 0x00000000 | ... |
光看沒有任何概念,我們用 gdb 來看看剛才那個簡單的程式,
$ gcc -g -o shellcode shellcode.c #要用gdb除錯,在編譯時需要加-g引數
$ gdb -q ./shellcode
(gdb) set args arg1 arg2 arg3 arg4 #為了測驗,設定幾個引數
(gdb) l #瀏覽代碼
1 /* shellcode.c */
2 void main()
3 {
4 __asm__ __volatile__("jmp forward;"
5 "backward:"
6 "popl %esi;"
7 "movl $4, %eax;"
8 "movl $2, %ebx;"
9 "movl %esi, %ecx;"
10 "movl $12, %edx;"
(gdb) break 4 #在匯編入口設定一個斷點,讓程式運行后停到這里
Breakpoint 1 at 0x8048332: file shellcode.c, line 4.
(gdb) r #運行程式
Starting program: /mnt/hda8/Temp/c/program/shellcode arg1 arg2 arg3 arg4
Breakpoint 1, main () at shellcode.c:4
4 __asm__ __volatile__("jmp forward;"
(gdb) print $esp #列印當前堆疊指標值,用于查找整個堆疊的堆疊頂
$1 = (void *) 0xbffe1584
(gdb) x/100s $esp+4000 #改變后面的4000,不斷往更大的空間找
(gdb) x/1s 0xbffe1fd9 #在 0xbffe1fd9 找到了程式名,這里是該次運行時的堆疊頂
0xbffe1fd9: "/mnt/hda8/Temp/c/program/shellcode"
(gdb) x/10s 0xbffe17b7 #其他環境變數資訊
0xbffe17b7: "CPLUS_INCLUDE_PATH=/usr/lib/qt/include"
0xbffe17de: "MANPATH=/usr/local/man:/usr/man:/usr/X11R6/man:/usr/lib/java/man:/usr/share/texmf/man"
0xbffe1834: "HOSTNAME=falcon.lzu.edu.cn"
0xbffe184f: "TERM=xterm"
0xbffe185a: "SSH_CLIENT=219.246.50.235 3099 22"
0xbffe187c: "QTDIR=/usr/lib/qt"
0xbffe188e: "SSH_TTY=/dev/pts/0"
0xbffe18a1: "USER=falcon"
...
(gdb) x/5s 0xbffe1780 #一些傳遞給main函式的引數,包括檔案名和其他引數
0xbffe1780: "/mnt/hda8/Temp/c/program/shellcode"
0xbffe17a3: "arg1"
0xbffe17a8: "arg2"
0xbffe17ad: "arg3"
0xbffe17b2: "arg4"
(gdb) print init #列印init函式的地址,這個是/usr/lib/crti.o里頭的函式,做一些初始化操作
$2 = {<text variable, no debug info>} 0xb7e73d00 <init>
(gdb) print fini #也在/usr/lib/crti.o中定義,在程式結束時做一些處理作業
$3 = {<text variable, no debug info>} 0xb7f4a380 <fini>
(gdb) print _start #在/usr/lib/crt1.o,這個才是程式的入口,必須的,ld會檢查這個
$4 = {<text variable, no debug info>} 0x8048280 <__libc_start_main@plt+20>
(gdb) print main #這里是我們的main函式
$5 = {void ()} 0x8048324 <main>
補充:在行程的記憶體映像中可能看到諸如 init,fini,_start 等函式(或者是入口),這些東西并不是我們自己寫的啊?為什么會跑到我們的代碼里頭呢?實際上這些東西是鏈接的時候 gcc 默認給連接進去的,主要用來做一些行程的初始化和終止的動作,更多相關的細節可以參考資料如何獲取當前行程之靜態影像檔案和"The Linux Kernel Primer", P234, Figure 4.11,如果想了解鏈接(ld)的具體程序,可以看看本節參考《Unix環境高級編程編程》第7章 "UnIx行程的環境", P127和P13,ELF: From The Programmer's Perspective,GNU-ld 連接腳本 Linker Scripts,
上面的操作對堆疊的操作比較少,下面我們用一個例子來演示堆疊在記憶體中的情況,
堆疊在記憶體中的組織
這一節主要介紹一個函式被呼叫時,引數是如何傳遞的,區域變數是如何存盤的,它們對應的堆疊的位置和變化情況,從而加深對堆疊的理解,在操作時發現和參考資料的結果不太一樣(參考資料中沒有 edi 和 esi 相關資訊,再第二部分的一個小程式里頭也沒有),可能是 gcc 版本的問題或者是它對不同源代碼的處理不同,我的版本是 4.1.2 (可以通過 gcc --version 查看),
先來一段簡單的程式,這個程式除了做一個加法操作外,還復制了一些字串,
/* testshellcode.c */
#include <stdio.h> /* printf */
#include <string.h> /* memset, memcpy */
#define BUF_SIZE 8
#ifndef STR_SRC
# define STR_SRC "AAAAAAA"
#endif
int func(int a, int b, int c)
{
int sum = 0;
char buffer[BUF_SIZE];
sum = a + b + c;
memset(buffer, '\0', BUF_SIZE);
memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);
return sum;
}
int main()
{
int sum;
sum = func(1, 2, 3);
printf("sum = %d\n", sum);
return 0;
}
上面這個代碼沒有什么問題,編譯執行一下:
$ make testshellcode
cc testshellcode.c -o testshellcode
$ ./testshellcode
sum = 6
下面除錯一下,看看在呼叫 func 后的堆疊的內容,
$ gcc -g -o testshellcode testshellcode.c #為了除錯,需要在編譯時加-g選項
$ gdb -q ./testshellcode #啟動gdb除錯
...
(gdb) set logging on #如果要記錄除錯程序中的資訊,可以把日志記錄功能打開
Copying output to gdb.txt.
(gdb) l main #列出源代碼
20
21 return sum;
22 }
23
24 int main()
25 {
26 int sum;
27
28 sum = func(1, 2, 3);
29
(gdb) break 28 #在呼叫func函式之前讓程式停一下,以便記錄當時的ebp(基指標)
Breakpoint 1 at 0x80483ac: file testshellcode.c, line 28.
(gdb) break func #設定斷點在函式入口,以便逐步記錄堆疊資訊
Breakpoint 2 at 0x804835c: file testshellcode.c, line 13.
(gdb) disassemble main #反編譯main函式,以便記錄呼叫func后的下一條指令地址
Dump of assembler code for function main:
0x0804839b <main+0>: lea 0x4(%esp),%ecx
0x0804839f <main+4>: and $0xfffffff0,%esp
0x080483a2 <main+7>: pushl 0xfffffffc(%ecx)
0x080483a5 <main+10>: push %ebp
0x080483a6 <main+11>: mov %esp,%ebp
0x080483a8 <main+13>: push %ecx
0x080483a9 <main+14>: sub $0x14,%esp
0x080483ac <main+17>: push $0x3
0x080483ae <main+19>: push $0x2
0x080483b0 <main+21>: push $0x1
0x080483b2 <main+23>: call 0x8048354 <func>
0x080483b7 <main+28>: add $0xc,%esp
0x080483ba <main+31>: mov %eax,0xfffffff8(%ebp)
0x080483bd <main+34>: sub $0x8,%esp
0x080483c0 <main+37>: pushl 0xfffffff8(%ebp)
0x080483c3 <main+40>: push $0x80484c0
0x080483c8 <main+45>: call 0x80482a0 <printf@plt>
0x080483cd <main+50>: add $0x10,%esp
0x080483d0 <main+53>: mov $0x0,%eax
0x080483d5 <main+58>: mov 0xfffffffc(%ebp),%ecx
0x080483d8 <main+61>: leave
0x080483d9 <main+62>: lea 0xfffffffc(%ecx),%esp
0x080483dc <main+65>: ret
End of assembler dump.
(gdb) r #運行程式
Starting program: /mnt/hda8/Temp/c/program/testshellcode
Breakpoint 1, main () at testshellcode.c:28
28 sum = func(1, 2, 3);
(gdb) print $ebp #列印呼叫func函式之前的基地址,即Previous frame pointer,
$1 = (void *) 0xbf84fdd8
(gdb) n #執行call指令并跳轉到func函式的入口
Breakpoint 2, func (a=1, b=2, c=3) at testshellcode.c:13
13 int sum = 0;
(gdb) n
16 sum = a + b + c;
(gdb) x/11x $esp #列印當前堆疊的內容,可以看出,地址從低到高,注意標記有藍色和紅色的值
#它們分別是前一個堆疊基地址(ebp)和call呼叫之后的下一條指令的指標(eip)
0xbf84fd94: 0x00000000 0x00000000 0x080482e0 0x00000000
0xbf84fda4: 0xb7f2bce0 0x00000000 0xbf84fdd8 0x080483b7
0xbf84fdb4: 0x00000001 0x00000002 0x00000003
(gdb) n #執行sum = a + b + c,后,比較堆疊內容第一行,第4列,由0變為6
18 memset(buffer, '\0', BUF_SIZE);
(gdb) x/11x $esp
0xbf84fd94: 0x00000000 0x00000000 0x080482e0 0x00000006
0xbf84fda4: 0xb7f2bce0 0x00000000 0xbf84fdd8 0x080483b7
0xbf84fdb4: 0x00000001 0x00000002 0x00000003
(gdb) n
19 memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);
(gdb) x/11x $esp #緩沖區初始化以后變成了0
0xbf84fd94: 0x00000000 0x00000000 0x00000000 0x00000006
0xbf84fda4: 0xb7f2bce0 0x00000000 0xbf84fdd8 0x080483b7
0xbf84fdb4: 0x00000001 0x00000002 0x00000003
(gdb) n
21 return sum;
(gdb) x/11x $esp #進行copy以后,這兩列的值變了,大小剛好是7個位元組,最后一個位元組為'\0'
0xbf84fd94: 0x00000000 0x41414141 0x00414141 0x00000006
0xbf84fda4: 0xb7f2bce0 0x00000000 0xbf84fdd8 0x080483b7
0xbf84fdb4: 0x00000001 0x00000002 0x00000003
(gdb) c
Continuing.
sum = 6
Program exited normally.
(gdb) quit
從上面的操作程序,我們可以得出大概的堆疊分布(func 函式結束之前)如下:
| 地址 | 值(hex) | 符號或者暫存器 | 注釋 |
|---|---|---|---|
| 低地址 | 堆疊頂方向 | ||
| 0xbf84fd98 | 0x41414141 | buf[0] | 可以看出little endian(小端,重要的資料在前面) |
| 0xbf84fd9c | 0x00414141 | buf[1] | |
| 0xbf84fda0 | 0x00000006 | sum | 可見這上面都是func函式里頭的區域變數 |
| 0xbf84fda4 | 0xb7f2bce0 | esi | 源索引指標,可以通過產生中間代碼查看,貌似沒什么作用 |
| 0xbf84fda8 | 0x00000000 | edi | 目的索引指標 |
| 0xbf84fdac | 0xbf84fdd8 | ebp | 呼叫func之前的堆疊的基地址,以便呼叫函式結束之后恢復 |
| 0xbf84fdb0 | 0x080483b7 | eip | 呼叫func之前的指令指標,以便呼叫函式結束之后繼續執行 |
| 0xbf84fdb4 | 0x00000001 | a | 第一個引數 |
| 0xbf84fdb8 | 0x00000002 | b | 第二個引數 |
| 0xbf84fdbc | 0x00000003 | c | 第三個引數,可見引數是從最后一個開始壓堆疊的 |
| 高地址 | 堆疊底方向 |
先說明一下 edi 和 esi 的由來(在上面的除錯程序中我們并沒有看到),是通過產生中間匯編代碼分析得出的,
$ gcc -S testshellcode.c
在產生的 testShellcode.s 代碼里頭的 func 部分看到 push ebp 之后就 push 了 edi 和 esi ,但是搜索了一下代碼,發現就這個函式里頭參考了這兩個暫存器,所以保存它們沒什么用,洗掉以后編譯產生目標代碼后證明是沒用的,
$ cat testshellcode.s
...
func:
pushl %ebp
movl %esp, %ebp
pushl %edi
pushl %esi
...
popl %esi
popl %edi
popl %ebp
...
下面就不管這兩部分(edi 和 esi)了,主要來分析和函式相關的這幾部分在堆疊內的分布:
- 函式區域變數,在靠近堆疊頂一端
- 呼叫函式之前的堆疊的基地址(
ebp,Previous Frame Pointer),在中間靠近堆疊頂方向 - 呼叫函式指令的下一條指令地址
` (eip`),在中間靠近堆疊底的方向 - 函式引數,在靠近堆疊底的一端,最后一個引數最先入堆疊
到這里,函式呼叫時的相關內容在堆疊內的分布就比較清楚了,在具體分析緩沖區溢位問題之前,我們再來看一個和函式關系很大的問題,即函式回傳值的存盤問題:函式的回傳值存放在暫存器 eax 中,
先來看這段代碼:
/**
* test_return.c -- the return of a function is stored in register eax
*/
#include <stdio.h>
int func()
{
__asm__ ("movl $1, %eax");
}
int main()
{
printf("the return of func: %d\n", func());
return 0;
}
編譯運行后,可以看到回傳值為 1,剛好是我們在 func 函式中 mov 到 eax 中的“立即數” 1,因此很容易理解回傳值存盤在 eax 中的事實,如果還有疑慮,可以再看看匯編代碼,在函式回傳之后,eax 中的值當作了 printf 的引數壓入了堆疊中,而在源代碼中我們正是把 func 的結果作為 printf 的第二個引數的,
$ make test_return
cc test_return.c -o test_return
$ ./test_return
the return of func: 1
$ gcc -S test_return.c
$ cat test_return.s
...
call func
subl $8, %esp
pushl %eax #printf的第二個引數,把func的回傳值壓入了堆疊底
pushl $.LC0 #printf的第一個引數the return of func: %d\n
call printf
...
對于系統呼叫,回傳值也存盤在 eax 暫存器中,
緩沖區溢位
實體分析:字串復制
先來看一段簡短的代碼,
/* testshellcode.c */
#include <stdio.h> /* printf */
#include <string.h> /* memset, memcpy */
#define BUF_SIZE 8
#ifdef STR1
# define STR_SRC "AAAAAAA\0\1\0\0\0"
#endif
#ifndef STR_SRC
# define STR_SRC "AAAAAAA"
#endif
int func(int a, int b, int c)
{
int sum = 0;
char buffer[BUF_SIZE];
sum = a + b + c;
memset(buffer, '\0', BUF_SIZE);
memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);
return sum;
}
int main()
{
int sum;
sum = func(1, 2, 3);
printf("sum = %d\n", sum);
return 0;
}
編譯一下看看結果:
$ gcc -DSTR1 -o testshellcode testshellcode.c #通過-D定義宏STR1,從而采用第一個STR_SRC的值
$ ./testshellcode
sum = 1
不知道你有沒有發現例外呢?上面用紅色標記的地方,本來 sum 為 1+2+3 即 6,但是實際回傳的竟然是 1 ,到底是什么原因呢?大家應該有所了解了,因為我們在復制字串 AAAAAAA\\0\\1\\0\\0\\0 到 buf 的時候超出 buf 本來的大小, buf 本來的大小是 BUF_SIZE,8 個位元組,而我們要復制的內容是 12 個位元組,所以超出了四個位元組,根據第一小節的分析,我們用堆疊的變化情況來表示一下這個復制程序(即執行 memcpy 的程序),
memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);
(低地址)
復制之前 ====> 復制之后
0x00000000 0x41414141 #char buf[8]
0x00000000 0x00414141
0x00000006 0x00000001 #int sum
(高地址)
下面通過 gdb 除錯來確認一下(只摘錄了一些片斷),
$ gcc -DSTR1 -g -o testshellcode testshellcode.c
$ gdb -q ./testshellcode
...
(gdb) l
21
22 memset(buffer, '\0', BUF_SIZE);
23 memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);
24
25 return sum;
...
(gdb) break 23
Breakpoint 1 at 0x804837f: file testshellcode.c, line 23.
(gdb) break 25
Breakpoint 2 at 0x8048393: file testshellcode.c, line 25.
(gdb) r
Starting program: /mnt/hda8/Temp/c/program/testshellcode
Breakpoint 1, func (a=1, b=2, c=3) at testshellcode.c:23
23 memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);
(gdb) x/3x $esp+4
0xbfec6bd8: 0x00000000 0x00000000 0x00000006
(gdb) n
Breakpoint 2, func (a=1, b=2, c=3) at testshellcode.c:25
25 return sum;
(gdb) x/3x $esp+4
0xbfec6bd8: 0x41414141 0x00414141 0x00000001
可以看出,因為 C 語言沒有對陣列的邊界進行限制,我們可以往陣列中存入預定義長度的字串,從而導致緩沖區溢位,
緩沖區溢位后果
溢位之后的問題是導致覆寫堆疊的其他內容,從而可能改變程式原來的行為,
如果這類問題被“黑客”利用那將產生非常可怕的后果,小則讓非法用戶獲取了系統權限,把你的服務器當成“僵尸”,用來對其他機器進行攻擊,嚴重的則可能被人洗掉資料(所以備份很重要),即使不被黑客利用,這類問題如果放在醫療領域,那將非常危險,可能那個被覆寫的數字剛好是用來控制治療癌癥的輻射量的,一旦出錯,那可能導致置人死地,當然,如果在航天領域,那可能就是好多個 0 的 money 甚至航天員的損失,呵呵,“緩沖區溢位,后果很嚴重!”
緩沖區溢位應對策略
那這個怎么辦呢?貌似Linux下緩沖區溢位攻擊的原理及對策提到有一個 libsafe 庫,可以至少用來檢測程式中出現的類似超出陣列邊界的問題,對于上面那個具體問題,為了保護 sum 不被修改,有一個小技巧,可以讓求和操作在字串復制操作之后來做,以便求和操作把溢位的部分給重寫,這個呆伙在下面一塊看效果吧,繼續看看緩沖區的溢位吧,
先來看看這個代碼,還是 testShellcode.c 的改進,
/* testshellcode.c */
#include <stdio.h> /* printf */
#include <string.h> /* memset, memcpy */
#define BUF_SIZE 8
#ifdef STR1
# define STR_SRC "AAAAAAAa\1\0\0\0"
#endif
#ifdef STR2
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBB"
#endif
#ifdef STR3
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBBCCCC"
#endif
#ifdef STR4
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBBCCCCDDDD"
#endif
#ifndef STR_SRC
# define STR_SRC "AAAAAAA"
#endif
int func(int a, int b, int c)
{
int sum = 0;
char buffer[BUF_SIZE] = "";
memset(buffer, '\0', BUF_SIZE);
memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);
sum = a + b + c; //把求和操作放在復制操作之后可以在一定情況下“保護”求和結果
return sum;
}
int main()
{
int sum;
sum = func(1, 2, 3);
printf("sum = %d\n", sum);
return 0;
}
看看運行情況:
$ gcc -D STR2 -o testshellcode testshellcode.c #再多復制8個位元組,結果和STR1時一樣
#原因是edi,esi這兩個沒什么用的,覆寫了也沒關系
$ ./testshellcode #看到沒?這種情況下,讓整數操作在字串復制之后做可以“保護‘整數結果
sum = 6
$ gcc -D STR3 -o testshellcode testshellcode.c #再多復制4個位元組,現在就會把ebp給覆寫
#了,這樣當main函式再要用ebp訪問資料
#時就會出現訪問非法記憶體而導致段錯誤,
$ ./testshellcode
Segmentation fault
如果感興趣,自己還可以用gdb類似之前一樣來查看復制字串以后堆疊的變化情況,
如何保護 ebp 不被修改
下面來做一個比較有趣的事情:如何設法保護我們的 ebp 不被修改,
首先要明確 ebp 這個暫存器的作用和“行為”,它是堆疊基地址,并且發現在呼叫任何一個函式時,這個 ebp 總是在第一條指令被壓入堆疊中,并在最后一條指令(ret)之前被彈出,類似這樣:
func: #函式
pushl %ebp #第一條指令
...
popl %ebp #倒數第二條指令
ret
還記得之前(第一部分)提到的函式的回傳值是存盤在 eax 暫存器中的么?如果我們在一個函式中僅僅做放這兩條指令:
popl %eax
pushl %eax
那不就剛好有:
func: #函式
pushl %ebp #第一條指令
popl %eax #把剛壓入堆疊中的ebp彈出存放到eax中
pushl %eax #又把ebp壓入堆疊
popl %ebp #倒數第二條指令
ret
這樣我們沒有改變堆疊的狀態,卻獲得了 ebp 的值,如果在呼叫任何一個函式之前,獲取這個 ebp,并且在任何一條字串復制陳述句(可能導致緩沖區溢位的陳述句)之后重新設定一下 ebp 的值,那么就可以保護 ebp 啦,具體怎么實作呢?看這個代碼,
/* testshellcode.c */
#include <stdio.h> /* printf */
#include <string.h> /* memset, memcpy */
#define BUF_SIZE 8
#ifdef STR1
# define STR_SRC "AAAAAAAa\1\0\0\0"
#endif
#ifdef STR2
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBB"
#endif
#ifdef STR3
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBBCCCC"
#endif
#ifdef STR4
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBBCCCCDDDD"
#endif
#ifndef STR_SRC
# define STR_SRC "AAAAAAA"
#endif
unsigned long get_ebp()
{
__asm__ ("popl %eax;"
"pushl %eax;");
}
int func(int a, int b, int c, unsigned long ebp)
{
int sum = 0;
char buffer[BUF_SIZE] = "";
sum = a + b + c;
memset(buffer, '\0', BUF_SIZE);
memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);
*(unsigned long *)(buffer+20) = ebp;
return sum;
}
int main()
{
int sum, ebp;
ebp = get_ebp();
sum = func(1, 2, 3, ebp);
printf("sum = %d\n", sum);
return 0;
}
這段代碼和之前的代碼的不同有:
- 給
func函式增加了一個引數ebp,(其實可以用全域變數替代的) - 利用了剛介紹的原理定義了一個函式
get_ebp以便獲取老的ebp - 在
main函式中呼叫func之前呼叫了get_ebp,并把它作為func的最后一個引數 - 在
func函式中呼叫memcpy函式(可能發生緩沖區溢位的地方)之后添加了一潭訓復設定ebp的陳述句,這條陳述句先把buffer+20這個地址(存放ebp的地址,你可以類似第一部分提到的用gdb來查看)強制轉換為指向一個unsigned long型的整數(4 個位元組),然后把它指向的內容修改為老的ebp,
看看效果:
$ gcc -D STR3 -o testshellcode testshellcode.c
$ ./testshellcode #現在沒有段錯誤了吧,因為ebp得到了“保護”
sum = 6
如何保護 eip 不被修改?
如果我們復制更多的位元組過去了,比如再多復制四個位元組進去,那么 eip 就被覆寫了,
$ gcc -D STR4 -o testshellcode testshellcode.c
$ ./testshellcode
Segmentation fault
同樣會出現段錯誤,因為下一條指令的位置都被改寫了,func 回傳后都不知道要訪問哪個”非法“地址啦,呵呵,如果是一個合法地址呢?
如果在緩沖區溢位時,eip 被覆寫了,并且被修改為了一條合法地址,那么問題就非常”有趣“了,如果這個地址剛好是呼叫func的那個地址,那么整個程式就成了死回圈,如果這個地址指向的位置剛好有一段關機代碼,那么系統正在運行的所有服務都將被關掉,如果那個地方是一段更惡意的代碼,那就?你可以盡情想像哦,如果是黑客故意利用這個,那么那些代碼貌似就叫做shellcode了,
有沒有保護 eip 的辦法呢?呵呵,應該是有的吧,不知道 gas 有沒有類似 masm 匯編器中 offset 的偽操作指令(查找了一下,貌似沒有),如果有的話在函式呼叫之前設定一個標號,在后面某個位置獲取,再加上一個可能的偏移(包括 call 指令的長度和一些 push 指令等),應該可以算出來,不過貌似比較麻煩(或許你靈感大作,找到好辦法了!),這里直接通過 gdb 反匯編求得它相對 main 的偏移算出來得了,求出來以后用它來”保護“堆疊中的值,
看看這個代碼:
/* testshellcode.c */
#include <stdio.h> /* printf */
#include <string.h> /* memset, memcpy */
#define BUF_SIZE 8
#ifdef STR1
# define STR_SRC "AAAAAAAa\1\0\0\0"
#endif
#ifdef STR2
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBB"
#endif
#ifdef STR3
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBBCCCC"
#endif
#ifdef STR4
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBBCCCCDDDD"
#endif
#ifndef STR_SRC
# define STR_SRC "AAAAAAA"
#endif
int main();
#define OFFSET 40
unsigned long get_ebp()
{
__asm__ ("popl %eax;"
"pushl %eax;");
}
int func(int a, int b, int c, unsigned long ebp)
{
int sum = 0;
char buffer[BUF_SIZE] = "";
memset(buffer, '\0', BUF_SIZE);
memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);
sum = a + b + c;
*(unsigned long *)(buffer+20) = ebp;
*(unsigned long *)(buffer+24) = (unsigned long)main+OFFSET;
return sum;
}
int main()
{
int sum, ebp;
ebp = get_ebp();
sum = func(1, 2, 3, ebp);
printf("sum = %d\n", sum);
return 0;
}
看看效果:
$ gcc -D STR4 -o testshellcode testshellcode.c
$ ./testshellcode
sum = 6
這樣,EIP 也得到了“保護”(這個方法很糟糕的,呵呵),
類似地,如果再多復制一些內容呢?那么堆疊后面的內容都將被覆寫,即傳遞給 func 函式的引數都將被覆寫,因此上面的方法,包括所謂的對 sum 和 ebp 等值的保護都沒有任何意義了(如果再對后面的引數進行進一步的保護呢?或許有點意義,呵呵),在這里,之所以提出類似這樣的保護方法,實際上只是為了討論一些有趣的細節并加深對緩沖區溢位這一問題的理解(或許有一些實際的價值哦,算是拋磚引玉吧),
緩沖區溢位檢測
要確實解決這類問題,從主觀上講,還得程式員來做相關的作業,比如限制將要復制的字串的長度,保證它不超過當初申請的緩沖區的大小,
例如,在上面的代碼中,我們在 memcpy 之前,可以加入一個判斷,并且可以對緩沖區溢位進行很好的檢查,如果能夠設計一些比較好的測驗實體把這些判斷覆寫到,那么相關的問題就可以得到比較不錯的檢查了,
/* testshellcode.c */
#include <stdio.h> /* printf */
#include <string.h> /* memset, memcpy */
#include <stdlib.h> /* exit */
#define BUF_SIZE 8
#ifdef STR4
# define STR_SRC "AAAAAAAa\1\0\0\0BBBBBBBBCCCCDDDD"
#endif
#ifndef STR_SRC
# define STR_SRC "AAAAAAA"
#endif
int func(int a, int b, int c)
{
int sum = 0;
char buffer[BUF_SIZE] = "";
memset(buffer, '\0', BUF_SIZE);
if ( sizeof(STR_SRC)-1 > BUF_SIZE ) {
printf("buffer overflow!\n");
exit(-1);
}
memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);
sum = a + b + c;
return sum;
}
int main()
{
int sum;
sum = func(1, 2, 3);
printf("sum = %d\n", sum);
return 0;
}
現在的效果如下:
$ gcc -DSTR4 -g -o testshellcode testshellcode.c
$ ./testshellcode #如果存在溢位,那么就會得到阻止并退出,從而阻止可能的破壞
buffer overflow!
$ gcc -g -o testshellcode testshellcode.c
$ ./testshellcode
sum = 6
當然,如果能夠在 C 標準里頭加入對陣列操作的限制可能會更好,或者在編譯器中擴展對可能引起緩沖區溢位的語法檢查,
緩沖區注入實體
最后給出一個利用上述緩沖區溢位來進行緩沖區注入的例子,也就是通過往某個緩沖區注入一些代碼,并把eip修改為這些代碼的入口從而達到破壞目標程式行為的目的,
這個例子來自Linux 下緩沖區溢位攻擊的原理及對策,這里主要利用上面介紹的知識對它進行了比較詳細的分析,
準備:把 C 語言函式轉換為字串序列
首先回到第一部分,看看那個 Shellcode.c 程式,我們想獲取它的匯編代碼,并以十六進制位元組的形式輸出,以便把這些指令當字串存放起來,從而作為緩沖區注入時的輸入字串,下面通過 gdb 獲取這些內容,
$ gcc -g -o shellcode shellcode.c
$ gdb -q ./shellcode
(gdb) disassemble main
Dump of assembler code for function main:
...
0x08048331 <main+13>: push %ecx
0x08048332 <main+14>: jmp 0x8048354 <forward>
0x08048334 <main+16>: pop %esi
0x08048335 <main+17>: mov $0x4,%eax
0x0804833a <main+22>: mov $0x2,%ebx
0x0804833f <main+27>: mov %esi,%ecx
0x08048341 <main+29>: mov $0xc,%edx
0x08048346 <main+34>: int $0x80
0x08048348 <main+36>: mov $0x1,%eax
0x0804834d <main+41>: mov $0x0,%ebx
0x08048352 <main+46>: int $0x80
0x08048354 <forward+0>: call 0x8048334 <main+16>
0x08048359 <forward+5>: dec %eax
0x0804835a <forward+6>: gs
0x0804835b <forward+7>: insb (%dx),%es:(%edi)
0x0804835c <forward+8>: insb (%dx),%es:(%edi)
0x0804835d <forward+9>: outsl %ds:(%esi),(%dx)
0x0804835e <forward+10>: and %dl,0x6f(%edi)
0x08048361 <forward+13>: jb 0x80483cf <__libc_csu_init+79>
0x08048363 <forward+15>: or %fs:(%eax),%al
...
End of assembler dump.
(gdb) set logging on #開啟日志功能,記錄操作結果
Copying output to gdb.txt.
(gdb) x/52bx main+14 #以十六進制單位元組(字符)方式列印出shellcode的核心代碼
0x8048332 <main+14>: 0xeb 0x20 0x5e 0xb8 0x04 0x00 0x00 0x00
0x804833a <main+22>: 0xbb 0x02 0x00 0x00 0x00 0x89 0xf1 0xba
0x8048342 <main+30>: 0x0c 0x00 0x00 0x00 0xcd 0x80 0xb8 0x01
0x804834a <main+38>: 0x00 0x00 0x00 0xbb 0x00 0x00 0x00 0x00
0x8048352 <main+46>: 0xcd 0x80 0xe8 0xdb 0xff 0xff 0xff 0x48
0x804835a <forward+6>: 0x65 0x6c 0x6c 0x6f 0x20 0x57 0x6f 0x72
0x8048362 <forward+14>: 0x6c 0x64 0x0a 0x00
(gdb) quit
$ cat gdb.txt | sed -e "s/^.*://g;s/\t/