CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Windows Native API编程
  • 🔥Windows x64 ShellCode入门教程
  • 🔥Windows Shellcode实战
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Windows Native API编程
  • 🔥Windows x64 ShellCode入门教程
  • 🔥Windows Shellcode实战
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
  • Windows Native API编程 专栏说明
  • 第1章 原生API(Native API)开发入门
  • 第2章 原生API(Native API)基础
  • 第3章 原生应用程序(Native Applications)
  • 第4章:系统信息
    • 4.1:查询与设置信息(Querying and Setting Information)
      • 4.1.1 通用系统信息(General System Information)
      • 4.1.1.1:SystemBasicInformation(0)(查询)
      • 4.1.1.2 SystemProcessorInformation(1)(查询)
      • 4.1.1.3 SystemPerformanceInformation(2)(查询)
      • 4.1.1.4 SystemTimeOfDayInformation(3)(查询)
      • 4.1.2 计时器分辨率(Timer Resolution)
      • 4.1.3 处理器动态信息(Processor Dynamic Information)
      • 4.1.4 模块信息(Module Information)
    • 4.2 进程和线程信息(Process and Thread Information)
    • 4.3 对象与句柄(Objects and Handles)
      • 4.3.1 句柄(Handles)
      • 4.3.2 对象类型(Object Types)
      • 4.3.3 对象名称(Object Names)
      • 4.4 KUSERSHAREDDATA 结构
      • 4.5 总结
  • 第5章:进程
  • 第6章:线程
  • 第7章:对象与句柄
  • 第 8 章:内存(第一部分)
  • 第9章:I/O
  • 第10章:ALPC
  • 第11章 安全性(Security)
  • 第12章 内存(第二部分)
  • 第13章 注册表
目录

第4章:系统信息

# 第4章:系统信息

本章我们将重点介绍获取和设置各种系统级别的详细信息。原生应用程序编程接口(API)的部分功能在Windows应用程序编程接口(API)中有对应的实现,但也有一些功能是独有的。本章并未涵盖所有相关内容——NtQueryInformationClass所使用的信息类(information classes)数量之多令人震惊。本章将介绍一些更“有趣”或非平凡的系统信息细节,这些细节的使用可能并不直观。

本章内容包括:

• 查询与设置信息 • 通用系统信息 • 进程与线程 • 句柄与对象 • KUSER_SHARED_DATA结构 • 其他信息

# 4.1:查询与设置信息(Querying and Setting Information)

有两个最常用的函数用于获取和设置与系统相关的信息:NtQuerySystemInformation及其配套函数NtSetSystemInformation:

NTSTATUS  NtQuerySystemInformation(
    _In_  SYSTEM_INFORMATION_CLASS  SystemInformationClass,
    _Out_writes_bytes_opt_ (SystemInformationLength)  PVOID  SystemInformation,
    _In_  ULONG  SystemInformationLength,
    _Out_opt_  PULONG  ReturnLength);

NTSTATUS  NtSetSystemInformation(
    _In_  SYSTEM_INFORMATION_CLASS  SystemInformationClass,
    _In_reads_bytes_opt_(SystemInformationLength)  PVOID  SystemInformation,
    _In_  ULONG  SystemInformationLength);
1
2
3
4
5
6
7
8
9
10

这两个函数都是通用的,基于SYSTEM_INFORMATION_CLASS枚举(enumeration)和一些相关的预期缓冲区(buffer)。对于查询操作,有些情况下预期的缓冲区大小是固定的,但在其他情况下,检索到的信息是动态的,因此缓冲区大小可能无法提前确定。一种典型的模式是,先调用NtQuerySystemInformation并传入NULL缓冲区和大小为0的参数,以获取所需的缓冲区大小(通过最后一个参数返回)(此时返回值为STATUS_INFO_LENGTH_MISMATCH)。接下来,分配缓冲区并重新调用该函数,这次传入实际的缓冲区和分配的大小。在某些情况下,需要注意分配比返回的大小稍大的缓冲区,以防在获取所需大小和执行第二次调用之间信息大小发生了增长。例如,当请求进程列表时,在返回所需大小后可能会有新的进程被添加。

SYSTEM_INFORMATION_CLASS枚举规模庞大,并且随着Windows新版本的发布不断扩充。由于这些API是通用的,因此它们的原型不需要更改。我们将花一些时间介绍该枚举的部分值及其使用方法。

phnt中对SYSTEM_INFORMATION_CLASS的定义为大多数值、预期的结构以及该枚举是否支持查询、设置或两者都支持提供了注释。》

还有另一种查询变体,它接受输入缓冲区(而非仅输出缓冲区):

NTSTATUS  NtQuerySystemInformationEx(
    _In_  SYSTEM_INFORMATION_CLASS  SystemInformationClass,
    _In_reads_bytes_(InputBufferLength)  PVOID  InputBuffer,
    _In_  ULONG  InputBufferLength,
    _Out_writes_bytes_opt_ (SystemInformationLength)  PVOID  SystemInformation,
    _In_  ULONG  SystemInformationLength,
    _Out_opt_  PULONG  ReturnLength);
1
2
3
4
5
6
7

输入缓冲区用作影响输出的额外细节。只有一部分系统信息类支持此API。传入NULL输入缓冲区或输入长度为0是无效的——该函数会直接失败,而不会调用NtQuerySystemInformation。

# 4.1.1 通用系统信息(General System Information)

本节中,我们将探讨一些提供整个系统各类通用细节的系统信息类。

# 4.1.1.1:SystemBasicInformation(0)(查询)

SystemBasicInformation仅支持查询操作,返回SYSTEM_BASIC_INFORMATION结构:

typedef  struct  _SYSTEM_BASIC_INFORMATION
{
    ULONG  Reserved;
    ULONG  TimerResolution;
    ULONG  PageSize;
    ULONG  NumberOfPhysicalPages;
    ULONG  LowestPhysicalPageNumber;
    ULONG  HighestPhysicalPageNumber;
    ULONG  AllocationGranularity;
    ULONG_PTR  MinimumUserModeAddress;
    ULONG_PTR  MaximumUserModeAddress;
    ULONG_PTR  ActiveProcessorsAffinityMask;
    CCHAR  NumberOfProcessors;
}  SYSTEM_BASIC_INFORMATION,   *PSYSTEM_BASIC_INFORMATION;
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上述部分信息可通过Windows API函数GetSystemInfo和GetNativeSystemInfo获取。

大多数成员的含义不言自明,或在SYSTEM_INFO结构的文档中有说明。以下几个成员可能需要额外解释:

• TimerResolution——用于默认计时器滴答(timer ticks)的100纳秒(nsec)单位数,用于线程调度(thread scheduling)。在大多数系统上,此值应为156250(14.625毫秒)。该值是最大计时器分辨率,当前计时器分辨率和最小计时器分辨率可通过本章后续讨论的另一个原生应用程序编程接口(API)NtQueryTimerResolution获取。 • LowestPhysicalPageNumber、HighestPhysicalPageNumber——系统支持的最低和最高物理页编号。最低物理页编号通常为1,这意味着物理内存中的第一页未被使用。 • NumberOfPhysicalPages——物理页的总数,表示系统支持的内存(RAM)容量(乘以PAGE_SIZE(4KB)可得到字节数)。 • NumberOfProcessors——这一点似乎显而易见,但请注意其类型(CCHAR)——取值范围在0到255之间。Windows支持超过255个逻辑处理器。实际上,该数值表示当前处理器组(processor group)中的处理器数量,范围在1到64之间。处理器的总数由下一节将要讨论的另一个系统信息类(SystemProcessorInformation)提供。

# 4.1.1.2 SystemProcessorInformation(1)(查询)

此信息类返回SYSTEM_PROCESSOR_INFORMATION类型的结构:

typedef  struct  _SYSTEM_PROCESSOR_INFORMATION
{
    USHORT  ProcessorArchitecture;
    USHORT  ProcessorLevel;
    USHORT  ProcessorRevision;
    USHORT  MaximumProcessors;
    ULONG  ProcessorFeatureBits;
}  SYSTEM_PROCESSOR_INFORMATION,  *PSYSTEM_PROCESSOR_INFORMATION;
1
2
3
4
5
6
7
8

MaximumProcessors返回系统中逻辑处理器的总数(而非仅当前处理器组中的数量)。除ProcessorFeatureBits(提供特定于CPU的功能位)外,其他成员均在SYSTEM_INFO结构的文档中有说明。

# 4.1.1.3 SystemPerformanceInformation(2)(查询)

返回的结构类型为SYSTEM_PERFORMANCE_INFORMATION,该结构非常庞大。大多数成员的含义不言自明。

# 4.1.1.4 SystemTimeOfDayInformation(3)(查询)

此信息类返回SYSTEM_TIMEOFDAY_INFORMATION类型的结构:

typedef  struct  _SYSTEM_TIMEOFDAY_INFORMATION
{
    LARGE_INTEGER  BootTime;
    LARGE_INTEGER  CurrentTime;
    LARGE_INTEGER  TimeZoneBias;
    ULONG  TimeZoneId;
    ULONG  Reserved;
    ULONGLONG  BootTimeBias;
    ULONGLONG  SleepTimeBias;
}  SYSTEM_TIMEOFDAY_INFORMATION,  *PSYSTEM_TIMEOFDAY_INFORMATION;
1
2
3
4
5
6
7
8
9
10

• BootTime——系统启动的本地时间(以1601年1月1日以来的标准100纳秒单位表示)。 • CurrentTime——当前本地时间。 • TimeZoneBias——与世界协调时间(Universal Time)的差值(以常用的100纳秒单位表示)。 • BootBiasTime和SleepBiasTime通常为0。 • TimeZoneId——已配置的时区索引。

当前时区信息可通过RtlQueryTimeZoneInformation获取:

typedef  struct  _RTL_TIME_ZONE_INFORMATION
{
    LONG  Bias;
    WCHAR  StandardName[32];
    TIME_FIELDS  StandardStart;
    LONG  StandardBias;
    WCHAR  DaylightName[32];
    TIME_FIELDS  DaylightStart;
    LONG  DaylightBias;
}  RTL_TIME_ZONE_INFORMATION,  *PRTL_TIME_ZONE_INFORMATION;

NTSTATUS  RtlQueryTimeZoneInformation(
    _Out_  PRTL_TIME_ZONE_INFORMATION  TimeZoneInformation);
1
2
3
4
5
6
7
8
9
10
11
12
13

返回的名称可能不是常规字符串,而是采用多用户界面(MUI,Multiple User Interface)格式,包含动态链接库(DLL)和标识符(ID)。例如:“@tzres.dll,-112”。这表示该字符串是名为tzres.dll的动态链接库(DLL)(位于某个系统目录中,通常是System32)或基于当前区域设置的动态链接库(DLL)中的资源标识符(ID)112。例如,如果区域设置为“en-US”,则该字符串实际所在的文件是System32\en-US\tzres.dll.mui。

直接访问这些动态链接库(DLL)以查找实际字符串既繁琐又容易出错。幸运的是,外壳(Shell)提供了一个应用程序编程接口(API)来完成此工作:

HRESULT  SHLoadIndirectString(
    _In_  PCWSTR  pszSource,
    _Out_writes_ (cchOutBuf)  PWSTR  pszOutBuf,
    _In_  UINT  cchOutBuf,
    _Reserved_  void  **ppvReserved);
1
2
3
4
5

以下是获取当前时区标准名称的示例用法:

RTL_TIME_ZONE_INFORMATION  tzinfo;
RtlQueryTimeZoneInformation(&tzinfo);
WCHAR  text[64];
SHLoadIndirectString(tzinfo.StandardName,  text,  _countof(text),  nullptr);
printf("Time  zone:  %ws\n " ,  text);
1
2
3
4
5

完整示例包含在本章的SysInfo示例项目中。

# 4.1.2 计时器分辨率(Timer Resolution)

内核(kernel)使用内部计时器来控制所有与计时相关的活动,例如等待时间(wait times)、调度程序唤醒(scheduler wakeups)等。可通过以下应用程序编程接口(API)查询和控制此计时器的基本“滴答”(tick):

NTSTATUS  NtQueryTimerResolution(
    _Out_  PULONG  MaximumTime,
    _Out_  PULONG  MinimumTime,
    _Out_  PULONG  CurrentTime);

NTSTATUS  NtSetTimerResolution(
    _In_  ULONG  DesiredTime,
    _In_  BOOLEAN  SetResolution,
    _Out_  PULONG  ActualTime);
1
2
3
4
5
6
7
8
9

NtQueryTimerResolution返回最大、最小和当前的计时器滴答间隔(以常用的100纳秒单位表示)。

Sysinternals的clockres工具恰好显示这些值。

由于多个进程可能希望影响计时器分辨率,内核会跟踪每个进程所做的更改。系统将使用所请求的最高分辨率,但一旦某个进程退出或取消其对分辨率的影响(调用NtSetTimerResolution并将SetResolution设为FALSE),如果其他进程没有请求如此小的滴答间隔,内核将降低分辨率(增加滴答计数)。

NtSetTimerResolution的最后一个参数返回实际设置的值(基于硬件功能和其他进程的请求),以100纳秒为单位(与DesiredTime使用的单位相同)。

# 4.1.3 处理器动态信息(Processor Dynamic Information)

多个信息类可提供当前处理器组或特定处理器组(如果调用NtQuerySystemInformationEx)中每个处理器的动态信息。表4-1列出了这些信息类及其对应的结构。处理器组编号可通过输入缓冲区(USHORT类型)指定,若调用NtQuerySystemInformation,则使用当前处理器组。请注意,特殊值ALL_PROCESSOR_GROUPS对此类调用无效。

表4-1:处理器信息类

信息类(Information class) 结构类型(Structure type)
SystemProcessorPerformanceInformation(8) SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION
SystemInterruptInformation(23) SYSTEM_INTERRUPT_INFORMATION
SystemProcessorIdleInformation(42) SYSTEM_PROCESSOR_IDLE_INFORMATION
SystemProcessorPowerInformation(61) SYSTEM_PROCESSOR_POWER_INFORMATION
SystemProcessorIdleCycleTimeInformation(83) SYSTEM_PROCESSOR_IDLE_CYCLE_TIME_INFORMATION
SystemProcessorPerformanceDistribution(100) SYSTEM_PROCESSOR_PERFORMANCE_DISTRIBUTION
SystemProcessorCycleTimeInformation(108) SYSTEM_PROCESSOR_CYCLE_TIME_INFORMATION
SystemProcessorPerformanceInformationEx(141) SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION_EX
SystemLogicalProcessorInformation(73) SYSTEM_LOGICAL_PROCESSOR_INFORMATION
SystemProcessorCycleStatsInformation(160) SYSTEM_PROCESSOR_CYCLE_STATS_INFORMATION

预期的输出缓冲区必须是单个结构大小的整数倍,每个结构实例对应一个所需的处理器信息。如果为输出缓冲区提供的大小为0,应用程序编程接口(API)将返回请求的处理器组中所有处理器所需的大小。

以下示例展示了获取其中一种处理器详细信息的方法:

ULONG len;
USHORT group = 0;       //  组0(group  0)
NtQuerySystemInformationEx(SystemProcessorPerformanceInformation,
    &group, sizeof(group), nullptr, 0, &len);
ULONG cpuCount = len / sizeof(SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION);
auto pi = std::make_unique<SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION[]>(cpuCount);
NtQuerySystemInformationEx(SystemProcessorPerformanceInformation,
    &group, sizeof(group), pi.get(), len, nullptr);
for (ULONG i = 0; i < cpuCount; i++)
{
    //  使用pi[i]访问CPU i的信息
}
1
2
3
4
5
6
7
8
9
10
11
12

让我们分析其中一个基本结构SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION:

typedef  struct  _SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION
{
    LARGE_INTEGER  IdleTime;
    LARGE_INTEGER  KernelTime;
    LARGE_INTEGER  UserTime;
    LARGE_INTEGER  DpcTime;
    LARGE_INTEGER  InterruptTime;
    ULONG  InterruptCount;
}  SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION,  *PSYSTEM_PROCESSOR_PERFORMANCE_INFORMATION;
1
2
3
4
5
6
7
8
9

各成员的详细说明如下(所有时间均以100纳秒单位表示):

• IdleTime——处理器处于空闲状态的时间。 • KernelTime——处理器执行内核模式(kernel-mode)代码的时间。请注意,该时间包含IdleTime(空闲时间被视为内核模式执行时间)。 • UserTime——处理器执行用户模式(user-mode)代码的时间。 • DpcTime——CPU执行延迟过程调用(DPC,Deferred Procedure Calls)的时间。这些例程(routines)由内核驱动程序(kernel drivers)提供,通常在中断服务例程(ISR,Interrupt Service Routines)执行后运行。关于延迟过程调用(DPCs)和中断服务例程(ISRs)的完整讨论超出了本专栏的范围。 • InterruptTime——CPU执行中断服务例程(ISRs)的时间。 • InterruptCount——处理器处理的中断次数。

CpuInfo示例提供了完整的实现代码。

# 4.1.4 模块信息(Module Information)

已加载的内核模块(内核本身、硬件抽象层(HAL)以及所有驱动程序)可通过两个信息类获取,分别是系统模块信息(SystemModuleInformation)和扩展系统模块信息(SystemModuleInformationEx)。相关结构定义如下:

typedef struct _RTL_PROCESS_MODULE_INFORMATION {
    HANDLE Section;
    PVOID MappedBase;
    PVOID ImageBase;
    ULONG ImageSize;
    ULONG Flags;
    USHORT LoadOrderIndex;
    USHORT InitOrderIndex;
    USHORT LoadCount;
    USHORT OffsetToFileName;
    UCHAR FullPathName[256];
} RTL_PROCESS_MODULE_INFORMATION, *PRTL_PROCESS_MODULE_INFORMATION;

typedef struct _RTL_PROCESS_MODULES {
    ULONG NumberOfModules;
    RTL_PROCESS_MODULE_INFORMATION Modules[1];
} RTL_PROCESS_MODULES, *PRTL_PROCESS_MODULES;

typedef struct _RTL_PROCESS_MODULE_INFORMATION_EX {
    USHORT NextOffset;
    RTL_PROCESS_MODULE_INFORMATION BaseInfo;
    ULONG ImageChecksum;
    ULONG TimeDateStamp;
    PVOID DefaultBase;
} RTL_PROCESS_MODULE_INFORMATION_EX, *PRTL_PROCESS_MODULE_INFORMATION_EX;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

尽管这些结构的名称中包含“进程(process)”一词,但它们仅用于描述模块——内核模块或用户模块。在上述信息类的语境中,此处特指内核模块。

这些成员的详细说明将在第5章展开。对于内核模块(本章所涉及的场景),节句柄(Section)和映射基址(MappedBase)始终为零。

以下示例展示了如何使用扩展信息类获取模块信息:

ULONG size;
NtQuerySystemInformation(SystemModuleInformationEx, nullptr, 0, &size);
std::unique_ptr<BYTE[]> buffer;
for (;;) {
    buffer = std::make_unique<BYTE[]>(size);
    auto status = NtQuerySystemInformation(SystemModuleInformationEx,
        buffer.get(), size, &size);
    if (NT_SUCCESS(status))
        break;
}

auto mod = (RTL_PROCESS_MODULE_INFORMATION_EX*)buffer.get();
for (;;) {
    // 对 mod 执行相关操作

    if (mod->NextOffset == 0)
        break;

    mod = (RTL_PROCESS_MODULE_INFORMATION_EX*)((PBYTE)mod + mod->NextOffset);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

完整代码位于 ModList 示例项目中。

# 4.2 进程和线程信息(Process and Thread Information)

系统信息查询函数(NtQuerySystemInformation)提供了多个信息类,用于获取系统中运行的进程和线程的详细信息。以下列出了各信息类的值及其关联的结构:

  • 系统进程信息(SystemProcessInformation,值为5):提供系统进程信息(SYSTEM_PROCESS_INFORMATION,进程相关)和系统线程信息(SYSTEM_THREAD_INFORMATION)数组(线程相关),详见图4-1。
  • 扩展系统进程信息(SystemExtendedProcessInformation,值为57):提供系统进程信息(SYSTEM_PROCESS_INFORMATION,进程相关)和扩展系统线程信息(SYSTEM_EXTENDED_THREAD_INFORMATION)数组(线程相关),详见图4-2。
  • 完整系统进程信息(SystemFullProcessInformation,值为148):提供系统进程信息(SYSTEM_PROCESS_INFORMATION),后跟扩展系统线程信息(SYSTEM_EXTENDED_THREAD_INFORMATION)数组(线程相关),再后跟扩展系统进程信息(SYSTEM_PROCESS_INFORMATION_EXTENSION,进程的额外信息),详见图4-3。使用此信息类需要管理员权限。

任务管理器(Task Manager)使用此API枚举进程和线程,Process Explorer也通过这些API枚举进程和线程。

对于进程,其基础结构定义如下:

typedef struct _SYSTEM_PROCESS_INFORMATION {
    ULONG NextEntryOffset;
    ULONG NumberOfThreads;
    LARGE_INTEGER WorkingSetPrivateSize;
    ULONG HardFaultCount;
    ULONG NumberOfThreadsHighWatermark;
    ULONGLONG CycleTime;
    LARGE_INTEGER CreateTime;
    LARGE_INTEGER UserTime;
    LARGE_INTEGER KernelTime;
    UNICODE_STRING ImageName;
    KPRIORITY BasePriority;
    HANDLE UniqueProcessId;
    HANDLE InheritedFromUniqueProcessId;
    ULONG HandleCount;
    ULONG SessionId;
    ULONG_PTR UniqueProcessKey;  // (需使用扩展系统进程信息(SystemExtendedProcessInformation))
    SIZE_T PeakVirtualSize;
    SIZE_T VirtualSize;
    ULONG PageFaultCount;
    SIZE_T PeakWorkingSetSize;
    SIZE_T WorkingSetSize;
    SIZE_T QuotaPeakPagedPoolUsage;
    SIZE_T QuotaPagedPoolUsage;
    SIZE_T QuotaPeakNonPagedPoolUsage;
    SIZE_T QuotaNonPagedPoolUsage;
    SIZE_T PagefileUsage;
    SIZE_T PeakPagefileUsage;
    SIZE_T PrivatePageCount;
    LARGE_INTEGER ReadOperationCount;
    LARGE_INTEGER WriteOperationCount;
    LARGE_INTEGER OtherOperationCount;
    LARGE_INTEGER ReadTransferCount;
    LARGE_INTEGER WriteTransferCount;
    LARGE_INTEGER OtherTransferCount;
    SYSTEM_THREAD_INFORMATION Threads[1];
} SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;
1
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

图4-1:系统进程信息(SystemProcessInformation)详情

该结构的大部分成员含义不言自明,以下是部分不易理解的成员说明:

  • 镜像名称(ImageName):可执行文件路径采用NT设备格式(如\Device\HarddiskVolume…),而非Win32路径。对于内核进程,会使用特殊名称:“System”(系统)、“Secure System”(安全系统)、“Registry”(注册表)、“Memory Compression”(内存压缩)。空闲进程(PID为0)无名称。
  • 继承的唯一进程ID(InheritedFromUniqueProcessId):进程的父进程ID。
  • 下一个条目偏移量(NextEntryOffset):用于定位下一个进程的关键值,即列表中下一个进程的字节偏移量。当该值为零时,枚举应终止。
  • 线程数峰值(NumberOfThreadsHighWatermark):该进程中曾经存在过的最大线程数。
  • 唯一进程键(UniqueProcessKey):与进程ID相同,因为只要进程对象存在,其唯一性就由进程ID定义。
  • 虚拟内存大小(VirtualSize):描述进程的地址空间总使用量(包含已提交和已保留的内存)。其他所有内存计数器均不包含已保留的内存。有关内存计数器的更多详情,请参见第7章。
  • 池(Pool)相关成员:表示归因于该进程的内核内存。例如,句柄条目是从内核的分页内存池(paged memory pool)中分配的。

以下示例展示了如何获取进程信息并遍历进程列表:

ULONG size = 0;
auto status = NtQuerySystemInformation(SystemProcessInformation, nullptr, 0, &size);
std::unique_ptr<BYTE[]> buffer;
while(status == STATUS_INFO_LENGTH_MISMATCH) {
    size += 1024;      // 以防有新进程创建
    buffer = std::make_unique<BYTE[]>(size);
    status = NtQuerySystemInformation(SystemProcessInformation,
        buffer.get(), size, &size);
}

auto p = (SYSTEM_PROCESS_INFORMATION*)buffer.get();

for(;;) {
    //
    // 使用 p...
    //
    if (p->NextEntryOffset == 0)
        break;

    //
    // 移动到下一个进程
    //
    p = (SYSTEM_PROCESS_INFORMATION*)((PBYTE)p + p->NextEntryOffset);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

完整示例位于 ProcList 示例项目中。

img

图4-2:扩展系统进程信息(SystemExtendedProcessInformation)详情

切换到下一个进程的操作可通过将当前系统进程信息(SYSTEM_PROCESS_INFORMATION)指针与下一个条目偏移量(NextEntryOffset)相加实现,需注意将指针强制转换为字节(BYTE)指针,确保加法运算以字节为单位进行。

每个进程的线程列表存储在线程(Threads)数组中,其基础结构定义如下:

typedef struct _SYSTEM_THREAD_INFORMATION {
    LARGE_INTEGER KernelTime;
    LARGE_INTEGER UserTime;
    LARGE_INTEGER CreateTime;
    ULONG WaitTime;
    PVOID StartAddress;
    CLIENT_ID ClientId;
    KPRIORITY Priority;
    LONG BasePriority;
    ULONG ContextSwitches;
    KTHREAD_STATE ThreadState;
    KWAIT_REASON WaitReason;
} SYSTEM_THREAD_INFORMATION, *PSYSTEM_THREAD_INFORMATION;
1
2
3
4
5
6
7
8
9
10
11
12
13

以下是部分非自明成员的说明:

  • 客户端ID(ClientId):包含线程ID和进程ID。
  • 优先级(Priority):线程的当前(动态)优先级;基础优先级(BasePriority):线程的基准优先级。有关线程和优先级的更多详情,请参见第6章。
  • 线程状态(ThreadState):由线程状态枚举(KTHREAD_STATE)表示的线程当前状态。线程状态的完整说明将在第6章展开。
  • 等待原因(WaitReason):线程处于等待状态时的等待原因(类型为等待原因枚举(KWAIT_REASON))。请勿依赖此值,它仅作为提示信息,在实际应用中无实际意义。
  • 起始地址(StartAddress):线程的起始地址。对于用户模式线程,该地址始终指向NtDll.Dll中的RtlUserThreadStart函数地址,与线程创建函数中指定的起始地址不同。后者被称为“Win32地址”,可在后续描述的扩展结构——扩展系统线程信息(SYSTEM_EXTENDED_THREAD_INFORMATION)中获取。

线程描述结构的大小是固定的,因此遍历通常更简单。给定一个系统进程信息(SYSTEM_PROCESS_INFORMATION)指针,线程遍历可采用以下形式(假设使用的信息类为系统进程信息(SystemProcessInformation)):

void EnumThreads(SYSTEM_PROCESS_INFORMATION* p) {
    auto t = p->Threads;
    for (ULONG i = 0; i < p->NumberOfThreads; i++) {
        // 对 t 执行相关操作
        t++;
    }
}
1
2
3
4
5
6
7

完整示例请参见 ThreadList 示例项目。

图4-3:完整系统进程信息(SystemFullProcessInformation)详情

扩展进程信息结构如下:

typedef enum _SYSTEM_PROCESS_CLASSIFICATION {
    SystemProcessClassificationNormal = 0,
    SystemProcessClassificationSystem = 1,  // System
    SystemProcessClassificationSecureSystem = 2,  // Secure Kernel
    SystemProcessClassificationMemCompression = 3,  // Memory Compression
    SystemProcessClassificationRegistry = 4,  // Registry
    SystemProcessClassificationMaximum = 5
} SYSTEM_PROCESS_CLASSIFICATION, *PSYSTEM_PROCESS_CLASSIFICATION;

typedef struct _PROCESS_DISK_COUNTERS {
    ULONGLONG BytesRead;
    ULONGLONG BytesWritten;
    ULONGLONG ReadOperationCount;
    ULONGLONG WriteOperationCount;
    ULONGLONG FlushOperationCount;
} PROCESS_DISK_COUNTERS, *PPROCESS_DISK_COUNTERS;

typedef struct _SYSTEM_PROCESS_INFORMATION_EXTENSION {
    PROCESS_DISK_COUNTERS DiskCounters;
    ULONGLONG ContextSwitches;
    union {
        ULONG Flags;
        struct {
            ULONG HasStrongId : 1;
            ULONG Classification : 4;  // SYSTEM_PROCESS_CLASSIFICATION
            ULONG BackgroundActivityModerated : 1;
            ULONG Spare : 26;
        };
    };
    ULONG UserSidOffset;
    ULONG PackageFullNameOffset;         // since THRESHOLD
    PROCESS_ENERGY_VALUES EnergyValues;  // since THRESHOLD
    ULONG AppIdOffset;                   // since THRESHOLD
    SIZE_T SharedCommitCharge;           // since THRESHOLD2
    ULONG JobObjectId;                   // since REDSTONE
    ULONG SpareUlong;                    // since REDSTONE
    ULONGLONG ProcessSequenceNumber;
} SYSTEM_PROCESS_INFORMATION_EXTENSION, *PSYSTEM_PROCESS_INFORMATION_EXTENSION;
1
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

以下是非常规成员的说明:

• UserSidOffset 是从 SYSTEM_PROCESS_INFORMATION_EXTENSION 结构起始位置到该进程运行用户的安全标识符(SID)二进制存储位置的偏移量。要转换为字符串,可按如下方式调用 RtlConvertSidToUnicodeString 函数:

std::wstring SidToString(PSID sid) {
    UNICODE_STRING str;
    if (NT_SUCCESS(RtlConvertSidToUnicodeString(&str, sid, TRUE))) {
        std::wstring result(str.Buffer, str.Length / sizeof(WCHAR));
        RtlFreeUnicodeString(&str);
        return result;
    }
    return L"";
}
1
2
3
4
5
6
7
8
9

另一种将安全标识符(SID)转换为字符串的方法是使用 Windows API 中的 ConvertSidToStringSid 函数。

• PackageFullNameOffset 是从 SYSTEM_PROCESS_INFORMATION_EXTENSION 对象起始位置到该进程所执行的通用 Windows 平台(UWP)应用完整包名的以空字符结尾的字符串的偏移量。如果相关进程并非此类进程,则 PackageFullNameOffset 为 0。此外,若 PackageFullNameOffset 非 0,则 HasStrongId 成员为 TRUE,反之亦然。

• AppIdOffset 是从 SYSTEM_PROCESS_INFORMATION_EXTENSION 对象起始位置到应用程序标识符(Application ID)的以空字符结尾的字符串的偏移量,该标识符用于某些外壳(Shell)功能,如任务栏跳转列表。如果进程未关联应用程序标识符(Application ID),则该值可能为 0。

• JobObjectId 是包含此进程的作业(job)的标识符(若存在)。作业标识符(job ID)主要用于 Windows 容器(Windows Containers)。

• ProcessSequenceNumber 是自 Windows 启动以来有效的唯一进程标识符,不会被重复使用(而进程标识符(process ID)在进程对象销毁后可能会被重复使用)。

若要在任意时间获取系统上的唯一进程标识符,可将进程标识符(process ID)与进程创建时间组合使用。二者结合可确保在同一台计算机上的任意时间都是唯一的。

如果 SYSTEM_PROCESS_INFORMATION 按前文所示声明,那么访问扩展进程信息会稍有难度。这是因为声明中包含了线程(Threads)数组的首个索引。若从声明中移除线程(Threads)项会相对简便,当然也可以这样做。phnt 头文件提供的声明便于使用 SystemProcessInformation,但会使 SystemFullProcessInformation 的使用变得繁琐。

不过,这仍可实现,如下列代码片段所示:

// 假设 NtQuerySystemInformation 调用成功
auto p = (SYSTEM_PROCESS_INFORMATION*)buffer.get();

for (;;) {
    auto px = (SYSTEM_PROCESS_INFORMATION_EXTENSION*)((PBYTE)p
        + sizeof(*p) - sizeof(SYSTEM_THREAD_INFORMATION)
        + p->NumberOfThreads * sizeof(SYSTEM_EXTENDED_THREAD_INFORMATION));
    //
    // 根据需要使用 p 和 px...
    //

    if (p->NextEntryOffset == 0)
        break;
    p = (SYSTEM_PROCESS_INFORMATION*)((PBYTE)p + p->NextEntryOffset);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

完整示例请参见 ProcList 样本程序。

从上述代码可以看出,对于 SystemFullProcessInformation(以及 SystemExtendedProcessInformation),每个线程信息现在都是 SYSTEM_EXTENDED_THREAD_INFORMATION 类型:

typedef struct _SYSTEM_EXTENDED_THREAD_INFORMATION {
    SYSTEM_THREAD_INFORMATION ThreadInfo;
    PVOID StackBase;
    PVOID StackLimit;
    PVOID Win32StartAddress;
    PTEB TebBase;
    ULONG_PTR Reserved2;
    ULONG_PTR Reserved3;
    ULONG_PTR Reserved4;
} SYSTEM_EXTENDED_THREAD_INFORMATION, *PSYSTEM_EXTENDED_THREAD_INFORMATION;
1
2
3
4
5
6
7
8
9
10

其基本结构为首个成员,因此是一种“扩展”(extended)结构,而非进程案例中与原始结构截然不同的“扩展”(extension)结构。

以下是额外成员的简要说明:

• StackBase 和 StackLimit 提供当前线程堆栈的起始地址和结束地址。对于用户模式线程,地址位于用户空间;而对于内核模式线程,地址位于内核空间。在英特尔/超微(Intel/AMD)平台上,由于堆栈在内存中向下增长,因此 StackBase 大于 StackLimit。

• Win32StartAddress 是提供给用户模式线程创建函数的地址,而非 SYSTEM_THREAD_INFORMATION 结构中看到的 RtlUserThreadStart 地址。对于内核线程,此值为 0。

• TebBase 是该线程的线程环境块(Thread Environment Block,TEB)地址(内核线程为 0)。有关线程环境块(TEB)的更多信息,请参见第 6 章。

扩展线程枚举示例以包含扩展信息。

# 4.3 对象与句柄(Objects and Handles)

Windows 是基于对象的操作系统(object-based operating system),这意味着许多功能都围绕对象展开,例如进程(processes)、线程(threads)、内存段(sections)、文件(files)、信号量(semaphores)等等。内核对象(kernel objects)在用户模式(以及可选的内核模式)中通过句柄(handles)进行访问,句柄充当指向系统空间中内核对象的间接“指针”,并附带一些特定于句柄的属性。

有关句柄和对象的完整讨论超出了本专栏的范围。

简要来说,句柄项(handle entry)关联以下数据:

• 其指向的对象的地址。

• 获取此句柄时使用的访问掩码(access mask),用于指定该句柄对对象拥有的“权限”。

• 三个可选标志:可继承句柄(inheritable handle,标记为“I”)——指示当前进程创建子进程时,该句柄是否应被子进程继承;禁止关闭(protect from close,标记为“P”)——指示必须先移除此标志才能关闭句柄,用于防止意外关闭句柄;关闭时审计(audit on close,标记为“A”)——指示关闭该句柄时应在事件日志中进行审计。

# 4.3.1 句柄(Handles)

NtQuerySystemInformation 提供了用于检索系统中句柄列表的信息类:SystemHandleInformation(16)和 SystemExtendedHandleInformation(64)。前者应被视为已过时,因为其提供的信息比后者更有限,而且它使用USHORT作为进程标识符(process IDs)和句柄值(handle values)的数据类型,这一类型过小。进程标识符(process IDs)和句柄值(handle values)限制为 26 位(而非仅 16 位);因此,我们仅使用 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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Object 是对象在内核空间中的地址。UniqueProcessId 是此句柄所属的进程标识符(process ID)句柄表,而 HandleValue 是句柄的数值。句柄值是 4 的倍数,其中第一个有效句柄为 4。GrantedAccess 是该句柄对对象的访问掩码(access mask)。ObjectTypeIndex 是引用对象类型(例如进程、线程、文件等)的类型索引。下一节将介绍如何根据该索引检索类型名称。最后,HandleAttributes 包含前文提到的可选句柄标志。可能的值包括 OBJ_PROTECT_CLOSE(1)、OBJ_INHERIT(2)和 OBJ_AUDIT_OBJECT_CLOSE(4)。据我所知,CreatorBackTraceIndex 始终为 0。

调用 NtQuerySystemInformation 时先传入 0 大小并获取所需大小的常规方法对 SystemExtendedHandleInformation 不起作用;更准确地说,它会返回 SYSTEM_HANDLE_INFORMATION_EX 的大小作为最小要求,而这仅能提供句柄计数。

一种解决方法是分配内存,检查其是否足够大,若不足则将分配大小加倍,依此类推,直到分配的内存足够大:

std::unique_ptr<BYTE[]> buffer;
ULONG size = 1 << 20;       // 起始大小为 1MB
NTSTATUS status;
do {
    buffer = std::make_unique<BYTE[]>(size);
    status = NtQuerySystemInformation(SystemExtendedHandleInformation,
        buffer.get(), size, nullptr);
    if (NT_SUCCESS(status))
        break;
    size *= 2;
} while (status == STATUS_INFO_LENGTH_MISMATCH);

if (!NT_SUCCESS(status)) {
    // 发生某种错误
}

auto p = (SYSTEM_HANDLE_INFORMATION_EX*)buffer.get();
for (ULONG_PTR i = 0; i < p->NumberOfHandles; i++) {
    auto& h = p->Handles[i];
    // 对 h 执行某些操作
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

完整示例位于 Handles 样本项目中。

# 4.3.2 对象类型(Object Types)

Windows 支持多种对象类型;不同的 Windows 版本可能支持略有不同的对象类型集合。你可以在我的工具 Object Explorer(图 4-4)中查看对象类型的详细信息。如何获取此信息?

Figure 4-4: Object Explorer 查看对象类型

对象类型信息可通过 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,
} 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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

此 API 通常需要一个对象的句柄才能提供所需的详细信息。然而,要获取所有现有对象类型的信息,可提供一个空句柄(NULL handle)并指定 ObjectTypesInformation。这种情况下,也需要“猜测”所需缓冲区的大小,而无法直接获取所需大小。不过,返回的信息大小是固定的,因为它涉及的是类型而非任何特定对象。

返回的指针类型为 OBJECT_TYPES_INFORMATION:

typedef struct _OBJECT_TYPES_INFORMATION {
    ULONG NumberOfTypes;
} OBJECT_TYPES_INFORMATION, *POBJECT_TYPES_INFORMATION;
1
2
3

以下展示了如何获取对象类型信息(假设分配了足够大的固定内存,省略了错误处理):

ULONG size = 1 << 14;
auto buffer = std::make_unique<BYTE[]>(size);
NtQueryObject(nullptr, ObjectTypesInformation, buffer.get(), size, nullptr);

auto p = (OBJECT_TYPES_INFORMATION*)buffer.get();
auto type = (OBJECT_TYPE_INFORMATION*)((PBYTE)p + sizeof(ULONG_PTR));
for (ULONG i = 0; i < p->NumberOfTypes; i++) {
    // 对 type 执行某些操作...

    // 移动到下一个类型
    auto offset = sizeof(OBJECT_TYPE_INFORMATION) + type->TypeName.MaximumLength;
    if (offset % sizeof(ULONG_PTR))
        offset += sizeof(ULONG_PTR) - ((ULONG_PTR)type + offset) % sizeof(ULONG_PTR);

    type = (OBJECT_TYPE_INFORMATION*)((PBYTE)type + offset);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

由于每个类型对象(type object)都起始于对齐地址(32位进程中为4字节,64位进程中为8字节),这使得代码变得复杂。下一个类型的偏移量必须考虑该类型的名称,并且如果最终地址不是对齐大小的整数倍,还需要添加填充(padding)。

对于每种类型,我们会获取以下信息:

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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

以下成员会被使用(其他所有成员均始终为零):

  • TypeName 是该类型的名称,例如“Process”(进程)、“Event”(事件)、“Desktop”(桌面)等。
  • TotalNumberOfObjects 是当前该类型的对象数量。
  • TotalNumberOfHandles 是当前指向该类型对象的句柄(handle)数量。
  • HighWaterNumberOfObjects 是自最近一次系统启动以来该类型对象的峰值数量。
  • HighWaterNumberOfHandles 是自最近一次系统启动以来指向该类型对象的句柄峰值数量。
  • InvalidAttributes 是对该类型的句柄无效的属性标志集(OBJ_xxx)。
  • GenericMapping 是标准权限(GENERIC_READ、GENERIC_WRITE、GENERIC_EXECUTE 和 GENERIC_ALL)到该类型对象特定权限的映射。请参阅 SDK 文档中的 GENERIC_MAPPING 结构说明。
  • ValidAccessMask 是该类型的有效访问位集。
  • SecurityRequired 指示该类型的对象是否必须使用安全描述符(Security Descriptor)创建。
  • MaintainHandleCount 指示是否在类型对象内维护指向该类型对象的句柄列表。此标志主要用于与用户界面(UI)相关的内核对象(窗口站、桌面、合成器等少数对象)。
  • TypeIndex 是该类型的索引。这允许与 SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX 中的 ObjectTypeIndex 成员相关联。
  • PoolType 是用于分配该类型对象的内存池(pool)类型。以下枚举(未在 phnt 中定义)提供了其取值:
enum class PoolType {
    PagedPool = 1,
    NonPagedPool = 0,
    NonPagedPoolNx = 0x200,
    NonPagedPoolSessionNx = NonPagedPoolNx + 32,
    PagedPoolSessionNx = NonPagedPoolNx + 33
};
1
2
3
4
5
6
7
  • DefaultPagedPoolCharge 和 DefaultNonPagedPoolCharge 是分配该类型对象时所需的分页/非分页内核内存的默认大小。

ObjectTypes 示例提供了完整的代码示例,用于显示一组类型及其部分成员。代码的主要部分如下:

const char* PoolTypeToString(ULONG poolType) {
    switch ((PoolType)poolType) {
        case PoolType::PagedPool:
            return "Paged";
        case PoolType::NonPagedPool:
            return "Non Paged";
        case PoolType::NonPagedPoolNx:
            return "Non Paged NX";
        case PoolType::NonPagedPoolSessionNx:
            return "Session Non Paged NX";
        case PoolType::PagedPoolSessionNx:
            return "Session Paged NX";
    }
    return "";
}

void DisplayType(OBJECT_TYPE_INFORMATION* type) {
    printf("%2d  %-34wZ  H:  %6u  O:  %6u  PH:  %6u  PO:  %6u  Pool:  %s\n",
           type->TypeIndex, &type->TypeName,
           type->TotalNumberOfHandles, type->TotalNumberOfObjects,
           type->HighWaterNumberOfHandles, type->HighWaterNumberOfObjects,
           PoolTypeToString(type->PoolType));
}

int main() {
    ULONG size = 1 << 14;
    auto buffer = std::make_unique<BYTE[]>(size);
    if (!NT_SUCCESS(NtQueryObject(nullptr, ObjectTypesInformation,
                                  buffer.get(), size, nullptr))) {
        return 1;
    }
    auto p = (OBJECT_TYPES_INFORMATION*)buffer.get();
    auto type = (OBJECT_TYPE_INFORMATION*)((PBYTE)p + sizeof(ULONG_PTR));
    long long handles = 0, objects = 0;
    for (ULONG i = 0; i < p->NumberOfTypes; i++) {
        DisplayType(type);
        handles += type->TotalNumberOfHandles;
        objects += type->TotalNumberOfObjects;

        auto offset = sizeof(OBJECT_TYPE_INFORMATION) + type->TypeName.MaximumLength;
        if (offset % sizeof(ULONG_PTR))
            offset += sizeof(ULONG_PTR) - ((ULONG_PTR)type + offset) % sizeof(ULONG_PTR);
        type = (OBJECT_TYPE_INFORMATION*)((PBYTE)type + offset);
    }
    printf("Types:  %u  Handles:  %llu  Objects:  %llu\n",
           p->NumberOfTypes, handles, objects);
    return 0;
}
1
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

以下是在 Windows 10 机器上的示例输出(已精简):

在 Windows 11 机器上进行测试,你会发现一些新增和移除的对象类型。

# 4.3.3 对象名称(Object Names)

一些内核对象(kernel object)关联有基于字符串的名称。使用带有 SystemExtendedHandleInformation 的 QuerySystemInformation 无法提供每个句柄所指向对象的名称(如果有的话)。原因之一是这可能会耗费大量资源,而且并非绝对必要。此外,指向同一对象的多个句柄可能会多次分配相同的名称,否则维护已检索名称的对象列表会非常昂贵。

要获取对象的名称(由句柄指向),原生 API(native API)提供了带有 ObjectNameInformation 信息类的 NtQueryObject。在枚举句柄时可能会发现一个问题:枚举返回的句柄值仅在其所属的进程中有效。这意味着要获取对象的名称,我们必须先将句柄复制(duplicate)到调用进程,然后再使用 NtQueryObject。

以下是一个初始版本的实现,给定进程 ID 和在该进程中有效的句柄,返回对象的名称(如果有的话),以 std::wstring 形式表示。第一步是打开目标进程的句柄,用于句柄复制:

NTSTATUS GetObjectName(HANDLE h, DWORD pid, std::wstring& name) {
    OBJECT_ATTRIBUTES procAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(nullptr, 0);
    CLIENT_ID cid{};
    cid.UniqueProcess = ULongToHandle(pid);
    HANDLE hProcess;
    auto status = NtOpenProcess(&hProcess, PROCESS_DUP_HANDLE, &procAttr, &cid);
    if (!NT_SUCCESS(status))
        return status;

    // 下一步是调用 NtDuplicateObject,该函数(尽管名称如此)等同于 Windows API 中的 DuplicateHandle。关于 NtDuplicateObject 的详细讨论将在第 7 章进行:
    HANDLE hDup;
    status = NtDuplicateObject(hProcess, h, NtCurrentProcess(),
                               &hDup, READ_CONTROL, 0, 0);

    // 我们将句柄复制到当前进程,因此 hDup 现在在当前进程中有效。假设调用成功,我们可以继续如下操作:
    BYTE buffer[1024];    // 希望足够大 :)
    auto sname = (UNICODE_STRING*)buffer;
    status = NtQueryObject(hDup, ObjectNameInformation,
                           sname, sizeof(buffer), nullptr);
    if (NT_SUCCESS(status))
        name.assign(sname->Buffer, sname->Length / sizeof(WCHAR));
    NtClose(hDup);

    NtClose(hProcess);
    return status;
}
1
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

这对于大多数对象类型都有效,但对于许多文件对象(file object)除外——调用 NtQueryObject 会挂起。这是因为许多文件对象在查询名称时需要获取相关文件系统维护的锁,而这些锁可能无法获取。

我发现的唯一合理的解决方法是跳过这些“有问题”的文件对象,具体做法是在单独的线程中执行查询,如果线程阻塞时间过长,则终止该线程并继续处理其他对象。

这会使代码变得更加复杂,但却是无法避免的。以下是完整的 GetObjectName 实现,其中一个布尔标志指示所涉及的句柄是否用于文件对象。关于 NtCreateThreadEx、NtWaitForSingleObject、NtTerminateThread 和 NtQueryInformationThread 的讨论将推迟到第 6 章(“线程”)和第 7 章(“对象和句柄”)。

NTSTATUS GetObjectName(HANDLE h, DWORD pid, std::wstring& name, bool file) {
    OBJECT_ATTRIBUTES procAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(nullptr, 0);
    CLIENT_ID cid{};
    cid.UniqueProcess = ULongToHandle(pid);
    HANDLE hProcess;
    auto status = NtOpenProcess(&hProcess, PROCESS_DUP_HANDLE, &procAttr, &cid);
    if (!NT_SUCCESS(status))
        return status;

    HANDLE hDup;
    status = NtDuplicateObject(hProcess, h, NtCurrentProcess(),
                               &hDup, READ_CONTROL, 0, 0);
    if (NT_SUCCESS(status)) {
        static BYTE buffer[1024];     // 希望足够大 :)
        auto sname = (UNICODE_STRING*)buffer;
        if (file) {
            //
            // 文件的特殊情况
            // 在单独的线程中获取名称
            // 如果等待一段时间后仍未返回,则终止线程
            //
            HANDLE hThread;
            //
            // 创建线程
            //
            status = NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS,
                                      &procAttr, NtCurrentProcess(),
                                      (PTHREAD_START_ROUTINE)[](auto param) -> DWORD {
                                          auto h = (HANDLE)param;
                                          auto status = NtQueryObject(h, ObjectNameInformation,
                                                                     buffer, sizeof(buffer), nullptr);

                                          //
                                          // 状态将作为线程的退出码
                                          //
                                          return status;
                                      },
                                      hDup, 0, 0, 0, 0, nullptr);
            if (NT_SUCCESS(status)) {
                LARGE_INTEGER interval;
                interval.QuadPart = -50 * 10000;         // 50 毫秒
                status = NtWaitForSingleObject(hThread, FALSE, &interval);
                if (status == STATUS_TIMEOUT) {
                    NtTerminateThread(hThread, 1);
                }
                else {
                    //
                    // 读取线程的退出码
                    //
                    THREAD_BASIC_INFORMATION tbi;
                    NtQueryInformationThread(hThread, ThreadBasicInformation,
                                             &tbi, sizeof(tbi), nullptr);
                    status = tbi.ExitStatus;
                }
                NtClose(hThread);
            }
        }
        else {
            //
            // 非文件对象,执行常规查询
            //
            status = NtQueryObject(hDup, ObjectNameInformation,
                                   sname, sizeof(buffer), nullptr);
        }
        //
        // 将结果赋值给 std::wstring
        //
        if (NT_SUCCESS(status))
            name.assign(sname->Buffer, sname->Length / sizeof(WCHAR));
        NtClose(hDup);
    }
    NtClose(hProcess);
    return status;
}
1
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
67
68
69
70
71
72
73
74

Handles 示例包含完整的代码。关于对象和句柄的更多信息将在第 7 章提供。

# 4.4 KUSER_SHARED_DATA 结构

KUSER_SHARED_DATA 数据结构(data structure)部分由微软文档化,提供全局系统信息(global system information)。该结构在用户模式(user-mode)下是只读的,并且映射到每个进程中的相同虚拟地址(0x7ffe0000)。因此,访问该数据非常简单:

auto data = (KUSER_SHARED_DATA*)0x7ffe0000;
// 访问数据...
1
2

该结构中的一些细节也可以通过 NtQuerySystemInformation 获取。无论如何,都可以直接访问它。例如,可以这样显示 Windows 版本:

auto data = (KUSER_SHARED_DATA*)0x7ffe0000;
printf("Version:  %d.%d.%d\n",
       data->NtMajorVersion, data->NtMinorVersion, data->NtBuildNumber);
1
2
3

phnt 头文件提供了 USER_SHARED_DATA 宏,无需强制类型转换即可访问该结构。

# 4.5 总结

仅通过一个函数 NtQuerySystemInformation,就可以获取大量的系统信息。我建议在查看各种信息类时保持探索的心态,包括通过尝试和错误的方式。尽管如此,本专栏的未来版本可能会对本章内容进行扩展。

第3章 原生应用程序(Native Applications)
第5章:进程

← 第3章 原生应用程序(Native Applications) 第5章:进程→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2026 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式