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