第7章:对象与句柄
# 第7章:对象与句柄(Objects and Handles)
Windows 中的大部分功能都是通过内核对象(kernel objects)提供的。用户模式(user-mode)下访问内核对象始终需要通过句柄(handles)间接进行。本章将介绍几种常见的对象类型,以及对象和句柄的通用操作方式。
本章内容包括:
- 对象(Objects)
- 枚举对象(Enumerating Objects)
- 对象管理器命名空间(Object Manager Namespace)
- 句柄(Handles)
- 枚举句柄(Enumerating Handles)
- 特定对象类型(Specific Object Types)
# 7.1 对象(Objects)
Windows 内核支持一组对象类型(object types),可以基于这些类型创建对象。每个对象都属于某一类型(类型本身也是一种对象)。Windows API 提供了许多函数用于创建对象或打开现有对象的句柄。每种对象类型都有其专属的打开和/或创建函数,以及用于操作现有对象的特定函数。
对象本身是位于内核空间(kernel space)中的数据结构,这也是用户模式无法直接访问对象的原因之一——即使知道对象的地址也不行。
# 7.1.1 对象类型(Object Types)
特定版本的 Windows 所支持的对象类型集合可以通过“对象资源管理器”(Object Explorer,图 7-1)等工具查看。

图 7-1:对象资源管理器显示的对象类型
对象资源管理器会为每种对象类型显示以下信息:对象总数、句柄总数、对象峰值数、句柄峰值数,以及后续将讨论的其他一些详细信息。
获取对象资源管理器中显示的这类信息,需要调用 NtQueryObject 函数:
typedef enum _OBJECT_INFORMATION_CLASS {
ObjectBasicInformation, // OBJECT_BASIC_INFORMATION
ObjectNameInformation, // OBJECT_NAME_INFORMATION
ObjectTypeInformation, // OBJECT_TYPE_INFORMATION
ObjectTypesInformation, // OBJECT_TYPES_INFORMATION
ObjectHandleFlagInformation, // OBJECT_HANDLE_FLAG_INFORMATION
ObjectSessionInformation,
ObjectSessionObjectInformation,
MaxObjectInfoClass
} OBJECT_INFORMATION_CLASS;
NTSTATUS NtQueryObject(
_In_opt_ HANDLE Handle,
_In_ OBJECT_INFORMATION_CLASS ObjectInformationClass,
_Out_writes_bytes_opt_ (ObjectInformationLength) PVOID ObjectInformation,
_In_ ULONG ObjectInformationLength,
_Out_opt_ PULONG ReturnLength);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
通常,调用 NtQueryObject 需要一个有效的句柄,但有一个例外:若要获取对象类型信息,只需指定 ObjectTypesInformation 作为信息类,并传入 NULL 句柄即可。返回的结构如下:
typedef struct _OBJECT_TYPE_INFORMATION {
UNICODE_STRING TypeName;
ULONG TotalNumberOfObjects;
ULONG TotalNumberOfHandles;
ULONG TotalPagedPoolUsage;
ULONG TotalNonPagedPoolUsage;
ULONG TotalNamePoolUsage;
ULONG TotalHandleTableUsage;
ULONG HighWaterNumberOfObjects;
ULONG HighWaterNumberOfHandles;
ULONG HighWaterPagedPoolUsage;
ULONG HighWaterNonPagedPoolUsage;
ULONG HighWaterNamePoolUsage;
ULONG HighWaterHandleTableUsage;
ULONG InvalidAttributes;
GENERIC_MAPPING GenericMapping;
ULONG ValidAccessMask;
BOOLEAN SecurityRequired;
BOOLEAN MaintainHandleCount;
UCHAR TypeIndex;
CHAR ReservedByte;
ULONG PoolType;
ULONG DefaultPagedPoolCharge;
ULONG DefaultNonPagedPoolCharge;
} OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;
typedef struct _OBJECT_TYPES_INFORMATION {
ULONG NumberOfTypes;
// 后续紧跟 OBJECT_TYPE_INFORMATION 结构
} OBJECT_TYPES_INFORMATION, *POBJECT_TYPES_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
每种对象类型的大小都不同(因为包含类型名称 TypeName 成员),因此切换到下一种对象类型需要一些技巧——下一种对象类型始终从指针对齐(pointer-aligned)的地址开始,第一种类型也同样从这样的对齐地址开始。以下代码展示了如何遍历所有对象类型并显示部分关键信息(省略了错误处理):
auto size = 1 << 16;
auto buffer = std::make_unique<BYTE[]>(size);
NtQueryObject(nullptr, ObjectTypesInformation, buffer.get(), size, nullptr);
auto types = (OBJECT_TYPES_INFORMATION*)buffer.get();
//
// 第一种类型从指针大小偏移量的地址开始
//
auto type = (OBJECT_TYPE_INFORMATION*)((PBYTE)types + sizeof(PVOID));
for (ULONG i = 0; i < types->NumberOfTypes; i++)
{
printf("%-33wZ (%2d) O: %7u H: %7u PO: %7u PH: %7u\n ",
&type->TypeName, type->TypeIndex,
type->TotalNumberOfObjects, type->TotalNumberOfHandles,
type->HighWaterNumberOfObjects, type->HighWaterNumberOfHandles);
//
// 切换到下一种对象类型
//
auto temp = (PBYTE)type + sizeof(OBJECT_TYPE_INFORMATION) +
type->TypeName.MaximumLength;
//
// 向上取整到下一个指针大小的地址
//
type = (OBJECT_TYPE_INFORMATION*)(((ULONG_PTR)temp +
sizeof(PVOID) - 1) / sizeof(PVOID) * sizeof(PVOID));
}
printf("Total Types: %u\n ", types->NumberOfTypes);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
完整代码位于 ObjectTypes 项目中。
以下是在一台较新的 Windows 10 机器上的示例输出:
| 类型(Type) | ( 2) O: | 69 | H: | 0 | PO: | 69 | PH: | 1 |
|---|---|---|---|---|---|---|---|---|
| 目录(Directory) | ( 3) O: | 219 | H: | 1670 | PO: | 222 | PH: | 1680 |
| 符号链接(SymbolicLink) | ( 4) O: | 789 | H: | 502 | PO: | 813 | PH: | 532 |
| 令牌(Token) | ( 5) O: | 21626 | H: | 11868 | PO: | 27878 | PH: | 12435 |
| 作业(Job) | ( 6) O: | 656 | H: | 584 | PO: | 5971 | PH: | 595 |
| 进程(Process) | ( 7) O: | 1693 | H: | 12957 | PO: | 8845 | PH: | 30693 |
| 线程(Thread) | ( 8) O: | 19402 | H: | 24711 | PO: | 22820 | PH: | 25008 |
| ... | ||||||||
| 跨虚拟机事件(CrossVmEvent) | (68) O: | 0 | H: | 0 | PO: | 0 | PH: | 0 |
| 跨虚拟机互斥体(CrossVmMutant) | (69) O: | 0 | H: | 0 | PO: | 0 | PH: | 0 |
| 虚拟注册表配置上下文(VRegConfigurationContext) | (70) O: | 23 | H: | 0 | PO: | 358 | PH: | 0 |
| 类型总数(Total Types): 69 |
OBJECT_TYPE_INFORMATION 结构为每种对象类型提供了以下详细信息:
TypeName:对象类型的名称。TotalNumberOfObjects:当前该类型的对象总数。TotalNumberOfHandles:当前该类型对象的已打开句柄总数。HighWaterNumberOfObjects和HighWaterNumberOfHandles:分别为系统启动以来该类型对象和句柄的峰值数量。InvalidAttributes:指示无法应用于该类型对象句柄的无效属性(OBJ_xxx)。GenericMapping:存储通用访问权限(generic access)到特定访问权限(specific access)的映射。GENERIC_MAPPING是文档化的结构,定义如下(位于 WinNt.h 中):
typedef struct _GENERIC_MAPPING {
ACCESS_MASK GenericRead;
ACCESS_MASK GenericWrite;
ACCESS_MASK GenericExecute;
ACCESS_MASK GenericAll;
} GENERIC_MAPPING;
2
3
4
5
6
对于每种对象类型,GENERIC_READ、GENERIC_WRITE、GENERIC_EXECUTE 和 GENERIC_ALL 的含义各不相同——此映射结构提供了具体细节。例如,进程(Process)对象将 GENERIC_READ 映射为 PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | READ_CONTROL。
ValidAccessMask:指示该类型对象句柄的有效访问权限掩码位(例如,进程对象类型的有效访问权限掩码为PROCESS_ALL_ACCESS)。SecurityRequired:指示如果该类型对象以默认安全性创建,是否必须具有安全描述符(security descriptor)。通常,可命名的对象会将此值设为TRUE。MaintainHandleCount:如果内核会维护每个进程中该类型对象的句柄数量,则设为TRUE。TypeIndex:该类型的内部类型索引。PoolType:指示用于分配该类型对象的池类型(pool type),取值为PagedPool(1)、NonPagedPool(0)、NonPagedPoolNx(0x200,无执行权限的非分页池)或PagedPoolSession(0x21)。例如,进程对象从NonPagedPoolNx分配,这意味着这些对象始终保存在内存(RAM)中。DefaultPagedPoolCharge和DefaultNonPagedPoolCharge:如果该类型对象分别从分页池(paged pool)或非分页池(non-paged pool)分配,创建者进程的默认分配费用。
其他所有成员的值始终为 0。
# 7.2 枚举对象(Enumerating Objects)
我们能否获取系统中所有对象的列表?查看 NtQuerySystemInformation 函数时,会发现一个看似可行的 SystemObjectInformation 参数值。但默认情况下,内核不会在某个列表中跟踪对象的创建。调用 NtQuerySystemInformation 并传入 SystemObjectInformation 会返回 STATUS_UNSUCCESSFUL。
要启用对象跟踪,必须设置一个全局标志(global flag)。最简单的设置方法是运行 Windows SDK 中的 Gflags 工具,并勾选“为每种类型维护对象列表”(Maintain a list of objects for each type,图 7-2)。此操作会修改注册表中的一个值,且需要重启系统才能使该标志生效。重启后,对象枚举功能即可正常使用。

图 7-2:Gflags 工具
| 为什么默认不启用此选项?因为启用后会产生一定的开销——在过去(遥远的)硬件性能较弱、内存容量较小的时代,这种开销尤其需要避免。虽然难以精确估算该开销的大小,但它确实存在:创建对象时需要消耗更多 CPU 时间,同时维护这些额外信息也需要更多内存。 |
|---|
勾选“为每种类型维护对象列表”会在注册表项
HKLM\SYSTEM\CurrentControlSet\Control\Session Manager中的GlobalFlag值添加 0x4000。系统启动时会读取该值。
枚举对象的第一步是分配足够大的缓冲区。如果传入的缓冲区大小为 0,API 不会返回所需的缓冲区大小——因为计算所需字节数需要遍历所有对象,效率较低。相反,API 会尝试将尽可能多的对象填入提供的缓冲区;如果缓冲区不足,则调用会失败并返回 STATUS_INFO_LENGTH_MISMATCH。此时需要分配更多内存并重新尝试。以下是一种实现方式:
ULONG size = 1 << 22;
auto buffer = std::make_unique<BYTE[]>(size);
NTSTATUS status;
while ((status = NtQuerySystemInformation(SystemObjectInformation, buffer.get(), size, nullptr)) == STATUS_INFO_LENGTH_MISMATCH)
{
size *= 2;
buffer = std::make_unique<BYTE[]>(size);
}
if (!NT_SUCCESS(status))
{
printf("Object tracking is not turned on.\n ");
return status;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上述代码初始分配 4MB(1 << 22)的缓冲区,每次缓冲区不足时将其容量翻倍。要使该调用成功,预计至少需要分配 12MB 的内存。
上述代码还处理了因其他原因导致调用失败的情况(这通常意味着未设置全局标志)。为简化起见,代码忽略了内存分配失败的情况。
现在可以开始枚举对象了。返回的信息分为两层:第一层是 SYSTEM_OBJECTTYPE_INFORMATION 结构,提供对象类型的相关信息(与 OBJECT_TYPE_INFORMATION 相似但不完全相同);紧接着是该类型的对象列表,以 SYSTEM_OBJECT_INFORMATION 结构表示。由于这些结构包含名称(类型名称和对象名称,对象名称可能不存在),因此没有固定大小,枚举过程较为复杂。
SYSTEM_OBJECTTYPE_INFORMATION 的定义如下:
typedef struct _SYSTEM_OBJECTTYPE_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfObjects;
ULONG NumberOfHandles;
ULONG TypeIndex;
ULONG InvalidAttributes;
GENERIC_MAPPING GenericMapping;
ULONG ValidAccessMask;
ULONG PoolType;
BOOLEAN SecurityRequired;
BOOLEAN WaitableObject;
UNICODE_STRING TypeName;
} SYSTEM_OBJECTTYPE_INFORMATION, *PSYSTEM_OBJECTTYPE_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
13
与对象类型信息(OBJECT_TYPE_INFORMATION)的区别如下:
- 下一个类型对象位于当前对象偏移下一个条目偏移量(NextEntryOffset)字节处。
- 新增可等待对象(WaitableObject)标志,用于指示该类型的对象是否为可等待对象(调度程序对象)。
- 与对象类型信息(OBJECT_TYPE_INFORMATION)相比,缺少部分详情。
每个对象都对应一个系统对象信息(SYSTEM_OBJECT_INFORMATION)结构:
typedef struct _SYSTEM_OBJECT_INFORMATION {
ULONG NextEntryOffset;
PVOID Object;
HANDLE CreatorUniqueProcess;
USHORT CreatorBackTraceIndex;
USHORT Flags;
LONG PointerCount;
LONG HandleCount;
ULONG PagedPoolCharge;
ULONG NonPagedPoolCharge;
HANDLE ExclusiveProcessId;
PVOID SecurityDescriptor;
UNICODE_STRING NameInfo;
} SYSTEM_OBJECT_INFORMATION, *PSYSTEM_OBJECT_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
13
14
以下是其成员的描述:
- 下一个条目偏移量(NextEntryOffset):存储从结果缓冲区起始位置到下一个对象的字节偏移量。
- 对象(Object):对象在内核空间中的地址。
- 创建者唯一进程(CreatorUniqueProcess):创建该对象的进程ID。
- 创建者回溯索引(CreatorBackTraceIndex):似乎始终为0。
- 标志(Flags):对象头部中设置的标志。可能的标志如下:
typedef enum {
OBF_NEW_OBJECT = 0x01,
OBF_KERNEL_OBJECT = 0x02,
OBF_KERNEL_ONLY_ACCESS = 0x04,
OBF_EXCLUSIVE_OBJECT = 0x08,
OBF_PERMANENT_OBJECT = 0x10,
OBF_DEFAULT_SECURITY_QUOTA = 0x20,
OBF_SINGLE_HANDLE_ENTRY = 0x40,
OBF_DELETED_INLINE = 0x80
} OBJECT_HEADER_FLAGS;
2
3
4
5
6
7
8
9
10
- 指针计数(PointerCount):对象的引用计数与反转的使用计数混合在一起(Windows 7.1及更高版本)。由于包含使用计数,不应依赖此值。更多详情请参见《Windows内部原理(第7版,第二部分)》(Windows Internals 7th edition, Part 2)一书。
- 句柄计数(HandleCount):指向该对象的已打开句柄数量(如果仅由内核代码使用,则可能为0)。
- 分页池占用(PagedPoolCharge)和非分页池占用(NonPagedPoolCharge):该对象的内存分配占用量,与该对象类型的报告值相同。
- 独占进程ID(ExclusiveProcessId):通常为0,但如果对象是独占的,则为拥有该对象的进程ID。独占对象不能被其他任何进程使用,这种权限仅内核模式代码拥有。
- 安全描述符(SecurityDescriptor):附加到该对象的安全描述符(如果有)。
- 名称信息(NameInfo):对象的名称(如果有)。
根据上述定义,以下是遍历所有类型以及每个类型下所有对象的完整代码:
auto type = (SYSTEM_OBJECTTYPE_INFORMATION*)buffer.get();
for(;;) {
//
// 对 type 执行相关操作
//
printf("%wZ O: %u H: %u\n",
&type->TypeName, type->NumberOfObjects, type->NumberOfHandles);
//
// 获取第一个对象
//
auto obj = (SYSTEM_OBJECT_INFORMATION*)
((PBYTE)type + sizeof(*type) + type->TypeName.MaximumLength);
for (;;) {
//
// 对对象执行相关操作
//
printf("0x%p (%wZ) P: 0x%X H: %u PID: %u EPID: %u\n",
obj->Object, &obj->NameInfo, obj->PointerCount, obj->HandleCount,
HandleToULong(obj->CreatorUniqueProcess),
HandleToULong(obj->ExclusiveProcessId));
//
// 若无更多对象,退出循环
//
if (obj->NextEntryOffset == 0)
break;
//
// 移动到下一个对象
//
obj = (SYSTEM_OBJECT_INFORMATION*)(buffer.get() + obj->NextEntryOffset);
}
//
// 若无更多类型,退出循环
//
if (type->NextEntryOffset == 0)
break;
//
// 移动到下一个类型
//
type = (SYSTEM_OBJECTTYPE_INFORMATION*)(buffer.get() + type->NextEntryOffset);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
注意,“对象类型(ObjectType)”及其对象不会作为独立对象在枚举中提供,因为类型对象本身就是这些对象。
完整代码可在 AllObjects 项目中找到。
# 7.3 对象管理器命名空间(Object Manager Namespace)
命名对象由内核的对象管理器(Object Manager)以树形层次结构进行管理。可通过WinObj(来自Sysinternals)和我自己开发的Object Explorer等工具查看该层次结构。图7-3展示了WinObj工具(建议以管理员权限运行,以查看完整的目录对象结构)。

图7-3:Sysinternals的WinObj工具
WinObj中显示的“目录”与文件系统无关,而是目录内核对象(Directory kernel objects),它们是其他内核对象(包括其他目录)的容器。以下简要描述部分较“知名”的目录对象:
\BaseNamedObjects:包含会话0(session 0)的命名对象。\Device:包含设备对象,代表物理或虚拟设备。\Driver:包含驱动程序对象,系统中加载的每个内核驱动程序对应一个该对象。\Global??(也称为\??):包含符号链接(symbolic links),这些符号链接可作为合法值传递给创建文件函数(CreateFile)Windows API(前缀为\\.\)。原生API对应函数(创建文件函数(NtCreateFile)和打开文件函数(NtOpenFile))不受限于此目录。\KnownDlls和\KnownDlls32:包含节对象(Section objects),这些对象映射到Windows启动初期(由会话管理器进程(Smss.exe))从注册表项HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs中读取的“已知动态链接库(known DLLs)”。\ObjectType:包含所有类型对象。\Callback:包含回调内核对象(Callback kernel objects)。\Sessions\n\BaseNamedObjects:包含由会话n(n为会话编号)中的进程创建的命名内核对象。
查询目录对象的内容需要打开该目录的句柄:
NTSTATUS NtOpenDirectoryObject(
_Out_ PHANDLE DirectoryHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes);
2
3
4
对象属性(ObjectAttributes)通过常用的初始化对象属性函数(InitializeObjectAttributes)指定目录名称。以下代码打开根目录的句柄:
HANDLE hDirectory;
UNICODE_STRING name;
RtlInitUnicodeString(&name, L"\\");
OBJECT_ATTRIBUTES attr;
InitializeObjectAttributes(&attr, &name, 0, nullptr, nullptr);
status = NtOpenDirectoryObject(&hDirectory, DIRECTORY_QUERY, &attr);
if(!NT_SUCCESS(status)) {
// 对 hDirectory 执行相关操作
NtClose(hDirectory);
}
2
3
4
5
6
7
8
9
10
访问掩码(AccessMask)参数指定对目录的请求访问权限,可组合以下访问标志:
#define DIRECTORY_QUERY 0x0001
#define DIRECTORY_TRAVERSE 0x0002
#define DIRECTORY_CREATE_OBJECT 0x0004
#define DIRECTORY_CREATE_SUBDIRECTORY 0x0008
#define DIRECTORY_ALL_ACCESS (STANDARD_RIGHTS_REQUIRED | 0xf)
2
3
4
5
获取目录句柄后,可通过查询目录对象函数(NtQueryDirectoryObject)获取目录内容:
typedef struct _OBJECT_DIRECTORY_INFORMATION {
UNICODE_STRING Name;
UNICODE_STRING TypeName;
} OBJECT_DIRECTORY_INFORMATION, *POBJECT_DIRECTORY_INFORMATION;
NTSTATUS NtQueryDirectoryObject(
_In_ HANDLE DirectoryHandle,
_Out_writes_bytes_opt_(Length) PVOID Buffer,
_In_ ULONG Length,
_In_ BOOLEAN ReturnSingleEntry,
_In_ BOOLEAN RestartScan,
_Inout_ PULONG Context,
_Out_opt_ PULONG ReturnLength);
2
3
4
5
6
7
8
9
10
11
12
13
该函数的使用方式并不直观,以下是参数描述:
- 目录句柄(DirectoryHandle):目标目录的输入句柄。
- 缓冲区(Buffer):调用者分配的用于接收结果的缓冲区,长度(Length)是该缓冲区的字节大小。
- 返回单个条目(ReturnSingleEntry):指定是否请求单个条目。如果为FALSE,缓冲区中会尽可能多地返回条目。
- 重新开始扫描(RestartScan):指定检索是否应从起始位置开始。通常,第一次调用(如果检索多个结果)时设置为TRUE,若提供的缓冲区大小不足,后续调用时设置为FALSE。
- 上下文(Context):请求的条目索引以及实际检索到的最后一个条目索引。通常,第一次调用时设置为0,若缓冲区过小,后续调用时继续使用该值。
- 返回长度(ReturnLength):常规可选返回值,指示实际使用的字节数(或若单个条目也超出缓冲区大小,则指示所需的字节数)。
为简化查询目录对象函数(NtQueryDirectoryObject)的使用,可传递一个大缓冲区,这样大概率无需进行第二次调用。但无法确定足够大的缓冲区大小,因为这很大程度上取决于目标目录和代码运行的系统,因此最好准备进行多次调用。
返回的缓冲区是对象目录信息(OBJECT_DIRECTORY_INFORMATION)对象数组,提供对象的名称及其类型名称。幸运的是,可直接通过数组访问,因为尽管提供了对象的名称和类型,但字符串本身存储在数组之后,无需进行特殊计算。
假设目录句柄(hDirectory)是有效的目录句柄,以下示例展示如何使用(可能)有限的缓冲区正确获取该目录中的所有对象:
ULONG index = 0;
bool first = true;
ULONG size = 1 << 12; // 示例大小(4KB)
auto buffer = std::make_unique<BYTE[]>(size);
auto data = (POBJECT_DIRECTORY_INFORMATION)buffer.get();
int start = 0;
for(;;) {
status = NtQueryDirectoryObject(hDirectory, buffer.get(), size,
FALSE, first, &index, nullptr);
if (!NT_SUCCESS(status))
break;
first = false;
//
// 循环处理上一次调用返回的条目
//
for (ULONG i = 0; i < index - start; i++) {
//
// 对 data[i].Name 和 data[i].TypeName 执行相关操作
//
}
start = index;
if (status != STATUS_MORE_ENTRIES)
break;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 7.3.1 符号链接(Symbolic Links)
符号链接(SymbolicLink)对象类型是一种内核对象,指向另一个内核对象(本质上类似于外壳快捷方式)。给定符号链接对象名称,可通过打开其句柄(打开符号链接对象函数(NtOpenSymbolicLinkObject)),然后查询其目标(查询符号链接对象函数(NtQuerySymbolicLinkObject))来获取其目标:
NTSTATUS NtOpenSymbolicLinkObject(
_Out_ PHANDLE LinkHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes);
NTSTATUS NtQuerySymbolicLinkObject(
_In_ HANDLE LinkHandle,
_Inout_ PUNICODE_STRING LinkTarget,
_Out_opt_ PULONG ReturnedLength);
2
3
4
5
6
7
8
9
此时,这些参数的含义应不言自明。以下示例展示如何根据C字符串格式的符号链接对象名称获取符号链接目标:
std::wstring GetSymbolicLinkTarget(PCWSTR symolicLinkName) {
UNICODE_STRING linkName;
RtlInitUnicodeString(&linkName, symolicLinkName);
OBJECT_ATTRIBUTES attr;
InitializeObjectAttributes(&attr, &linkName, 0, nullptr, nullptr);
//
// 打开符号链接的句柄
//
HANDLE hLink;
auto status = NtOpenSymbolicLinkObject(&hLink, GENERIC_READ, &attr);
if (!NT_SUCCESS(status))
return L"";
WCHAR buffer[512]{}; // 实际使用中足够大的缓冲区
UNICODE_STRING target;
RtlInitUnicodeString(&target, buffer);
target.MaximumLength = sizeof(buffer);
//
// 查询目标
//
status = NtQuerySymbolicLinkObject(hLink, &target, nullptr);
NtClose(hLink);
return buffer;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
如果权限足够,也可以创建符号链接:
NTSTATUS NtCreateSymbolicLinkObject(
_Out_ PHANDLE LinkHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ PUNICODE_STRING LinkTarget);
2
3
4
5
# 7.3.2 ObjDir示例(The ObjDir Sample)
ObjDir示例整合了本节介绍的大部分内容,通过命令行查询指定目录的内容,显示对象名称、类型名称以及符号链接目标(当然,仅针对符号链接对象)。
主函数(main函数)获取目录,调用辅助函数获取结果,若成功则显示结果:
int wmain(int argc, const wchar_t* argv[]) {
if (argc < 2) {
printf("Usage: ObjDir <directory>\n");
return 0;
}
NTSTATUS status;
auto objects = EnumDirectoryObjects(argv[1], status);
if (objects.empty() && !NT_SUCCESS(status)) {
printf("Error: 0x%X\n", status);
}
else {
printf("Directory: %ws\n", argv[1]);
printf("%-20s %-50s Link Target\n", "Type", "Name");
printf("%-20s %-50s -----------\n", "----", "----");
for (auto& obj : objects) {
printf("%-20ws %-50ws %ws\n",
obj.TypeName.c_str(), obj.Name.c_str(), obj.LinkTarget.c_str());
}
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
返回的向量(vector)类型为对象信息(ObjectInfo):
struct ObjectInfo {
std::wstring Name;
std::wstring TypeName;
std::wstring LinkTarget;
};
2
3
4
5
核心函数是枚举目录对象函数(EnumDirectoryObjects):
std::vector<ObjectInfo> EnumDirectoryObjects(PCWSTR directory, NTSTATUS& status) {
HANDLE hDirectory;
UNICODE_STRING name;
RtlInitUnicodeString(&name, directory);
OBJECT_ATTRIBUTES attr;
InitializeObjectAttributes(&attr, &name, 0, nullptr, nullptr);
status = NtOpenDirectoryObject(&hDirectory, DIRECTORY_QUERY, &attr);
if (!NT_SUCCESS(status))
return {};
ULONG index = 0;
bool first = true;
ULONG size = 1 << 12;
auto buffer = std::make_unique<BYTE[]>(size);
auto data = (POBJECT_DIRECTORY_INFORMATION)buffer.get();
int start = 0;
std::vector<ObjectInfo> objects;
for(;;) {
status = NtQueryDirectoryObject(hDirectory, buffer.get(), size, FALSE,
first, &index, nullptr);
if (!NT_SUCCESS(status))
break;
first = false;
for (ULONG i = 0; i < index - start; i++) {
ObjectInfo info;
//
// 将 UNICODE_STRING 转换为 std::wstring
//
info.Name.assign(data[i].Name.Buffer,
data[i].Name.Length / sizeof(WCHAR));
info.TypeName.assign(data[i].TypeName.Buffer,
data[i].TypeName.Length / sizeof(WCHAR));
if (info.TypeName == L"SymbolicLink")
info.LinkTarget = GetSymbolicLinkTarget(directory, info.Name);
objects.push_back(std::move(info));
}
start = index;
if (status != STATUS_MORE_ENTRIES)
break;
}
NtClose(hDirectory);
return objects;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
该代码与前面展示的示例基本相同,不同之处在于结果存储在vector<ObjectInfo>中,且状态(status)作为第二个参数返回。最后一部分是获取符号链接目标函数(GetSymbolicLinkTarget),仅当当前对象的类型名称为“SymbolicLink”时调用:
std::wstring
GetSymbolicLinkTarget(std::wstring const& directory, std::wstring const& name) {
auto fullName = directory == L"\\" ? (L"\\" + name) : (directory + L"\\" + name);
UNICODE_STRING linkName;
RtlInitUnicodeString(&linkName, fullName.c_str());
OBJECT_ATTRIBUTES attr;
InitializeObjectAttributes(&attr, &linkName, 0, nullptr, nullptr);
HANDLE hLink;
auto status = NtOpenSymbolicLinkObject(&hLink, GENERIC_READ, &attr);
if (!NT_SUCCESS(status))
return L"";
WCHAR buffer[512]{};
UNICODE_STRING target;
RtlInitUnicodeString(&target, buffer);
target.MaximumLength = sizeof(buffer);
status = NtQuerySymbolicLinkObject(hLink, &target, nullptr);
NtClose(hLink);
return buffer;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
与前面示例的唯一区别是,该函数接收目录和对象名称作为参数,并且需要将它们拼接在一起,同时注意避免根目录的反斜杠重复。
以下是运行ObjDir的几个示例(为了更好地适配页面,对部分格式进行了调整):
| 目录(Directory):\ | 类型(Type) | 名称(Name) | 链接目标(Link Target) |
|---|---|---|---|
![]() | ![]() | ||
| 事件(Event) | DSYSDBG.Debug.Trace.Memory.530 | ||
| 互斥体(Mutant) | PendingRenameMutex | ||
| 目录(Directory) | ObjectTypes | ||
| 筛选连接端口(FilterConnectionPort) | storqosfltport | ||
| 筛选连接端口(FilterConnectionPort) | MicrosoftMalwareProtectionRemoteIoPortWD | ||
| 符号链接(SymbolicLink) | SystemRoot | \Device\BootDevice\Windows | |
| ... | |||
| 区段(Section) | LsaPerformance | ||
| ALPC端口(ALPC Port) | SmApiPort | ||
| 筛选连接端口(FilterConnectionPort) | CLDMSGPORT | ||
| 筛选连接端口(FilterConnectionPort) | DtdSel03 | ||
| 筛选连接端口(FilterConnectionPort) | MicrosoftMalwareProtectionPortWD | ||
| 符号链接(SymbolicLink) | OSDataRoot | \Device\OSDataDevice | |
| 事件(Event) | SAM_SERVICE_STARTED | ||
| 目录(Directory) | Driver | ||
| 目录(Directory) | DriverStores |
c:\>objdir \KernelObjects
目录(Directory):\KernelObjects
| 类型(Type) | 名称(Name) | 链接目标(Link Target) |
|---|---|---|
| 符号链接(SymbolicLink) | MemoryErrors | |
| 事件(Event) | LowNonPagedPoolCondition | |
| 会话(Session) | Session1 | |
| 事件(Event) | SuperfetchScenarioNotify | |
| 事件(Event) | SuperfetchParametersChanged | |
| 符号链接(SymbolicLink) | PhysicalMemoryChange | |
| 符号链接(SymbolicLink) | HighCommitCondition | |
| 互斥体(Mutant) | BcdSyncMutant | |
| 符号链接(SymbolicLink) | HighMemoryCondition | |
| 事件(Event) | HighNonPagedPoolCondition | |
| 分区(Partition) | MemoryPartition0 | |
| 键控事件(KeyedEvent) | CritSecOutOfMemoryEvent | |
| 事件(Event) | SystemErrorPortReady | |
| 符号链接(SymbolicLink) | MaximumCommitCondition | |
| 符号链接(SymbolicLink) | LowCommitCondition | |
| 事件(Event) | HighPagedPoolCondition | |
| 符号链接(SymbolicLink) | LowMemoryCondition | |
| 会话(Session) | Session0 | |
| 事件(Event) | LowPagedPoolCondition | |
| 事件(Event) | PrefetchTracesReady |

你可能会在上述输出中注意到,有些符号链接没有目标。这些被称为动态符号链接(dynamic symbolic links),其目标由内核回调(kernel callback)设置。上述所有无目标的链接实际上都指向事件(Event)对象(例如“HighMemoryCondition”)。

某些目录需要提升权限(elevated permissions)才能访问(例如“\ObjectTypes”)。以管理员身份运行`ObjDir`以获得额外的访问权限。
## 7.4:句柄(Handles)
从用户模式(user-mode)访问内核对象(kernel objects)必须通过句柄(handle)进行。句柄是句柄表(handle table)中的索引,按进程维护。本节中,我们将介绍与句柄相关的原生应用程序编程接口(API),这些接口适用于任何(或多种)对象类型。
句柄不再需要时,应使用我们之前多次使用过的`NtClose`函数关闭:
```c
NTSTATUS NtClose(_In_ _Post_ptr_invalid_ HANDLE Handle);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 7.4.1 句柄复制(Handle Duplication)
有时,基于现有句柄获取同一对象的另一个句柄会很有用。例如,不同进程中的句柄不能被调用进程用于访问同一对象,除非该句柄位于调用进程的句柄表中。句柄复制提供了一种解决方案。
相关函数名为NtDuplicateObject,Windows应用程序编程接口(API)的DuplicateHandle函数会调用它:
NTSTATUS NtDuplicateObject(
_In_ HANDLE SourceProcessHandle,
_In_ HANDLE SourceHandle,
_In_opt_ HANDLE TargetProcessHandle,
_Out_opt_ PHANDLE TargetHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ ULONG HandleAttributes,
_In_ ULONG Options);
2
3
4
5
6
7
8
该应用程序编程接口(API)的名称(NtDuplicateObject)具有误导性——它不会复制对象,只会复制句柄。Windows应用程序编程接口(API)的名称更为准确。
源进程(source process)中的句柄会被复制到目标进程(target process)的句柄表中。源进程和目标进程可以是同一个进程,当需要不同的访问掩码时,这有时会很有用。源进程句柄(SourceHandleProcess)和目标进程句柄(TargetHandleProcess)必须具有PROCESS_DUP_HANDLE访问掩码(NtCurrentProcess()始终具有该掩码)。SourceHandle是要复制的句柄,执行成功后会将结果返回至TargetHandle。
如果Options包含DUPLICATE_SAME_ACCESS标志,则DesiredAccess可以设为0;否则,它会指定所需的访问掩码(如果可以获取)。如果请求的访问掩码是原始访问掩码的子集,则该操作始终会成功。
HandleAttributes可以包含以下可选标志:
• OBJ_INHERIT(2)——复制的句柄被标记为可继承(inheritable)。
• OBJ_PROTECT_CLOSE(1)——复制的句柄在移除该标志之前无法关闭。
Options可以设为0或以下标志的组合:
• DUPLICATE_SAME_ACCESS(2)——如前所述,使用源句柄的访问掩码。
• DUPLICATE_CLOSE_SOURCE(1)——复制后关闭源句柄(本质上是将句柄从源进程“移动”到目标进程)。如果使用此标志,源进程句柄和目标句柄指针可以设为NULL,这会导致源句柄被关闭,而不会发生任何复制操作。
目标进程会获得复制的句柄,但如果目标进程不是调用进程,它不会“知道”其句柄表中创建了新句柄。这意味着复制进程必须以某种方式告知目标进程新句柄的值(通常,这两个进程希望共享对该对象的访问权限)。在这种情况下,调用进程和目标进程之间必须存在某种形式的进程间通信(IPC,Interprocess Communication)机制。
# 7.4.2 查询信息(Querying Information)
通过句柄获取对象相关信息以及句柄项本身信息的通用方法是NtQueryObject,为方便起见,在此重复列出:
typedef enum _OBJECT_INFORMATION_CLASS
{
ObjectBasicInformation, // OBJECT_BASIC_INFORMATION
ObjectNameInformation, // OBJECT_NAME_INFORMATION
ObjectTypeInformation, // OBJECT_TYPE_INFORMATION
ObjectTypesInformation, // OBJECT_TYPES_INFORMATION
ObjectHandleFlagInformation, // OBJECT_HANDLE_FLAG_INFORMATION
ObjectSessionInformation,
ObjectSessionObjectInformation,
MaxObjectInfoClass
} OBJECT_INFORMATION_CLASS;
NTSTATUS NtQueryObject(
_In_opt_ HANDLE Handle,
_In_ OBJECT_INFORMATION_CLASS ObjectInformationClass,
_Out_writes_bytes_opt_ (ObjectInformationLength) PVOID ObjectInformation,
_In_ ULONG ObjectInformationLength,
_Out_opt_ PULONG ReturnLength);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 7.4.2.1 基本信息(Basic Information)
ObjectBasicInformation返回OBJECT_BASIC_INFORMATION结构:
typedef struct _OBJECT_BASIC_INFORMATION
{
ULONG Attributes;
ACCESS_MASK GrantedAccess;
ULONG HandleCount;
ULONG PointerCount;
ULONG PagedPoolCharge;
ULONG NonPagedPoolCharge;
ULONG Reserved[3];
ULONG NameInfoSize;
ULONG TypeInfoSize;
ULONG SecurityDescriptorSize;
LARGE_INTEGER CreationTime;
} OBJECT_BASIC_INFORMATION, *POBJECT_BASIC_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
13
14
部分成员提供进程句柄表中维护的句柄项相关信息,另一些成员则提供句柄所“指向”的对象的详细信息。各成员说明如下:
• Attributes包含句柄和对象属性。它可以设为0,或以下标志的组合:
OBJ_PROTECT_CLOSE(1)——句柄不可关闭(除非移除该标志)。OBJ_INHERIT(2)——句柄可继承(适用于创建新进程时使用句柄继承的情况)。OBJ_AUDIT_OBJECT_CLOSE(4)——关闭句柄时,安全事件日志(security event log)中会添加一条审计事件长条目。OBJ_PERMANENT(0x10)——对象是永久性的,无法从用户模式销毁。OBJ_EXCLUSIVE(0x20)——对象是独占的,无法从其他进程打开。 •GrantedAccess是句柄对对象的“操作权限”。 •HandleCount是对象的打开句柄数量。 •PointerCount本应是对象的总引用计数(包括句柄计数)。然而,从Windows 7.1开始,指针计数包含句柄的反向使用计数(有关详细信息,请参阅《Windows Internals》一书(第2部分))。归根结底,这个值大多没有实际用途。 •PagedPoolCharge和NonPagedPoolQuota分别是对象使用的分页内存(paged memory)和非分页内存(non-paged memory),并配额给创建进程。 •NameInfoSize是对象名称信息的大小(以字节为单位,若有)。这包括OBJECT_NAME_INFORMATION的大小(见下一小节)和名称本身的大小。 •TypeInfoSize是对象类型信息的大小(以字节为单位)。这包括OBJECT_TYPE_INFORMATION的大小和类型名称的大小。 •SecurityDescriptorSize是对象安全描述符(Security Descriptor)的大小(以字节为单位,若有)。 •CreationTime看似有用,但仅适用于符号链接(Symbolic Link)对象(表示其创建时间)。

对于没有名称的对象,以及不在对象管理器(Object Manager)命名空间中的对象(即使它们有某种形式的名称),NameInfoSize会返回0。典型示例包括文件(file)和桌面(desktop)对象。
下一小节将展示使用此信息的示例。
# 7.4.2.2 对象名称(Object Name)
可以使用ObjectNameInformation信息类检索对象的名称(若有)。它返回OBJECT_NAME_INFORMATION结构,该结构本质上是UNICODE_STRING的增强版:
typedef struct _OBJECT_NAME_INFORMATION
{
UNICODE_STRING Name;
} OBJECT_NAME_INFORMATION, *POBJECT_NAME_INFORMATION;
2
3
4
需要注意的是,提供的缓冲区必须足够大以容纳名称——应用程序编程接口(API)不会为你分配缓冲区。获取该信息的一种方法是使用上一小节中的ObjectBasicInformation。以下示例展示了如何根据句柄和进程标识符(PID)显示对象的名称:
std::wstring GetObjectName(HANDLE hObject, ULONG pid)
{
static const WCHAR accessDeniedName[] = L"<access denied>" ;
//
// 打开目标进程的句柄(open handle to target process)
//
HANDLE hProcess;
CLIENT_ID cid{ ULongToHandle(pid) };
OBJECT_ATTRIBUTES procAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(nullptr , 0);
auto status = NtOpenProcess(&hProcess, PROCESS_DUP_HANDLE, &procAttr, &cid);
if (!NT_SUCCESS(status))
return accessDeniedName;
HANDLE hDup = nullptr;
std::wstring name;
do
{
//
// 将句柄复制到当前进程,以便访问对象(duplicate handle to our process so we can access object)
//
status = NtDuplicateObject(hProcess, hObject, NtCurrentProcess(),
&hDup, 0 , 0 , 0);
if ( !NT_SUCCESS(status))
{
name = accessDeniedName;
break;
}
//
// 获取基本信息(get basic information)
//
OBJECT_BASIC_INFORMATION info;
status = NtQueryObject(hDup, ObjectBasicInformation,
&info, sizeof(info), nullptr);
if ( !NT_SUCCESS(status))
{
name = accessDeniedName;
break;
}
if (info .NameInfoSize == 0) // 无名称(no name)
break;
//
// 查询实际名称(query the actual name)
//
auto buffer = std::make_unique<BYTE[]>(info.NameInfoSize);
status = NtQueryObject(hDup, ObjectNameInformation, buffer .get(),
info.NameInfoSize, nullptr);
if (!NT_SUCCESS(status))
break;
auto uname = (UNICODE_STRING*)buffer .get();
name .assign(uname->Buffer, uname->Length / sizeof(WCHAR));
} while (false);
if (hProcess)
NtClose(hProcess);
if (hDup)
NtClose(hDup);
return name;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
上述代码检索对象管理器命名空间中可访问对象的名称(你可以以管理员身份运行它以获得更多访问权限)。例如,文件对象不会被报告为具有名称(NameInfoSize设为0)。
对于此类有名称但不在对象管理器命名空间维护的层次结构中的对象(如文件和桌面),我们如何获取其名称呢?我们可以忽略NameInfoSize为0这一情况,直接调用NtQueryObject并指定ObjectNameInformation。
特别是文件对象,会带来一个意想不到的问题:查询文件对象的名称可能会导致NtQueryObject调用挂起。原因与执行体(Executive)I/O系统内部深层次的同步相关操作有关(文件名可能随时更改,文件系统有自己的缓存等)。
我发现针对这种情况的最佳方法是启动一个线程来获取文件对象名称,如果在预定义时间后名称仍不可用(调用卡住),则终止该线程并继续执行。以下是实现此功能的部分代码:
std::wstring GetFileObjectName(HANDLE hFile)
{
//
// 传递给线程的上下文数据(context data to pass to thread)
//
struct Data
{
std::wstring Name;
HANDLE hFile;
} data;
data .hFile = hFile;
HANDLE hThread;
OBJECT_ATTRIBUTES threadAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(nullptr , 0);
//
// 创建线程(create thread)
//
auto status = RtlCreateUserThread(NtCurrentProcess(), nullptr , FALSE, 0 , 0 , 0 ,
[](auto p) -> NTSTATUS
{
auto data = (Data*)p;
//
// 分配足够大的缓冲区(allocate a buffer that should be large enough)
//
auto buffer = std::make_unique<BYTE[]>(MAX_PATH * 4);
auto status = NtQueryObject(data->hFile, ObjectNameInformation,
buffer.get(), MAX_PATH * 4 , nullptr);
if (!NT_SUCCESS(status))
return 1;
//
// 提取名称(extract the name)
//
auto uname = (UNICODE_STRING*)buffer .get();
data->Name .assign(uname->Buffer, uname->Length / sizeof(WCHAR));
return 0;
}, &data, &hThread, nullptr);
if (!NT_SUCCESS(status))
return L"" ;
LARGE_INTEGER timeout;
timeout.QuadPart = -10 * 10000; // 10 毫秒(10 msec)
//
// 等待线程终止(wait for the thread to terminate)
//
if (STATUS_TIMEOUT == NtWaitForSingleObject(hThread, FALSE, &timeout))
{
//
// 强制终止(terminate forefully)
//
NtTerminateThread(hThread, 1);
}
NtClose(hThread);
return data.Name;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
要使此功能正常工作,我们需要知道所需名称对应的对象类型。获取此信息可以通过下一节讨论的ObjectTypeInformation实现。
# 7.4.2.3 对象类型(Object Type)
对指定句柄(handle)使用 ObjectTypeInformation 信息类,会返回 OBJECT_TYPE_INFORMATION 结构,本章前文已对此结构进行过介绍。有关详细信息,请参阅“对象类型(Object Types)”部分。
编写一个应用程序,要求根据给定的句柄值(handle value)和进程标识符(process ID)显示对象名称。确保正确处理文件对象(file objects)。
# 7.4.2.4 句柄信息(Handle Information)
ObjectHandleInformation 会返回一个简单结构,用于指示给定句柄的“禁止关闭(protect from close)”和“继承(inherit)”标志的状态:
typedef struct _OBJECT_HANDLE_FLAG_INFORMATION {
BOOLEAN Inherit;
BOOLEAN ProtectFromClose;
} OBJECT_HANDLE_FLAG_INFORMATION, *POBJECT_HANDLE_FLAG_INFORMATION;
2
3
4
如前文所述,这些细节也可通过 ObjectBasicInformation 在 Attributes 成员中获取。
OBJECT_INFORMATION_CLASS 的最后两个成员(ObjectSessionInformation 和 ObjectSessionObjectInformation)仅用于设置信息。
# 7.4.3 设置信息(Setting Information)
与 NtQueryObject 对应的函数是:
NTSTATUS NtSetInformationObject(
_In_ HANDLE Handle,
_In_ OBJECT_INFORMATION_CLASS ObjectInformationClass,
_In_reads_bytes_(ObjectInformationLength) PVOID ObjectInformation,
_In_ ULONG ObjectInformationLength);
2
3
4
5
OBJECT_INFORMATION_CLASS 中的部分值可用于设置信息。
ObjectHandleFlagInformation 可通过 OBJECT_HANDLE_FLAG_INFORMATION 结构修改“禁止关闭(protect from close)”和“继承(inherit)”标志的状态(与 Windows API 中的 SetHandleInformation 功能等效)。
ObjectSessionInformation 仅适用于目录对象(Directory objects)。它可用于将目录对象(Directory object)与调用者的会话(session)相关联。这种情况下无需缓冲区,但需要 TCB 权限(该权限通常不授予任何用户)。不过,服务(Services)若有需要,可申请该权限。
ObjectSessionObjectInformation 与 ObjectSessionInformation 功能相同,但仅适用于当前未与任何会话相关联的目录对象(Directory objects)。其优势在于无需特殊权限。
# 7.5 枚举句柄(Enumerating Handles)
Process Explorer 等工具能够显示任意进程中的句柄列表,这是通过原生 API(native API)提供的句柄枚举功能实现的。句柄枚举有两种方式:第一种是枚举系统中的所有句柄;第二种是枚举特定目标进程中的句柄。
要枚举系统中的所有句柄,需调用 NtQuerySystemInformation 并指定 SystemExtendedHandleInformation。你也会看到 SystemHandleInformation,但该方式已过时,因为其返回结果可能被截断(例如,进程标识符(process IDs)以 USHORT 类型返回,这一类型可能不够大)。
SystemExtendedHandleInformation 的返回值包含一个顶层结构,其后是一个结构数组,数组中的每个结构对应系统中的一个句柄:
typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX {
PVOID Object;
ULONG_PTR UniqueProcessId;
ULONG_PTR HandleValue;
ULONG GrantedAccess;
USHORT CreatorBackTraceIndex;
USHORT ObjectTypeIndex;
ULONG HandleAttributes;
ULONG Reserved;
} SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX, *PSYSTEM_HANDLE_TABLE_ENTRY_INFO_EX;
typedef struct _SYSTEM_HANDLE_INFORMATION_EX {
ULONG_PTR NumberOfHandles;
ULONG_PTR Reserved;
SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX Handles[1];
} SYSTEM_HANDLE_INFORMATION_EX, *PSYSTEM_HANDLE_INFORMATION_EX;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
根据前文内容,SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX 的成员应该已为你所熟悉。注意,该结构未提供对象名称(object name),且 CreatorBackTraceIndex 似乎始终为 0。
使用此枚举功能时,需传递一个足够大的缓冲区以容纳返回数据。与之前的示例类似,最佳做法是先指定一个初始大小,每次缓冲区不足时将其扩大一倍,或使用返回的大小(但需适当增加,因为在第二次调用前可能会创建新的句柄)。以下是示例:
ULONG size = 0;
std::unique_ptr<BYTE[]> buffer;
auto status = STATUS_SUCCESS;
do {
if (size)
buffer = std::make_unique<BYTE[]>(size);
status = NtQuerySystemInformation(SystemExtendedHandleInformation,
buffer.get(), size, &size);
if (status == STATUS_INFO_LENGTH_MISMATCH) {
size += 1 << 12;
continue;
}
} while (!NT_SUCCESS(status));
if (!NT_SUCCESS(status)) {
printf("Error: 0x%X\n ", status);
return 1;
}
auto handles = (SYSTEM_HANDLE_INFORMATION_EX*)buffer.get();
for (ULONG i = 0; i < handles->NumberOfHandles; i++) {
// 对 handles->Handles[i] 执行某些操作
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 7.5.1 进程句柄枚举(Process Handle Enumeration)
枚举特定进程中的句柄有两种方式。一种是使用前文所述的系统级句柄枚举,然后筛选出目标进程的句柄;另一种是通过调用 NtQueryInformationProcess 并指定 ProcessHandleInformation(51),获取目标进程的句柄。该方式返回以下结构:
typedef struct _PROCESS_HANDLE_TABLE_ENTRY_INFO {
HANDLE HandleValue;
ULONG_PTR HandleCount;
ULONG_PTR PointerCount;
ULONG GrantedAccess;
ULONG ObjectTypeIndex;
ULONG HandleAttributes;
ULONG Reserved;
} PROCESS_HANDLE_TABLE_ENTRY_INFO, *PPROCESS_HANDLE_TABLE_ENTRY_INFO;
typedef struct _PROCESS_HANDLE_SNAPSHOT_INFORMATION {
ULONG_PTR NumberOfHandles;
ULONG_PTR Reserved;
PROCESS_HANDLE_TABLE_ENTRY_INFO Handles[1];
} PROCESS_HANDLE_SNAPSHOT_INFORMATION, *PPROCESS_HANDLE_SNAPSHOT_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
尽管这种方式看似足够好用,但使用进程特定调用时存在以下两个问题:
• 需要具有 PROCESS_QUERY_INFORMATION 权限的进程句柄(process handle),而该句柄并非总能获取。例如,受保护进程(protected processes)永远无法通过此访问掩码(access mask)打开。
• PROCESS_HANDLE_TABLE_ENTRY_INFO 结构缺少对象在内核空间中的地址(object’s address in kernel space)。虽然该地址对用户模式(user-mode)而言无直接实际用途,但调用者可能仍需获取该地址。
如果这两个问题对你的使用场景无影响,则完全可以采用这种方式。
Process Explorer 等工具使用 NtQuerySystemInformation 调用,因此能够枚举所有进程的句柄,无需获取任何进程的句柄。
Handles 样本项目展示了如何执行系统级句柄枚举,还展示了如何获取对象名称。你可以自行添加进程筛选、显示对象类型、基于对象类型或名称筛选等功能。
# 7.6 特定对象类型(Specific Object Types)
原生 API(native API)提供了用于创建、打开和操作各种内核对象(kernel objects)的函数。本节将介绍用户模式代码(user-mode code)中最常用的几种对象类型。
# 7.6.1 互斥体(Mutex)
互斥体对象(Mutex objects)用于线程同步(thread synchronization)。互斥体要么处于空闲状态(已触发,signaled),要么被某个线程拥有(未触发,non-signaled)。第 6 章讨论的等待函数(wait functions)适用于互斥体(所有调度程序对象(dispatcher objects)均适用)。创建命名互斥体(named mutex)可通过以下函数实现:
NTSTATUS NtCreateMutant(
_Out_ PHANDLE MutantHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ BOOLEAN InitialOwner);
2
3
4
5
创建互斥体时,所需访问权限(desired access)通常为 MUTEX_ALL_ACCESS。如果互斥体未命名(通过 ObjectAttributes 指定),则会创建一个新的互斥体;如果提供了名称且该名称的互斥体已存在,函数会返回 STATUS_OBJECT_NAME_COLLISION 错误。若指定了名称,该名称必须是完整名称(例如 \KernelObjects\MyMutex),或者在对象属性(object attributes)中提供了目录句柄(Directory handle)时,该名称可以是相对名称。如果要创建未命名互斥体且无需特殊属性,ObjectAttributes 可设为 NULL。
最后,InitialOwner 用于指示创建的互斥体是否最初由调用线程拥有。
原生 API 中使用的“mutant”一词是互斥体(mutexes)的原始名称。出于兼容性原因,内核对象类型(kernel object type)仍被称为“Mutant”。实际上,二者本质相同。
以下示例展示了如何在“KernelObjects”目录中创建一个命名互斥体(需要以管理员权限运行):
HANDLE hMutex;
UNICODE_STRING name;
RtlInitUnicodeString(&name, L"\\KernelObjects\\MySuperMutex");
OBJECT_ATTRIBUTES mutexAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&name, 0);
auto status = NtCreateMutant(&hMutex, MUTEX_ALL_ACCESS, &mutexAttr, FALSE);
2
3
4
5
打开现有互斥体的句柄可通过 NtOpenMutant 实现:
NTSTATUS NtOpenMutant(
_Out_ PHANDLE MutantHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes);
2
3
4
ObjectAttributes 必须存储互斥体的名称,否则调用会失败。此外,由于安全原因(调用者可能未被授予访问权限),调用也可能失败。
以下示例打开上述创建的命名互斥体的句柄,仅允许等待操作:
HANDLE hMutex;
UNICODE_STRING name;
RtlInitUnicodeString(&name, L"\\KernelObjects\\MySuperMutex");
OBJECT_ATTRIBUTES mutexAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&name, 0);
auto status = NtOpenMutant(&hMutex, SYNCHRONIZATION, &mutexAttr);
2
3
4
5
互斥体成功获取后,拥有该互斥体的线程最终必须释放它:
NTSTATUS NtReleaseMutant(
_In_ HANDLE MutantHandle,
_Out_opt_ PLONG PreviousCount);
2
3
创建未被拥有的互斥体时,其内部计数(internal count)为 1;创建已被拥有的互斥体时,其内部计数为 0。每次获取互斥体,计数减 1;每次释放互斥体,计数加 1。
同一线程可以递归方式多次获取互斥体。等待次数(number of waits)必须与 NtReleaseMutant 调用次数匹配,才能真正释放互斥体。可选参数 PreviousCount 会返回之前的所有权计数(previous count of ownership)。如果调用线程并非互斥体的拥有者,NtReleaseMutant 会调用失败。
最后,可通过 NtQueryMutant 查询互斥体的状态:
typedef enum _MUTANT_INFORMATION_CLASS {
MutantBasicInformation,
MutantOwnerInformation
} MUTANT_INFORMATION_CLASS;
typedef struct _MUTANT_BASIC_INFORMATION {
LONG CurrentCount;
BOOLEAN OwnedByCaller;
BOOLEAN AbandonedState;
} MUTANT_BASIC_INFORMATION, *PMUTANT_BASIC_INFORMATION;
typedef struct _MUTANT_OWNER_INFORMATION {
CLIENT_ID ClientId;
} MUTANT_OWNER_INFORMATION, *PMUTANT_OWNER_INFORMATION;
NTSTATUS NtQueryMutant(
_In_ HANDLE MutantHandle,
_In_ MUTANT_INFORMATION_CLASS MutantInformationClass,
_Out_writes_bytes_(MutantInformationLength) PVOID MutantInformation,
_In_ ULONG MutantInformationLength,
_Out_opt_ PULONG ReturnLength);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这些成员的含义大多不言自明。AbandonedState 用于指示互斥体是否被遗弃(abandoned)——即拥有该互斥体的线程在未释放它的情况下终止。由于只有互斥体的拥有者才能释放互斥体,因此在线程终止时,内核会介入并强制释放互斥体,同时将互斥体状态设为遗弃状态(abandoned)。从功能上来说,遗弃状态与空闲状态(free)相同,下一个获取该互斥体的线程能够成功获取,但遗弃状态表明发生了异常情况,可能暗示应用程序存在 bug。一旦互斥体再次被拥有,其遗弃状态会被清除(等待函数会返回 STATUS_ABANDONED,而非 STATUS_SUCCESS)。
请记住,查询调度程序对象(如互斥体)的状态并非安全操作,因为调用后其他线程可能会立即改变该状态;但该操作仍可用于调试目的。
# 7.6.2 信号量(Semaphore)
信号量对象(Semaphore objects)维护一个最大计数(maximum count)和一个当前计数(current count),这两个计数在创建时初始化。当信号量的当前计数大于 0 时,它处于已触发状态(signaled);当计数降至 0 时,它变为未触发状态(non-signaled)。每次成功的等待操作都会使当前计数减 1。信号量对象的创建和打开方式与互斥体类似:
NTSTATUS NtCreateSemaphore(
_Out_ PHANDLE SemaphoreHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ LONG InitialCount,
_In_ LONG MaximumCount);
NTSTATUS NtOpenSemaphore(
_Out_ PHANDLE SemaphoreHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes);
2
3
4
5
6
7
8
9
10
11
典型信号量的用途是在多个线程操作某个“资源”(如队列)时,限制该资源的使用规模。
获取信号量的一个计数可通过调用等待函数实现。释放信号量可通过 NtReleaseSemaphore 实现:
NTSTATUS NtReleaseSemaphore(
_In_ HANDLE SemaphoreHandle,
_In_ LONG ReleaseCount,
_Out_opt_ PLONG PreviousCount);
2
3
4
线程一次可以释放多个“计数”(通过 ReleaseCount 指定)。可选参数 PreviousCount 会返回当前操作前信号量的计数。
与互斥体类似,也可查询信号量的状态(主要用于调试目的):
typedef enum _SEMAPHORE_INFORMATION_CLASS {
SemaphoreBasicInformation
} SEMAPHORE_INFORMATION_CLASS;
typedef struct _SEMAPHORE_BASIC_INFORMATION {
LONG CurrentCount;
LONG MaximumCount;
} SEMAPHORE_BASIC_INFORMATION, *PSEMAPHORE_BASIC_INFORMATION;
NTSTATUS NtQuerySemaphore(
_In_ HANDLE SemaphoreHandle,
_In_ SEMAPHORE_INFORMATION_CLASS SemaphoreInformationClass,
_Out_writes_bytes_(SemaphoreInformationLength) PVOID SemaphoreInformation,
_In_ ULONG SemaphoreInformationLength,
_Out_opt_ PULONG ReturnLength);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 7.6.3 事件(Event)
事件对象(Event objects)是简单的标志,可处于已触发状态(set,TRUE)或未触发状态(reset,FALSE)。事件分为两种类型:
• 通知事件(Notification Event)(Windows API 术语中称为手动重置事件(Manual reset))
• 同步事件(Synchronization Event)(Windows API 术语中称为自动重置事件(Auto reset))
通知事件被设置时,会释放任意数量的等待线程;而同步事件被设置时,仅释放一个等待线程,随后立即返回未触发状态(reset),需再次设置才能释放另一个线程。
如果在设置同步事件时,没有线程在等待它,该事件会保持已触发状态,直到有线程开始等待,此时会立即释放该线程并返回未触发状态。
事件的创建和打开方式与互斥体和信号量类似:
typedef enum _EVENT_TYPE {
NotificationEvent,
SynchronizationEvent
} EVENT_TYPE;
NTSTATUS NtCreateEvent(
_Out_ PHANDLE EventHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ EVENT_TYPE EventType,
_In_ BOOLEAN InitialState);
NTSTATUS NtOpenEvent(
_Out_ PHANDLE EventHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
设置和重置事件可通过以下函数实现:
NTSTATUS NtSetEvent(
_In_ HANDLE EventHandle,
_Out_opt_ PLONG PreviousState);
NTSTATUS NtClearEvent(_In_ HANDLE EventHandle);
NTSTATUS NtResetEvent(
_In_ HANDLE EventHandle,
_Out_opt_ PLONG PreviousState);
2
3
4
5
6
7
8
9
NtClearEvent 和 NtResetEvent 功能相同,但后者会返回事件的先前状态(可能为 0 或 1)。
还有一个用于设置事件的 API:
NTSTATUS NtSetEventBoostPriority(_In_ HANDLE EventHandle);
该函数仅适用于同步事件,它会将被释放线程的优先级提升至当前线程的优先级(如果当前线程优先级更高)。NtSetEvent 的标准行为是将被释放线程的优先级提升 +1。
此外,还可使用 NtPulseEvent,其参数与 NtSetEvent 相同。该函数会设置事件并立即重置它,但存在潜在问题(可在网上查询相关信息),因此不建议使用。
最后,可查询事件的信息:
typedef enum _EVENT_INFORMATION_CLASS {
EventBasicInformation
} EVENT_INFORMATION_CLASS;
typedef struct _EVENT_BASIC_INFORMATION {
EVENT_TYPE EventType;
LONG EventState;
} EVENT_BASIC_INFORMATION, *PEVENT_BASIC_INFORMATION;
NTSTATUS NtQueryEvent(
_In_ HANDLE EventHandle,
_In_ EVENT_INFORMATION_CLASS EventInformationClass,
_Out_writes_bytes_(EventInformationLength) PVOID EventInformation,
_In_ ULONG EventInformationLength,
_Out_opt_ PULONG ReturnLength);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 7.7 其他对象类型(Other Object Types)
原生 API 支持许多其他对象类型,表 7-1 列出了部分常见对象类型及其对应的常用函数。
表 7-1:其他对象类型
| 对象类型(Object Type) | 示例 API |
|---|---|
| 计时器(Timer) | NtCreateTimer、NtOpenTimer、NtSetTimer(Ex)、NtCancelTimer |
| TpWorkerFactory | NtCreateWorkerFactory、NtWorkerFactoryWorkerReady |
| 性能分析器(Profile) | NtCreateProfile、NtStartProfile、NtStopProfile |
| 键控事件(KeyedEvent) | NtCreateKeyedEvent、NtOpenKeyedEvent、NtWaitForKeyedEvent |
| 事务(Transaction) | NtCreateTransaction、NtOpenTransaction、NtCommitTransaction |
| 作业(Job) | NtCreateJobObject、NtOpenJobObject、NtSetInformationJobObject |
这些对象类型及其他未列出的对象类型将在本书的后续版本中详细介绍。
# 7.8 总结(Summary)
内核对象(Kernel objects)封装了 Windows 中的核心功能,包括进程、线程、作业、互斥体等。本章介绍了对象和句柄的通用操作方法,并重点讲解了几种实用的对象类型,后续章节还将介绍更多对象类型。

