虽然程序加载以及动态符号链接都已经很理解了,但是这伙却被进程的内存映像给”纠缠"住。看着看着就一发不可收拾——很有趣。
下面一起来探究“缓冲区溢出和注入”问题(主要是关心程序的内存映像)。
永远的 Hello World,太熟悉了吧,
Hello World
#include <stdio.h>int main(void){ printf("Hello World\n"); return 0;}
#include <stdio.h>
int main(void)
{
printf("Hello World\n");
return 0;
}
如果要用内联汇编(inline assembly)来写呢?
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 }
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 。不过这个非常有意思。先简单分析一下流程:
forward
backward
为了更好的理解上面的代码和后续的分析,先来介绍几个比较重要的内容。
X86 处理器平台有三个常用寄存器:程序指令指针、程序堆栈指针与程序基指针:
X86
当然,上面都是扩展的寄存器,用于 32 位系统,对应的 16 系统为 ip,sp,bp 。
ip
sp
bp
call 指令
call
跳转到某个位置,并在之前把下一条指令的地址(EIP)入栈(为了方便”程序“返回以后能够接着执行)。这样的话就有:
EIP
call backward ==> push eip jmp backward
call backward ==> push eip
jmp backward
ret 指令
ret
通常 call 指令和 ret 是配合使用的,前者压入跳转前的下一条指令地址,后者弹出 call 指令压入的那条指令,从而可以在函数调用结束以后接着执行后面的指令。
ret ==> pop eip
通常在函数调用后,还需要恢复 esp 和 ebp,恢复 esp 即恢复当前栈指针,以便释放调用函数时为存储函数的局部变量而自动分配的空间;恢复 ebp 是从栈中弹出一个数据项(通常函数调用过后的第一条语句就是 push ebp),从而恢复当前的函数指针为函数调用者本身。这两个动作可以通过一条 leave 指令完成。
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)
用户空间 应用程序(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 中。
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...)来声明好参数。
/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 寄存器中。
int 0x80
eax
/usr/include/asm-i386/unistd.h
ebx
ecx
edx
esi
edi
下面我们就很容易解释前面的 Shellcode.c 程序流程的 2,3 两部分了。因为都用了 int 0x80 中断,所以都用到了系统调用。
Shellcode.c
第 3 部分很简单,用到的系统调用号是 1,通过查表(查 /usr/include/asm-i386/unistd.h 或 arch/i386/kernel/syscall_table.S)可以发现这里是 sys_exit 调用,再从 /usr/include/unistd.h 文件看这个系统调用的声明,发现参数 ebx 是程序退出状态。
sys_exit
/usr/include/unistd.h
第 2 部分比较有趣,而且复杂一点。我们依次来看各个寄存器,首先根据 eax 为 4 确定(同样查表)系统调用为 sys_write,而查看它的声明(从 /usr/include/unistd.h),我们找到了参数依次为文件描述符、字符串指针和字符串长度。
sys_write
popl %esi;
.string
Hello World\\n
popl
eip
这里的 ELF 不是“精灵”,而是 Executable and Linking Format 文件,是 Linux 下用来做目标文件、可执行文件和共享库的一种文件格式,它有专门的标准,例如:X86 ELF format and ABI,中文版。
下面简单描述 ELF 的格式。
ELF
ELF 文件主要有三种,分别是:
gcc
-c
ar
ELF 文件的大体结构:
ELF Header #程序头,有该文件的Magic number(参考man magic),类型等Program Header Table #对可执行文件和共享库有效,它描述下面各个节(section)组成的段Section1Section2Section3.....Program Section Table #仅对可重定位目标文件和静态库有效,用于描述各个Section的重定位信息等。
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 等工具删除掉以后,就不可恢复了。因为这些信息对程序的运行一般没有任何用处。
Program Section Table
Section
.comment
.note.XXX.debug
strip
objcopy
ELF 文件的主要节区(section)有 .data,.text,.bss,.interp 等,而主要段(segment)有 LOAD,INTERP 等。它们之间(节区和段)的主要对应关系如下:
section
.data
.text
.bss
.interp
segment
LOAD
INTERP
int a=10
char sum[100];
/lib/ld-linux.so
而程序在执行以后,.data,.bss,.text 等一些节区会被 Program header table 映射到 LOAD 段,.interp 则被映射到了 INTERP 段。
Program header table
对于 ELF 文件的分析,建议使用 file,size,readelf,objdump,strip,objcopy,gdb,nm 等工具。
file
size
readelf
objdump
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 fileshellcode: 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 0x8048280There are 7 program headers, starting at offset 52Program 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-i386Contents of section .interp: 8048114 2f6c6962 2f6c642d 6c696e75 782e736f /lib/ld-linux.so 8048124 2e3200 .2.
$ 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 #“减肥”后效果“明显”,对于嵌入式系统应该有很大的作用
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 工具。
sstrip
在命令行下,敲入程序的名字或者是全路径,然后按下回车就可以启动程序,这个具体是怎么工作的呢?
首先要再认识一下我们的命令行,命令行是内核和用户之间的接口,它本身也是一个程序。在 Linux 系统启动以后会为每个终端用户建立一个进程执行一个 Shell 解释程序,这个程序解释并执行用户输入的命令,以实现用户和内核之间的接口。这类解释程序有哪些呢?目前 Linux 下比较常用的有 /bin/bash 。那么该程序接收并执行命令的过程是怎么样的呢?
/bin/bash
先简单描述一下这个过程:
execve
fork
wait4
&
现在用 strace 来跟踪一下程序执行过程中用到的系统调用。
strace
$ strace -f -o strace.out test$ cat strace.out | grep \(.*\) | sed -e "s#[0-9]* \([a-zA-Z0-9_]*\)(.*).*#\1#g"execvebrkaccessopenfstat64mmap2closeopenreadfstat64mmap2mmap2mmap2mmap2closemmap2set_thread_areamprotectmunmapbrkbrkopenfstat64mmap2closeclosecloseexit_group
$ strace -f -o strace.out test
$ cat strace.out | grep \(.*\) | sed -e "s#[0-9]* \([a-zA-Z0-9_]*\)(.*).*#\1#g"
brk
access
open
fstat64
mmap2
close
read
set_thread_area
mprotect
munmap
exit_group
相关的系统调用基本体现了上面的执行过程,需要注意的是,里头还涉及到内存映射(mmap2)等。
下面再罗嗦一些比较有意思的内容,参考《深入理解 Linux 内核》的程序的执行(P681)。
Linux 支持很多不同的可执行文件格式,这些不同的格式是如何解释的呢?平时我们在命令行下敲入一个命令就完了,也没有去管这些细节。实际上 Linux 下有一个 struct linux_binfmt 结构来管理不同的可执行文件类型,这个结构中有对应的可执行文件的处理函数。大概的过程如下:
struct linux_binfmt
在用户态执行了 execve 后,引发 int 0x80 中断,进入内核态,执行内核态的相应函数 do_sys_execve,该函数又调用 do_execve 函数。 do_execve 函数读入可执行文件,检查权限,如果没问题,继续读入可执行文件需要的相关信息(struct linux_binprm 描述的)。
do_sys_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)。
search_binary_handler
linux_binfmt
formats
register_binfmt
unregister_binfmt
load_binary
interpreted script
load_script
#!
SHELL
对于 ELF 类型文件,其处理函数是 load_elf_binary,它先读入 ELF 文件的头部,根据头部信息读入各种数据,再次扫描程序段描述表(Program Header Table),找到类型为 PT_LOAD 的段(即 .text,.data,.bss 等节区),将其映射(elf_map)到内存的固定地址上,如果没有动态连接器的描述段,把返回的入口地址设置成应用程序入口。完成这个功能的是 start_thread,它不启动一个线程,而只是用来修改了 pt_regs 中保存的 PC 等寄存器的值,使其指向加载的应用程序的入口。当内核操作结束,返回用户态时接着就执行应用程序本身了。
load_elf_binary
Program Header Table
PT_LOAD
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 的共享库装载是在有外部引用请求时才执行的)。
ld-linux.so
PT_INTERP
load_elf_interp
readelf -h
Entry point address
LIB_BIND_NOW
对于内核态的函数调用过程,没有办法通过 strace(它只能跟踪到系统调用层)来做的,因此要想跟踪内核中各个系统调用的执行细节,需要用其他工具。比如可以通过 Ftrace 来跟踪内核具体调用了哪些函数。当然,也可以通过 ctags/cscope/LXR 等工具分析内核的源代码。
ctags/cscope/LXR
Linux 允许自己注册我们自己定义的可执行格式,主要接口是 /procy/sys/fs/binfmt_misc/register,可以往里头写入特定格式的字符串来实现。该字符串格式如下: :name:type:offset:string:mask:interpreter:
/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
interpreter
Linux 下是如何给进程分配内存(这里仅讨论虚拟内存的分配)的呢?可以从 /proc/<pid>/maps 文件中看到个大概。这里的 pid 是进程号。
/proc/<pid>/maps
pid
/proc 下有一个文件比较特殊,是 self,它链接到当前进程的进程号,例如:
/proc
self
$ ls /proc/self -llrwxrwxrwx 1 root root 64 2000-01-10 18:26 /proc/self -> 11291/$ ls /proc/self -llrwxrwxrwx 1 root root 64 2000-01-10 18:26 /proc/self -> 11292/
$ ls /proc/self -l
lrwxrwxrwx 1 root root 64 2000-01-10 18:26 /proc/self -> 11291/
lrwxrwxrwx 1 root root 64 2000-01-10 18:26 /proc/self -> 11292/
看到没?每次都不一样,这样我们通过 cat /proc/self/maps 就可以看到 cat 程序执行时的内存映像了。
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]
$ 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 参数加上去的。我们从中可以得到如下信息:
-n
总结一下:
0x00000000
0xbfffffff
2.6.21.5-smp
bfbf8000
0xC0000000
0xffffffff
3G
1G
4G
heap
stack
结合相关资料,可以得到这么一个比较详细的进程内存映像表(以 Linux 2.6.21.5-smp 为例):
Linux 2.6.21.5-smp
光看没有任何概念,我们用 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 arg4Breakpoint 1, main () at shellcode.c:44 __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>
$ gcc -g -o shellcode shellcode.c #要用gdb调试,在编译时需要加-g参数
$ gdb -q ./shellcode
(gdb) set args arg1 arg2 arg3 arg4 #为了测试,设置几个参数
(gdb) l #浏览代码
(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
(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。
init
fini
_start
上面的操作对堆栈的操作比较少,下面我们用一个例子来演示栈在内存中的情况。
这一节主要介绍一个函数被调用时,参数是如何传递的,局部变量是如何存储的,它们对应的栈的位置和变化情况,从而加深对栈的理解。在操作时发现和参考资料的结果不太一样(参考资料中没有 edi 和 esi 相关信息,再第二部分的一个小程序里头也没有),可能是 gcc 版本的问题或者是它对不同源代码的处理不同。我的版本是 4.1.2 (可以通过 gcc --version 查看)。
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"#endifint 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;}
/* 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);
上面这个代码没有什么问题,编译执行一下:
$ make testshellcodecc testshellcode.c -o testshellcode$ ./testshellcodesum = 6
$ make testshellcode
cc testshellcode.c -o testshellcode
$ ./testshellcode
sum = 6
下面调试一下,看看在调用 func 后的栈的内容。
func
$ gcc -g -o testshellcode testshellcode.c #为了调试,需要在编译时加-g选项$ gdb -q ./testshellcode #启动gdb调试...(gdb) set logging on #如果要记录调试过程中的信息,可以把日志记录功能打开Copying output to gdb.txt.(gdb) l main #列出源代码2021 return sum;22 }2324 int main()25 {26 int sum;2728 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),%ecx0x0804839f <main+4>: and $0xfffffff0,%esp0x080483a2 <main+7>: pushl 0xfffffffc(%ecx)0x080483a5 <main+10>: push %ebp0x080483a6 <main+11>: mov %esp,%ebp0x080483a8 <main+13>: push %ecx0x080483a9 <main+14>: sub $0x14,%esp0x080483ac <main+17>: push $0x30x080483ae <main+19>: push $0x20x080483b0 <main+21>: push $0x10x080483b2 <main+23>: call 0x8048354 <func>0x080483b7 <main+28>: add $0xc,%esp0x080483ba <main+31>: mov %eax,0xfffffff8(%ebp)0x080483bd <main+34>: sub $0x8,%esp0x080483c0 <main+37>: pushl 0xfffffff8(%ebp)0x080483c3 <main+40>: push $0x80484c00x080483c8 <main+45>: call 0x80482a0 <printf@plt>0x080483cd <main+50>: add $0x10,%esp0x080483d0 <main+53>: mov $0x0,%eax0x080483d5 <main+58>: mov 0xfffffffc(%ebp),%ecx0x080483d8 <main+61>: leave0x080483d9 <main+62>: lea 0xfffffffc(%ecx),%esp0x080483dc <main+65>: retEnd of assembler dump.(gdb) r #运行程序Starting program: /mnt/hda8/Temp/c/program/testshellcodeBreakpoint 1, main () at testshellcode.c:2828 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:1313 int sum = 0;(gdb) n16 sum = a + b + c;(gdb) x/11x $esp #打印当前栈的内容,可以看出,地址从低到高,注意标记有蓝色和红色的值 #它们分别是前一个栈基地址(ebp)和call调用之后的下一条指令的指针(eip)0xbf84fd94: 0x00000000 0x00000000 0x080482e0 0x000000000xbf84fda4: 0xb7f2bce0 0x00000000 0xbf84fdd8 0x080483b70xbf84fdb4: 0x00000001 0x00000002 0x00000003(gdb) n #执行sum = a + b + c,后,比较栈内容第一行,第4列,由0变为618 memset(buffer, '\0', BUF_SIZE);(gdb) x/11x $esp0xbf84fd94: 0x00000000 0x00000000 0x080482e0 0x000000060xbf84fda4: 0xb7f2bce0 0x00000000 0xbf84fdd8 0x080483b70xbf84fdb4: 0x00000001 0x00000002 0x00000003(gdb) n19 memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);(gdb) x/11x $esp #缓冲区初始化以后变成了00xbf84fd94: 0x00000000 0x00000000 0x00000000 0x000000060xbf84fda4: 0xb7f2bce0 0x00000000 0xbf84fdd8 0x080483b70xbf84fdb4: 0x00000001 0x00000002 0x00000003(gdb) n21 return sum;(gdb) x/11x $esp #进行copy以后,这两列的值变了,大小刚好是7个字节,最后一个字节为'\0'0xbf84fd94: 0x00000000 0x41414141 0x00414141 0x000000060xbf84fda4: 0xb7f2bce0 0x00000000 0xbf84fdd8 0x080483b70xbf84fdb4:
$ 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.
Starting program: /mnt/hda8/Temp/c/program/testshellcode
Breakpoint 1, main () at testshellcode.c:28
(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
19 memcpy(buffer, STR_SRC, sizeof(STR_SRC)-1);
(gdb) x/11x $esp #缓冲区初始化以后变成了0
0xbf84fd94: 0x00000000 0x00000000 0x00000000 0x00000006
(gdb) x/11x $esp #进行copy以后,这两列的值变了,大小刚好是7个字节,最后一个字节为'\0'
0xbf84fd94: 0x00000000 0x41414141 0x00414141 0x00000006
0xbf84fdb4: