雖然程序加載以及動態(tài)符號鏈接都已經(jīng)很理解了,但是這伙卻被進(jìn)程的內(nèi)存映像給”糾纏"住。看著看著就一發(fā)不可收拾——很有趣。
下面一起來探究“緩沖區(qū)溢出和注入”問題(主要是關(guān)心程序的內(nèi)存映像)。
永遠(yuǎn)的 Hello World
,太熟悉了吧,
#include <stdio.h>
int main(void)
{
printf("Hello World\n");
return 0;
}
如果要用內(nèi)聯(lián)匯編(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 }
看起來很復(fù)雜,實際上就做了一個事情,往終端上寫了個 Hello World
。不過這個非常有意思。先簡單分析一下流程:
forward
標(biāo)記處),接著執(zhí)行第 16 行。backward
,跳轉(zhuǎn)到第 5 行,接著執(zhí)行 6 到 14 行。Hello World
字符串(等一下詳細(xì)介紹)。為了更好的理解上面的代碼和后續(xù)的分析,先來介紹幾個比較重要的內(nèi)容。
X86
處理器平臺有三個常用寄存器:程序指令指針、程序堆棧指針與程序基指針:
寄存器 | 名稱 | 注釋 |
---|---|---|
EIP | 程序指令指針 | 通常指向下一條指令的位置 |
ESP | 程序堆棧指針 | 通常指向當(dāng)前堆棧的當(dāng)前位置 |
EBP | 程序基指針 | 通常指向函數(shù)使用的堆棧頂端 |
當(dāng)然,上面都是擴展的寄存器,用于 32 位系統(tǒng),對應(yīng)的 16 系統(tǒng)為 ip
,sp
,bp
。
call
指令
跳轉(zhuǎn)到某個位置,并在之前把下一條指令的地址(EIP
)入棧(為了方便”程序“返回以后能夠接著執(zhí)行)。這樣的話就有:
call backward ==> push eip
jmp backward
ret
指令
通常 call
指令和 ret
是配合使用的,前者壓入跳轉(zhuǎn)前的下一條指令地址,后者彈出 call
指令壓入的那條指令,從而可以在函數(shù)調(diào)用結(jié)束以后接著執(zhí)行后面的指令。
ret ==> pop eip
通常在函數(shù)調(diào)用后,還需要恢復(fù) esp
和 ebp
,恢復(fù) esp
即恢復(fù)當(dāng)前棧指針,以便釋放調(diào)用函數(shù)時為存儲函數(shù)的局部變量而自動分配的空間;恢復(fù) ebp
是從棧中彈出一個數(shù)據(jù)項(通常函數(shù)調(diào)用過后的第一條語句就是 push ebp
),從而恢復(fù)當(dāng)前的函數(shù)指針為函數(shù)調(diào)用者本身。這兩個動作可以通過一條 leave
指令完成。
這三個指令對我們后續(xù)的解釋會很有幫助。更多關(guān)于 Intel 的指令集,請參考:Intel 386 Manual, x86 Assembly Language FAQ:part1, part2, part3.
系統(tǒng)調(diào)用是用戶和內(nèi)核之間的接口,用戶如果想寫程序,很多時候直接調(diào)用了 C 庫,并沒有關(guān)心系統(tǒng)調(diào)用,而實際上 C 庫也是基于系統(tǒng)調(diào)用的。這樣應(yīng)用程序和內(nèi)核之間就可以通過系統(tǒng)調(diào)用聯(lián)系起來。它們分別處于操作系統(tǒng)的用戶空間和內(nèi)核空間(主要是內(nèi)存地址空間的隔離)。
用戶空間 應(yīng)用程序(Applications)
| |
| C庫(如glibc)
| |
系統(tǒng)調(diào)用(System Calls,如sys_read, sys_write, sys_exit)
|
內(nèi)核空間 內(nèi)核(Kernel)
系統(tǒng)調(diào)用實際上也是一些函數(shù),它們被定義在 arch/i386/kernel/sys_i386.c
(老的在 arch/i386/kernel/sys.c
)文件中,并且通過一張系統(tǒng)調(diào)用表組織,該表在內(nèi)核啟動時就已經(jīng)加載了,這個表的入口在內(nèi)核源代碼的 arch/i386/kernel/syscall_table.S
里頭(老的在 arch/i386/kernel/entry.S
)。這樣,如果想添加一個新的系統(tǒng)調(diào)用,修改上面兩個內(nèi)核中的文件,并重新編譯內(nèi)核就可以。當(dāng)然,如果要在應(yīng)用程序中使用它們,還得把它寫到 include/asm/unistd.h
中。
如果要在 C 語言中使用某個系統(tǒng)調(diào)用,需要包含頭文件 /usr/include/asm/unistd.h
,里頭有各個系統(tǒng)調(diào)用的聲明以及系統(tǒng)調(diào)用號(對應(yīng)于調(diào)用表的入口,即在調(diào)用表中的索引,為方便查找調(diào)用表而設(shè)立的)。如果是自己定義的新系統(tǒng)調(diào)用,可能還要在開頭用宏 _syscall(type, name, type1, name1...)
來聲明好參數(shù)。
如果要在匯編語言中使用,需要用到 int 0x80
調(diào)用,這個是系統(tǒng)調(diào)用的中斷入口。涉及到傳送參數(shù)的寄存器有這么幾個,eax
是系統(tǒng)調(diào)用號(可以到 /usr/include/asm-i386/unistd.h
或者直接到 arch/i386/kernel/syscall_table.S
查到),其他寄存器如 ebx
,ecx
,edx
,esi
,edi
一次存放系統(tǒng)調(diào)用的參數(shù)。而系統(tǒng)調(diào)用的返回值存放在 eax
寄存器中。
下面我們就很容易解釋前面的 Shellcode.c
程序流程的 2,3 兩部分了。因為都用了 int 0x80
中斷,所以都用到了系統(tǒng)調(diào)用。
第 3 部分很簡單,用到的系統(tǒng)調(diào)用號是 1,通過查表(查 /usr/include/asm-i386/unistd.h
或 arch/i386/kernel/syscall_table.S
)可以發(fā)現(xiàn)這里是 sys_exit
調(diào)用,再從 /usr/include/unistd.h
文件看這個系統(tǒng)調(diào)用的聲明,發(fā)現(xiàn)參數(shù) ebx
是程序退出狀態(tài)。
第 2 部分比較有趣,而且復(fù)雜一點。我們依次來看各個寄存器,首先根據(jù) eax
為 4 確定(同樣查表)系統(tǒng)調(diào)用為 sys_write
,而查看它的聲明(從 /usr/include/unistd.h
),我們找到了參數(shù)依次為文件描述符、字符串指針和字符串長度。
ebx
,正好是 2,即標(biāo)準(zhǔn)錯誤輸出,默認(rèn)為終端。ecx
,而 ecx
的內(nèi)容來自 esi
,esi
來自剛彈出棧的值(見第 6 行 popl %esi;
),而之前剛好有 call
指令引起了最近一次壓棧操作,入棧的內(nèi)容剛好是 call
指令的下一條指令的地址,即 .string
所在行的地址,這樣 ecx
剛好引用了 Hello World\\n
字符串的地址。edx
,剛好是 12,即 Hello World\\n
字符串的長度(包括一個空字符)。這樣,Shellcode.c
的執(zhí)行流程就很清楚了,第 4,5,15,16 行指令的巧妙之處也就容易理解了(把 .string
存放在 call
指令之后,并用 popl
指令把 eip
彈出當(dāng)作字符串的入口)。這里的 ELF 不是“精靈”,而是 Executable and Linking Format 文件,是 Linux 下用來做目標(biāo)文件、可執(zhí)行文件和共享庫的一種文件格式,它有專門的標(biāo)準(zhǔn),例如:X86 ELF format and ABI,中文版。
下面簡單描述 ELF
的格式。
ELF
文件主要有三種,分別是:
gcc
的 -c
參數(shù)時產(chǎn)生。ar
命令組織的。ELF
文件的大體結(jié)構(gòu):
ELF Header #程序頭,有該文件的Magic number(參考man magic),類型等
Program Header Table #對可執(zhí)行文件和共享庫有效,它描述下面各個節(jié)(section)組成的段
Section1
Section2
Section3
.....
Program Section Table #僅對可重定位目標(biāo)文件和靜態(tài)庫有效,用于描述各個Section的重定位信息等。
對于可執(zhí)行文件,文件最后的 Program Section Table
(節(jié)區(qū)表)和一些非重定位的 Section
,比如 .comment
,.note.XXX.debug
等信息都可以刪除掉,不過如果用 strip
,objcopy
等工具刪除掉以后,就不可恢復(fù)了。因為這些信息對程序的運行一般沒有任何用處。
ELF
文件的主要節(jié)區(qū)(section
)有 .data
,.text
,.bss
,.interp
等,而主要段(segment
)有 LOAD
,INTERP
等。它們之間(節(jié)區(qū)和段)的主要對應(yīng)關(guān)系如下:
Section | 解釋 | 實例 |
---|---|---|
.data | 初始化的數(shù)據(jù) | 比如 int a=10 |
.bss | 未初始化的數(shù)據(jù) | 比如 char sum[100]; 這個在程序執(zhí)行之前,內(nèi)核將初始化為 0 |
.text | 程序代碼正文 | 即可執(zhí)行指令集 |
.interp | 描述程序需要的解釋器(動態(tài)連接和裝載程序) | 存有解釋器的全路徑,如 /lib/ld-linux.so |
而程序在執(zhí)行以后,.data
,.bss
,.text
等一些節(jié)區(qū)會被 Program header table
映射到 LOAD
段,.interp
則被映射到了 INTERP
段。
對于 ELF
文件的分析,建議使用 file
,size
,readelf
,objdump
,strip
,objcopy
,gdb
,nm
等工具。
這里簡單地演示這幾個工具:
$ gcc -g -o shellcode shellcode.c #如果要用gdb調(diào)試,編譯時加上-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)和節(jié)區(qū)(section)的關(guān)系,即各個段包含哪些節(jié)區(qū)。
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命令查看各個段(對應(yīng)后面將分析的進(jìn)程內(nèi)存映像)的大小
text data bss dec hex filename
815 256 4 1075 433 shellcode
$ strip -R .note.ABI-tag shellcode #可用strip來給可執(zhí)行文件“減肥”,刪除無用信息
$ size shellcode #“減肥”后效果“明顯”,對于嵌入式系統(tǒng)應(yīng)該有很大的作用
text data bss dec hex filename
783 256 4 1043 413 shellcode
$ objdump -s -j .interp shellcode #這個主要工作是反編譯,不過用來查看各個節(jié)區(qū)也很厲害
shellcode: file format elf32-i386
Contents of section .interp:
8048114 2f6c6962 2f6c642d 6c696e75 782e736f /lib/ld-linux.so
8048124 2e3200 .2.
補充:如果要刪除可執(zhí)行文件的 Program Section Table
,可以用 A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux 一文的作者寫的 elf kicker 工具鏈中的 sstrip
工具。
在命令行下,敲入程序的名字或者是全路徑,然后按下回車就可以啟動程序,這個具體是怎么工作的呢?
首先要再認(rèn)識一下我們的命令行,命令行是內(nèi)核和用戶之間的接口,它本身也是一個程序。在 Linux 系統(tǒng)啟動以后會為每個終端用戶建立一個進(jìn)程執(zhí)行一個 Shell 解釋程序,這個程序解釋并執(zhí)行用戶輸入的命令,以實現(xiàn)用戶和內(nèi)核之間的接口。這類解釋程序有哪些呢?目前 Linux 下比較常用的有 /bin/bash
。那么該程序接收并執(zhí)行命令的過程是怎么樣的呢?
先簡單描述一下這個過程:
execve
內(nèi)部處理所要求的形式。fork
建立一個子進(jìn)程。wait4
來等待子進(jìn)程完成(如果是后臺命令,則不等待)。當(dāng)子進(jìn)程運行時調(diào)用 execve
,子進(jìn)程根據(jù)文件名(即命令名)到目錄中查找有關(guān)文件(這是命令解釋程序構(gòu)成的文件),將它調(diào)入內(nèi)存,執(zhí)行這個程序(解釋這條命令)。&
號(后臺命令符號),則終端進(jìn)程不用系統(tǒng)調(diào)用 wait4
等待,立即發(fā)提示符,讓用戶輸入下一個命令,轉(zhuǎn) 1)。如果命令末尾沒有 &
號,則終端進(jìn)程要一直等待,當(dāng)子進(jìn)程(即運行命令的進(jìn)程)完成處理后終止,向父進(jìn)程(終端進(jìn)程)報告,此時終端進(jìn)程醒來,在做必要的判別等工作后,終端進(jìn)程發(fā)提示符,讓用戶輸入新的命令,重復(fù)上述處理過程。現(xiàn)在用 strace
來跟蹤一下程序執(zhí)行過程中用到的系統(tǒng)調(diào)用。
$ 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
相關(guān)的系統(tǒng)調(diào)用基本體現(xiàn)了上面的執(zhí)行過程,需要注意的是,里頭還涉及到內(nèi)存映射(mmap2
)等。
下面再羅嗦一些比較有意思的內(nèi)容,參考《深入理解 Linux 內(nèi)核》的程序的執(zhí)行(P681)。
Linux 支持很多不同的可執(zhí)行文件格式,這些不同的格式是如何解釋的呢?平時我們在命令行下敲入一個命令就完了,也沒有去管這些細(xì)節(jié)。實際上 Linux 下有一個 struct linux_binfmt
結(jié)構(gòu)來管理不同的可執(zhí)行文件類型,這個結(jié)構(gòu)中有對應(yīng)的可執(zhí)行文件的處理函數(shù)。大概的過程如下:
在用戶態(tài)執(zhí)行了 execve
后,引發(fā) int 0x80
中斷,進(jìn)入內(nèi)核態(tài),執(zhí)行內(nèi)核態(tài)的相應(yīng)函數(shù) do_sys_execve
,該函數(shù)又調(diào)用 do_execve
函數(shù)。 do_execve
函數(shù)讀入可執(zhí)行文件,檢查權(quán)限,如果沒問題,繼續(xù)讀入可執(zhí)行文件需要的相關(guān)信息(struct linux_binprm
描述的)。
接著執(zhí)行 search_binary_handler
,根據(jù)可執(zhí)行文件的類型(由上一步的最后確定),在 linux_binfmt
結(jié)構(gòu)鏈表(formats
,這個鏈表可以通過 register_binfmt
和 unregister_binfmt
注冊和刪除某些可執(zhí)行文件的信息,因此注冊新的可執(zhí)行文件成為可能,后面再介紹)上查找,找到相應(yīng)的結(jié)構(gòu),然后執(zhí)行相應(yīng)的 load_binary
函數(shù)開始加載可執(zhí)行文件。在該鏈表的最后一個元素總是對解釋腳本(interpreted script
)的可執(zhí)行文件格式進(jìn)行描述的一個對象。這種格式只定義了 load_binary
方法,其相應(yīng)的 load_script
函數(shù)檢查這種可執(zhí)行文件是否以兩個 #!
字符開始,如果是,這個函數(shù)就以另一個可執(zhí)行文件的路徑名作為參數(shù)解釋第一行的其余部分,并把腳本文件名作為參數(shù)傳遞以執(zhí)行這個腳本(實際上腳本程序把自身的內(nèi)容當(dāng)作一個參數(shù)傳遞給了解釋程序(如 /bin/bash
),而這個解釋程序通常在腳本文件的開頭用 #!
標(biāo)記,如果沒有標(biāo)記,那么默認(rèn)解釋程序為當(dāng)前 SHELL
)。
對于 ELF
類型文件,其處理函數(shù)是 load_elf_binary
,它先讀入 ELF
文件的頭部,根據(jù)頭部信息讀入各種數(shù)據(jù),再次掃描程序段描述表(Program Header Table
),找到類型為 PT_LOAD
的段(即 .text
,.data
,.bss
等節(jié)區(qū)),將其映射(elf_map
)到內(nèi)存的固定地址上,如果沒有動態(tài)連接器的描述段,把返回的入口地址設(shè)置成應(yīng)用程序入口。完成這個功能的是 start_thread
,它不啟動一個線程,而只是用來修改了 pt_regs
中保存的 PC
等寄存器的值,使其指向加載的應(yīng)用程序的入口。當(dāng)內(nèi)核操作結(jié)束,返回用戶態(tài)時接著就執(zhí)行應(yīng)用程序本身了。
如果應(yīng)用程序使用了動態(tài)連接庫,內(nèi)核除了加載指定的可執(zhí)行文件外,還要把控制權(quán)交給動態(tài)連接器(ld-linux.so
)以便處理動態(tài)連接的程序。內(nèi)核搜尋段表(Program Header Table
),找到標(biāo)記為 PT_INTERP
段中所對應(yīng)的動態(tài)連接器的名稱,并使用 load_elf_interp
加載其映像,并把返回的入口地址設(shè)置成 load_elf_interp
的返回值,即動態(tài)鏈接器的入口。當(dāng) execve
系統(tǒng)調(diào)用退出時,動態(tài)連接器接著運行,它檢查應(yīng)用程序?qū)蚕礞溄訋斓囊蕾囆?,并在需要時對其加載,對程序的外部引用進(jìn)行重定位(具體過程見《進(jìn)程和進(jìn)程的基本操作》)。然后把控制權(quán)交給應(yīng)用程序,從 ELF
文件頭部中定義的程序進(jìn)入點(用 readelf -h
可以出看到,Entry point address
即是)開始執(zhí)行。(不過對于非 LIB_BIND_NOW
的共享庫裝載是在有外部引用請求時才執(zhí)行的)。
對于內(nèi)核態(tài)的函數(shù)調(diào)用過程,沒有辦法通過 strace
(它只能跟蹤到系統(tǒng)調(diào)用層)來做的,因此要想跟蹤內(nèi)核中各個系統(tǒng)調(diào)用的執(zhí)行細(xì)節(jié),需要用其他工具。比如可以通過 Ftrace 來跟蹤內(nèi)核具體調(diào)用了哪些函數(shù)。當(dāng)然,也可以通過 ctags/cscope/LXR
等工具分析內(nèi)核的源代碼。
Linux 允許自己注冊我們自己定義的可執(zhí)行格式,主要接口是 /procy/sys/fs/binfmt_misc/register
,可以往里頭寫入特定格式的字符串來實現(xiàn)。該字符串格式如下::name:type:offset:string:mask:interpreter:
name
新格式的標(biāo)示符type
識別類型(M
表示魔數(shù),E
表示擴展)offset
魔數(shù)(magic number
,請參考 man magic
和 man file
)在文件中的啟始偏移量string
以魔數(shù)或者以擴展名匹配的字節(jié)序列mask
用來屏蔽掉 string
的一些位interpreter
程序解釋器的完整路徑名Linux 下是如何給進(jìn)程分配內(nèi)存(這里僅討論虛擬內(nèi)存的分配)的呢?可以從 /proc/<pid>/maps
文件中看到個大概。這里的 pid
是進(jìn)程號。
/proc
下有一個文件比較特殊,是 self
,它鏈接到當(dāng)前進(jìn)程的進(jìn)程號,例如:
$ 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/
看到?jīng)]?每次都不一樣,這樣我們通過 cat /proc/self/maps
就可以看到 cat
程序執(zhí)行時的內(nèi)存映像了。
$ 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
參數(shù)加上去的。我們從中可以得到如下信息:
總結(jié)一下:
0x00000000
到 0xbfffffff
(在測試的 2.6.21.5-smp
上只到 bfbf8000
),而內(nèi)核空間從 0xC0000000
到 0xffffffff
,分別是 3G
和 1G
,所以對于每一個進(jìn)程來說,共占用 4G
的虛擬內(nèi)存空間heap
,后者是 stack
),再到內(nèi)核空間,地址是從低到高的0xC0000000
下的一個固定數(shù)值結(jié)合相關(guān)資料,可以得到這么一個比較詳細(xì)的進(jìn)程內(nèi)存映像表(以 Linux 2.6.21.5-smp
為例):
地址 | 內(nèi)核空間 | 描述 |
---|---|---|
0xC0000000 | ||
(program flie) 程序名 | execve 的第一個參數(shù) | |
(environment) 環(huán)境變量 | execve 的第三個參數(shù),main 的第三個參數(shù) | |
(arguments) 參數(shù) | execve 的第二個參數(shù),main 的形參 | |
(stack) 棧 | 自動變量以及每次函數(shù)調(diào)用時所需保存的信息都 | |
存放在此,包括函數(shù)返回地址、調(diào)用者的 | ||
環(huán)境信息等,函數(shù)的參數(shù),局部變量都存放在此 | ||
(shared memory) 共享內(nèi)存 | 共享內(nèi)存的大概位置 | |
... | ||
... | ||
(heap) 堆 | 主要在這里進(jìn)行動態(tài)存儲分配,比如 malloc,new 等。 | |
... | ||
.bss (uninitilized data) | 沒有初始化的數(shù)據(jù)(全局變量哦) | |
.data (initilized global data) | 已經(jīng)初始化的全局?jǐn)?shù)據(jù)(全局變量) | |
.text (Executable Instructions) | 通常是可執(zhí)行指令 | |
0x08048000 | ||
0x00000000 | ... |
光看沒有任何概念,我們用 gdb
來看看剛才那個簡單的程序。
$ gcc -g -o shellcode shellcode.c #要用gdb調(diào)試,在編譯時需要加-g參數(shù)
$ gdb -q ./shellcode
(gdb) set args arg1 arg2 arg3 arg4 #為了測試,設(shè)置幾個參數(shù)
(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 #在匯編入口設(shè)置一個斷點,讓程序運行后停到這里
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 #打印當(dāng)前堆棧指針值,用于查找整個棧的棧頂
$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 #其他環(huán)境變量信息
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函數(shù)的參數(shù),包括文件名和其他參數(shù)
0xbffe1780: "/mnt/hda8/Temp/c/program/shellcode"
0xbffe17a3: "arg1"
0xbffe17a8: "arg2"
0xbffe17ad: "arg3"
0xbffe17b2: "arg4"
(gdb) print init #打印init函數(shù)的地址,這個是/usr/lib/crti.o里頭的函數(shù),做一些初始化操作
$2 = {<text variable, no debug info>} 0xb7e73d00 <init>
(gdb) print fini #也在/usr/lib/crti.o中定義,在程序結(jié)束時做一些處理工作
$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函數(shù)
$5 = {void ()} 0x8048324 <main>
補充:在進(jìn)程的內(nèi)存映像中可能看到諸如 init
,fini
,_start
等函數(shù)(或者是入口),這些東西并不是我們自己寫的?。繛槭裁磿艿轿覀兊拇a里頭呢?實際上這些東西是鏈接的時候 gcc
默認(rèn)給連接進(jìn)去的,主要用來做一些進(jìn)程的初始化和終止的動作。更多相關(guān)的細(xì)節(jié)可以參考資料如何獲取當(dāng)前進(jìn)程之靜態(tài)影像文件和"The Linux Kernel Primer", P234, Figure 4.11,如果想了解鏈接(ld)的具體過程,可以看看本節(jié)參考《Unix環(huán)境高級編程編程》第7章 "UnIx進(jìn)程的環(huán)境", P127和P13,ELF: From The Programmer's Perspective,GNU-ld 連接腳本 Linker Scripts。
上面的操作對堆棧的操作比較少,下面我們用一個例子來演示棧在內(nèi)存中的情況。
這一節(jié)主要介紹一個函數(shù)被調(diào)用時,參數(shù)是如何傳遞的,局部變量是如何存儲的,它們對應(yīng)的棧的位置和變化情況,從而加深對棧的理解。在操作時發(fā)現(xiàn)和參考資料的結(jié)果不太一樣(參考資料中沒有 edi
和 esi
相關(guān)信息,再第二部分的一個小程序里頭也沒有),可能是 gcc
版本的問題或者是它對不同源代碼的處理不同。我的版本是 4.1.2
(可以通過 gcc --version
查看)。
先來一段簡單的程序,這個程序除了做一個加法操作外,還復(fù)制了一些字符串。
/* 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;
}
上面這個代碼沒有什么問題,編譯執(zhí)行一下:
$ make testshellcode
cc testshellcode.c -o testshellcode
$ ./testshellcode
sum = 6
下面調(diào)試一下,看看在調(diào)用 func
后的棧的內(nèi)容。
$ gcc -g -o testshellcode testshellcode.c #為了調(diào)試,需要在編譯時加-g選項
$ gdb -q ./testshellcode #啟動gdb調(diào)試
...
(gdb) set logging on #如果要記錄調(diào)試過程中的信息,可以把日志記錄功能打開
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 #在調(diào)用func函數(shù)之前讓程序停一下,以便記錄當(dāng)時的ebp(基指針)
Breakpoint 1 at 0x80483ac: file testshellcode.c, line 28.
(gdb) break func #設(shè)置斷點在函數(shù)入口,以便逐步記錄棧信息
Breakpoint 2 at 0x804835c: file testshellcode.c, line 13.
(gdb) disassemble main #反編譯main函數(shù),以便記錄調(diào)用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 #打印調(diào)用func函數(shù)之前的基地址,即Previous frame pointer。
$1 = (void *) 0xbf84fdd8
(gdb) n #執(zhí)行call指令并跳轉(zhuǎn)到func函數(shù)的入口
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 #打印當(dāng)前棧的內(nèi)容,可以看出,地址從低到高,注意標(biāo)記有藍(lán)色和紅色的值
#它們分別是前一個?;刂?ebp)和call調(diào)用之后的下一條指令的指針(eip)
0xbf84fd94: 0x00000000 0x00000000 0x080482e0 0x00000000
0xbf84fda4: 0xb7f2bce0 0x00000000 0xbf84fdd8 0x080483b7
0xbf84fdb4: 0x00000001 0x00000002 0x00000003
(gdb) n #執(zhí)行sum = a + b + c,后,比較棧內(nèi)容第一行,第4列,由0變?yōu)?
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 #緩沖區(qū)初始化以后變成了0
0xbf84fd94: 0x00000000 0x00000000 0x00000000 0x00000006
0xbf84fda4: 0xb7f2bce0 0x00000000 0xbf84fdd8 0x080483b7
0xbf84fdb4: 0x00000001 0x00000002 0x00000003
(gdb) n
21 return sum;
(gdb) x/11x $esp #進(jìn)行copy以后,這兩列的值變了,大小剛好是7個字節(jié),最后一個字節(jié)為'\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
函數(shù)結(jié)束之前)如下:
地址 | 值(hex) | 符號或者寄存器 | 注釋 |
---|---|---|---|
低地址 | 棧頂方向 | ||
0xbf84fd98 | 0x41414141 | buf[0] | 可以看出little endian(小端,重要的數(shù)據(jù)在前面) |
0xbf84fd9c | 0x00414141 | buf[1] | |
0xbf84fda0 | 0x00000006 | sum | 可見這上面都是func函數(shù)里頭的局部變量 |
0xbf84fda4 | 0xb7f2bce0 | esi | 源索引指針,可以通過產(chǎn)生中間代碼查看,貌似沒什么作用 |
0xbf84fda8 | 0x00000000 | edi | 目的索引指針 |
0xbf84fdac | 0xbf84fdd8 | ebp | 調(diào)用func之前的棧的基地址,以便調(diào)用函數(shù)結(jié)束之后恢復(fù) |
0xbf84fdb0 | 0x080483b7 | eip | 調(diào)用func之前的指令指針,以便調(diào)用函數(shù)結(jié)束之后繼續(xù)執(zhí)行 |
0xbf84fdb4 | 0x00000001 | a | 第一個參數(shù) |
0xbf84fdb8 | 0x00000002 | b | 第二個參數(shù) |
0xbf84fdbc | 0x00000003 | c | 第三個參數(shù),可見參數(shù)是從最后一個開始壓棧的 |
高地址 | 棧底方向 |
先說明一下 edi
和 esi
的由來(在上面的調(diào)試過程中我們并沒有看到),是通過產(chǎn)生中間匯編代碼分析得出的。
$ gcc -S testshellcode.c
在產(chǎn)生的 testShellcode.s
代碼里頭的 func
部分看到 push ebp
之后就 push
了 edi
和 esi
。但是搜索了一下代碼,發(fā)現(xiàn)就這個函數(shù)里頭引用了這兩個寄存器,所以保存它們沒什么用,刪除以后編譯產(chǎn)生目標(biāo)代碼后證明是沒用的。
$ cat testshellcode.s
...
func:
pushl %ebp
movl %esp, %ebp
pushl %edi
pushl %esi
...
popl %esi
popl %edi
popl %ebp
...
下面就不管這兩部分(edi
和 esi
)了,主要來分析和函數(shù)相關(guān)的這幾部分在棧內(nèi)的分布:
ebp
,Previous Frame Pointer
),在中間靠近棧頂方向` (
eip`),在中間靠近棧底的方向到這里,函數(shù)調(diào)用時的相關(guān)內(nèi)容在棧內(nèi)的分布就比較清楚了,在具體分析緩沖區(qū)溢出問題之前,我們再來看一個和函數(shù)關(guān)系很大的問題,即函數(shù)返回值的存儲問題:函數(shù)的返回值存放在寄存器 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
函數(shù)中 mov
到 eax
中的“立即數(shù)” 1,因此很容易理解返回值存儲在 eax
中的事實,如果還有疑慮,可以再看看匯編代碼。在函數(shù)返回之后,eax
中的值當(dāng)作了 printf
的參數(shù)壓入了棧中,而在源代碼中我們正是把 func
的結(jié)果作為 printf
的第二個參數(shù)的。
$ 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的第二個參數(shù),把func的返回值壓入了棧底
pushl $.LC0 #printf的第一個參數(shù)the return of func: %d\n
call printf
...
對于系統(tǒng)調(diào)用,返回值也存儲在 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;
}
編譯一下看看結(jié)果:
$ gcc -DSTR1 -o testshellcode testshellcode.c #通過-D定義宏STR1,從而采用第一個STR_SRC的值
$ ./testshellcode
sum = 1
不知道你有沒有發(fā)現(xiàn)異常呢?上面用紅色標(biāo)記的地方,本來 sum
為 1+2+3
即 6,但是實際返回的竟然是 1 。到底是什么原因呢?大家應(yīng)該有所了解了,因為我們在復(fù)制字符串 AAAAAAA\\0\\1\\0\\0\\0
到 buf
的時候超出 buf
本來的大小。 buf
本來的大小是 BUF_SIZE
,8 個字節(jié),而我們要復(fù)制的內(nèi)容是 12 個字節(jié),所以超出了四個字節(jié)。根據(jù)第一小節(jié)的分析,我們用棧的變化情況來表示一下這個復(fù)制過程(即執(zhí)行 memcpy
的過程)。
memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);
(低地址)
復(fù)制之前 ====> 復(fù)制之后
0x00000000 0x41414141 #char buf[8]
0x00000000 0x00414141
0x00000006 0x00000001 #int sum
(高地址)
下面通過 gdb
調(diào)
更多建議: