# 如何在Windows 10下隐藏驱动 > 原文: [Hiding Drivers on Windows 10](https://revers.engineering/hiding-drivers-on-windows-10/) > > 作者:Daax Rynd > > 翻译:[DarcJC](https://darc.pro) > > 原文并未遵循任何协议,因此本文作者拥有一切解释权。在原作者授权后,本文可以基于[知识共享署名-相同方式共享 4.0 国际许可协议](http://creativecommons.org/licenses/by-sa/4.0/)转载。 免责声明:所有实验与开发均基于**Windows 10 x64 (Version 1703, Build 15063.540)**。在任何不同版本的操作系统上尝试的,结果可能不能复现。 ----- 在游戏辅助社区,尝试加载并隐藏内核模块,使其免于被检测是个很常见的目标。其中大多数的方法都会留下痕迹,或者在任何时间引发错误检查(bugchecks),从而导致反反作弊不可靠。 让我们看看驱动程序加载和初始化的细节,并了解其中发生了什么。在我们了解了用于从受保护资源中删除对象、隐藏初始化和越过某些验证检查的技术细节后,会发现这个过程的非常有意义的。 ## NtLoadDriver的流程  **NtLoadDriver** 是一个定义在 ntoskrnl 中的系统例程(routine),用于按照注册表路径(registry path)中的 **DriverServiceName** 来初始化并加载驱动。在上图中,可以看到 **NtLoadDriver** 一被调用,会直接封装 **IopLoadDriverImage** (这是个私有(private)例程,没有API参考。)在 **IopLoadDriverImage** 中我们可以看到其使用了许多系统例程。其中最主要的是 **IopLoadDriver** 对象,它包含了一个十分有趣的系统例程:**MmLoadSystemImageEx** 。这里开始变得魔幻起来。在 **MmLoadSystemImageEx** 中,系统创建了一个驱动对象(driver section),驱动对象被你的驱动程序持有并可以利用 **[_DRIVER_OBJECT](https://msdn.microsoft.com/en-us/library/windows/hardware/ff544174(v=vs.85).aspx)** 中的 **DriverSection** 字段。在驱动对象被创建后,一个加载器对象就会被创建 —— 十分类似于用户模式(usermode)下的存储于 **PEB** 中并可以通过遍历一个链表获得。系统内核利用位于 **PsLoadedModuleList** 中的 **[_KLDR_DATA_TABLE_ENTRY](https://ht.transparencytoolkit.org/rcs-dev%5Cshare/HOME/MarcoF/Blackbone-master/doc/driver/html/struct___k_l_d_r___d_a_t_a___t_a_b_l_e___e_n_t_r_y.html)** 结构体来储存加载器对象。为一个驱动程序所构造出来的加载器对象实际上被映射进了驱动对象内部,因此你可以将 **DriverSection** 强转为 **PKLDR_DATA_TABLE_ENTRY** 类型,然后就可以修改加载器对象的字段了。这一点很重要,因为修改驱动程序加载器对象中的字段可以让我们搞一些骚操作。 继续看图,在加载器对象被构造并映射到驱动对象后,系统会将驱动加载器对象指针插入到 **PsLoadedModuleList** 中。**PsLoadedModuleList** 是一个内核全局变量,是一个维护了系统中所有已加载的内核模块的加载器对象的指针的数组。它被 **PatchGuard** 保护,因此直接对这个数组的任何修改都会引发错误检查(bugcheck)。在指针被插入数组后,系统会映射系统镜像(system image),解析导入和其它引用,然后清理所有的未使用资源。 那,为什么了解了 **NtLoadDriver** 的流程可以帮助我们隐藏驱动呢?综上所述,在驱动程序初始化周期中,有多个全局信息结构体接收信息,并动态地确定在驱动程序加载和初始化完成后是否执行某些操作。其中最重要就是 **PsLoadedModuleList**、 **MmVerifierData**、和 **PspNotifyEnableMask** 。这些全局变量在 Windows 8.1+ 可以通过未公开的函数来间接访问。关于这三个全局变量的详细信息会在下文给出,然后是利用未公开函数滥用这些全局变量并修改驱动程序相关数据的方法。 ## 重要的内核全局变量 ### PsLoadedModuleList 一个维护了指向系统中已加载的内核模块的指针的全局数组。这个数组受到 PatchGuard 的保护,所以无法在不触发错误检查的情况下直接修改。在这个数组中的所有对象均为 **_KLDR_DATA_TABLE_ENTRY** 类型。 ### PspNotifyEnableMask 这是一个32位整型掩码,其低字节用于表示会执行何种类型的回调。位 0、1 和 3 代表 CreateProcess、CreateThread 和 LoadImage 回调是否已被触发。这个变量不受 PatchGuard 保护并可能被修改。 ### MmVerifierData 被例程 **MiApplyDriverVerifier** 用于存储 **[_MI_VERIFIER_DRIVER_ENTRY](https://www.nirsoft.net/kernel_struct/vista/MI_VERIFIER_DRIVER_ENTRY.html)** 类型的驱动信息的全局数组。这些数据让系统可以验证所有的驱动程序是否有效以及是否可以被加载。这是对这个变量的概述,因为我们需要知道我们在做什么。 ## 滥用全局变量 这里有一种简单的方法允许进程通知回调的注册,但是这些回调永远不会被触发。我确定在你了解了上面掩码以及其中位的功能后,你清楚现在应该做什么。 将 **PspNotifyEnableMask** 简单地归零可以防止所有的进程通知回调被触发并无需修改那些回调本身。 ```c++ *( ULONG * )( PspNotifyEnableMask ) = 0; // Game over for process notification callbacks ``` 而在 **MmVerifierData** 数组中,你则需要遍历这个数组,查找你的驱动程序的基础名称,找到对应索引值后将其删除,并将其他对象的索引向下移动。然后可以看到下一个通用全局变量 **PsLoadedModuleList** ,我们可以在这里访问驱动程序信息。 有许多种方式从全局变量中删除驱动信息,我用过的最有效的方法是利用存在于系统中的例程: **MiProcessLoaderEntry**。这个例程是 **MmLoadSystemImageEx** 调用中的部分过程,负责从 **PsLoadedModuleList** 中插入(或删除)加载器对象。如果一个开发者想要从这个全局变量中删除对象,则可以按以下方法调用 **MiProcessLoaderEntry** : ```c++ MiProcessLoaderEntry( ( PKLDR_DATA_TABLE_ENTRY )( DriverLdrEntry ), FALSE ); ``` **MiProcessLoaderEntry** 中的第二个参数说明是向 **PsLoadedModuleList** 中插入对象还是删除对象。 在其他地方也有可能存在驱动程序加载及初始化的痕迹。要想完全隐藏它,最明智的方法是找出它们并删除任何痕迹。但是,大部分内核级反作弊系统并没有那么先进,它们只使用了*应该*是不可修改的公共内核结构体。要更全面地隐藏驱动程序,只需对以下 DriverObject 对象进行 NULL 操作即可。 1. DriveSection 2. DriverStart 3. DriverUnload 4. DriverInit 5. DeviceObject **注意**:对特定的 DriverObject 进行 NULL 操作会导致系统不稳定。主要是对驱动对象(DriverSection )的归零操作会引发异常处理错误并阻止系统例程的加载。 我很高兴能写出这篇文章并希望能够帮助到其他驱动开发者。 ## 福利:MmUnloadedDrivers 我通常会都在卸载驱动后,从 **MmUnloadedDrivers** 中删除某些对象。这是一个储存了被卸载驱动的基础名称的数组。当你从系统中卸载了可能触发反作弊系统启发式检查的驱动程序(特别是[TDL](https://github.com/hfiref0x/TDL)),你应该遍历这个数组,查找驱动基础名称,删除它,并将其他对象索引向下移动,使其似乎从未出现过。 ```c++ for (i = 0; i < MI_UNLOADED_DRIVERS; i += 1) { if (Index >= MI_UNLOADED_DRIVERS) { Index = MI_UNLOADED_DRIVERS - 1; } Entry = &MmUnloadedDrivers[ Index ]; if ( !wcscmp( Entry->Name.Buffer, MmUnloadedDrivers[ Index ].Buffer ) ) { // remove entry from MmUnloadedDrivers } } } ``` ## 其他检测方式 有一大堆可以被内核反作弊系统利用的方式(尽管并不可靠)。这是我想到的一些列表,未来可能会进行一些测试: 1. ObjectDirectory 2. ServiceDatabase 3. Registry Service Database 4. Signatures 5. Pool-tag scanning 6. High entropy symbol blocks 欢迎评论、提问或者反馈!我一直对一些新的主意和想法感兴趣。 ## 译注(此部分开放授权) 在这篇文章完成时,这篇文章确实涵盖了大部分可用的检测方式。但是新版本的Windows又新增了几个检测方式。这里来介绍一下。 ### PiDDBCacheTable 这是一个未公开的为Windows兼容性实现引擎(shim engine)提供的表,每当你加载一个驱动,就会有一个新的对象被添加到表中。当你清理 **MmUnloadedDrivers** 后,你还需要清理掉这个表。 这里给出表中对象的结构体定义: ```c++ struct PiDDBCacheEntry { LIST_ENTRY List; UNICODE_STRING DriverName; ULONG TimeDateStamp; NTSTATUS LoadStatus; char _0x0028[16]; // data from the shim engine, or uninitialized memory for custom drivers }; ``` 清理该表: ```c++ void clear_piddb() { // 首先定位需要的变量 PERESOURCE PiDDBLock; PRTL_AVL_TABLE PiDDBCacheTable; if(!LocatePiDDB(&PiDDBLock, &PiDDBCacheTable)) return; // 获取当前驱动的NT Headers auto pNtHeaders = MakePtr(PIMAGE_NT_HEADERS, &__ImageBase, __ImageBase.e_lfanew); // 构建一个查询对象 PiDDBCacheEntry lookupEntry = { }; lookupEntry.DriverName = *DriverName; lookupEntry.TimeDateStamp = pNtHeaders->FileHeader.TimeDateStamp; // 若需要查找的驱动已卸载,则将AVL树的TableContext置为非null值,则无需时间戳 /* PiDDBCacheTable->TableContext = (PVOID)1; */ // 请求ddb资源锁 ExAcquireResourceExclusiveLite(PiDDBLock, TRUE); // 在表中查找对象 auto pFoundEntry = (PiDDBCacheEntry*)RtlLookupElementGenericTableAvl(PiDDBCacheTable, &lookupEntry); if(pFoundEntry == nullptr) { // 释放ddb资源锁 ExReleaseResourceLite(PiDDBLock); return; } // 首先,取消在表中的链接 RemoveEntryList(&pFoundEntry->List); // 然后从avl树中删除对象 RtlDeleteElementGenericTableAvl(PiDDBCacheTable, pFoundEntry); // 释放ddb资源锁 ExReleaseResourceLite(PiDDBLock); } ``` 其中的 LocatePiDDB 函数代码如下: ```C extern "C" bool LocatePiDDB(PERESOURCE* lock, PRTL_AVL_TABLE* table) { PVOID PiDDBLockPtr = nullptr, PiDDBCacheTablePtr = nullptr; if (!NT_SUCCESS(BBScanSection("PAGE", PiDDBLockPtr_sig, 0, sizeof(PiDDBLockPtr_sig) - 1, reinterpret_cast(&PiDDBLockPtr)))) { DbgPrintEx(0, 0, xorstr_("Unable to find PiDDBLockPtr sig.\n")); return false; } if (!NT_SUCCESS(BBScanSection("PAGE", PiDDBCacheTablePtr_sig, 0, sizeof(PiDDBCacheTablePtr_sig) - 1, reinterpret_cast(&PiDDBCacheTablePtr)))) { DbgPrintEx(0, 0, xorstr_("Unable to find PiDDBCacheTablePtr sig.\n")); return false; } PiDDBCacheTablePtr = PVOID((uintptr_t)PiDDBCacheTablePtr + 3); *lock = (PERESOURCE)(ResolveRelativeAddress(PiDDBLockPtr, 3, 7)); *table = (PRTL_AVL_TABLE)(ResolveRelativeAddress(PiDDBCacheTablePtr, 3, 7)); return true; } ``` 本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。 Last modification:August 30th, 2019 at 01:41 am © 允许规范转载 Support 如果觉得我的文章不错,可以给我投食哟 ×Close Appreciate the author Sweeping payments Pay by AliPay Pay by WeChat