为您的软件建立强大的安全性至关重要。恶意行为者不断使用各种类型的恶意软件和网络安全攻击来破坏所有平台上的应用程序。您需要了解最常见的攻击并找到缓解它们的方法。
本文不是关于堆溢出或堆利用的教程。在其中,我们探讨了允许攻击者利用应用程序中的漏洞并执行恶意代码的堆喷射技术。我们定义什么是堆喷射,探索它的工作原理,并展示如何保护您的应用程序免受它的影响。
什么是堆喷射技术,它是如何工作的?
堆喷射是一种用于促进执行任意代码的漏洞利用技术。这个想法是在目标应用程序中的可预测地址上提供一个shellcode,以便使用漏洞执行这个 shellcode。该技术是由称为heap spray的漏洞利用源代码的一部分实现的。
在实现动态内存管理器时,开发人员面临许多挑战,包括堆碎片。一个常见的解决方案是以固定大小的块分配内存。通常,堆管理器对块的大小以及分配这些块的一个或多个保留池有自己的偏好。堆喷射使目标进程连续地逐块分配所需内容的内存,依靠将 shellcode 放置在所需地址的分配之一(不检查任何条件)。
堆喷射本身不会利用任何安全问题,但它可用于使现有漏洞更容易被利用。
必须了解攻击者如何使用堆喷射技术来了解如何缓解它。以下是普通攻击的样子:
堆喷射如何影响进程内存
堆喷射攻击有两个主要阶段:
1.内存分配阶段。一些流连续分配大量具有相同内容的固定大小的内存块。
2.执行阶段。这些堆分配之一接收对进程内存的控制。
如您所见,堆喷射漏洞利用技术看起来像连续的垃圾邮件,形式为大小相同且内容相同的块。如果堆喷射攻击成功,控制权将传递给这些块之一。
为了执行这种攻击,恶意行为者需要有机会在目标进程中分配大量所需大小的内存,并用相同的内容填充这些分配。这个要求可能看起来过于大胆,但最常见的堆喷射攻击案例包括破坏Web 应用程序漏洞。任何支持脚本语言的应用程序(例如,带有 Visual Basic 的 Microsoft Office)都是堆喷射攻击的潜在受害者。
因此,在一个流的上下文中预期攻击是有意义的,因为脚本通常在单个流中执行。
但是,攻击者不仅可以使用脚本语言执行堆喷射攻击。其他方法包括将图像文件加载到进程中,并通过使用 HTML5 引入的技术以非常高的分配粒度喷射堆。
这里的问题是哪个阶段可疑,我们可以干预并试图弄清楚是否存在正在进行的攻击?
内存分配阶段,当一些流填满大量内存时,已经很可疑了。但是,您应该问自己是否可能存在误报。例如,您的应用程序中可能存在确实在一个循环中分配内存的脚本或代码,例如数组或特殊内存池。当然,脚本在完全相同的堆块中分配内存的可能性很小。但是,它仍然不是堆喷射的关键要求。
相反,您应该注意执行阶段,因为分析接收进程内存控制权的堆分配总是有意义的。因此,我们的分析将特别关注包含潜在 shellcode 的分配内存。
为了将堆喷射 shellcode 的执行与普通JIT代码生成区分开来,您可以分析分配某个内存块的最新流分配,包括流中的相邻分配。请注意,堆中的内存始终分配有执行权限,这允许攻击者使用堆喷射技术。
堆喷射缓解基础知识
为了成功缓解堆喷射攻击,我们需要管理接收内存控制的过程,应用钩子,并使用额外的安全机制。
保护您的应用程序免受堆喷射执行的三个步骤是:
1.拦截NtAllocateVirtualMemory调用
2.在尝试分配可执行内存期间使其无法执行
3.注册结构化异常处理程序 (SEH) 以处理由于执行不可执行内存而发生的异常
现在让我们详细探讨每个步骤。
接收对内存的控制
我们既需要监控目标进程如何分配内存,又需要检测动态分配内存的执行情况。后者假设在堆喷射期间分配的内存具有执行权限。如果数据执行保护 ( DEP ) 处于活动状态(对于 x64,默认情况下始终处于活动状态)并且尝试执行没有执行权限分配的内存,则会生成异常访问冲突。
恶意 shellcode 可以预期在没有 DEP 的应用程序中执行(这不太可能),或者使用脚本引擎在默认情况下具有执行权限的堆中分配内存。
我们可以通过拦截可执行内存的分配并以分配它的漏洞无法察觉的方式使其不可执行来防止恶意代码的执行。因此,当漏洞利用认为喷射是安全的执行并尝试将控制权委托给喷射的堆时,将触发系统异常。然后,我们可以分析这个系统异常。
首先,让我们从用户模式进程的角度来探索 Windows 中的内存工作是什么样的。以下是通常分配大量内存的方式:
在哪里:
- HeapAlloc和RtlAllocateHeap是从堆中分配一块内存的函数。
- NtAllocateVirtualMemory是一个低级函数,它是 NTDLL 的一部分,不应直接调用。
- sysenter是用于切换到内核模式的处理器指令。
如果我们设法替换NtAllocateVirtualMemory,我们将能够拦截进程内存中的堆分配流量。
应用挂钩
为了拦截目标函数NtAllocateVirtualMemory的执行,我们将使用 mhook 库。您可以选择原始库或改进版本。
使用 mhook 库很容易:您需要创建一个与目标函数具有相同签名的钩子,并通过调用Mhook_SetHook来实现它。钩子是通过在函数体上使用jmp指令覆盖函数prolog来实现的。如果您已经使用过钩子,那么您应该没有任何困难。
安全机制
有两种安全机制可以帮助我们缓解堆喷射攻击:数据执行预防和结构化异常处理。
结构化异常处理或 SEH是一种特定于 Windows 操作系统的错误处理机制。当发生错误(例如,除以零)时,应用程序的控制权被重定向到内核,内核会找到一系列处理程序并逐个调用它们,直到其中一个处理程序将异常标记为“已处理”。通常,内核将允许流程从检测到错误的那一刻起继续执行。
从进程的角度来看,DEP 看起来像是在内存执行时出现 EXCEPTION_ACCESS_VIOLATION 错误代码的 SEH 异常。
对于 x86 应用程序,我们有两个陷阱:
DEP可以在系统参数中关闭。
- 指向处理程序列表的指针存储在堆栈中,它提供了两个潜在的攻击向量:处理程序指示器覆盖和堆栈替换。
- 在 x64 应用程序中,不会出现这些问题。
防止堆喷射攻击
现在,让我们开始练习。为了减轻堆喷射攻击,我们将采取以下步骤:
1.形成分配历史
2.检测 shellcode 执行
3.检测喷雾
形成分配历史
为了拦截动态分配内存的执行,我们将 PAGE_EXECUTE_READWRITE 标志更改为 PAGE_READWRITE。
让我们创建一个结构来保存分配:
接下来,我们将为NtAllocateVirtualMemory定义一个钩子。此挂钩将重置 PAGE_EXECUTE_READWRITE 标志并保存已重置标志的分配:
一旦我们设置了钩子,任何带有 PAGE_EXECUTE_READWRITE 位的内存分配都会被修改。当试图将控制权传递给该内存时,处理器将生成一个我们可以检测和分析的异常。
在本文中,我们忽略了多线程问题。然而,在现实生活中,最好单独存储每个流的分配,因为 shellcode 执行预计是单线程的。
检测 shellcode 执行
现在,我们将为 SEH 注册一个处理程序。这就是这个处理程序通常的工作方式:
1.提取触发异常的指令的地址。如果此地址属于我们保存的区域之一,则此异常已由我们的操作触发。否则,我们可以跳过它,让系统继续搜索相关的处理程序。
2.搜索堆喷射。如果动态分配的内存被可疑执行,我们必须对检测到的攻击做出反应。否则,我们需要恢复原样,以便应用程序可以继续工作。
3.使用NtProtect函数 (PAGE_EXECUTE_READWRITE)恢复区域的原始参数。
4.将控制权交还给工艺流程。
下面是一个 shellcode 检测的代码示例:
目前,我们有一种机制可以监控应用程序中的 shellcode,并可以检测其执行时刻。在现实生活中,我们需要再执行两个步骤:
- 拦截NtProtectVirtualMemory和NtFreeVirtualMemory函数。否则,我们将没有机会监控进程内存的相关状态。这是一个碎片问题:我们需要存储和更新进程的可执行内存的映射,这是一项不平凡的任务。例如,我们的应用程序可以使用NtFree函数释放我们保存区域中间的部分页面,或者将它们的标志更改为 NtProtect。我们需要跟踪和监控此类案件。
- 使用 Execute 分析所有可能的标志(一组允许我们执行内存内容的可能值),例如 PAGE_EXECUTE_WRITECOPY 标志。
检测堆喷射
使用上面的代码,我们在动态内存执行时停止了一个应用程序,并获得了最新分配的历史记录。我们将使用这些信息来确定我们的应用程序是否受到攻击。让我们探索一下我们的堆喷射检测技术的两个步骤:
- 首先,我们需要确定我们将存储多少分配以及在发生异常时我们将分析其中的多少。请注意,我们对相同大小的分配感兴趣。因此,如果流中的内存以不同的大小分配,我们可以允许流继续执行,因为这不太可能是堆喷射攻击。此外,在分配边界之间存在空间的情况下,我们可以排除堆喷射攻击的可能性,因为堆喷射意味着连续的内存分配。
- 接下来,我们需要选择堆喷射检测的标准。检测堆喷射的一种有效方法是在内存分配中搜索相同的内容。这个重复的内容很可能是shellcode的副本。例如,假设我们有 10,000 个分配具有相同数据的相同位移。在这种情况下,最好从接收控制的当前分配的位移开始搜索。
用于识别堆喷射的建议算法
我们建议使用所描述的技术并注意以下四个标准,以排除可能会显着减慢您的应用程序的不必要检查:
1.为每个线程定义已保存的内存分配数量。
2.设置已保存内存分配的最小大小。拦截大小为一页的分配将导致不合理地节省内存。堆喷射通常使用为某个应用程序的特定堆管理器选择的巨大值进行操作。数十页和数百页似乎更相关。
3.定义发生异常时将分析的最新分配数。如果我们处理过多的分配,它会降低应用程序的效率,因为对于动态内存的每次执行,我们都必须读取大区域的内容。
4.设置 shellcode 的预期最小大小。如果我们要搜索的代码太小,就会增加误报的数量。
结论
我们探索了一种使用钩子和内存保护机制检测堆喷射攻击的方法。在我们的项目中,这种方法在测试和堆喷射检测过程中显示出出色的效果。