实验概览
在本文中,我想介绍一种有趣的方法,通过一种替代(实验性)方式来执行类似于Windows对象回调所启用的功能。众所周知,Windows系统上的反恶意软件、反作弊和通用监控工具经常使用这些回调。然而,它们的可用性仅限于拥有签名模块的实体,并且这些回调伴随一些风险,主要是如果未经过适当验证,这些回调很容易被篡改。
我将展示一个利用这种未记录方法的简单示例。我们将探讨所提出的方法如何在时间有限或禁用PG(PatchGuard)的限制下实现类似的结果。我不会花太多时间介绍Windows对象的高级细节——我强烈推荐《Windows Internals》或《Windows Kernel Programming》以获取更多详细信息。
我们将不分先后地介绍对象构造、各种类型、通知例程以及用例,特别是在反恶意软件和反作弊软件中,然后探讨一些问题,并详细介绍替代进程通知和反调试方法的实现。
免责声明
此实现在Windows 11 23H2(操作系统内部版本 22631.3085)上进行了测试。如果这些方法利用了相同的机制(除了那些被PatchGuard哈希处理的情况,如本文所述),则可能适用于早期版本的Windows。未来部署的Windows 11可能会更改这些机制及其组织或保护措施。
较晚的Windows版本显示,PatchGuard会在5分钟到6小时之间的任何时间引发错误。PsProcessType和IoDriverObjectType这两个类型被明确放置在PG上下文中,ObpTypeObjectType也是如此。ObpObjectTypes列表也使用SHA256进行哈希处理并放置在PG上下文中。处理任何对象类型时,请注意潜在的崩溃风险。所有结构都受到PG保护。然而,_OBJECT_TYPE.CallbackList条目不受保护,可以在运行时取消链接/重新组织以插入或移除回调。修改各种对象类型(如PsProcessType)的回调列表可能会产生类似的效果。
‡ 109是对Windows的CRITICAL_STRUCTURE_CORRUPTION错误检查(蓝屏死机)代码的简写引用。
基础构建块
Windows内核中的对象是操作系统运行和记账的基础。我假设您对Windows对象有初步了解,但如果您需要复习,可以举一些例子:进程、线程、文件、互斥体、信号量、IoRing等。它们都由各自的组件在操作系统初始化期间构建,并由对象管理器(例程前缀为Ob,位于ntoskrnl中)管理。我们将在以下小节中使用一个熟悉的对象:进程。
进程创建与通知
Windows中的进程通知回调是系统监控和安全的基石。这些回调主要由反恶意软件和反作弊系统使用,提供关于进程创建和终止事件的实时通知。它们将初始化适当的结构,然后调用PsSetCreateProcessNotifyRoutine来注册回调。对于不熟悉的人来说,安全产品利用此机制的原因可能显而易见,但它为各种操作提供了支持,从通用日志记录到首次验证或基于回调中提供的信息终止进程。
当软件注册此通知例程时,它将被添加到内核中标记为PspCreateProcessNotifyRoutine的回调列表中。每当通过诸如NtCreateUserProcess或NtCreateProcess等API创建进程时,结果将始终包括枚举此列表并随后执行任何已添加的回调。从调用到通知的一般流程如下:
|
|
如果我们查看PspCallProcessNotifyRoutines的内部实现,我们会看到在添加每个回调时枚举和执行它们的过程。
已经记录了几种方法来防止攻击者获得进程创建的首次访问机会。本博客上的一篇较早文章提到了一种潜在方法,而从上面看到的下一个逻辑步骤是定位感兴趣的回调条目,并将其从PspCreateProcessNotifyRoutine列表中移除。有一篇文章详细介绍了这种方法。要点在于,反恶意软件/反作弊/通用安全产品通常依赖这些回调,并可能假设它们未被篡改;然而,正如所述——通过滥用硬件和/或安全供应商推出的永无止境的WHQL签名驱动程序的无限数量,攻击这些机制的可靠性和可用性有些微不足道。
现在,让我们考虑不那么合法的角度。在过去几年中,您可以使用未签名的驱动程序(即使用允许无限制访问系统资源的WHQL签名驱动程序之一来映射您自己的驱动程序)注册对象回调和进程通知回调。一种方法是在DriverObject->DriverSection上进行一些小技巧,这里已经记录。然而,如今,当Windows未处于测试签名模式或没有签名模块时,您将在尝试注册对象通知时遇到STATUS_ACCESS_DENIED的结果。此方法绕过了修改驱动程序节属性、签名驱动程序或在测试签名模式下运行以获得与传统对象回调相同功能的需求。
函数指针重绑定
好了,不再进行无聊的解释。让我们直接深入了解如何通过完全避免对象回调列表来实现进程通知回调。我将呈现一张图片;我相信您会立即明白其工作原理。如果没有,别担心……当第一个概念验证出现时,您就会明白了。准备好了吗?
![内核对象类型初始化器函数指针示例图片描述]
很好。
在PspInitPhase0函数中为变量应用适当的类型后,立即可以注意到指向几种方法的指针。太好了,那么如何找到这些方法的调用点呢?很高兴您没有问,让我展示给您看。我编写了一个IDA Python脚本来查找从起点开始N层深度的函数引用。这对于定位目标模块内的机会非常有用(是的,我本可以在PspProcessOpen上设置断点,但我对调用图中所有间接调用感到好奇)。
让我们看一下从数千条结果中抽取的一部分:
|
|
[2]和[9]处的条目立即引起了我的兴趣,因为我不熟悉这些例程中执行的间接调用。进一步检查地址0x14064B733…
让我们对其进行一些符号化。
既然我们有DFS,谁还需要打开WinDbg呢?但为了彻底并验证此调用确实发生,我们还是需要……如果我们回顾最初的图片,我们会看到PsProcessType的ObTypeInit.OpenProcedure指向PspProcessOpen。我将在WinDbg中设置断点以确认我的假设:bp nt!PspProcessOpen "kb;g"。结果很多,但有一个确认了:
|
|
这是在进程创建时命中的,这足以证明我花费时间研究这个方法是值得的。那么,我们究竟该如何利用这一点呢?好吧,让我们列出一些我们知道的事情。
- 对象类型在内核初始化时创建。
- REF:
PspInitPhase0
- REF:
- 每个对象类型都有一个关联的名称。
- REF:
ObCreateObjectType(&ObjectTypeName, ...)
- REF:
- 对象类型对象存储在其各自索引的
ObTypeIndexTable中。- REF:
ObCreateObjectTypeEx[Index] = ObTypeObjectN
- REF:
- 初始图片中的过程存储在
_OBJECT_TYPE结构的TypeInfo字段中,该结构是ObTypeIndexTable中每个条目的类型。 - PG检查这些结构,但您要么在假设PG被禁用的前提下操作,要么此方法只会在非常短的时间内保持有效。
- 可以通过
MmGetSystemRoutineAddress获取ObGetObjectType。 - Zydis存在。
lock xchg发挥作用。- ???
- 盈利。
知道以上信息后,我们可以利用这些函数来实现我们的目标。首先,如果您想复制,这里有一些您需要的结构定义:
|
|
正如我们在“我们知道的事情”列表中所指出的……我们可以找到ObGetObjectType函数,在其中找到ObTypeIndexTable。我们将使用Zydis来实现:
|
|
让我们将其集成到DriverEntry中并验证结果。
|
|
处理 ObTypeIndexTable
对于已经熟悉此概念的读者来说,这个名字可能是不言自明的,但为了完整性,ObTypeIndexTable是一个指向_OBJECT_TYPE结构的指针数组,这些结构描述了在操作系统初始化时创建/注册的各种Windows内核对象。如果我们转储前几个条目,然后将数组的第3个元素(索引为2)转换为_OBJECT_TYPE,我们将看到以下数据。
![ObTypeIndexTable 条目示例]
此数组的第0个和第1个索引是无效条目,因此在枚举表时我们将跳过这些条目。如果我们认为这是一个_OBJECT_TYPE*数组,并且我们想从特定索引(2,第一个有效条目)开始,我们可以编写一个辅助函数,如下所示:
|
|
奇怪的遗留问题
需要索引到ObTypeIndexTable的前两个条目之后的要求有点奇怪。看来这些是占位符条目。它们无效的原因很可能是历史性的;我最好的猜测是他们以前使用了不同的结构来处理对象类型列表。第二个条目指向MmBadPointer。然而,这是最近才有的情况。2018年末,他们使用了另一个魔法值0x0bad0b0b,如这里所述。所有当前的初始化代码都将ObTypeIndexTable的起始索引设置为2。这可以通过分析ObInitSystem和ObCreateObjectTypeEx来验证。我验证了在启用Hyper-V和Windows沙盒时,未使用这些索引;只引入了两种新的对象类型:CrossVmMutant和CrossVmEvent。
如果有人知道为什么前两个条目无效,我很想知道原因。
更新 DriverEntry
剩下的就是在DriverEntry中添加一些逻辑,并验证我们枚举和找出目标类型(PsProcessType)的逻辑是否正确。
|
|
对象类型转储验证
看起来不错;这些都是对象表中的所有对象类型。我通过WinDbg对照了之前转储的其他引用,一切看起来都没问题。剩下的就是编写我们的函数,以替换PsProcessType对象条目中_OBJECT_TYPE_INITIALIZER结构中的原始函数指针。然而,要做到这一点,我们需要逆向PspProcessOpen以了解其工作原理。根据初步分析,我们只知道它是在进程初始化期间的某个时间点被调用的。PspProcessOpen的函数原型如下:
|
|
PsProcessType.OpenProcedure (PspProcessOpen) 钩子
|
|
最终的 DriverEntry 和结果
|
|
结果是,只有在此函数指针重绑定之后创建的进程才会被记录。您必须验证access_mode是否为0,并验证传递给函数的次要对象(不是主要进程对象)的名称,因为这是正在创建的应用程序。主要进程对象(第3个参数)是“父进程”。对于新进程,当调用PspProcessOpen时,主要进程对象将是System。如果您不管access_mode和open_reason都记录对象,您将被大量无关信息淹没。
利用 SecurityProcedure
另一种方法是利用对象类型初始化器结构中的安全过程。在进程初始化期间,它会被调用,操作码为AssignSecurityDescriptor,在进程终止时,您可以捕获DeleteSecurityDescriptor情况并检查是否为PsProcessType——在正常操作期间,这两者始终指示进程启动/终止。
为了确定其发生位置,我最初只是用前面提到的脚本进行跟踪,看看在哪里调用了该方法……毫不奇怪,它在ObInsertObjectEx内部。
|
|
供将来参考
如果您不确定在哪里可以找到这些函数的引用,而不仅仅是在WinDbg中中断该函数,请考虑对对象执行的操作。当创建对象时,例如互斥体、节、信号量、进程、线程等,必须将其插入到相应的列表中。从逻辑上讲,您可能会在相关函数(如ObInsertObjectEx)中找到对相应对象过程的调用——这通常是调用堆栈上最高的Ob相关条目,随后将调用OpenProcedure/SecurityProcedure之一,有时不止一次。当然,如果对象类型未初始化这些过程,您在实践中就不会走到那条路径。
OkayToClose/Close/Delete过程将在处理释放/关闭对象句柄的函数中看到引用,例如:ObCloseHandleTableEntry。您还会看到在关闭操作中调用SecurityProcedure。这是有道理的,因为当对象在构造时分配了安全描述符时,在销毁时也必须释放SD。作为一个思考练习,考虑您可能在哪里看到QueryName或ParseProcedure的引用。我稍后会给出答案。
在用WinDbg再次检查后,产生的序列是:
|
|
根据这些信息,我们可以实现类似于OpenProcedure的方法。
|
|
以下是登录并启动几个进程后的结果。
![安全过程监控进程创建结果]
通过一些其他检查来确定进程是否处于初始启动阶段,您可以在不向对象管理器注册的情况下拥有自己的进程创建回调。
简单的系统级反调试
我们使用进程对象是因为它可能是读者最熟悉的。然而,我们不必就此止步。在上一节中,我指出,所有设置了这些过程的对象类型都会在某个时刻调用它们。关于调试我们知道什么?如果您不熟悉调试的内部原理,那么这是另一篇文章的内容。对于这部分,唯一需要知道的相关信息是调试器会调用`DbgUiConnect