ASLR 和 DEP 是如何工作的?

信息安全 开发 aslr 部门 外壳代码
2021-08-16 00:59:17

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

2个回答

地址空间布局随机化 (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设置eip000ff6b4- 堆栈中的下一个偏移量。这将导致0xcc被执行,这是一条int3指令。由于int3是软件中断断点,它会引发异常并且调试器将停止。这使我们能够验证漏洞利用是否成功。

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

000ff6b4现在我们可以通过改变我们的有效载荷来用 shellcode替换内存。我们的利用到此结束。

为了防止这些漏洞利用成功,开发了数据执行保护。DEP 强制将某些结构(包括堆栈)标记为不可执行。无执行 (NX) 位(也称为 XD 位、EVP 位或 XN 位)的 CPU 支持使这一点变得更强大,它允许 CPU 在硬件级别强制执行权限。DEP 于 2004 年在 Linux 中引入(内核 2.6.8),微软于 2004 年将它作为 WinXP SP2 的一部分引入。Apple 在 2006 年迁移到 x86 架构时添加了 DEP 支持。启用 DEP 后,我们之前的漏洞利用将无法工作:

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

这失败了,因为堆栈被标记为不可执行,我们已经尝试执行它。为了解决这个问题,开发了一种称为面向返回的编程 (ROP) 的技术。这涉及在进程内的合法模块中查找称为 ROP 小工具的小代码片段。这些小工具由一个或多个指令组成,然后是一个返回。将这些与堆栈中的适当值链接在一起可以执行代码。

首先,让我们看看我们的堆栈现在的样子:

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

我们知道我们无法在 处执行代码000ff6b4,因此我们必须找到一些可以使用的合法代码。想象一下,我们的第一个任务是获取一个值到eax寄存器中。pop eax; ret我们在流程中的任何模块中的某处搜索组合。一旦我们找到了一个,比如说 at 00401f60,我们将它的地址放入堆栈:

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

当这个 shellcode 被执行时,我们会再次遇到访问冲突:

> Access violation - code c0000005 (!!! second chance !!!)
eax=cccccccc ebx=01020304 ecx=7abcdef0 edx=00000000 esi=7777f000 edi=0000f0f1
eip=43434343 esp=000ff6ba ebp=000ff6ff

CPU 现在已经完成了以下工作:

  • 跳转到 处的pop eax指令00401f60
  • 从堆栈中弹出cccccccc,进入eax.
  • 执行ret弹出43434343eip
  • 43434343因为不是有效的内存地址而引发访问冲突。

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

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

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

加载 DLL 时,它们会进入 和 之间的共享内存0x50000000区域0x78000000要加载的第一个 DLL 始终是 ntdll.dll,它在 加载0x78000000 - bias * 0x100000,其中bias是在启动时计算的系统范围的偏差值。如果您知道 ntdll.dll 的基地址,那么计算模块的偏移量将是微不足道的,因此加载模块的顺序也是随机的。

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

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

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

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

资料来源:

进一步阅读:

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

在旧 x86 硬件上进行 DEP 的简单但有限的方法是使用段寄存器。对于此类系统上的当前操作系统,地址是 4 GB 地址空间中的 32 位值,但在内部,每个内存访问都隐式使用 32 位地址一个特殊的 16 位寄存器,称为“段寄存器”。

在所谓的保护模式下,段寄存器指向一个内部表(“描述符表”——实际上有两个这样的表,但这是技术性的),表中的每个条目都指定了段的特征。特别是允许访问的类型和段的大小此外,代码执行隐式使用 CS 段寄存器,而数据访问主要使用 DS(和堆栈访问,例如使用pushandpop操作码,使用 SS)。这允许操作系统将地址空间分成两部分;CS和DS的低地址在范围内,而CS的高地址超出范围。例如,由 CS 描述的段的大小为 512 MB。这意味着任何超过 0x20000000 的地址都可以作为数据访问(使用 DS 作为基址寄存器读取或写入),但执行尝试将使用 CS,此时 CPU 将引发异常(内核将转换为合适的信号,如SIGILL 或 SIGSEGV,通常意味着违规进程的死亡)。

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

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

执行 DEP 的有趣但更昂贵的方法是使用称为PaX的东西。要了解它的作用,必须深入一些细节。

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

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

但是,有一个窍门。内存很慢。要执行内存访问,处理器必须首先读取主表以定位它必须查询的子表,然后再读取该子表,只有在这一点上,处理器才知道是否应该访问内存是否允许,以及访问的数据在物理 RAM 中的实际位置。这些是具有完全依赖关系的读取访问(每次访问都取决于前一个读取的值),因此这会产生完全延迟,在现代 CPU 上,它可以代表数百个时钟周期。因此,CPU 包含一个特定的缓存,其中包含最近访问的 MMU 表条目。此缓存是Translation Lookaside Buffer

从 80486 开始,x86 CPU 没有一个TLB,而是两个. 缓存适用于启发式,而启发式依赖于访问模式,而代码的访问模式往往不同于数据的访问模式。因此,Intel/AMD/other 的聪明人发现拥有一个专门用于代码访问(执行)的 TLB 和另一个用于数据访问的 TLB 是值得的。此外,80486 有一个操作码 ( invlpg),可以从 TLB 中删除特定条目。

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

最终结果是,当执行返回到进程代码时,代码的 TLB 或数据的 TLB 包含适当的条目,但另一个 TLB没有也不会,因为 RAM 中的表仍然说“不存在” . 此时,内核可以决定是否允许执行,而与是否允许数据访问无关。因此,它可以强制执行类似 NX 的语义。

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

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

NX 位使所有这些都过时了。请注意,PaX 补丁集还包含一些其他与安全相关的功能,例如 ASLR,这对于更新的官方内核的某些功能是多余的。