虽然程序加载以及动态符号连接都已经很理解了,可是这伙却被进程的内存映像给”纠缠"住。看着看着就一发不可收拾——颇有趣。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/