0x00 前言
在过去的一年中,安全社区(特别是红方运营团队和蓝方防御团队)持续关注Windows恶意软件如何实现后漏洞利用活动,以及如何绕过终端检测与响应(EDR)设备。
现在,对于某些防御人员来说,这种技术的使用还是比较陌生的,但对于攻击者来说却并非如此。许多年来,很多恶意软件作者、开发人员甚至是游戏破解者都在尝试利用系统调用和内存加载。其最初目标是绕过某些通过反病毒和反作弊引擎之类的工具来实现的限制和安全措施。
在一些文章中,已经介绍过如何利用这些系统调用技术,例如:如何绕过EDR的内存保护、关于挂钩的介绍、结合直接系统调用和sRDI绕过AV或EDR等等。作为红队成员,这些技术的使用对于秘密行动来说至关重要,因为它们使我们能够在网络范围内进行后漏洞利用活动,同时又能够避开监控。
这些技术的实现大部分都是在C++中完成的,以便轻松地与Win32 API和系统进行交互。但是,使用C++来编写工具存在着一个缺点,就是我们将代码编译后,总会得到一个EXE文件。为了实现秘密行动的目标,作为红队运营者来说更倾向于避免接触磁盘,我们不想盲目地把文件复制到系统上并执行。因此,我们倾向于寻找一种方法,将这些工具以一种更安全的方式注入到内存中。
尽管在任何恶意软件相关的领域,C++都是一种非常不错的语言,但当我在尝试编写一些后漏洞利用工具时,我开始认真考虑将系统调用集成到C#之中。在FuzzySec和THe Wover在BlueHatIL 2020发表了一篇题目为《保持#,将隐性注入引入.NET》的演讲后,我决定更加深入地研究如何在C#中实现这一点。
经过一些复杂的研究、失败的尝试、漫长的夜晚和大量的咖啡之后,我终于成功找到了在C#中实现系统调用的方式。尽管该技术本身对于增强隐蔽性很有帮助,但其代码却稍有繁琐,我们将会在稍后详细分析原因。
总而言之,这一系列文章的重点是探讨如何通过利用非托管代码来绕过EDR和API挂钩,从而在C#中使用直接系统调用。
但是,在开始编写代码之前,必须首先了解一些基本概念,例如系统调用的工作方式、某些.NET的内部结构、托管代码和非托管代码、P/Invoke和委托。了解这些基础知识将真正帮助我们理解C#代码的工作方式和原因。
好了,我们的前言部分已经足够,接下来让我们开始基础工作。
0x01 理解系统调用
在Windows中,进程体系结构分为两种处理器访问模式——用户模式和内核模式。这些模式实现背后的想法是希望防止用户应用程序访问和修改任何重要的操作系统数据。奇热用户应用程序(例如Chrome、Word等)都是以用户模式运行,而操作系统代码(例如系统服务、设备驱动程序等)都是以内核模式运行。
内核模式特指在处理器中执行的一种模式,该模式授予对所有系统内存和所有CPU指令的访问权限。一些x86和x64处理器通过使用另一个称为Ring Level的术语来区分这些模式。
使用Ring Level特权模式的处理器定义了四个特权级别,也称为Ring,用于保护系统代码和数据。这些Ring Level的示例如下所示。
在Windows中,仅使用其中两个Ring。Ring 0用于内核模式,Ring 3用于用户模式。在正常的处理器操作期间,处理器将根据其上面运行的代码类型,在这两种模式之间切换。
那么,为什么这种Ring Level可以提供安全性呢?当我们启动用户模式应用程序时,Windows将为应用程序创建一个新的进程,并将为该应用程序提供私有虚拟地址空间和私有句柄表。
这个句柄表,就是包含句柄的内核对象。句柄只是对特定系统资源(例如:内存区域和位置、打开的文件或管道)的抽象引用值,最初的目的是向API用户隐藏真实的内存地址,从而使系统能够执行某些管理功能,例如重组物理内存等。
总体而言,句柄的工作是对内部结构执行任务,例如令牌、进程、线程等。句柄的示例如下:
因为应用程序的虚拟地址空间是私有的,所以一个应用程序不能更改属于另一个应用程序的数据,除非该进程通过文件映射或VirtualProtect函数,将其私有地址空间的一部分用于共享内存段,或者一个进程有权打开另一个进程以使用跨进程的内存函数(例如:ReadProcessMemory和WriteProcessMemory)。
现在,与用户模式不同,所有在内核模式下运行的代码都共享一个称为系统空间的虚拟地址空间。这意味着,内核模式驱动程序不会与其他驱动程序以及操作系统本身相隔离。因此,如果驱动程序无意写入了错误的地址空间,或者进行了恶意操作,就可能会影响系统或其他驱动程序。尽管还有一些保护措施(例如内核补丁保护)可以防止操作系统出现混乱,但这并非我们所关注的重点。
由于内核在用户模式应用程序需要访问这些数据结构,或者需要调用Windows例程,以在执行特权操作(例如:读取文件)的过程中随时容纳操作系统的大多数内部数据结构(例如:句柄表),因此它必须首先从用户模式切换到内核模式,这也就是系统调用作用的位置。
为了使用户应用程序以内核模式访问这些数据结构,进程使用了一种名为syscall的特殊处理器指令触发器。该指令触发处理器访问模式之间的转换,并允许处理器访问内核中的系统服务处理代码。依次调用Ntoskrnl.exe或Win32k.sys中的相应内部函数,这些函数包含内核和操作系统应用程序级逻辑。
在任何应用程序中,都可以观察到这种“开关”的例子。例如,通过使用Process Monitor查看记事本,我们可以查看特定的Read/Write操作属性及其调用栈。
在上图中,我们可以看到从用户模式到内核模式之间的切换。请大家关注,在直接调用本地API NtCreateFile之前,是如何立即执行Win32 API CreateFile函数调用的。
但是,如果我们仔细观察,会发现其中可能会有一些不同寻常之处。我们观察到,有两个不同的NtCreateFile函数调用,其