5.4.9 段RELRO
最初,编译时链接器并没有过多考虑ELF文件中各个段的布局,一个ELF文件各个段的大致布局如图5-38所示。
图 5-38 早期ELF文件段的布局
可见,动态链接器重定位涉及的GOT表、段.dynamic都位于数据段的后面,一旦数据段发生溢出,动态链接器使用的GOT表、段.dynamic都可能受到破坏,尤其是作为函数跳转表的GOT表,更容易被攻击者利用。而事实上,除了函数被延迟到运行时重定位外,变量等的重定位在加载时就已经完成了,后续动态链接器不再会对这些段进行写操作,也就是说完全可以在完成加载时重定位后,把这部分数据修改为只读。
因此,如今的链接器重新安排了各个段的布局,将动态链接器涉及到的段提到了数据段的前面,并将GOT拆分为两个部分:.got和.got.plt。.got部分用于记录需要重定位的变量,.got.plt部分用于记录需要重定位的函数。在加载时完成重定位后,除了.got.plt仍然保留可写属性,允许在运行时进行重定位外,包括.got在内的其余部分全部更改为只读,减少被攻击的可能。
这些在重定位后更改为只读的段被称为RELRO段。从Program Header Table的角度看,段RELRO仍然包含于数据段中,只不过是数据段开头部分一块只读的数据而已。经过上述调整后,一个ELF文件的大致布局演化为如图5-39所示的形式。
图 5-39 使用RELRO后ELF文件的布局
在加载时完成重定位后,动态链接器将检查ELF文件的Program Header Table中是否存在段RELRO。如果这个段存在,则将这个段更改为只读,从而达到保护更多数据的目的。相关代码如下:
其中_dl_relocate_object就是动态链接中负责加载时重定位的函数。在这个函数的最后,也就是加载时重定位完成后,这个函数调用_dl_protect_relro修改段RELRO的权限为只读。函数_dl_protect_relro逻辑非常简单,就是通过函数__mprotect请求内核更改段RELRO的属性为PROT_READ。
编译时链接器并没有强制使用RELRO这个特性,如果需要使用这个特性,在链接时需要向链接器传递参数"-z relro"。以笔者使用的Ubuntu12.10为例,可以看到在编译时编译器确实给链接器传递了这个参数,注意下面使用黑体标识的部分:
在我们构建的工具链中,为简单起见,并没有默认启用RELRO特性。
理解了RELRO的设计动机以及理论背景后,我们结合一个实例来具体体验一下这个特性。以下面程序为例:
我们使用如下命令分别编译不支持RELRO特性和支持RELRO特性的两个可执行程序:
其中,hello是不支持RELRO特性的,hello_relro是支持RELRO特性的。我们首先对比一下这两个程序的Program Header Table,hello的Program Header Table如下:
hello_relro的Program Header Table如下:
留意hello_relro的Program Header Table中使用黑体标识的部分。显然,相比于程序hello,hello_relro中多了段"GNU_RELRO"。
读者可能会有个疑问,前面不是提到内核只加载ELF文件中类型为LOAD的段吗,那么这个类型为GNU_RELRO的段会被加载吗?请仔细观察段RELRO与第2个类型为LOAD的段(即数据段)的Offset一列,可见,RELRO段在hello_relro中偏移与数据段在hello_relro文件中的偏移相同。换句话说,RELRO段正是数据段的开头部分,所以在加载数据段时,已经隐含着将段RELRO加载了。
接下来,我们再来动态的观察一下特性RELRO。这里偷个懒,因为目标系统也是x86的,所以笔者直接在宿主系统上运行了,读者当然可以将hello_relro复制到目标系统做这个试验。hello的进程地址空间的映射情况如下:
hello_relro的进程地址空间的映射情况如下:
对比hello和hello_relro的进程空间的映射情况,注意hello_relro映射中使用黑体标识的部分,可见,hello_relro在0x08049000~0x0804a000多映射了一个只读的段,没错,这个段就是段RELRO。显然,因为其与后面的数据段权限不同,所以内核为这个段单独分配了一个vm_struct_area对象。
事实上,不仅是可执行程序,动态库也是如此。读者可以自己做些对比试验,这里不再赘述。