检测SyscallNumber的新方法

为在成熟环境中运行而设计的 Post-exploitation 工具经常需要绕过在目标上运行的终端检测和响应 ( EDR ) 软件 。EDR 经常通过挂钩 Windows API 函数进行操作,尤其是那些由 ntdll 导出的函数(特别是基于 Nt/Zw* ( ) 系统调用的 API 函数) 。由于在正常情况下与底层操作系统的所有交互都将通过这些函数,这样在检测不需要的应用程序行为时就提供了一个理想的拦截点 。
之前 MDSec 已经在《绕过用户模式挂钩和直接调用红队的系统调用》中讨论了绕过这些挂钩的各种方法,但是由于 EDR 经常与攻击者斗法,因此 EDR 的检测技术只有实时更新,以检测识别用于实现绕过挂钩的新技术 。
在 Nighthawk C2的开发过程中,MDSec 偶然发现了一种新的技术,用于识别某些系统调用的 Syscall Number,然后可以使用该技术将 ntdll 的新副本加载到内存中,从而允许剩余的系统调用在不触发任何已安装的函数挂钩的情况下成功读取 。该技术涉及滥用新的 Windows 10 并行加载程序在进程初始化期间早期生成的某些系统调用的副本 。
Windows 10 并行加载程序
从 Windows 10 开始,微软引入了并行 DLL 加载的概念 。并行加载允许进程执行递归映射通过进程模块导入表导入的 DLL 的过程,而不是在单个线程上同步,从而在初始应用程序启动期间提高性能 。
当我们试图理解为什么在一个简单的单线程应用程序中会创建三到四个额外的线程时,我们第一次注意到并行加载程序的存在 。检查这些线程可以确定它们是线程池的工作线程 。
通过上网搜索其中原委时,有一篇来自 2017 年黑莓的 Jeffery Tang 的博客文章以及来自用户 RbMm 的回答帮助了我,他在提供伪代码以帮助阐明该过程中涉及的步骤方面也做得非常出色 。这两篇文章清楚地说明了背后的运行原因,如果对进一步了解并行加载程序工作原理有兴趣,我推荐这两篇文章 。
在阅读 StackOverflow 答案时,有一件事立即引起了我的注意,那就是如果发现有几个核心低级别本机 API 被绕过,并行加载程序就会使并行 DLL 加载短路,并退回到同步模式 。这些 API 参与了从磁盘打开和映射映像的过程 。
以下是用户 RbMm 的部分答复,我们将在下面进行详细分析:
【检测SyscallNumber的新方法】 上面函数 LdrpDetectDetour ( ) 的伪代码本质上是检查 5 个本机 API 函数 NtOpenFile ( ),NtCreateSection ( ),ZwQueryAttributeFile ( ),ZwOpenSection ( ) 和 ZwMapViewOfFile ( ),并确定这些字节是否已经从存储在 ntdll 中的 LdrpThunkSignature 数组中已知的正确字节被修改 。
通过对 LdrpDetectDetour ( ) 函数的快速反汇编确认上述伪代码中描述的行为仍然存在,但应该提到的是,该函数现在额外地验证了另外 27 个本机 API 的完整性,但仍然只是比较了详细的 5 个函数的精确的系统调用存根 。
用 IDA Pro 检查 ntdll 发现 LdrpDetectDetour ( ) 函数是从两个地方调用的:LdrpLoadDllInternal ( ) (直接从 LdrpLoadDll ( ) 调用)和 LdrpEnableParallelLoading ( ) (在 LdrpInitializeProcess ( ) 的后期调用) 。由于 LdrpDetectDetour ( ) 函数配置了一个全局变量,该变量可以停止并行加载并强制进一步加载同步发生,并且许多安装了 detours 的 DLL(例如 EDR 用户空间组件)在加载到进程时立即执行此操作,它在加载每个新的 DLL 依赖项时重复调用 detour 检测函数是有意义的 。
然而,对这一过程的研究又引发了另一个问题,已知的 5 个本机 API 函数的已知良好存根从何而来?最初以为系统调用存根将在代码生成期间的编译时被硬编码,但是静态检查 LdrpThunkSignature 数组表明情况并非如此,因为在映射 ntdll 之前数组没有初始化,原因是数组驻留在未初始化的。data 中部分 。
LdrpThunkSignature 标识的数据交叉引用了另一个数组的使用,在 LdrpCaptureCriticalThunks ( ),这个函数反过来调用早期 LdrpInitializeProcess ( ) 之前进口依赖加载,因此第三方模块安装的 detours 可能已加载到进程中 。
快速手动反编译 LdrpCaptureCriticalThunks ( ) 会显示类似于以下伪代码的实现:
从上面可以清楚地看到,LdrpCaptureCriticalThunks ( ) 将五个关键函数的每个系统调用存根的前 16 个字节从每个函数中复制到 LdrpThunkSignature 数组中 。
从 LdrpThunkSignature 中恢复系统调用
具有 post-exploitation 工具开发经验的读者无疑会收获颇多,有了这五个关键函数和它们的 Syscall Number(直接从 LdrpThunkSignature 读取)的知识,我们有足够的本机 API 函数能够使用系统调用从磁盘读取 ntdll 的新副本 。
由于 LdrpThunkSignature 数组不是由 ntdll 导出的,我们需要在 ntdll。data 节中找到它 。该数组可以通过公共系统调用序言的出现被识别出来:
下面的代码能够使用这个信息来恢复所需的系统调用(MDSec 提供的用于恢复所有系统调用代码段):
可以在 MDSec ActiveBreach GitHub 存储库中找到使用上述方法从磁盘读取 ntdll 来恢复所有系统调用的实现 。
这个实现当然是一个 PoC,从 opsec 的角度来看并不是最优的,例如,系统调用存根是用使用 VirtualAlloc ( ) 创建的 RWX 内存分配的 。

    推荐阅读