地址空间布局随机化技术(ASLR)是一种被应用在大多数现代操作系统中的漏洞利用防护技术。简单来说,ASLR 的思想就是把进程的内存空间地址给随机化,从而防止攻击者在进行漏洞利用时找到确定的函数或者 gadgets 的地址。因为对 ASLR 进行一个详细深入的解释不是本篇博文的目的,所以如果你想要对 ASLR 进行深入的研究,我建议你从这些文章开始(I,II)。
ASLR 的好处
Linux 系统在 2005 年发布的 kernel 2.6.12 中引入 ASLR 技术,然后在 2007 年,微软也在 Vista 系统中引入了 ASLR。在 Linux 系统中,ASLR 是在每一个可执行文件中被强制开启的,与 Linux 不同的是,微软要求二进制文件对 ASLR 的支持应当在链接过程中完成。上面提及到的随机化同时影响到共享库以及可执行文件,从而为进程提供一个完全随机化的进程地址空间。从 2013 年开始,人们认为这项已经被广泛采用的 ALSR 技术是成熟的,从而提高了现代操作系统的整体安全性(是我太天真了?)。
Linux 系统上的 ASLR 等级可以通过文件 /proc/sys/kernel/randomize_va_space 来进行设置,它支持以下取值:
- 0 – 关闭的随机化。一切都是静止的。
- 1 – 保守的随机化。共享库、栈、mmap()、VDSO 以及堆将被随机化。
- 2 – 完全的随机化。除了上面列举的要素外,通过 brk() 分配得到的内存空间也将被随机化。
我并不想谈论 ASLR 牵涉到的数学原理,但是简单地说一下是有需要而不是完全没有必要的。理论上来说,一个 32 位的系统能够提供给 ASLR 使用的熵是要比 64 位系统少的。然而,还有其它的约束条件同样会影响到熵的数量,比如说那些和内存布局相关的因素。举个例子,为了使栈能够持续地从内存高地址向着堆所在的低地址生长,栈的地址最高有效位往往是不被随机化的。在一些情景中,它把 32 位系统的 mmap() 的熵限制成仅仅为 16 位,而 PAX 补丁可以用来把熵提升到 24 位。
ASLR 的坏处
把数学原理放在一边,很明显地,如果要使得 ASLR 有效,那么进程的内存空间的所有段都需要被随机化。哪怕只有一片内存区域没有被完全随机化都会有违 ASLR 的目标,这是因为攻击者能够利用那一片没有被随机化的内存区域去定位到有价值的 gadgets,从而实现一次成功的漏洞利用。在 Windows 对 ASLR 的实现机制下,这个问题已经多次出现了,因为第三方的软件(经常是 Windows 软件)包含一些没有启用 ASLR 的 DLL,利用这些库来作为翘板是很容易去构造一次漏洞利用的。而内核版本小于 2.6.22 的 Linux 系统同样有着类似的问题,因为 VDSO(linux-vdso.so)总是会映射到一个固定的位置。
另一方面,当前的 Linux 系统有着它自己的一系列问题。尽管 ASLR 在每一个进程中都被强制开启,但对于所有可执行文件来说,还是存在着没有被随机化的内存区域。就比如说代码段(或者叫文本段;.text)只有在可执行文件被编译成位置独立可执行文件(PIE)时才会被映射到随机地址。被编译成 PIE 的可执行文件可以被映射到内存中的任意地方却仍然能够正确执行而不需要经过修改,这是通过使用相对地址而不是绝对地址来实现的。所有的共享对象(.so, libraries)都被会编译成 PIE,这对它们的工作来说是强制需要的,因此当 ASLR 开启时它们总是处于随机的内存地址。
基于以上信息,我们可以作出假设,Linux 系统上的可执行文件如果没有被编译为 PIE,那么即使 ASLR 等级被设置成了 2(完全的随机化),它也是不会被 ASLR 有效地保护的。因此攻击者可以在开启了 ASLR 的系统上对一个非 PIE 可执行文件,利用其代码段以及其他一些位于主程序段的区域,比如说 GOT/PLT 表等作为翘板,去构造一次成功的漏洞利用。结果就是,任何的非 PIE 可执行文件都给 return-2-plt/GOT 以及 ROP 攻击打开了一扇门。
以下代码用于说明即使 ASLR 已经被开启了,主程序段也是不会被随机化的,除非被编译成 PIE。
#include <stdlib.h>
#include <stdio.h>
void* getEIP () {
return __builtin_return_address(0)-0x5;
};
int main(int argc, char** argv){
printf("EBP located at: %p\n",getEIP());
return 0;
}
上述代码编译成非 PIE,执行结果如下图所示:
非 PIE 的 .text 没有被随机化
可见每一次运行,库都被定位到随机的地址处,而代码段的地址却总是静止的。当可执行文件被编译成 PIE 后,从下图可以看到代码段的地址同样被随机化了,因此对于攻击者来说代码段的地址就变得不可猜测了。
在 PIE 文件中代码段也被随机化了
ASLR 的丑陋
到目前为止一切都还不错。非 PIE 可执行文件不会从 ASLR 保护中受益,所以该怎么办?Linux 的二进制文件肯定要编译成 PIE 来减缓大多数的漏洞利用,所以他们这样做了吗?根据一些研究,在 Linux 发行版中被用作 web 服务器最多的是 CentOS、Ubuntu Server 以及 Debian(尽管我猜 RedHat enterprise 同样会有一个不错的份额)。基于这个研究结果,我收集了一些有关 PIE 可执行文件在以上 Linux 发行版中的数量的数据。具体的系统环境为:
- Ubuntu Server 12.10 x86_64 + apache2 + mysql + php5 +sshd
- Debian 6 x86_64 + web server + mysql + php5 + sshd
- CentOS 6.3 x86_64 + apache2 + mysql + php5 + sshd
所有系统均安装为标准选项。下列的结果是通过 checksec 来收集的,checksec 是一个非常棒的脚本,它可以用来检查一些主流的安全机制如 ASLR、NX、Canaries 以及 RELRO 等是否被启用。你可以从这里获取到 checksec。下面是数据:
发行版 | 二进制文件数目 | 启用PIE | 非PIE |
---|---|---|---|
Ubuntu 12.10 | 646 | 111 (17.18%) | 535 |
Debian 6 | 592 | 61 (10.30%) | 531 |
CentOS 6.3 | 1340 | 217 (16.19%) | 1123 |
结果令人十分惊讶,PIE 并没有广泛地被上述 Linux 系统所接受。然而,网络守护进程总是编译为 PIE,以此来缓解问题并减少攻击面。不管不开启 PIE 是不是因为性能的原因(PIE二进制文件需要额外的间接操作),但安全性方面的影响极大地补偿了这一点。快速但并不十分准确的数据显示在 Linux 系统上 82.82% 到 89.7% 的二进制文件并没有被 ASLR 有效地保护。
同样地,在这次练习中其它的一些保护比如说 stack canaries 以及 RELRO 同样被考虑进去了,而结果却并不规律且同样使人惊讶。然而,为了获取到这些安全机制的当前状态的一个更真实的视角,还有更多的工作需要完成。例如说,GCC 仅仅会在满足一定条件的函数中启用 satck canaries,换句话说,那些 GCC 认为可能会成为缓冲区溢出攻击的函数才是目标函数。结果就是,一些没有启用 stack canaries 的二进制文件并不是说它就是缺少了一些缓解攻击的机制,而很有可能是它本来就不需要。
好喽,在一个周日的晚上写下这些已经足够啦。如果你们有任何的问题或者纠正意见,去评论吧!再见。