首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Windows内核对象管理深度解析:从句柄到安全描述符的实现与漏洞利用

Windows内核对象管理深度解析:从句柄到安全描述符的实现与漏洞利用

原创
作者头像
用户12566962
发布2026-08-12 11:51:08
发布2026-08-12 11:51:08
1210
举报

Windows内核对象管理深度解析:从句柄到安全描述符的实现与漏洞利用

本文深入Windows内核对象管理器内部,剖析对象类型、句柄表、引用计数、安全描述符等核心机制,并附带驱动级代码实战,展示如何枚举进程句柄、劫持Token及检测隐藏对象。适合安全研究员与内核开发者。


一、引言

Windows内核设计中最精妙也最复杂的子系统之一便是对象管理器(Object Manager)。几乎所有的系统资源——进程、线程、文件、注册表键、同步事件、令牌(Token)等——均以“内核对象”的形式存在,并通过统一的接口进行生命周期管理、访问控制和命名空间组织。

理解对象管理器,不仅是编写稳定驱动的基础,更是进行内核安全研究(如进程隐藏、提权、句柄劫持、EOP漏洞挖掘)的必修课。本文将从数据结构出发,结合WRK(Windows Research Kernel)和公开符号,逐层拆解对象管理器,并给出可直接运行的驱动程序代码,演示如何遍历句柄表、解析对象头、修改安全描述符,最终实现一个简易的“隐藏进程检测器”。


二、内核对象的内存布局

2.1 _OBJECT_HEADER 与对象体

每个内核对象在内存中并非孤立的“对象体”本身,而是在其前部附加一个 _OBJECT_HEADER 结构(以及若干可选头)。布局如下:

代码语言:javascript
复制
+-----------------------+  <- 对象体起始地址 (Body)
|   Object Body         |
|   (e.g., EPROCESS)    |
+-----------------------+  <- 对象体偏移 - 0x30 (典型)
|   _OBJECT_HEADER      |
|   - PointerCount      |
|   - HandleCount       |
|   - TypeIndex         |
|   - SecurityDescriptor|
|   - ...               |
+-----------------------+  <- 对象头起始
|   (可选头: QUOTA, HANDLEINFO, etc.) |
+-----------------------+

在64位Windows 10/11上,_OBJECT_HEADER 大小为0x30字节。对象体地址 = 对象头地址 + 0x30(并非固定,可通过 OBJECT_HEADER.BodyOffset 获取,但通常为0x30)。

关键字段:

  • PointerCount:对象引用计数(ObReferenceObject/ObDereferenceObject 操作)。
  • HandleCount:当前打开的句柄数量。
  • TypeIndex:指向 _OBJECT_TYPE 数组的索引,标识对象类型(进程、线程、文件等)。
  • SecurityDescriptor:指向自相对(Self-Relative)格式的安全描述符,用于访问检查。

2.2 对象类型结构 _OBJECT_TYPE

每个类型(如 PsProcessTypePsThreadType)都是一个 _OBJECT_TYPE 实例,包含类型名称、默认非页池配额、打开/关闭回调、以及关键的方法表(如 ParseSecurityQueryName 等)。

可通过 ObGetObjectType 宏从对象头获取类型指针:

代码语言:javascript
复制
POBJECT_TYPE ObGetObjectType(PVOID ObjectBody) {
    POBJECT_HEADER header = OBJECT_HEADER_FROM_BODY(ObjectBody);
    return ObpTypeObjectType[header->TypeIndex];
}

三、句柄管理:从 HANDLE 到对象指针

用户态通过 HANDLE 引用内核对象,而内核实际维护的是每个进程独立的句柄表(Handle Table)。句柄表是一个稀疏数组,存储了 _HANDLE_TABLE_ENTRY 结构,其中包含:

  • ObjectPointerBits:对象体地址(低几位用于标志)。
  • GrantedAccess:该句柄拥有的访问掩码(如 PROCESS_ALL_ACCESS)。
  • HandleAttributes:如 OBJ_INHERITOBJ_PROTECT_CLOSE

3.1 遍历进程句柄表

下面给出驱动代码,枚举指定进程ID的所有句柄,并打印每个句柄对应的对象类型和访问权限。

代码语言:javascript
复制
#include <ntddk.h>

NTSTATUS EnumerateHandlesForProcess(HANDLE ProcessId) {
    PEPROCESS pTarget = NULL;
    NTSTATUS status = PsLookupProcessByProcessId(ProcessId, &pTarget);
    if (!NT_SUCCESS(status)) return status;

    // 获取句柄表地址(不同Windows版本偏移不同,此处动态获取)
    PHANDLE_TABLE pHandleTable = *(PHANDLE_TABLE*)((PUCHAR)pTarget + 0x4f8); // Win10 x64偏移,请根据实际更新
    if (!pHandleTable) {
        ObDereferenceObject(pTarget);
        return STATUS_UNSUCCESSFUL;
    }

    // 获取句柄表锁
    FAST_MUTEX* pMutex = &pHandleTable->HandleTableMutex;
    KeAcquireGuardedMutex(pMutex);

    // 遍历句柄表(简化:仅遍历一级表,实际需处理多层页表)
    ULONG TableSize = pHandleTable->TableSize;
    PHANDLE_TABLE_ENTRY pEntry = (PHANDLE_TABLE_ENTRY*)pHandleTable->Table;
    for (ULONG i = 0; i < TableSize; i++) {
        HANDLE_TABLE_ENTRY entry = pEntry[i];
        if (!(entry.ObjectPointerBits & 0x1)) continue; // 空闲项
        PVOID ObjectBody = (PVOID)(entry.ObjectPointerBits & ~0xF);
        POBJECT_HEADER pHeader = OBJECT_HEADER_FROM_BODY(ObjectBody);
        POBJECT_TYPE pType = ObGetObjectType(ObjectBody);
        DbgPrint("[Handle 0x%X] Type: %ws, Access: 0x%X, Body: %p\n",
                 (HANDLE)(i << 2), pType->Name.Buffer, entry.GrantedAccess, ObjectBody);
    }

    KeReleaseGuardedMutex(pMutex);
    ObDereferenceObject(pTarget);
    return STATUS_SUCCESS;
}

注意:句柄表实际是三级甚至四级页表结构(取决于系统配置),上述代码仅演示原理。生产驱动应使用 ExpLookupHandleTableEntry 等未导出函数或通过 NtQuerySystemInformation 实现。


四、安全描述符(Security Descriptor)与访问检查

每个内核对象都关联一个安全描述符,存储在 _OBJECT_HEADER.SecurityDescriptor 中(自相对格式)。当用户态调用 OpenProcessCreateFile 等API时,内核会调用 SeAccessCheck 函数,将调用者的Token与对象的DACL进行比对。

4.1 获取并解析安全描述符

以下代码演示如何从进程对象中提取安全描述符,并打印DACL中的ACE数量:

代码语言:javascript
复制
NTSTATUS GetObjectSecurityDescriptor(PVOID ObjectBody) {
    POBJECT_HEADER pHeader = OBJECT_HEADER_FROM_BODY(ObjectBody);
    PSECURITY_DESCRIPTOR pSD = (PSECURITY_DESCRIPTOR)pHeader->SecurityDescriptor;
    if (!pSD) return STATUS_NO_SECURITY_ON_OBJECT;

    // 安全描述符可能是自相对格式,需先转换成绝对格式
    SECURITY_DESCRIPTOR absoluteSD;
    ULONG sdSize = sizeof(SECURITY_DESCRIPTOR);
    RtlAbsoluteToSelfRelativeSD(pSD, &absoluteSD, &sdSize);

    PACL pDacl = NULL;
    BOOLEAN present, defaulted;
    SeQuerySecurityDescriptorInfo(NULL, &pDacl, &present, &defaulted, pSD, &sdSize, NULL);
    if (present && pDacl) {
        DbgPrint("DACL has %u ACEs\n", pDacl->AceCount);
    }
    return STATUS_SUCCESS;
}

4.2 修改安全描述符以实现权限提升(演示)

在驱动中,通过 ObSetSecurityDescriptor 可以替换对象的安全描述符(需要 SeSecurityPrivilege)。下面是一个危险但常见的提权示例——将任意进程对象的DACL改为允许所有访问:

代码语言:javascript
复制
NTSTATUS GrantFullAccessToProcess(PEPROCESS Process) {
    POBJECT_HEADER pHeader = OBJECT_HEADER_FROM_BODY(Process);
    // 构造一个允许所有访问的DACL(实际应调用SeCreateSecurityDescriptor等API)
    // 此仅为简化演示,真实场景需分配非页池并设置正确ACE。
    PACL pNewDacl = NULL; // 需实际构造
    NTSTATUS status = SeSetSecurityDescriptorInfo(NULL, &pNewDacl, &pHeader->SecurityDescriptor,
                                                   SE_OBJECT_TYPE_PROCESS, FALSE, FALSE);
    return status;
}

警告:直接修改内核对象的SD会绕过所有安全检查,极易导致系统不稳定或被检测为Rootkit,仅供研究使用。


五、对象引用计数与生命周期

PointerCountHandleCount 共同决定对象的销毁时机。当 PointerCount 降为0时,对象将被释放回池。

  • ObReferenceObjectByHandle 会增加引用计数。
  • ObDereferenceObject 减少引用计数。
  • 句柄关闭时(NtClose),HandleCount 减1,若变为0且引用计数为0,则对象销毁。

5.1 检测“无句柄进程”隐藏对象

某些Rootkit通过创建进程后关闭所有句柄,并移除EPROCESS链表,使得任务管理器无法发现。但对象管理器仍保留其引用(如驱动自己持有的引用)。我们可以遍历对象目录或句柄表来发现此类对象。

利用 ObReferenceObjectByNameZwQuerySystemInformation(SystemHandleInformation)可以枚举全局句柄。以下代码通过 ZwQuerySystemInformation 枚举所有句柄,并统计每个进程对象的句柄数,输出无句柄但活动的进程:

代码语言:javascript
复制
NTSTATUS DetectHandleLessProcess() {
    PSYSTEM_HANDLE_INFORMATION pInfo = NULL;
    ULONG size = 0;
    ZwQuerySystemInformation(SystemHandleInformation, NULL, 0, &size);
    pInfo = (PSYSTEM_HANDLE_INFORMATION)ExAllocatePool(NonPagedPool, size);
    if (!pInfo) return STATUS_NO_MEMORY;

    ZwQuerySystemInformation(SystemHandleInformation, pInfo, size, &size);

    // 统计每个PID的句柄数
    for (ULONG i = 0; i < pInfo->NumberOfHandles; i++) {
        // 忽略无效
    }
    // 然后对比进程列表,若EPROCESS存在但句柄数为0且未终止,则可能是隐藏对象。

    ExFreePool(pInfo);
    return STATUS_SUCCESS;
}

六、对象命名空间与符号链接

命名对象(如 \Device\\BaseNamedObjects\)通过 ObpRootDirectoryObject 管理,使用 ObCreateObject 并指定 ObjectAttributes 中的名称即可。

遍历命名空间可使用 ZwQueryDirectoryObject,驱动中可枚举所有设备对象。以下是枚举 \Device 下所有对象的代码:

代码语言:javascript
复制
NTSTATUS EnumerateDeviceObjects() {
    UNICODE_STRING deviceDir = RTL_CONSTANT_STRING(L"\\Device");
    OBJECT_ATTRIBUTES attr;
    InitializeObjectAttributes(&attr, &deviceDir, OBJ_CASE_INSENSITIVE, NULL, NULL);
    HANDLE hDir;
    NTSTATUS status = ZwOpenDirectoryObject(&hDir, DIRECTORY_QUERY, &attr);
    if (!NT_SUCCESS(status)) return status;

    ULONG bufSize = 0x1000;
    POBJECT_DIRECTORY_INFORMATION pInfo = (POBJECT_DIRECTORY_INFORMATION)ExAllocatePool(PagedPool, bufSize);
    ULONG returnLen;
    status = ZwQueryDirectoryObject(hDir, pInfo, bufSize, FALSE, TRUE, &returnLen, NULL);
    if (NT_SUCCESS(status)) {
        for (ULONG i = 0; pInfo[i].Name.Buffer; i++) {
            DbgPrint("Object: %wZ, Type: %wZ\n", &pInfo[i].Name, &pInfo[i].TypeName);
        }
    }
    ExFreePool(pInfo);
    ZwClose(hDir);
    return status;
}

七、实战:编写驱动监视进程创建并记录安全描述符

下面给出一个完整的小型驱动,在 DriverEntry 中注册进程创建回调,每次新进程创建时,打印其对象头的安全描述符和句柄计数。

代码语言:javascript
复制
VOID ProcessCreateNotify(HANDLE ParentId, HANDLE ProcessId, BOOLEAN Create) {
    if (!Create) return;
    PEPROCESS pProcess;
    if (NT_SUCCESS(PsLookupProcessByProcessId(ProcessId, &pProcess))) {
        POBJECT_HEADER pHeader = OBJECT_HEADER_FROM_BODY(pProcess);
        DbgPrint("[Process %d] HandleCount=%d, PointerCount=%d, SD=%p\n",
                 ProcessId, pHeader->HandleCount, pHeader->PointerCount, pHeader->SecurityDescriptor);
        ObDereferenceObject(pProcess);
    }
}

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pRegPath) {
    PsSetCreateProcessNotifyRoutine(ProcessCreateNotify, FALSE);
    return STATUS_SUCCESS;
}

VOID DriverUnload(PDRIVER_OBJECT pDriver) {
    PsSetCreateProcessNotifyRoutine(ProcessCreateNotify, TRUE);
}

八、安全检测与对抗思路

  1. 对象头篡改检测:校验 _OBJECT_HEADERTypeIndex 是否在合法范围内,以及 SecurityDescriptor 是否指向有效的自相对SD。
  2. 句柄表完整性:遍历句柄表,检查 ObjectPointerBits 是否指向有效的池内存(可使用 MmIsAddressValid)。
  3. 回调扫描:枚举 PsProcessNotify 回调、CmRegisterCallback 等,检测是否被恶意驱动劫持。

九、总结

Windows内核对象管理器是操作系统的骨架,本文从内存布局、句柄表、安全描述符到引用计数,给出了详尽的代码示例。理解这些底层机制,能够帮助开发者编写高效、安全的驱动,也为安全研究人员提供了检测高级威胁(如Rootkit、内存驻留木马)的知识基础。

在实际开发中,务必注意版本差异(不同Windows build的偏移和结构体大小不同),建议结合微软公开符号(ntoskrnl.pdb)动态定位。同时,操作安全描述符和引用计数需极其谨慎,否则极易引发蓝屏。

延伸阅读:WRK中的 ob.hntobapi.h;Windows Internals 7th Edition Part 1 Chapter 5。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • Windows内核对象管理深度解析:从句柄到安全描述符的实现与漏洞利用
    • 一、引言
    • 二、内核对象的内存布局
      • 2.1 _OBJECT_HEADER 与对象体
      • 2.2 对象类型结构 _OBJECT_TYPE
    • 三、句柄管理:从 HANDLE 到对象指针
      • 3.1 遍历进程句柄表
    • 四、安全描述符(Security Descriptor)与访问检查
      • 4.1 获取并解析安全描述符
      • 4.2 修改安全描述符以实现权限提升(演示)
    • 五、对象引用计数与生命周期
      • 5.1 检测“无句柄进程”隐藏对象
    • 六、对象命名空间与符号链接
    • 七、实战:编写驱动监视进程创建并记录安全描述符
    • 八、安全检测与对抗思路
    • 九、总结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档