模块与驱动隐藏:内核模块链表、驱动对象、设备对象、隐藏驱动模块、反隐藏技术

驱动隐藏这个话题,说实话,在安全圈子里一直是个敏感又热门的技术。我早年做Rootkit分析时,跟这玩意儿打了太多交道。今天咱们就把它彻底聊透——从内核怎么管理驱动,到怎么藏起来,再到怎么揪出来。

内核怎么管理驱动?

Windows内核维护着一套链表结构,用来跟踪所有已加载的驱动模块。这套机制的核心,就是驱动对象(DRIVER_OBJECT)。

每个驱动加载后,内核都会给它分配一个DRIVER_OBJECT结构体。这个结构体里记录着驱动的名字、入口点、卸载例程、设备对象链表等等。说白了,这就是驱动的“身份证”。

// DRIVER_OBJECT 的核心字段(简化版)
typedef struct _DRIVER_OBJECT {
    CSHORT Type;                // 类型,固定为 IO_TYPE_DRIVER
    CSHORT Size;                // 结构体大小
    PDEVICE_OBJECT DeviceObject; // 指向该驱动创建的设备对象链表
    ULONG Flags;
    PVOID DriverStart;          // 驱动在内存中的起始地址
    ULONG DriverSize;           // 驱动占用内存大小
    PVOID DriverSection;        // 指向 LDR_DATA_TABLE_ENTRY
    PDRIVER_INITIALIZE DriverInit;
    PDRIVER_UNLOAD DriverUnload;
    UNICODE_STRING DriverName;  // 驱动名称
    // ... 其他字段
} DRIVER_OBJECT, *PDRIVER_OBJECT;

嗯,这里要注意DriverSection这个字段。它指向的是LDR_DATA_TABLE_ENTRY结构体,而这个结构体才是真正被挂入内核模块链表的东西。

两条链表:驱动对象链表 vs 模块链表

很多人以为内核只有一条链表来管理驱动,其实不是。Windows维护了两条独立的链表:

链表名称 管理对象 遍历方式 用途
驱动对象链表 DRIVER_OBJECT ObGetFilterVersion / 内核调试命令 I/O 管理器使用
模块链表 LDR_DATA_TABLE_ENTRY PsLoadedModuleList 内存管理器、调试器使用

我在项目中遇到过一个问题:用WinDbg的lm命令能看到驱动,但用!drvobj却看不到。当时排查了半天,才发现是两条链表不同步导致的。你想想看,如果只隐藏其中一条,另一条还挂着,那等于白藏。

设备对象与驱动的关系

驱动对象里有个DeviceObject字段,指向该驱动创建的第一个设备对象。设备对象之间通过NextDevice指针形成链表。

// 遍历驱动下的所有设备对象
PDEVICE_OBJECT devObj = driverObj->DeviceObject;
while (devObj) {
    DbgPrint("设备对象: %wZ\n", &devObj->DeviceName);
    devObj = devObj->NextDevice;
}

为什么要提设备对象?因为很多隐藏驱动的检测手段,就是通过扫描设备对象链表来反推驱动存在的。你藏了驱动对象,但设备对象还暴露着,那就等于没藏。

核心要点:驱动隐藏的本质,就是从内核的各个管理链表中“摘除”该驱动的记录。但摘除不等于卸载,驱动代码还在内存里跑着。

隐藏驱动模块的常见手法

我见过不下十种隐藏手法,但万变不离其宗,核心就三板斧:

  1. 摘除模块链表节点——从PsLoadedModuleList中移除LDR_DATA_TABLE_ENTRY
  2. 清零驱动对象——把DRIVER_OBJECT的关键字段清空或篡改
  3. 断开设备对象链——从设备对象链表中摘除本驱动的设备

具体代码实现,我挑最核心的给你看:

// 从模块链表中摘除驱动(简化示例)
VOID HideDriverFromModuleList(PDRIVER_OBJECT DriverObject) {
    PLDR_DATA_TABLE_ENTRY entry = 
        (PLDR_DATA_TABLE_ENTRY)DriverObject->DriverSection;
    
    if (entry == NULL) return;
    
    // 摘除双向链表节点
    entry->InLoadOrderLinks.Flink->Blink = 
        entry->InLoadOrderLinks.Blink;
    entry->InLoadOrderLinks.Blink->Flink = 
        entry->InLoadOrderLinks.Flink;
    
    // 把指针置空,防止被遍历到
    DriverObject->DriverSection = NULL;
}

警告:直接摘除链表节点而不做其他处理,会导致蓝屏。因为内核在卸载驱动时,会遍历链表来找你的entry。摘除后,卸载例程可能找不到入口,造成内存泄漏。

我曾经犯过一个低级错误:只摘除了模块链表,没处理驱动对象。结果用!object \Driver\MyDrv一查,对象管理器里还挂着呢。所以,隐藏要彻底,得从三个层面同时下手:

  • 模块链表(PsLoadedModuleList)
  • 驱动对象(\Driver\ 对象目录)
  • 设备对象(\Device\ 对象目录)

反隐藏技术:怎么揪出藏起来的驱动?

道高一尺,魔高一丈。有隐藏就有反隐藏。我总结了几种有效的检测手段:

1. 内存扫描法

驱动藏得再深,代码总得在内存里跑着。通过扫描内核内存范围,寻找符合PE结构的区域,就能发现隐藏驱动。

// 伪代码:扫描内核内存中的PE头
for (each memory page in kernel range) {
    if (page starts with "MZ") {
        // 检查是否在已知模块列表中
        if (!IsInKnownModuleList(page)) {
            // 发现隐藏驱动!
            ReportHiddenDriver(page);
        }
    }
}

2. 对象目录遍历法

直接遍历\Driver\Device对象目录,对比已知驱动列表。多出来的,就是可疑对象。

3. 系统服务表钩子检测

很多隐藏驱动会Hook系统服务表(SSDT)来隐藏自己。通过对比原始服务表与当前服务表,就能发现异常。

我的经验:最有效的反隐藏手段,其实是结合多种方法交叉验证。单一方法总有漏洞可钻。比如内存扫描法,如果驱动用了内存加密或动态解密,你就扫不到。但对象目录遍历法不受影响。

知识体系总览

下面这张图,把驱动隐藏与反隐藏的完整逻辑串起来了:

驱动隐藏与反隐藏知识体系 内核管理结构 驱动对象链表 模块链表 设备对象链表 隐藏手法(三管齐下) 摘除模块链表节点 清零/篡改驱动对象 断开设备对象链 反隐藏技术(交叉验证) 内存扫描法 对象目录遍历法 SSDT钩子检测

从图上你能看到,整个体系是环环相扣的。内核用三条链表管理驱动,隐藏手法就针对这三条链表下手,反隐藏技术则从多个维度进行交叉验证。

说实话,驱动隐藏和反隐藏的对抗,本质上是一场“信息不对称”的博弈。隐藏方想方设法让内核“忘记”自己的存在,检测方则想尽办法从各种蛛丝马迹中把驱动揪出来。

我个人建议,如果你是在做安全产品,不要只依赖单一检测手段。把内存扫描、对象目录遍历、行为分析结合起来,才能构建起有效的防御。如果你是在做驱动开发,也请记住:隐藏技术是把双刃剑,用在正途上是保护知识产权,用歪了就是恶意软件。

最后说一句:无论隐藏还是反隐藏,都要在内核层面操作,稍有不慎就是蓝屏。我建议你在虚拟机里反复测试,确认无误后再部署到生产环境。

公众号:蓝海资料掘金营,微信 deep3321