Exec-shield 是一类安全功能的开关,由红帽在很多年前主导搞的对 buffer overflow 攻击的一系列增强,具体可以参看这几个连接 1、2,3,4,exec shield 在实现和使用上一直有问题,也破坏了有些旧程序的兼容性【6】,因此一直没进主干,只在 redhat 家族 6.x 及其派生系统上使用。
这个功能有一个开关 /proc/sys/kernel/exec-shield,根据链接【6】上的说明,exec-shield 可以设置为 0、1、2、3,分别表示:强制关闭/默认关闭除非可执行程序指定打开/默认打开除非可执行程序指定关闭/强制打开。
mm->get_unmapped_area 是进程需要进行 mmap 时调用的最终函数, arch_get_unmap_area() 用来以传统方式从低位开始搜索合适的位置,arch_get_unmapped_area_topdown() 则以 flexible layout 的方式从高位开始搜索合适的位置,关键点在于 125 ~ 129 行,exec-shield 引进了另一种专门针对 32 位进程的内存分配方式,这种方式指定如果要分配的内存需要可执行权限,那么应该从 mm->shlib_base 这里开始搜索合适的位置,shlib_base 的值为 SHLIB_BASE 加上一个小的随机偏移,而 SHLIB_BASE 的值为【7】:
图 - 9
注意到该地址位于 32 位进程的代码段之前(0x8048000),所以这就解释了为什么 32 位的进程,它的动态库被加载到了低位甚至穿插进了 brk 和数据段之间的空隙,本来这个特殊的搜索内存空间的方式是只针对需要可执行权限的内存,但由于 elf 加载器在加载动态库时是分段(PT_LOAD)进行加载【8】,第一个段的位置由 mm->get_unmap_area() 搜索合适的位置分配,后续的段则使用 MAP_FIXED 强制放在了第一个段的后面,所以导致数据段也映射到了低位.【9】
下图 1641 行展示了 mmap 时怎样从 mm 结构里获取 get_area 函数,可以看到,只要 mm->get_unmmapped_exec_area 不为空,且要分配的内存需要可执行权限,就优先使用 mm->get_unmmapped_exec_area 进行搜索。
图 - 10
上面这种针对 exec 内存的分配方式实际上很容易引起冲突,redhat 在这里也是打了不少补丁,参看1,2,3。
问题并没有解决上面的解释说明了为什么 32 位进程的内存布局会异常,但是这里的问题是,为什么用 32 位进程起 64 位进程时,64 位的进程也同样受到了影响。要搞清楚这里的问题,就得看看 fs/binfmt_elf.c: load_elf_binary() 这个函数,它用来在当前进程中加载 elf 格式可执行文件并跳过去执行,此函数被 32 位的 elf 与 64 位 elf 所共用(借助了比较隐蔽的宏),它做的事情总结起来包括如下:
1、读取和解析 elf 文件里包含的各种信息,关键信息如代码段,数据段,动态链接器等。
2、flush_old_exec(): 停止当前进程内的所有线程,清空当前内存空间,重置各种状态等。
3、设置新进程的状态,如分配内存空间,初始化等。
4、加载动态连接器并跳过去执行。
图 - 11
现在回到我们问题,当前进程是 32 位的,在 64 位的系统上执行 32 位的进程需要内核支持,当内核发现 elf 是 32 位的程序时,会在 task 内部置一个标志,这个标志在上图 load_elf_binary() 函数里 740 行调用 SET_PERSONALITY() 才会被清除,所以在 721 行时,当前进程仍认为自己是 32 位的,flush_old_exec() 做了什么事情呢,参看:fs/exec.c: flush_old_exec()
图 - 12
注意其中 1039 行,bprm->mm 表示新的内存空间(旧的还在,但马上就要释放并切换新的),这里需要对新的内存空间进行设置,参看: fs/exec.c: exec_mmap()
图 - 13
我们可以看到在当前进程还是 32 位的时候,内核对新的内存空间进行了初始化,导致 arch_pick_mmap_layout() 错误地将 arch_get_unmaped_exec_area 赋值给了 bprm->mm->get_unmapped_exec_area 这个成员变量,虽然图-11中 load_elf_binary() 函数在 748 行,32 位的标志被清空之后再次调用 set_up_new_exec() -> arch_get_unmapped_exec_area(),但 arch_get_unmaped_exec_area() 并没有清空 mm->get_unmapped_exec_area 这个变量,导致 execv 后虽然进程是 64 位的,但仍然以 mm->shlib_base 这里作为起始地址搜索内存空间给动态库使用, oops.
解决方案最直接可靠的做法是在进入 arch_pick_mmap_layout() 时,先把 mm->get_unmapped_exec_area 置为 NULL,但这就要修改内核了,用户态要规避的话有以下方式:
1、设置 ulimit -s unlimited,并设置 exec-shield 为 0 或 1,再起进程,这样一来,因为用户态的栈是无限长的,内核只能以传统的方式来对 32 位进程分配内存,不会掉进 exec-shield 的坑里。
2、把 randomize_va_space 禁掉,但这个做法只是把头埋进了沙子里。
总的来说,上面两种用户态的规避方案基本是哪里疼往哪贴膏药,并非解决问题之道(且有安全隐患),退一步来说,不要用 32 位的进程来起动 64 位进程还相对稳妥点.
参考【0】https://en.wikipedia.org/wiki/C_dynamic_memory_allocation
【1】https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/5/html/Tuning_and_Optimizing_Red_Hat_Enterprise_Linux_for_Oracle_9i_and_10g_Databases/sect-Oracle_9i_and_10g_Tuning_Guide-Growing_the_Oracle_SGA_to_2.7_GB_in_x86_Red_Hat_Enterprise_Linux_2.1_Without_VLM-Linux_Memory_Layout.html
【2】understanding the linux kernel, page 819, flexible memory region layout:
【3】https://gist.github.com/CMCDragonkai/10ab53654b2aa6ce55c11cfc5b2432a4
【4】
【5】 https://access.redhat.com/blogs/766093/posts/1975793
【6】https://lwn.net/Articles/31032/
【7】https://lwn.net/Articles/454949/
【8】
【9】
【10】类似问题: https://bugzilla.redhat.com/show_bug.cgi?id=870914 https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=522849
posted on