您好,欢迎来到测品娱乐。
搜索
您的当前位置:首页Linux ELF 运行时内存详解 - 黑客防线官方站

Linux ELF 运行时内存详解 - 黑客防线官方站

来源:测品娱乐
2012年第6期

Linux ELF运行时内存详解

4/22/2012

前一段时间做ROP(return-oriented programming)的东西,想要系统的了解Linux中程序的内存格式(memory layout),网上有很多文章,却没有一个深入完整的介绍。所以花了些时间做深入的了解,不放过一个细节。由于最初写的是英文文档,所以文中的图都是用英文标识的,不过应该不影响阅读。

本文详细解释了Linux ELF文件的虚拟地址空间。另外本文也大概介绍了ASLR

(Address Space Layout Randomization)技术对ELF虚拟地址空间的影响。作者的测试系统是Linux Ubuntu 2.6.32-24和Vmware Workstation 7。另外所有的分析都基于Intel x86架构。

当代的操作系统中每个进程都有自己的虚拟地址空间。在32位系统上,该虚拟地址空间有4G大小。为了将虚拟地址转换为物理地址,Linux内核使用了一个两级(事实上是三级,但是中间一级没有任何实质操作)分页机制,即页目录表和页表。分页机制与MMU(Memory Management Unit)合作将虚拟地址转换为物理地址。当操作系统引入虚拟地址后,所有的用户操作系统和内核线程(事实上Linux只有进程概念而没有线程概念,Linux通过页表机制来模拟实现内核线程)都将运行于虚拟地址模式。

另外Linux(以及Windows)使用了CPU提供的权限机制。内核代码将运行于ring 0而用户程序运行于ring 3。 因此为了适应该分级机制以及适应多任务机制,Linux的虚拟地址空间被分为两部分,如图1所示:

Linux Virtual Address SplitWindows Virtual Address Splitww客防w转.线h载a c请k注er明.c出om处.c虚拟地址空间

Kernel Space(1 GB)0xffff ffff0xffff ffff0xc0000000黑Kernel Space(2 GB)0x80000000User Space(3 GB)User Space(2 GB)0x00x0图1. Linux/Windows虚拟地址空间的内核部分和用户部分。

Linux中,内核空间为0xc0000000到0xffffffff的地址,因此内核代码将被映射到区域。而在Windows中,默认的分割方式为内核与用户各占2GB。本文仅详细分析Linux的地址空间而不再涉及Windows。下面分两部分介绍Linux地址空间,首先是内核地址空间然后再介绍用户地址空间。

1. 内核地址空间

n

2012年第6期

内核地址空间属于ring 0,因此用户程序无法读取或修改该地址空间(除非通过特殊手段,如果系统调用)。如果用户程序强制涉及内核空间的话,系统将产生一个段错误(该错误对于Linux程序员是再熟悉不过了)。另外,内核地址空间常驻于内存并且所有的进程共享相同的内核空间,然而用户地址空间随着进程的切换而改变。内核地址空间的详细格式如图2所示。

黑用户空间可以进一步被分为以下几个部分:栈,mmap段,堆,BSS段,数据段和代码段。其分布如图2所示。需要注意的是该分布格式是我的测试系统的结果,在其他系统上可能略有差别,如mmap段可能被置于堆和栈之间。下面将详细解释每个段。

栈向下增长并且栈的大小受参数RLIMIT_STACK。因此当程序向一个未映射的内存区写入数据时,如果该内存区位于RLIMIT_STACK内,那么将不会产生段错误而只会调用函数expand_stack()来动态增长栈大小。另外,需要注意的是在内核地址空间与栈的起始地址之间有一个空白区。该空白区有ASLR生成。本文将在第二节介绍ASLR。

ww客防w转.线h载a c请k注er明.c出om处.c2.

用户地址空间

图2. 内核地址空间。

内核空间的起始地址有PAGE_OFFSET定义,对于32位x86系统,该值为0xc0000000。PAGE_OFFSET和VMALLOC_OFFSET之间的区域是直接内存映射。VMALLOC_OFFSET是一个8M的空隙,用来防止越界。

PKMAP_BASE开始的一段内存提供给kmap()使用。

某些设备需要在编译时就知道虚拟地址如APIC,FIXADDR_START和FIXADDR_TOP之间的内存就是提供给这些设备使用的。

最后一个页是vsyscall页。在2.4内核中,该页是空白页,即该页不可用。在2.6中,该页提供了一种心的从用户层进入内核层中的方法。现在,用户程序可以使用”call 0xfffff000”来代替”int 0x80”来进入内核层。

n 2012年第6期

Kernel Space(1 GB)0xc0000000 == TASK_SIZErandom stack offsetStackRLIMIT_STACKprogram brkHeapstart_brkrandom brk offsetBSS SegmentData SegmentText Segment0x08048000random mmap offsetww客防w转.线h载a c请k注er明.c出om处.cMemory Mapping Segment(libc, vdso, ld, ...)0x0图2. 用户地址空间分布。

argcESPargv[0] (4 bytes)argv[1] (4 bytes)….program nameargv[n] (4 bytes)NULLenvp[0] (4 bytes)envp[1] (4 bytes)….黑envp[term] (4 bytes)NULLauxv[0] (8 bytes)auxv[1] (8 bytes)….auxv[term] (8 bytes)AT_NULLPRNG seeding(16 bytes)arg strings(>= 0 bytes)env strings(>= 0 bytes)end (4 bytes)0xbffffffcNULLVritual0xc0000000(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

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- cepb.cn 版权所有 湘ICP备2022005869号-7

违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务