就防止漏洞被利用而言,地址空间布局随机化(ASLR)和数据执行保护(DEP)如何工作?可以绕开它们吗?

#1 楼

地址空间布局随机化(ASLR)是一种用于帮助防止Shellcode成功的技术。它通过随机偏移模块和某些内存结构的位置来实现。数据执行保护(DEP)可以防止某些内存扇区,例如堆栈,从被执行。当结合使用时,使用Shellcode或面向返回的编程(ROP)技术利用应用程序中的漏洞变得极为困难。

首先,让我们看一下如何利用正常漏洞。我们将跳过所有详细信息,但仅说明我们正在使用堆栈缓冲区溢出漏洞。我们已经将0x41414141值的一大块加载到有效负载中,并且eip已设置为0x41414141,因此我们知道它可以被利用。然后,我们使用适当的工具(例如Metasploit的pattern_create.rb)来发现要加载到eip中的值的偏移量。这是我们的攻击代码的起始偏移量。为了验证,我们在此偏移量之前加载0x41,在偏移量之前加载0x42424242,在偏移量之后加载0x43

在非ASLR和非DEP进程中,每次运行时堆栈地址都相同过程。我们确切知道它在内存中的位置。因此,让我们看看上面描述的测试数据的堆栈是什么:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 42424242   > esp points here
 000ff6b4  | 43434343
 000ff6b8  | 43434343


我们可以看到,esp指向000ff6b0,它已设置为0x42424242 。正如我们所说的,此之前的值为0x41,之后的值为0x43。现在我们知道存储在000ff6b0的地址将被跳转到。因此,我们将其设置为可以控制的某些内存的地址:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 000ff6b4
 000ff6b4  | cccccccc
 000ff6b8  | 43434343


我们在000ff6b0处设置了值,以便将eip设置为000ff6b4-堆栈中的下一个偏移量。这将导致执行0xcc,这是一个int3指令。由于int3是软件中断断点,因此它将引发异常,并且调试器将停止。这使我们能够验证该漏洞利用是否成功。

现在,我们可以通过更改有效负载,用shellcode替换000ff6b4处的内存。我们的攻击到此结束。

为了防止这些攻击成功,开发了数据执行保护。 DEP强制将某些结构(包括堆栈)标记为不可执行。通过No-Execute(NX)位(也称为XD位,EVP位或XN位)对CPU的支持,该功能变得更加强大,从而允许CPU在硬件级别上强制执行权限。 DEP于2004年在Linux(内核2.6.8)中引入,Microsoft在2004年作为WinXP SP2的一部分引入了DEP。 Apple在2006年迁移到x86架构时增加了DEP支持。启用DEP后,我们先前的漏洞利用将无法正常工作:

> Break instruction exception - code 80000003 (first chance)
[snip]
eip=000ff6b4


此操作失败,因为堆栈已标记不可执行,我们已经尝试执行它。为了解决这个问题,开发了一种称为面向返回的编程(ROP)的技术。这涉及在流程中的合法模块中寻找小的代码片段,称为ROP小工具。这些小工具包含一个或多个说明,然后返回。将它们与堆栈中的适当值链接在一起可以执行代码。首先,让我们看一下堆栈的当前外观:

> Access violation - code c0000005 (!!! second chance !!!)
[snip]
eip=000ff6b4


我们知道我们无法在000ff6b4处执行代码,因此我们必须找到一些可用的合法代码。想象一下,我们的首要任务是将一个值存入eax寄存器。我们在过程中任何模块的某个位置搜索pop eax; ret组合。找到一个地址后,假设在00401f60处,我们将其地址放入堆栈中:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 000ff6b4
 000ff6b4  | cccccccc
 000ff6b8  | 43434343


执行此shellcode时,将发生访问冲突再次:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 00401f60
 000ff6b4  | cccccccc
 000ff6b8  | 43434343


CPU现在已执行以下操作:


跳转到pop eax00401f60指令。
cccccccc弹出堆栈,放入eax中。
执行ret,将43434343弹出到eip中。
由于43434343不是有效的内存地址,因此引发访问冲突。

现在,假设不是43434343而是000ff6b8的值设置为另一个ROP小工具的地址。这意味着先执行pop eax,然后执行下一个小工具。我们可以像这样将小工具链接在一起。我们的最终目标通常是找到内存保护API的地址,例如VirtualProtect,并将堆栈标记为可执行。然后,我们将包括一个最终的ROP小工具来执行jmp esp等效指令,并执行shellcode。我们已经成功绕过了DEP!

为了对抗这些技巧,开发了ASLR。 ASLR涉及随机偏移内存结构和模块基地址,以使猜测ROP小工具和API的位置非常困难。

在Windows Vista和7上,ASLR随机分配可执行文件和DLL在内存中以及堆栈和堆中的位置。当将可执行文件加载到内存中时,Windows获取处理器的时间戳记计数器(TSC),将其移位四位,执行除法254,然后加1。然后将该数字乘以64KB,并以该偏移量加载可执行映像。 。这意味着可执行文件有256个可能的位置。由于DLL在各个进程的内存中共享,因此它们的偏移量由启动时计算的系统范围的偏差值确定。首次调用,移位和屏蔽MiInitializeRelocations函数时,该值被计算为CPU的TSC。该值每次引导仅计算一次。

加载DLL时,它们进入0x500000000x78000000之间的共享内存区域。要加载的第一个DLL始终是ntdll.dll,该文件在0x78000000 - bias * 0x100000处加载,其中bias是在引导时计算的系统范围的偏差值。如果您知道ntdll.dll的基址,那么计算模块的偏移量将很简单,因此模块的加载顺序也是随机的。

创建线程时,它们的堆栈基址为随机的。这是通过在内存中找到32个合适的位置,然后根据被掩盖为5位值的当前TSC选择一个位置来完成的。一旦计算出基本地址,便从TSC导出另一个9位值,以计算最终的堆栈基本地址。这提供了较高的理论随机性。

最后,堆的位置和堆分配是随机的。这是通过5位TSC派生值乘以64KB得出的,可能的堆范围为00000000001f0000

当所有这些机制与DEP结合使用时,我们将无法执行Shellcode。这是因为我们无法执行堆栈,但是我们也不知道任何ROP指令将在内存中的何处。可以使用nop雪橇完成某些技巧以创建概率利用,但它们并非完全成功,而且并非总是可以创建。

可靠地绕过DEP和ASLR的唯一方法是通过指针泄漏。在这种情况下,可能会使用堆栈中可靠位置上的值来定位可用的函数指针或ROP小工具。完成此操作后,有时可以创建有效绕过这两种保护机制的有效负载。

来源:


Windows Internals 5th Edition-Mark Russinovich
Windows Vista中的ASLR分析-Symantec
Wikipedia上的ASLR
Wikipedia上的DEP

进一步阅读:


堆栈基于漏洞的编写-CoreLAN
绕过堆栈cookie,SafeSEH,SEHOP,HW DEP和ASLR-CoreLAN
绕过ASLR / DEP-exploit-db


评论


必填tl; dr

–user10211
2012年8月12日15:30

@TerryChia你是说ta; cr? -太棒了,无法阅读;)

–多项式
2012年8月12日15:37

我非正式地授予您多项式爵士的头衔。整洁的答案。 xD

–罗汉·杜尔维(Rohan Durve)
2012年8月14日13:05

是的...好像是要回答这个问题。哇。

– tylerl
2012年8月15日在6:34

“唯一可靠的方式可以降低DEP和ASLR的负担”:这是棘手的问题。攻击者不需要可靠的方法。通常,只需要千次尝试一次的方法就足够了,因为他只需要尝试一千次(通常可以编写脚本)。这说明了ASLR和DEP作为安全机制的有效性受到的限制。

–托马斯·波宁(Thomas Pornin)
2012年8月19日下午14:40

#2 楼

为了补充@Polynomial的自我解答:实际上可以在较旧的x86机器(早于NX位)上强制执行DEP,但要付出一定的代价。

在旧的x86硬件上进行DEP的简单但有限的方法是使用段寄存器。在此类系统上的当前操作系统中,地址是平面4 GB地址空间中的32位值,但是内部每个内存访问都隐式使用32位地址和特殊的16位寄存器(称为“段寄存器”)。 br />
在所谓的保护模式下,段寄存器指向一个内部表(“描述符表”-实际上有两个这样的表,但这是技术性的),并且该表中的每个条目都指定了特征段中的。特别是允许访问的类型和段的大小。此外,代码执行隐式使用CS段寄存器,而数据访问主要使用DS(而堆栈访问(例如,使用pushpop操作码使用SS))。这允许操作系统将地址空间分为两部分;低位地址在CS和DS的范围内,而高位地址超出CS的范围。例如,由CS描述的段的大小为512 MB。这意味着超过0x20000000的任何地址都可以作为数据访问(使用DS作为基址寄存器读取或写入),但是执行尝试将使用CS,这时CPU会引发异常(内核会将其转换为合适的信号,例如SIGILL或SIGSEGV,通常暗示着违规进程的终止。

(请注意,段应用于地址空间; MMU仍处于活动状态,位于较低层,因此上述技巧是针对-process。)

这样做很便宜:x86硬件确实可以系统地执行分段(并且第一个80386已经在执行分段;实际上,80286已经具有带边界的分段,但是只有16位偏移量)。我们通常会忘记它们,因为理智的操作系统将这些段设置为从偏移量0开始并为4 GB长,但是否则设置它们并不意味着我们没有任何开销。但是,作为DEP机制,它是不灵活的:当从内核请求某些数据块时,内核必须决定这是否用于代码,因为边界是固定的。我们不能决定在代码模式和数据模式之间动态转换任何给定页面。

做DEP的有趣但更昂贵的方法是使用一种称为PaX的方法。要了解它的作用,必须详细说明。

x86硬件上的MMU使用内存表,这些表描述了地址空间中每4 kB页的状态。地址空间为4 GB,因此有1048576页。每个页面由子表中的32位条目描述;有1024个子表,每个子表包含1024个条目,并且有一个主表,其中1024个条目指向1024个子表。每个条目都告诉所指向的对象(子表或页面)在RAM中的位置,或者它是否在那里,以及它的访问权限是什么。问题的根源在于访问权限与特权级别(内核代码与用户区)有关,并且访问类型只有一位,因此允许“读写”或“只读”。 “执行”被认为是一种读访问。因此,MMU没有与数据访问不同的“执行”概念。可读的是可执行的。

(自从上个世纪的Pentium Pro以来,x86处理器就知道表的另一种格式,称为PAE。它使条目的大小加倍,从而为寻址更多的物理RAM和添加NX位留了空间-但该特定位仅在2004年左右才由硬件实现。)

但是有一个窍门。 RAM很慢。要执行内存访问,处理器必须首先读取主表以找到它必须查询的子表,然后再次读取该子表,并且只有在这一点上,处理器才知道是否应该进行内存访问。是否允许,以及访问的数据实际在物理RAM中的何处。这些是具有完全依赖关系的读取访问(每个访问都取决于前者读取的值),因此需要付出全部的等待时间,在现代CPU上,这可以代表数百个时钟周期。因此,CPU包含一个特定的高速缓存,其中包含最近访问的MMU表条目。此缓存是转换后备缓冲区。

从80486开始,x86 CPU没有一个TLB,而是两个。缓存是基于启发式的,并且启发式取决于访问模式,并且代码的访问模式往往与数据的访问模式不同。因此,英特尔/ AMD /其他公司的聪明人发现,有一个专用于代码访问(执行)的TLB和另一个用于数据访问的TLB是值得的。此外,80486具有一个操作码(invlpg),可以从TLB中删除特定条目。

因此,思路如下:使两个TLB对同一条目具有不同的视图。所有页面在表中(在RAM中)都标记为“不存在”,从而在访问时触发异常。内核捕获该异常,并且该异常包括有关访问类型的一些数据,尤其是是否用于代码执行。然后,内核使新读取的TLB条目(表示“不存在”的条目)无效,然后在RAM中为该条目填充一些允许访问的权限,然后强制进行所需类型的一次访问(数据读取或代码执行),这将条目提供给相应的TLB,并且只有该条目。然后内核立即将RAM中的条目设置为不存在,最后返回到进程(返回再次尝试触发异常的操作码)。

最终的效果是,当执行执行时返回到过程代码,用于代码的TLB或用于数据的TLB包含适当的条目,但是另一个TLB不包含,并且也不会,因为RAM中的表仍然说“不存在”。此时,内核可以独立于是否允许数据访问来决定是否允许执行。因此,它可以强制执行类似NX的语义。

Devil隐藏在细节中;在这种情况下,整个魔族都有空间。与硬件的这种共舞并不容易正确实现。特别是在多核系统上。

开销如下:当执行访问并且TLB不包含相关条目时,必须访问RAM中的表,这仅意味着丢失了一个几百个周期。为此,PaX添加了异常的开销以及填充正确的TLB的管理代码,从而将“几百个周期”变成了“几千个周期”。幸运的是,TLB失误是正确的。 PaX人声称在大型编译工作中测得的速度降低了2.7%(尽管这取决于CPU类型)。

NX位使所有这些过时。请注意,PaX修补程序集还包含其他一些与安全性相关的功能,例如ASLR,该功能与较新的官方内核的某些功能无关。

评论


+1。我的答案非常棒,其中包括我不知道的许多事实。

–多项式
2012年8月16日在20:22

我已将我的答案标记为已接受,但您的回答给了我很多思考,因此,我为您添加了+100赏金奖励。如果超时,我会在24小时内授予它:)

–多项式
2012年8月17日在9:42



PaX实际上提供了一种不同的高级ASLR实现。对于SMEP / SMAP之类的东西,PaX的UDEREF仍然很优越(由于与SMEP / SMAP相比,禁用UDEREF的困难,而SDE泄漏页表,而UDEREF没有)。

–森林
17年11月29日在4:08