如何防范内核代码注入:深入分析Win10新增的任意代码保护(ACG)机制

概述

内核代码注入是Wannacry和Slingshot等恶意软件使用的常见攻击手段。而在最近发布的Windows 10 Creators Update中,引入了用于缓解远程代码执行的新技术,其中最值得关注的就是任意代码保护机制(Arbitrary Code Guard),这是一个动态代码限制(Dynamic Code Restrictions)的缓解更新。在这篇博客文章中,我们将详细分析任意代码保护机制的工作原理,并尝试如何对抗该机制,实现内核代码注入。

 

任意代码保护(ACG)机制分析

任意代码保护(ACG)是新加入到Windows操作系统之中的可选缓解措施,该机制主要尝试检查如下行为并进行阻止:
1、现有代码被修改(代码页不能变为可写);
2、在数据段上写入代码并执行(数据不能变为代码)。
为了实现上述目标,任意代码保护(ACG)机制会执行以下规则:
内存不能同时具有写入(W)和执行(X)属性。
通过将任意代码保护机制与代码完整性保护机制(Code Integrity Guard,只有正确签名的映像才能被加载)相结合,我们可以看出,微软致力于阻止攻击者将不安全或不可信的代码加载到内存之中。
下面是一个代码注入的例子,它会将进程的内存更改为任意代码保护机制试图阻止的状态:

如我们所见,有些代码已经被注入的页具有执行和写入属性。

任意代码保护的工作流程

当在特定进程上进行缓解时( https://docs.microsoft.com/en-us/windows/security/threat-protection/overview-of-threat-mitigations-in-windows-10 ),将注册表项添加到以下注册表路径中:

HKEY_LOCAL_MACHINESoftwareMicrosoftWindows NTCurrentVersionImage File Execution Options

接下来,Windows的缓解会在创建进程期间被设置好。下面是新流程创建的调用栈:

nt!PspAllocateProcess+0xb4b
nt!NtCreateUserProcess+0x723
nt!KiSystemServiceCopyEnd+0x13
ntdll!NtCreateUserProcess+0x14
KERNELBASE!CreateProcessInternalW+0x1b3f
KERNELBASE!CreateProcessW

接下来,我们看看PspAllocateProcess函数中的一些代码:

在上面的代码中,有一些负责加载缓解措施的函数:
PspReadIFEOMitigationOptions
PspReadIFEOMitigationAuditOptions
这些函数从注册表中读取以下密钥:

HKEY_LOCAL_MACHINESoftwareMicrosoftWindows NT
CurrentVersionImage File Execution Options

我们可以使用Process Monitor,看到如下动作:

接下来,将MitigationsFlagsValues保存在EPROCESS结构中(在我们的样本中,任意代码保护机制已经启用):

如何检测并阻止动态代码?

正如前文已经指出的那样,任意代码保护机制会监视内存分配,从而禁止写入与执行属性同时存在。所以,我们来看看在分配虚拟内存时的调用栈:

nt!MiArbitraryCodeBlocked+0x30
nt!MiAllocateVirtualMemory+0x96d
nt!NtAllocateVirtualMemory+0x44
nt!KiSystemServiceCopyEnd+0x13
ntdll+0xa05c4

一个名为MiArbitraryCodeBlocked的函数被调用,我们来具体看一下它的作用:

可以看出,该函数负责检查EPROCESS结构中的缓解措施,并能够允许或禁止虚拟内存分配。我们可以通过下面的流程图,来更好地理解这个函数:

上述代码完成了以下三项工作:
1、获取EPROCESS并检查是否启用了缓解措施。MitigationsFlags(缓解标志)位于EPROCESS的0x828偏移量的位置。
2、检查位于0x6d0位置的ETHREAD上的CrossThreadFlags,以便确定线程是否具有分配内存的特殊权限,从而清楚其是否能够绕过缓解。
3、追踪缓解和返回状态0xC0000604(STATUS_DYNAMIC_CODE_BLOCKED)的结果。

 

尝试进行内核代码注入

现在,让我们看看当我们尝试从内核注入代码时,ACG机制的行为。在这里,我们会使用恶意软件经常使用的两种内核代码注入方式:
1、创建一个新线程,并加载一个动态链接库(DLL)。
2、使用异步过程调用(Asynchronous Procedure Call),在现有线程中加载DLL。
为了理解ACG是如何处理这些代码注入的,我们来回顾一下使用这两种方法将代码注入到用户进程的必要步骤:
1、附加到目标进程;
2、获取Ntdll地址;
3、获取LdrLoadDll地址;
4、为Shellcode分配虚拟内存;
5、在Shellcode上调用LdrLoadDll。
在用一个新线程注入Shellcode时,我们使用NtCreateThreadEx在Shellcode上调用LdrLoadDll。另一种方式,如果我们想使用异步过程调用的方式注入Shellcode,就需要在合适的线程上使用异步过程调用函数,在Shellcode上调用LdrLoadDll。
在这两种注入方法中,我们都需要分配具有写入和执行属性的虚拟内存来执行Shellcode。如前所述,任意代码保护机制应该不允许执行这样的代码注入。我们来看一下实际的结果如何:
这是分配虚拟内存的代码:

如果我们在调用ZwAllocateVirtualMemory之后,在MiArbitraryCodeBlocked上设置了一个断点,我们就可以在WinDBG上看到以下返回值:

其返回值是STATUS_DYNAMIC_CODE_BLOCKED。前面提到的两种注入技术都不能绕过缓解,因为这两种技术都试图分配可执行和可写入的内存(与之前的判断原则相违背),因此任意代码保护机制会对其进行阻拦。

 

总结

任意代码保护(ACG)是一种非常好的缓解技术,可以有效防止来自用户或内核的代码注入。但也存在一个问题,就是这种缓解机制是可选项,如果目标进程没有启用任意代码保护机制,恶意软件就能够进行代码注入。
感谢大家花费时间阅读这篇文章!

免责声明:文章内容不代表本站立场,本站不对其内容的真实性、完整性、准确性给予任何担保、暗示和承诺,仅供读者参考,文章版权归原作者所有。如本文内容影响到您的合法权益(内容、图片等),请及时联系本站,我们会及时删除处理。查看原文

为您推荐