雖然程序加載以及動態符號連接都已經很理解了,可是這夥卻被進程的內存映像給」糾纏"住。看着看着就一發不可收拾——頗有趣。php
下面一塊兒來探究「緩衝區溢出和注入」問題(主要是關心程序的內存映像)。html
永遠的 Hello World
,太熟悉了吧,java
#include <stdio.h> int main(void) { printf("Hello World\n"); return 0; }
若是要用內聯彙編(inline assembly
)來寫呢?linux
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
。不過這個很是有意思。先簡單分析一下流程:nginx
forward
標記處),接着執行第 16 行。backward
,跳轉到第 5 行,接着執行 6 到 14 行。Hello World
字符串(等一下詳細介紹)。爲了更好的理解上面的代碼和後續的分析,先來介紹幾個比較重要的內容。程序員
X86
處理器平臺有三個經常使用寄存器:程序指令指針、程序堆棧指針與程序基指針:shell
寄存器 | 名稱 | 註釋 |
---|---|---|
EIP | 程序指令指針 | 一般指向下一條指令的位置 |
ESP | 程序堆棧指針 | 一般指向當前堆棧的當前位置 |
EBP | 程序基指針 | 一般指向函數使用的堆棧頂端 |
固然,上面都是擴展的寄存器,用於 32 位系統,對應的 16 系統爲 ip
,sp
,bp
。編程
call
指令小程序
跳轉到某個位置,並在以前把下一條指令的地址(EIP
)入棧(爲了方便」程序「返回之後可以接着執行)。這樣的話就有:swift
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.
系統調用是用戶和內核之間的接口,用戶若是想寫程序,不少時候直接調用了 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 不是「精靈」,而是 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 下是如何給進程分配內存(這裏僅討論虛擬內存的分配)的呢?能夠從 /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
參數加上去的。咱們從中能夠獲得以下信息:
總結一下:
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
老是在第一條指令被壓入棧中,並在最後一條指令(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
就被覆蓋了。
$ 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 下緩衝區溢出攻擊的原理及對策,這裏主要利用上面介紹的知識對它進行了比較詳細的分析。
首先回到第一部分,看看那個 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/