文章总结: 本文探讨了动态规避EDR系统用户模式函数钩子的技术,提出从依赖轶事证据转向基于能力的方法。文章详细介绍了函数钩子的工作原理、检测方法及规避技术,开发了SHAPESHIFTER工具实现动态检测被钩取函数并生成规避载荷。作者指出EDR代理只是从有限数据源收集信息,切断数据源可避免检测,并呼吁Microsoft开放内核模式ETW提供者以提高安全性。
综合评分: 92
文章分类: 红队,内网渗透,渗透测试,二进制安全,漏洞分析
动态规避技术探索
Matt Hand
securitainment
2025年12月5日 10:25
中国香港
我曾合作过的大多数团队在规避技术方面严重依赖轶事证据。如果询问操作员为何选择某种技术而非另一种,我听到最多的回答是”因为上次有效”。在遭遇新的防御解决方案时,我们结合过去的经验和最佳实践,希望能够成功落地,但归根结底这只是在黑暗中摸索。
即使是一些颇具影响力的组织也在使用同样的策略
来源:https://threatintel.blog/OPBlueRaven-Part2/
虽然这是解决问题的有效方法,但显然还有改进空间。例如,如果我们的流程严重依赖轶事证据,那么当供应商相较于上次遭遇时做出改变,会发生什么?在首次遭遇某个解决方案的情况下,我们如何通过使用数据而非最佳猜测来提高成功率?
另一种方法
如果我们不再针对每个解决方案测试 TTP(战术、技术和程序),记录哪些有效并随时间更新,而是先研究解决方案的能力,然后从更大的可用 TTP 列表中进行筛选,会怎么样?
为了探索这个想法,我研究了众多 EDR 供应商在目标系统上实现的用户模式 API/函数钩子,并寻求创建一个能够动态规避它们的解决方案,而无需依赖任何先前的经验或信息。
注意:这只是规避难题中一小部分的一种方法。选择函数钩子作为本文主题是因为其简单性,但还有大量其他因素在起作用,本文中的技术也可应用于这些因素(例如驱动回调、ETW、过滤器等)。关于规避函数钩子也有大量研究,尤其是最近。对此感兴趣的读者可以参考本文末尾的附加链接。
什么是用户模式函数钩子?
这个话题最近受到了大量关注,因此我不会深入细节。本文末尾的链接比我为了简洁而阐述的内容要详细得多。
简而言之,EDR 解决方案监控可疑 API 调用最常见的方法之一是拦截它们。这通过将自己的 DLL 注入进程,修补 ntdll.dll内部的重要函数使其跳转到 EDR 的 DLL 中的函数,代理 API 调用并从 EDR 的 DLL 内部提取调用和返回值的信息,然后将 API 调用的返回值返回给用户来实现。这被称为函数钩子,对用户来说应该是完全无缝的。
极度简化的被钩取的 NtCreateFile() 示意图
EDR 最常钩取 ntdll.dll内部这些函数对应的原生 API(例如 NtOpenProcess、NtAllocateVirtualMemory)。在拦截和收集之后,提供给这些函数的参数会与其他数据点关联,以确定系统上是否发生了恶意活动。例如,如果使用 OpenProcess()打开远程进程的句柄,使用 VirtualAlloc()分配 RWX 内存段,使用 WriteMemory()将数据写入已分配的内存块,使用 CreateRemoteThread()创建新线程,并且创建了新进程,那么可以合理推测刚刚发生了远程进程注入。
检测用户模式函数钩子
幸运的是,由于一些可预测的值被改变,检测这些钩子是微不足道的。因为这些钩子修补了 ntdll.dll,我们可以检查特定函数,查看它们的指令是否与预期值不匹配。这些函数遵循可预测的格式,因为它们的工作是进行 SYSCALL。
所有 x64 Windows SYSCALL 必须遵循这个通用调用约定:
MOV R10, RCX
MOV EAX, <SYSCALL_NUMBER>h
SYSCALL
RETN
如果这个特定流程被修改,这是函数钩子存在的高保真指标。让我们看看这可能是什么样子。下面是未修改的 NtAllocateVirtualMemory()的示例。
未修改的 NtAllocateVirtualMemory()
现在如果我们查看被 EDR 钩取的进程,NtAllocateVirtualMemory()看起来有点不同。
被 EDR 钩取的 NtAllocateVirtualMemory()
如您所见,中间插入了一个跳转到 0x7ffc01e60168的指令(跳转后的 3 个断点陷阱不重要)。这通常被称为跳板(trampoline)。这个跳板将执行流程转向替代函数,很可能是 EDR 注入的 DLL 中用于收集信息的函数。该 DLL 内部的这个函数将代表用户执行真实函数,并可能观察任何返回值。
重现 EDR
由于大多数 EDR 公司不喜欢研究人员探查他们的产品,而合法购买许可证对个人来说成本过高,我决定自己编写一个来模拟我们感兴趣的功能。
有几个选项可以让我们钩取函数,包括 Microsoft 的 Detours、EasyHook 和 MinHook。我选择使用 Frida,因为它的 Python 绑定简化了操作,并且有相对完善的文档。
注意:如果您想尽可能接近地复制 EDR,我建议使用 Detours,因为我接触过的大多数代理都在底层使用它。
Frida 可以完成我们需要做的一切——以主线程挂起状态生成进程,用跳板修补我们想要挂钩的目标函数,恢复线程,然后给我们返回输出。我编写了一个非常简单的 Frida 脚本来挂钩在教科书式的基于 CreateRemoteThread的 shellcode 注入期间使用的一些原生 API,并在此处发布
https://gist.github.com/matterpreter/cf9c8c48d0a95a9699f240c4f37d8fd7
使用 Frida 脚本检测到的 CreateRemoteThread shellcode 注入
以编程方式检测钩子
因为我们知道这些函数遵循特定格式,并且对此格式的任何修改都可以假定为钩子,所以检测钩子相当简单直接。
要检测钩子,我们只需获取指向当前进程加载的 ntdll.dll中我们关注的函数的指针,然后检查是否有任何偏离标准指令模式的情况。为了演示这一点,我随本文一起发布了一个简单的 POC,可以在此处找到,作为 OffensiveCSharp 存储库的一部分。
HookDetector 检测到的 Frida 脚本放置的钩子
规避钩子
现在我们可以作为模拟 EDR 代理可靠地检测这种特定的 shellcode 注入技术,并且拥有检测哪些原生 API 函数被钩取的工具,我们可以创建一个新的载荷,战略性地规避被钩取的函数。已经有大量与规避用户模式钩子相关的工作,但大多数集中在两种技术上——进行直接 SYSCALL 或模块重映射。我最喜欢的一些是:
-
D/Invoke
—— https://github.com/cobbr/SharpSploit/tree/master/SharpSploit/Execution/DynamicInvoke
-
SysWhispers
—— https://github.com/jthuraisamy/SysWhispers
-
Dumpert
—— https://github.com/outflanknl/Dumpert
上述工具采用的任何技术都是可行的选项,但每种都有其权衡。为了简单起见,我将仅使用 Jack Halon 的文章中描述的通过 C# 委托使用最优质、手工制作、匠心独运的 SYSCALL 来测试我们的假设。以下是调用 NtAllocateVirtualMemory()的示例:
为了测试我们的规避能力,我用 SYSCALL 代码替换了上述 SimpleCRT.exePoC 中对 VirtualAlloc()的调用,并通过 Frida 脚本运行它。对 WriteProcessMemory()的调用被检测到,因为它没有被修改,但 Frida 脚本无法捕获我们的内存分配 😎
使用 SYSCALL 规避内存分配检测
动态规避
如果我们将自动检测哪些函数被钩取的能力应用到我们的工具中会怎样?为了展示这可能是什么样子,我编写了一个分段服务器的简单实现,它从分段器接收有关函数钩子的数据,仅使用未被钩取的 API 或通过直接调用被钩取函数的 SYSCALL 来编译下一阶段载荷。然后服务器将这个编译的程序集返回给分段器,分段器内联运行它。
这是架构:
动态规避架构图
我选择在载荷和服务器实现中坚持使用 .NET Framework,是为了简单性、易于(反)序列化数据的能力、关于通过委托进行 SYSCALL 的现有研究,以及对
ICodeCompiler接口的支持,该接口允许轻松地以编程方式编译源文件。不过这种技术是语言无关的,可以在其他语言中以最小的努力实现。
测试我们的技术
为了证明这个概念,我编写了一个名为 SHAPESHIFTER 的工具,它充当分段服务器的角色。启动时,它会编译 Stage 0 载荷并启动一个简单的 TCP 服务器。
当 Stage 0 在目标上落地时,它使用与 HookDetector 相同的技术检查钩子,然后将结果发送回服务器。
服务器接收来自 Stage 0 的数据,动态生成并编译 Stage 1,该载荷仅使用未被钩取的 API 调用以及对被钩取函数的 SYSCALL。服务器将这个编译的 Stage 1 发送回 Stage 0,后者加载并调用 Stage 1 的入口点。
https://github.com/matterpreter/SHAPESHIFTER
您可能会想”他为什么不只是制作一个规避所有钩子的载荷?”这是一个完全合理的观点,但需要考虑的一点是,从长期支持此类工具的角度来看,使用 SYSCALL 和未记录的 API 是有风险的。因为 SYSCALL 编号和未记录的 API 调用约定可能随时被 Microsoft 更改,我们使用其中任何一个越多,我们就需要花费更多时间针对我们目标的特定 Windows 版本进行测试。此外,通过使用”动态”载荷,您不太可能成为静态签名的受害者。
未来工作
我们可以将此服务器用作目标和主 C2 之间的中介,或者可能将其作为 C2 本身。如果我们能够维护每个代理被钩取的函数的状态,我们可以对所有后渗透工具应用相同的技术,即仅调用未被钩取的 API 或直接 SYSCALL。这种技术也可以被使用 fork-and-run 模式的现有 C2 框架/代理利用。
防御考虑
在防御方面,我们需要理解虽然攻击者可以避开我们的遥测源,但还有其他关卡可以捕获恶意活动。例如,即使我们可以使用 SHAPESHIFTER 绕过用户模式函数钩子,我们仍然可以被 Stage 0 载荷上的静态 AV 签名、EDR 驱动(通过进程创建和打开进程句柄的请求)、ETW 提供者(例如 Microsoft-DotNET-Runtime)以及捕获回调的网络监控所捕获。与函数钩子一样,这些源也可以被篡改或彻底禁用。
不幸的是,试图检测这种特定技术相当困难。我们可以尝试通过使用 .NET 中 Microsoft.Windows.EventTracing.Syscalls命名空间之类的东西来检测直接 SYSCALL,但由于数据量大,大规模收集和分类来自此源的数据可能很困难。
一个更合理的方法(尽管不那么令人兴奋)是将您依赖的端点检测工具分解为能力/遥测组,研究这些能力如何被颠覆,并将它们作为检测策略中盲点和考虑因素的一部分。
附注:对 Microsoft 的呼吁
由于用户模式函数钩子容易被规避,我不相信随着攻击手法的不断发展,这种方法在不久的将来会是可行的。我认为更好的解决方案是通过 MVI 向供应商开放 EtwTi*内核模式 ETW 提供者,类似于 AMSI 所做的。因为这些钩子存在于内核中,与进行直接/未修改 SYSCALL 相关的规避是无效的。将此与用于注入的大多数常见 API 的现有覆盖范围、在内核中获取代码执行以篡改提供者的相对难度以及消除处理 DLL 注入的复杂性相结合,很明显这一能力将是巨大的进步。
2020 年 12 月 16 日更新:Microsoft 似乎没有关于此的任何公开文档,但可以使用 ELAM 驱动和受保护进程从 Microsoft-Windows-Threat-Intelligence提供者获取事件。@pathtofile 有一篇很棒的博客文章和工具演示如何做到这一点,请参阅此处。
结论
从本质上看,EDR 代理只是从主机上有限数量的可用源收集数据的软件。如果我们能够切断向代理提供的数据,无论在服务器组件上运行的关联引擎有多复杂,我们都可以避免检测。因为代理从多个协同工作的源(例如被钩取的函数、ETW、驱动回调)收集数据,我们需要了解每个源的局限性以识别盲点。一旦我们确定了盲点,我们就可以调整我们的 TTP 以避免触发尽可能多的”传感器”,从而在行动中获得更高的成功机会。
通过从我们的规避攻击手法中消除对轶事证据的严重依赖,转而使用基于能力的方法,我们将对那些我们可能不熟悉的新的或未知的 PSP 更加有效。
附加资源
- https://thewover.github.io/Dynamic-Invoke/
- https://www.mdsec.co.uk/2020/08/firewalker-a-new-approach-to-generically-bypass-user-space-edr-hooking/
- https://www.ired.team/offensive-security/defense-evasion/bypassing-cylance-and-other-avs-edrs-by-unhooking-windows-apis
- https://jhalon.github.io/utilizing-syscalls-in-csharp-2/
- https://www.fuzzysecurity.com/tutorials/29.html
Adventures in Dynamic Evasion
免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:securitainment Matt Hand《动态规避技术探索》