为您的软件建立强大的安全性至关重要。恶意行为者不断使用各种恶意软件和网络安全攻击来破坏所有平台上的应用程序。你需要知道最常见的攻击,并找到缓解它们的方法。
本文不是关于堆积溢出或堆积使用的教程。其中,我们讨论了允许攻击者使用应用程序中的漏洞并执行恶意代码的堆积喷射技术。我们定义什么是堆积喷射,探索其工作原理,并展示如何保护您的应用程序免受其影响。
堆喷技术是什么,它是如何工作的?
堆喷是促进任何代码执行的漏洞利用技术。这个想法是在目标应用程序中的可预测地址上提供一个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副本。例如,假设我们有相同数据的相同位移 1这种情况下,最好从当前接收控制的位移开始搜索。
建议算法用于识别堆喷射
我们建议使用描述的技术,并注意以下四个标准,以消除不必要的检查可能显著减缓您的应用程序:
1.将保存的内存分配数量定义为每个线程。
2.设置已保存的内存分配的最小尺寸。拦截大小为一页的分配将导致内存节省不合理。堆喷通常用于为应用程序的特定堆管理器选择的巨大值。几十页似乎更相关。
3.当定义出现异常时,分析的最新分配数。如果我们处理太多的分配,它将降低应用程序的效率,因为我们必须阅读大面积的动态内存。
4.设置 shellcode 的预期最小大小。如果我们要搜索的代码太小,就会增加误报的数量。
结论
我们探索了一种利用钩子和内存保护机制检测堆喷射攻击的方法。在我们的项目中,该方法在测试和堆喷测试过程中表现出良好的效果。