[关闭]
@SmashStack 2017-03-15T05:22:15.000000Z 字数 4004 阅读 159

An Evil Copy: How the Loader Betrays You

Binary


论文在NDSS 2017中发表,介绍了在Linux操作系统上存在的Dynamic Linking机制导致程序中常量值语义可被破坏的安全问题。

作者
- Xinyang Ge @ Microsoft Research
- Mathias Payer @ Purdue University
- Trent Jaeger @ The Pennsylvania State University

简介

先来看一个使用了动态链接库的小程序

makefile

  1. all: main.c test.c
  2. gcc -fPIC -shared test.c -o libtest.so
  3. gcc main.c -L. -ltest
  4. run: a.out libtest.so
  5. LD_LIBRARY_PATH=. ./a.out

动态链接库libtest.so的代码只有一行,非常简单:

test.c

  1. const int foo = 10;

主程序也很简单,强行试图修改const int foo的内容:

main.c

  1. extern const int foo;
  2. int main()
  3. {
  4. int *p = (int *)&foo;
  5. *p = 100; // page fault!
  6. return 0;
  7. }

上述代码在Ubuntu 16.04和Fedora 25 Server上编译运行均不会有异常,与之对比,如果我们将test.c编译成静态库并静态链接到可执行文件,再执行同样的代码就会得到Segmentation fault的错误提示

问题分析

fig1.png-59.5kB

这样的程序运行结果肯定是存在问题的,是什么原因导致了问题的发生。作者指出,这种问题源自现代软件编译的分离编译实践。现代软件编译一般将源代码文件分为独立的模块进行编译(见上图),对于一个模块引用了外部的符号的情况,链接器处理可分为静态链接和动态链接两种基本方式,不论哪一种链接方式,首先在编译器遇到当前模块引用外部符号的地方,会用一个占位符(placeholder)来替代外部符号真实的内存地址,直到最终链接的时候,占位符会被实际的值替换。对于静态链接的模块,在链接器进行链接之后,占位符会被换成实际的值,例如一条

call placeholder

指令会被换成

call address

而对于动态链接方式,占位符所指向的外部符号处理起来就比较麻烦,动态链接库的灵活性决定了占位符只有在动态运行时才能被决议。对于外部引用有两种常见情况,编译工具链会分开处理这两种情况:

  1. 首先对于调用外部函数的行为,一般采取的措施是将占位符指向一个编译器生成的跳板(trampoline)指令,这个跳板指令被放置在程序的Procedure Linkage Table (PLT) 中,执行一个间接跳转的功能,将控制流导向由 Global Offset Tables (GOT) 指定的外部函数在这次运行时的实际值(GOT中的地址是在动态链接库加载后被确定的)。这样就保证了动态链接库中的函数可被调用到。

  2. 对于访问动态链接库中的外部数据变量,和访问外部函数不同,链接器不太方便去将数据引用也通过跳板指令来中继,因而有两种设计策略,一种策略是允许动态加载器在加载一个动态库的时候,也将原有代码上外部数据引用的地址修改掉,然而这会带来的问题是这要求动态修改代码段,而代码段通常在运行期是要求只读的(不仅仅是为了安全,更重要的是如果一个程序同时启动了多个进程,多个进程默认是共享同一份code section的内存页面的,如果修改了某个进程的代码内存页面则其他进程无法共享),所以动态调整起来会非常麻烦。另一种策略是让链接器给外部变量生成一份初始值为0的代替,放在程序的.bss段,当动态加载器加载外部链接库的时候,就把外部的变量复制一份到这个本地代替上去,并且在这个进程中,所有其他外部库(包括这个变量的Host library)的GOT上都要更新关于这个变量的引用地址(从原先在Host library上改成现在放在.bss段的实际位置)。这样就保证了所有的模块中的一致性。这也是现有的Linux系统所采取的默认策略,论文将其称为copy relocation(见下图)

fig3.png-58.6kB

然而,如我们上面的例子所示,copy relocation对于外部变量的处理会造成常量性丧失的问题,也许你会问为什么在做copy relocation时候不能把const定义也一并带到新的位置上呢?由于动态链接机制允许符号覆盖(也就是后一个加载的库里面的同名符号可以覆盖前一个库的相关的符号),链接器并不能默认就决定了某个符号的内存保护属性。而系统设计者认为如果一个只读变量在copy relocation后可写,并不会破坏到程序的功能性,而如果反过来让一个可能应该允许写的变量被设置为只读,会造成 page fault 继而导致程序崩溃。在这种考虑程序的兼容性优先于安全性的设计哲学指导下,我们就会面临作者称之为Copy Relocation Violation (COREV)的新型attack vector

问题影响

介于 copy relocation 对外部变量的处理造成的常量性丧失,多种安全防御机制失去了本来的作用。特别的,对下述外部常量的非法修改将会导致一系列的安全隐患。

C++ 虚函数表

c++ 通过虚函数表实现继承多态性。对于一条继承链上的不同的类,都有其独有的虚函数表,这些虚函数表保存了相关类的各个虚函数指针(如下图),在进行方法调用时,程序通过对象结构体头部的虚函数指针来获取相应的函数指针。

image_1bb20dbiu10j41okn49fuk98col.png-196.6kB

为了防止攻击者对控制流进行劫持,现在的安全机制都会虚函数调用进行一系列的检查。下图示例的时两种最为常见的安全检查方式:左右两幅图都是对图4(e)中所示程序的安全改进,不同点在于,左图对虚函数指针进行了检查,而右图仅仅检查了虚表指针。

image_1bb2118u615k580j8sj10vt1rju12.png-139.8kB

事实上,右图的效率明显大于左图,其主要有以下两点原因:

1.  虚函数表的个数远远少于虚函数的个数,使得检查次数大大减少
2.  对于同一对象连续的函数调用,虚函数表的检查只需要一次

所以,现在大部分的安全机制采用的是右图的检查方法。但是,这恰恰是问题的关键,右图检查方法严重依赖于虚函数表的只读性,在COREV的协助下,攻击者将有新的方法绕过相关检查。

格式化字符串

另一个受到影响以致于可以重新加以利用的漏洞是臭名昭著的格式化字符串攻击。对于 printf 一类的函数,如果格式化模版的字符串能够被攻击者修改,将极有可能造成重大的安全问题。所以在现在的安全体系下,格式化字符串通常都只读不可写,有效的回避了相关的安全问题。可是由于COREV的出现,很多曾经不能够利用的漏洞又可以重新被利用了。

其他只读数据

在现存的多种攻击方式中(如confused deputy attacks),辅以COREV, 可以通过修改如IP地址、文件名等只读静态数据,实现受攻击的程序重定向到攻击者可控的区域,导致信息泄漏、非授权访问等问题。

防范

文中作者对相关问题提出了三种不同层面上的解决/缓解方案,并且在一定程度上进行了相关实验和开发

发现潜在的常量性丧失问题

在整个问题中,最关键的一点就是发现copy relocation可能导致的常量性缺失,作者对此指出三个关键性步骤:

1.  从ELF段表信息中提取建立出会进行copy的符号表
2.  确定符号表中各符号的来源
3.  对相关动态链接库进行检查,确定是否满足:
    1) 定义了符号表中某一符号
    2)该符号在动态链接库中的只读部分

借由pyELF库,作者实现了两个脚本工具(共174行)。第一个工具通过上述步骤实现了可疑符号的提取,第二个工具实现了可疑符号的分类:

通过这两个工具,作者对 Ubuntu 16.04 上的 58,862 个动态库和 34,291 个可执行文件进行了分析,得到了如下结果。

image_1bb23eoq81pj6hsb1vos18131rd61f.png-240.9kB

可见COREV的影响面还是相当广的。同时值得一提的是,作者同样总结了十大最容易被常量丧失性复制的变量(如下图),发现其全部来自于libstdc++, 同时,其中八个是极为常用的类对象的虚函数表。考虑到libstdc++的广泛使用,可以预见在将来,基于COREV的攻击将变得越来越普遍。

image_1bb23rjpnttru7ajj1c3j15c51s.png-214.2kB

重编译软件

抵御COREV攻击的一种有效的方式是取消copy relocation,加入 -fPIC 选项重新编译是一个行之有效的办法。对于地址无关代码,对连接库数据和本身数据的访问有着不同的方法,下图是加入了 -fPIC 编译选项对两种数据两种访问方式。

image_1bb245e7rtkp1u63176o1ea0h5629.png-135.5kB

这样一来,对外部数据的访问将通过GOT表实现,也就间接避免了copy relocation的问题。值得注意的是 -fPIC 是编译动态链接库地址无关代码的编译选项。 而 编译可执行代码地址无关的选项 -fPIE 在避免copy relocation上没有作用, 因为该选项仅仅启用了访问自身数据的 IP 相对寻址模式,并没有启用对链接库数据的GOT表的访问模式。

当然,这样的方法也存在着效率低下的缺陷,其主要有两点原因:

1.  对链接库的数据访问将要涉及两次内存访问
2.  在32位 x86的框架下,相对寻址尤为低效

改变编译链

从根本上解决copy relocation的问题的方式是从编译工具着手,现有工具的问题在于从编译到链接再到加载的过程中符号信息的丢失。作者考虑修订现有的工具,并加入一个 COREV Section 对copy relocation涉及的常量性丧失的变量进行记录,并在加载过程中对此进行特殊化处理。

总结

本文针对copy relocation过程中部分外部变量常数性质的丧失提出了一系列可能的攻击方式,并在Ubuntu 16.04上进行了具体的实验,证实了问题的严重性。虽然Windows平台上不存在相关问题,OSX平台上仅存在部分可能利用的机会,但也足够引起安全人员的注意。后半部分提出了可靠高效的监测手段,但解决方法或难以达到效率需求,或仍需大力度推进改革,整个问题仍等待一种两全其美的解决方案。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注