(0 byte)图 3. ELF刚载入内存时栈的内容。n
2012年第6期
关于栈的一个很重要的点是,当ELF文件刚被载入内存时,栈不是空的(参考源代码fs/binfmt_elf.c/create_elf_tables())。至少,很明显main()函数的参数需要被存于栈上。事实上栈上还存储了更多的信息,具体如图3所示。如果程序的参数为字符串,那么字符串本身存储与栈底(main()函数的参数和环境变量)。PRNG (Pseudo Random Noise
Generation)是一个16字节的伪随机数种子。接下来,是一个叫auxiliary vector的数组。该数组的内容定义在源代码的include/linux/auxvec.h和arch/x86/include/asm/auxvec.h文件中。为了得到某个程序的auxiliary vector的内容,可以使用类似于“LD_SHOW_AUXV=true cat /proc/self/maps”的命令。图4是本文使用的测试系统上的一个例子。注意其中的AT_SYSINFO_EHDR,其值为0x242000。事实上该值是VDSO(Virtual Dynamic Shared Object)的起始地址。真是巧了,在系统使用ASLR后我们就无法预先知道VDSO的地址了,这个东西正好动态地告诉了我们。事实上,这正是系统告诉库函数VDSO地址的机制。紧接着auxiliary vector的是一组环境变量。最后,main()函数的参数被压入栈中并且位于当前栈顶的是argc (即参数数量)。
ww客防w转.线h载a c请k注er明.c出om处.c图 4. 一个auxiliary vector的例子。
n
紧接着栈的堆。堆用于动态内存分配。对于C程序员,该段可以使用mallco()/free()函数来管理。当程序员请求一块内存时,如果在堆中有足够的空闲块,那么库函数就可以直接满足该请求而不需要与内核交互。否则的话,将调用系统调用brk()来增加堆的大小。
接下来我们可以看到3个连续的段:BSS, 数据段和代码段。ELF文件格式中也含有同名的3个段。BSS段和数据段含有C语言中的静态或全局变量。不同之处在于BSS段含有未初始化的值而数据段含有初始化后的值。在对象文件中(即命令gcc –c xxx.c的输出文件),只有未初始化的静态变量存储于.bss段(ELF文件中而非内存)而未初始化的全局变量放于COMMON块(ELF文件格式)。例如,某个C程序中有两个未初始化的全局变量gCount和gName,它们将被存储在BSS段中,如图5所示。可以看到,BSS段被初始化为0。这也就是为什么相关的C语言教材告诉我们不需要初始化全局变量。它们将被编译器初始化为0或NULL。
另一方面,例如该C程序还有一个全局变量gVersion被初始化为1.0。该变量将被存于数据段,如图5所示。gInfo是指向一个字符串的指针。由于该字符串的地址为
0x08049062并位于代码段中。为什么字符串被放于代码段中?因为字符串是只读的,而数据段和BSS段均是可写的,只有代码段是只读的。
黑2012年第6期
接下来看代码段。代码段将binary image映射到内存中。由于代码段是只读的,所有如果程序试图想该段写入数据,将会产生一个段错误。代码段的起始地址为0x08048000,这是一个约定俗成的地址。
Memory LayoutBSSR/WDataR/WTextR/XELF Image on DiskFilled with 00x080490621.0“This is an example”mov eax, ebxjmp eaxgCountgNamegInfogVersion0x080490620x08048000
图 5. BSS,数据段和代码段的内存格式。
ww客防w转.线h载a c请k注er明.c出om处.c图 6. 命令“cat /proc/self/maps”的输出。
内存中的最后一个段是mmap段。内核将文件的内容直接映射到该区域。用户可以使用系统调用mmap()来实现该目的。mmap是一种方便的高性能文件IO方法。因此Linux的程序Loader将动态链接库映射到该内存。如图6中,我们可以看到libc和ld都被映射到该区域。这其中有个重要的区域叫做VDSO(前文已经看到过)。VDSO是一种最新的系统调用机制,此处不做深入介绍。另外,我们也可以建立匿名映射。该映射不对应于任何文件而只是用于动态地存储程序的数据。例如,在Linux中,程序员调用malloc()来申请一块很的大内存时,glibc会建立一个匿名映射来满足申请而非从堆中分配。默认情况下,当申请的内存达到128KB时(内核中由变量MMAP_THRESHOLD指定),就会使用匿名映射。可以调用函数mallopt()来修改MMAP_THRESHOLD的值。
黑n
如果想查看程序的内存格式,可以读取/proc/[pid]/maps中的内容。例如,命令”cat /proc/self/maps”的输出如图6所示。本文的输出可能会与其他机器有所不同并且为了更简
2012年第6期
洁,本文删除了堆与栈之间的某些内容。图7为另一次运行的输出。与图6相比,我们可以发现库的加载顺序和起始地址都发生了变化。这是由于系统使用了ASLR。如果我们将ASLR禁止,那么每次运行时的内存格式都是一样的。此处本文省略了相应的输出。如果读者感兴趣,可以使用命令“sudo echo 0 > /proc/sys/kenel/randomize_va_space”来禁用ASLR。要使用ASLR,使用命令“sudo echo 1 > /proc/sys/kenel/randomize_va_space”。
最后,如果想查看二进制文件的内容,可以使用binutils中的工具如readelf和objdump等。
ww客防w转.线h载a c请k注er明.c出om处.c图 7. 另一次运行命 “cat /proc/self/maps”的输出。
n
从图2我们已经看到有几个段的起始地址之前有个随机的偏移的量。这种技术叫做Address Space Layout Randomization (ASLR)。 ASLR 通过引入一个随机偏移量来防止return-to-libc攻击。Linux有两种ASLR实现方式,PaX和Exec Shield。本文仅介绍PaX ASLR.
...jibuf[200]old EBPret addressparameter 1parameter 2parameter 3...黑ASLR
Stack Frame 1Stack Frame 2 图 8. 基本的栈缓冲溢出攻击。
2012年第6期
首先让我们来看下为什么需要ASLR。ASLR主要针对缓冲溢出和return-to-libc攻击。本文不是关于缓冲溢出攻击的,所以本文只是给出一个大概的介绍。栈缓冲溢出是最基本的缓冲溢出攻击,其原理如图8所示。很明显如果攻击者故意提供一个超出buf[]大小的字符串并且被攻击程序没有检查字符串的大小,那么栈上的函数返回地址就能够被覆盖成任意的地址。在没有引入ASLR的时候,由于每次动态链接库都加载到相同的地址而glibc又是每个程序基本都会加载的库,攻击者就可以用某个glibc中的函数地址来覆盖栈上的返回地址,从而在函数返回时程序就可以执行攻击者指定的函数来实现攻击,比如
system()。事实上return-to-libc是一个特殊的缓冲溢出攻击。return-to-libc很重要的原因是攻击者不需要注入shell code,因此某些传统的防护方法无法防止这类攻击。所以工程师就发明了ASLR技术。
图6和7已经解释了ASLR的效果。事实上,真正的实现很简单。在进程创建的时候,fs/binfmt_elf.c中的load_elf_binary()函数负责实现随机化。其中具体的实现中包含3个随机变量:delta_exec, delta_mmap和delta_stack。需要注意的是delta_exec是用来实现堆基址随机化的,而非代码段(前文已经说过ELF有个固定的代码段基址)。
为了绕过ASLR,大概也有两个方法:guessing和brute forcing。当攻击者无法获得任何信息时,即每次尝试都是相互事件时,攻击者也就只能猜测了。相反,攻击者就可以使用brute forcing的方法。现实中,brute forcing可以适用于为每个连接fork()一个子进程来处理的网络守护服务器,因为fork()完整地复制了父进程的地址空间。然而exec()却会重新进行随机化处理。
本文详细介绍了Linux中程序的地址空间。本文虽未涉及具体的攻击实现,但是本文的内容对于进一步学习和实现Linux上的缓冲溢出等攻击非常重要。基于本文的内容,接下来作者将进一步写一些具体的攻击实现。
ww客防w转.线h载a c请k注er明.c出om处.c结语
黑n