我将这个项目命名为 Mordor,因为出于某种奇怪的原因,Hell's Gate / Halo's Gate / Tartarus' Gate 让我想起了 《指环王》 中的 魔多黑门,哈哈,而且这个项目需要一个酷炫的名字,那为什么不呢?
鸣谢 Middle-earth: Shadow of War Wiki
挂钩是一种用于拦截对已有函数的调用,或将合法 API 的执行流重定向到内存中另一位置的技术。该内存位置可能由攻击者、反病毒软件(AV)、端点检测与响应系统(EDR)或反作弊系统(AC)控制。
内联挂钩(Inline Hooking): 这允许我们将目标函数的前几个字节(汇编指令)替换为一条跳转指令,从而将执行流重定向到内存中的另一位置。
导入地址表挂钩(IAT hooking): 导入地址表是一张查找表,包含从 DLL 或可执行文件导入的函数的函数指针。IAT 挂钩允许我们将导入地址表中的函数地址替换为另一个地址,从而重定向程序的执行流。
鸣谢 Kyle Mistele
Hell's Gate 是一种进程注入技术,它允许我们从 ntdll.dll 模块中搜索一段称为系统调用存根(syscall stub)的字节序列,以提取系统调用号并将其保存到专用的内存表中,随后使用该表直接调用系统 API。然而,Hell's Gate 的局限性在于:如果函数已被挂钩,它需要访问一个干净的 ntdll.dll 模块。否则,它无法填充所需的系统调用号,最终无法完成原生 API 调用。为了解决这个问题,一个孪生姐妹诞生了,名为 Halo's Gate,它只是基于一个非常简单的观察对 Hell's Gate 打的补丁。
当函数上被放置了挂钩(jmp <address>)时,我们将无法动态检索系统调用号,因此为了解决这个问题,我们可以查看相邻函数的系统调用号并相应地调整计算以获取我们的系统调用号,因为系统调用存根中的系统调用 ID 是依次递增的。
Hell's Gate 仅按以下顺序检查字节序列 4c8bd1b8,其在汇编中看起来如下:
mov r10, rcx
mov eax, <syscall>
Halo's Gate 与 Hell's Gate 的作用相同,但额外增加了一项检查,通过检查导出函数的第一个字节是否为 e9(jmp)来判断是否存在钩子;如果存在钩子,Halo's Gate 就会开始查看相邻函数并相应地调整计算,以获取我们的系统调用号,因为系统调用存根中的系统调用 ID 是依次递增的。
然而,并非所有 EDR 都在同一位置(函数开头)挂钩。如果 EDR 恰好在 mov r10, rcx(4c8bd1)之后挂钩呢?这会破坏我们的代码。
Tartarus' Gate 通过在 Halo's Gate 的基础上增加一项额外检查来解决这个问题,即同样按顺序搜索这些字节 4c8bd1e9,其在汇编中看起来如下所示。
mov r10, rcx
jmp <address>
然而,如果所有函数都被挂钩,那么我们可以找到第一个函数,并从调用 ID 0 开始逐个解除所有挂钩。这种方法在 SEKTOR7 中被称为 Veles' Reek。
FreshyCalls 技术会在 Export Directory 中搜索以 Nt 开头的函数(排除 Ntdll),并按地址对它们进行排序。令人惊讶的是,最低的地址对应系统调用号 0,接下来的地址则依次对应系统调用号 1、2、3……你可以在 x64 dbg 或 Windbg 中自行验证这一点。
Syswhispers2 做的是同样的事情,但它不是在 ntdll.dll 的导出目录中搜索以 Nt 开头的函数,而是搜索以 Zw 开头的函数。令人惊讶的是,Zw 函数和 Nt 函数指向相同的系统调用存根,并且具有相同的系统调用号。
我们可以在这里验证这一点:
Zw 函数与 Nt 函数之间的区别由 Microsoft 在此处 进行了解释:
“*Windows 本机操作系统服务 API 实现为一组在内核模式下运行的例程。这些例程的名称以 Nt 或 Zw 前缀开头。内核模式驱动程序可以直接调用这些例程。用户模式应用程序可以通过系统调用来访问这些例程。
除少数例外,每个本机系统服务例程都有两个名称相似但前缀略有不同的版本。例如,对 NtCreateFile 和 ZwCreateFile 的调用执行类似的操作,并且实际上由同一个内核模式系统例程提供服务。
对于来自用户模式的系统调用,例程的 Nt 版本和 Zw 版本行为完全相同。对于来自内核模式驱动程序的调用,例程的 Nt 版本和 Zw 版本在处理调用方传递给例程的参数值方面有所不同。
内核模式驱动程序调用本机系统服务例程的 Zw 版本,以告知该例程参数来自可信的内核模式来源。在这种情况下,该例程假定它可以安全地使用这些参数,而无需先对其进行验证。然而,如果参数可能来自用户模式来源或内核模式来源,则驱动程序改为调用例程的 Nt 版本,该版本根据调用线程的历史记录来确定参数是源自用户模式还是内核模式*”
Hell's Gate: 这会解析 ntdll.dll 以找到系统调用存根的起始位置(4c8bd1b8),然后检索系统调用 ID。然而,如果系统调用存根被挂钩,那么我们的代码就会失效。
Halo's Gate: 与 Hell's Gate 相同,但增加了一项额外检查,通过检查第一个字节是否为 e9(jmp)来判断是否已存在挂钩;如果存在挂钩,Halo's Gate 就会开始查看相邻函数,并相应地调整计算以获取我们的系统调用号,因为系统调用存根中的系统调用 ID 是依次递增的。
Tartarus' Gate:与 Halo's Gate 相同,但增加了一项额外检查,通过搜索以下字节序列来判断系统调用存根是否在第二行(即汇编指令 mov r10, rcx 之后)被挂钩:4c8bd1e9。
FreshyCalls: 这会在 Export Directory 中搜索以 Nt 开头的函数,并按地址对它们进行排序,最低的地址即为系统调用标识符 0。
SysWhispers1: 使用操作系统版本信息来选择正确的系统调用号。
Syswhispers2: 与 FreshyCalls 相同,但它会在 Export Directory 中搜索 Zw 函数,并通过将 Zw 替换为 Nt 来存储名称。
SysWhispers3 这与 SysWhispers2 非常相似,不同之处在于它还支持 x86/WoW64、使用 EGG 替换 syscalls 指令(以便动态替换)、在 x86/x64 模式下直接跳转到 syscalls(在 WOW64 中这几乎是标准做法)、直接跳转到随机 syscalls(借鉴了 @ElephantSeal 的想法)。
留给读者的练习,作者: Alice Climent-Pommeret 和 Kelzvirus 的一篇优秀博客
https://github.com/am0nsec/HellsGate - smelly__vx(@RtlMateusz)和 Paul Laîné(@am0nsec)
https://vxug.fakedoma.in/papers/VXUG/Exclusive/HellsGate.pdf
https://blog.sektor7.net/#!res/2021/halosgate.md - @Reenz0h / @SEKTOR7net
https://github.com/trickster0/TartarusGate([trickster0 / @trickster012](https://twitter.com/trickster012))
https://klezvirus.github.io/RedTeaming/AV_Evasion/NoSysWhisper/
https://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-invocation-of-system-calls-for-red-teams/ - @modexpblog
https://kylemistele.medium.com/a-beginners-guide-to-edr-evasion-b98cc076eb9a
https://alice.climent-pommeret.red/posts/direct-syscalls-hells-halos-syswhispers2/
https://github.com/crummie5/FreshyCalls - crummie5
https://github.com/jthuraisamy/SysWhispers - jthuraisamy
https://github.com/jthuraisamy/SysWhispers2 - jthuraisamy