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章:系统信息
  • 第5章:进程
    • 5.1 创建进程(Creating Processes)
    • 5.2 进程信息
      • 5.2.1 ProcessBasicInformation
      • 5.2.2 ProcessIoCounters
      • 5.2.3 ProcessVmCounters
      • 5.2.4 ProcessTimes
      • 5.2.5 ProcessBasePriority 和 ProcessPriorityClass
      • 5.2.6 ProcessHandleCount
      • 5.2.7 ProcessSessionInformation
      • 5.2.8 ProcessImageFileName 和 ProcessImageFileNameWin32
      • 5.2.9 ProcessImageInformation
      • 5.2.10 ProcessHandleInformation
      • 5.2.11 ProcessHandleTable
      • 5.2.12 ProcessCommandLineInformation
    • 5.3 进程环境块(Process Environment Block,PEB)
    • 5.4 挂起和恢复进程(Suspending and Resuming Processes)
    • 5.5 枚举进程(第二种方法)
    • 5.6 总结
  • 第6章:线程
  • 第7章:对象与句柄
  • 第 8 章:内存(第一部分)
  • 第9章:I/O
  • 第10章:ALPC
  • 第11章 安全性(Security)
  • 第12章 内存(第二部分)
  • 第13章 注册表
目录

第5章:进程

# 第5章:进程

进程可能是所有内核对象类型中最易识别的一种。在本章中,我们将探讨与进程创建、枚举和操作相关的原生API(native API)。

本章包含以下内容:

  • 创建进程
  • 进程信息
  • 进程环境块(The Process Environment Block)
  • 挂起和恢复进程(Suspending and Resuming Processes)
  • 枚举进程(Enumerating Processes)
  • 作业(Jobs)

# 5.1 创建进程(Creating Processes)

Windows API提供了多个创建进程的函数,例如CreateProcess和CreateProcessAsUser。尽管这些API最终会调用原生API,但它们会执行大量工作,这使得在创建属于Windows子系统(即非原生应用程序)的进程时,很难重现相同的行为。

原生API提供了RtlCreateUserProcess(Ex)函数,以及配套的结构体和函数。我们在第3章中使用了RtlCreateUserProcess来运行原生应用程序,因为CreateProcess并未被设计用于处理这种情况。在本节中,我将介绍原生API的一些相关内容,但建议仅将其用于创建原生应用程序的进程。

信息安全(Infosec)领域曾有多次尝试利用RtlCreateUserProcess和/或NtCreateUserProcess来运行Windows子系统应用程序,取得了不同程度的成功。部分问题在于需要与Windows子系统进程(csrss.exe)通信,以通知它新进程和线程的创建。这一过程实际上很不稳定,因为不同的Windows版本可能有略微不同的要求。这或许仍是一个有趣的研究课题,但超出了本专栏的范围。

以下是RtlCreateUserProcess(Ex)函数的定义,若调用成功,返回的结构中将包含所创建进程的相关信息:

第5章:进程 91

typedef struct _RTL_USER_PROCESS_INFORMATION {
    ULONG Length;
    HANDLE ProcessHandle;
    HANDLE ThreadHandle;
    CLIENT_ID ClientId;
    SECTION_IMAGE_INFORMATION ImageInformation;
} RTL_USER_PROCESS_INFORMATION, *PRTL_USER_PROCESS_INFORMATION;

NTSTATUS RtlCreateUserProcess(
    _In_ PUNICODE_STRING NtImagePathName,
    _In_ ULONG AttributesDeprecated,
    _In_ PRTL_USER_PROCESS_PARAMETERS ProcessParameters,
    _In_opt_ PSECURITY_DESCRIPTOR ProcessSecurityDescriptor,
    _In_opt_ PSECURITY_DESCRIPTOR ThreadSecurityDescriptor,
    _In_opt_ HANDLE ParentProcess,
    _In_ BOOLEAN InheritHandles,
    _In_opt_ HANDLE DebugPort,
    _In_opt_ HANDLE TokenHandle,  // 曾用名为ExceptionPort
    _Out_ PRTL_USER_PROCESS_INFORMATION ProcessInformation);

NTSTATUS RtlCreateUserProcessEx(
    _In_ PUNICODE_STRING NtImagePathName,
    _In_ PRTL_USER_PROCESS_PARAMETERS ProcessParameters,
    _In_ BOOLEAN InheritHandles,
    _In_opt_ PRTL_USER_PROCESS_EXTENDED_PARAMETERS ExtendedParameters,
    _Out_ PRTL_USER_PROCESS_INFORMATION ProcessInformation);
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

可能令人惊讶的是,扩展函数(RtlCreateUserProcessEx)的参数比原始函数更少。在这种情况下,原始函数中一些不太常用的参数被整合到了一个扩展参数结构中,该结构定义如下:

typedef struct _RTL_USER_PROCESS_EXTENDED_PARAMETERS {
    USHORT Version;         // 当前必须设置为1
    USHORT NodeNumber;
    PSECURITY_DESCRIPTOR ProcessSecurityDescriptor;
    PSECURITY_DESCRIPTOR ThreadSecurityDescriptor;
    HANDLE ParentProcess;       // 用于继承
    HANDLE DebugPort;
    HANDLE TokenHandle;         // 需要SeAssignPrimaryTokenPrivilege权限
    HANDLE JobHandle;
} RTL_USER_PROCESS_EXTENDED_PARAMETERS, *PRTL_USER_PROCESS_EXTENDED_PARAMETERS;
1
2
3
4
5
6
7
8
9
10

在撰写本文时,上述结构尚未在phnt头文件中定义。

非可选的进程参数结构(RTL_USER_PROCESS_PARAMETERS)规模较大,定义如下:

typedef struct _RTL_USER_PROCESS_PARAMETERS {
    ULONG MaximumLength;
    ULONG Length;

    ULONG Flags;
    ULONG DebugFlags;

    HANDLE ConsoleHandle;
    ULONG ConsoleFlags;
    HANDLE StandardInput;      // 对应STARTUPINFO.hStdInput
    HANDLE StandardOutput;     // 对应STARTUPINFO.hStdOutput
    HANDLE StandardError;      // 对应STARTUPINFO.hStdError

    CURDIR CurrentDirectory;
    UNICODE_STRING DllPath;
    UNICODE_STRING ImagePathName;
    UNICODE_STRING CommandLine;
    PVOID Environment;

    ULONG StartingX;         // 对应STARTUPINFO.dwX
    ULONG StartingY;         // 对应STARTUPINFO.dwY
    ULONG CountX;            // 对应STARTUPINFO.dwXSize
    ULONG CountY;            // 对应STARTUPINFO.dwYSize
    ULONG CountCharsX;    	 // 对应STARTUPINFO.dwXCountChars
    ULONG CountCharsY;       // 对应STARTUPINFO.dwYCountChars
    ULONG FillAttribute;     // 对应STARTUPINFO.dwFillAttribute

    ULONG WindowFlags;           // 对应STARTUPINFO.dwFlags
    ULONG ShowWindowFlags;       // 对应STARTUPINFO.wShowWindow
    UNICODE_STRING WindowTitle;  // 对应STARTUPINFO.lpTitle
    UNICODE_STRING DesktopInfo;
    UNICODE_STRING ShellInfo;
    UNICODE_STRING RuntimeData;
    RTL_DRIVE_LETTER_CURDIR CurrentDirectories[RTL_MAX_DRIVE_LETTERS];

    ULONG_PTR EnvironmentSize;

    ULONG_PTR EnvironmentVersion;
    PVOID PackageDependencyData;
    ULONG ProcessGroupId;
    ULONG LoaderThreads;

    UNICODE_STRING RedirectionDllName;  // 从REDSTONE4版本开始支持
    UNICODE_STRING HeapPartitionName;   // 从19H1版本开始支持
    ULONG_PTR DefaultThreadpoolCpuSetMasks;
    ULONG DefaultThreadpoolCpuSetMaskCount;
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
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

该结构的部分成员与Windows API中定义的、用于CreateProcess的STARTUPINFO结构直接对应。其他成员则较为晦涩。进程创建后,可以在进程环境块(PEB)的ProcessParameters成员中找到该结构。

这是一个变长结构,因为其中包含一些字符串。原生API提供了两个函数用于创建并部分初始化RTL_USER_PROCESS_PARAMETERS,以及一个用于释放该结构的函数:

NTSTATUS RtlCreateProcessParameters(
    _Out_ PRTL_USER_PROCESS_PARAMETERS *pProcessParameters,
    _In_ PUNICODE_STRING ImagePathName,
    _In_opt_ PUNICODE_STRING DllPath,
    _In_opt_ PUNICODE_STRING CurrentDirectory,
    _In_opt_ PUNICODE_STRING CommandLine,
    _In_opt_ PVOID Environment,
    _In_opt_ PUNICODE_STRING WindowTitle,
    _In_opt_ PUNICODE_STRING DesktopInfo,
    _In_opt_ PUNICODE_STRING ShellInfo,
    _In_opt_ PUNICODE_STRING RuntimeData);

NTSTATUS RtlCreateProcessParametersEx(
    _Out_ PRTL_USER_PROCESS_PARAMETERS *pProcessParameters,
    _In_ PUNICODE_STRING ImagePathName,
    _In_opt_ PUNICODE_STRING DllPath,
    _In_opt_ PUNICODE_STRING CurrentDirectory,
    _In_opt_ PUNICODE_STRING CommandLine,
    _In_opt_ PVOID Environment,
    _In_opt_ PUNICODE_STRING WindowTitle,
    _In_opt_ PUNICODE_STRING DesktopInfo,
    _In_opt_ PUNICODE_STRING ShellInfo,
    _In_opt_ PUNICODE_STRING RuntimeData,
    _In_ ULONG Flags);

NTSTATUS RtlDestroyProcessParameters(
    _In_ _Post_invalid_ PRTL_USER_PROCESS_PARAMETERS ProcessParameters);
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

这两个创建函数实际上几乎相同,唯一的区别是RtlCreateProcessParametersEx多了一个Flags参数。最常用的标志是RTL_CREATE_PROC_PARAMS_NORMALIZED(值为1),该标志指示结构应初始化为“标准化”(normalized)状态。标准化意味着结构中的字符串指针是真实指针,可以直接用于后续需要该结构的调用中。其缺点是无法复制该结构供后续使用,因为指针是绝对地址。若创建的结构为非标准化(de-normalized)状态,则各个UNICODE_STRING.Buffer成员中仅存储偏移量,因此可以通过类似memcpy的简单调用来复制该结构。非扩展函数(RtlCreateProcessParameters)会将结构初始化为非标准化状态。RtlCreateUserProcess在使用该结构前,会根据需要将其标准化。

原生API提供了两个函数用于执行标准化或非标准化操作:

PRTL_USER_PROCESS_PARAMETERS RtlNormalizeProcessParams(
    _Inout_ PRTL_USER_PROCESS_PARAMETERS ProcessParameters);

PRTL_USER_PROCESS_PARAMETERS RtlDeNormalizeProcessParams(
    _Inout_ PRTL_USER_PROCESS_PARAMETERS ProcessParameters);
1
2
3
4
5

RTL_USER_PROCESS_PARAMETERS的Flags成员会跟踪标准化状态,因此仅在需要时才执行相应操作。

以下是RTL_USER_PROCESS_PARAMETERS各成员的更多信息:

  • ShellInfo:任意字符串或缓冲区,将原样传递给新进程。一些控制台应用程序会在内部使用该成员。
  • DesktopInfo:可选字符串,格式为“窗口站名\桌面名”(winstaname\desktopname),指向另一个窗口站(Window Station)和/或桌面(Desktop)。这与STARTUPINFO.lpDesktop中传递的参数相同。
  • RuntimeData:任意字符串或缓冲区,将原样传递给新进程。需注意,它不一定是字符串,因为UNICODE_STRING包含长度信息,这意味着缓冲区可以指向任何任意数据。
  • DllPath:可选的NT路径,作为加载器(loader)为新进程查找动态链接库(DLL)的额外目录。
  • CommandLine:传递给新进程的可选命令行参数。
  • Environment:通过调用RtlCreateEnvironment创建的可选环境块(environment block)。若为NULL,则从父进程复制环境。有关详细信息,请参阅CreateProcess的lpEnvironment参数文档。
  • RedirectionDllName:可选的NT路径DLL,指示加载器执行函数重定向。若指定了该参数,该DLL必须导出一个名为__RedirectionInformation__的全局变量,其类型为REDIRECTION_DESCRIPTOR,定义如下:
typedef struct _REDIRECTION_FUNCTION_DESCRIPTOR {
    PCSTR DllName;
    PCSTR FunctionName;
    PVOID RedirectionTarget;
} REDIRECTION_FUNCTION_DESCRIPTOR, *PREDIRECTION_FUNCTION_DESCRIPTOR;

typedef const REDIRECTION_FUNCTION_DESCRIPTOR *PCREDIRECTION_FUNCTION_DESCRIPTOR;

typedef struct _REDIRECTION_DESCRIPTOR {
    ULONG Version;
    ULONG FunctionCount;
    PCREDIRECTION_FUNCTION_DESCRIPTOR Redirections;
} REDIRECTION_DESCRIPTOR, *PREDIRECTION_DESCRIPTOR;
1
2
3
4
5
6
7
8
9
10
11
12
13

该DLL还可进一步导出两个可选函数,分别名为__ShouldApplyRedirection__和__ShouldApplyRedirectionToFunction__,其预期原型如下:

typedef BOOLEAN (*RedirectCbFunc)(PCWSTR);    				// 参数:name
typedef BOOLEAN (*RedirectByFunctionCbFunc)(PWSTR, ULONG);  // 参数:name, index
1
2

# 5.2 进程信息

获取或设置进程信息的第一步是通过调用NtOpenProcess,获取具有适当访问掩码(access mask)的目标进程句柄(handle):

NTSTATUS NtOpenProcess(
    _Out_ PHANDLE ProcessHandle,
    _In_ ACCESS_MASK DesiredAccess,
    _In_ POBJECT_ATTRIBUTES ObjectAttributes,
    _In_opt_ PCLIENT_ID ClientId);
1
2
3
4
5

DesiredAccess是所需的访问掩码,可以是通用访问掩码(例如GENERIC_READ),或者更优的选择是进程特定访问掩码的组合(例如PROCESS_TERMINATE、PROCESS_VM_OPERATION)——所有这些访问掩码均已在官方文档中说明。

ObjectAttributes不需要名称(因为进程没有名称),因此可以使用RTL_CONST_OBJECT_ATTRIBUTES非常简单地初始化:

OBJECT_ATTRIBUTES processAttributes = RTL_CONSTANT_OBJECT_ATTRIBUTES(nullptr, 0);
1

最重要的部分是ClientId,其中指定了进程ID。请注意,线程ID必须为零,否则调用会失败。以下示例展示了如何根据给定的进程ID和访问掩码返回进程句柄(若成功):

HANDLE OpenProcess(ULONG pid, ACCESS_MASK access) {
    OBJECT_ATTRIBUTES processAttributes = RTL_CONSTANT_OBJECT_ATTRIBUTES(nullptr, 0);
    CLIENT_ID cid {};
    cid.UniqueProcess = ULongToHandle(pid);
    HANDLE hProcess = nullptr;
    NtOpenProcess(&hProcess, access, &processAttributes, &cid);
    return hProcess;
}
1
2
3
4
5
6
7
8

获取进程句柄后,用于获取和设置信息的主要API如下:

NTSTATUS NtQueryInformationProcess(
    _In_ HANDLE ProcessHandle,
    _In_ PROCESSINFOCLASS ProcessInformationClass,
    _Out_writes_bytes_(ProcessInformationLength) PVOID ProcessInformation,
    _In_ ULONG ProcessInformationLength,
    _Out_opt_ PULONG ReturnLength);

NTSTATUS NtSetInformationProcess(
    _In_ HANDLE ProcessHandle,
    _In_ PROCESSINFOCLASS ProcessInformationClass,
    _In_reads_bytes_(ProcessInformationLength) PVOID ProcessInformation,
    _In_ ULONG ProcessInformationLength);
1
2
3
4
5
6
7
8
9
10
11
12

这种模式应该很熟悉——NtQuerySystemInformation和NtSetSystemInformation也使用相同的模式。

PROCESSINFOCLASS枚举提供了可检索和修改的各类数据。为简洁起见,此处不列出该枚举的完整内容,但以下小节将介绍其中一些更实用的枚举值。

除非另有说明,进程句柄所需的访问掩码为PROCESS_QUERY_LIMITED_INFORMATION或PROCESS_QUERY_INFORMATION。

# 5.2.1 ProcessBasicInformation

该信息类提供进程的一些基本详情。查询时可接受以下两种结构之一:

typedef struct _PROCESS_BASIC_INFORMATION {
    NTSTATUS ExitStatus;
    PPEB PebBaseAddress;
    ULONG_PTR AffinityMask;
    KPRIORITY BasePriority;
    HANDLE UniqueProcessId;
    HANDLE InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION, *PPROCESS_BASIC_INFORMATION;

typedef struct _PROCESS_EXTENDED_BASIC_INFORMATION {
    SIZE_T Size;
    PROCESS_BASIC_INFORMATION BasicInfo;
    union {
        ULONG Flags;
        struct {
            ULONG IsProtectedProcess : 1;
            ULONG IsWow64Process : 1;
            ULONG IsProcessDeleting : 1;
            ULONG IsCrossSessionCreate : 1;
            ULONG IsFrozen : 1;
            ULONG IsBackground : 1;
            ULONG IsStronglyNamed : 1;
            ULONG IsSecureProcess : 1;
            ULONG IsSubsystemProcess : 1;
            ULONG SpareBits : 23;
        };
    };
} PROCESS_EXTENDED_BASIC_INFORMATION, *PPROCESS_EXTENDED_BASIC_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

以下是PROCESS_BASIC_INFORMATION各成员的描述:

  • ExitStatus:进程的退出码(若进程已退出)。否则,返回STATUS_PENDING(值为0x103);在Windows API头文件中,该值被定义为STILL_ACTIVE。
  • PebBaseAddress:进程环境块(Process Environment Block,PEB)的地址。下一节将详细介绍PEB。
  • AffinityMask:处理器位掩码,指示该进程中的线程可使用哪些处理器(针对当前进程组)。可用处理器用1位表示。
  • UniqueProcessId:进程ID。
  • InheritedFromUniqueProcessId:父进程ID,该进程的某些属性(如当前目录、环境变量等)会从父进程继承(除非明确指定)。

扩展结构(PROCESS_EXTENDED_BASIC_INFORMATION)包含一些额外的进程标志:

  • IsProtectedProcess:指示该进程是受保护进程(“标准”保护或轻量级受保护进程(Protected Process Light,PPL))。
  • IsWow64Process:指示该进程是32位进程,且运行在64位系统上。
  • IsProcessDeleting:指示该进程不再运行代码,但由于仍被引用(例如存在打开的句柄)而保持活跃状态。这种进程有时被称为“僵尸进程”(Zombie Process)。你可以通过我的对象资源管理器工具(System / Zombie Processes菜单项)查看此类进程。
  • IsCrossSessionCreate:指示该进程由运行在不同会话中的进程创建。最常见的情况是通用Windows平台(UWP)进程,这类进程始终由DCOM Launch服务启动,该服务托管在会话0中运行的标准SvcHost.exe进程中。
  • IsFrozen:指示该进程的所有线程均已挂起。这在UWP进程的窗口最小化时很常见。
  • IsBackground:指示该进程处于后台模式(Background Mode)。该进程的所有线程均以基准优先级4运行,进程的I/O优先级为“极低”(Very Low),内存优先级为1。有关后台模式的更多信息,请参阅官方文档。第6章将提供更多细节。
  • IsStronglyNamed:指示该进程关联有某个应用程序标识符。典型情况是UWP进程拥有其完整的包名。
  • IsSecureProcess:指示该进程运行在虚拟信任级别(Virtual Trust Level,VTL 1),前提是基于虚拟化的安全性(Virtualization Based Security,VBS)已启用。典型的安全进程包括安全内核(Secure Kernel)和Lsaiso.exe。
  • IsSubsystemProcess:指示该进程运行在Linux的Windows子系统(Windows Subsystem for Linux,WSL)版本1中——即运行Linux应用程序的进程。这类进程也被称为微型进程(Pico Process)。

无需设置Size成员;API会根据传入的缓冲区大小自动填充该成员。

以下是使用 ProcessBasicInformation 显示部分进程详细信息的示例:

std::string ProcessFlagsToString(PROCESS_EXTENDED_BASIC_INFORMATION const& ebi) {
    std::string flags;
    if (ebi.IsProtectedProcess)
        flags += "Protected, ";
    if (ebi.IsFrozen)
        flags += "Frozen, ";
    if (ebi.IsSecureProcess)
        flags += "Secure, ";
    if (ebi.IsCrossSessionCreate)
        flags += "Cross Session, ";
    if (ebi.IsBackground)
        flags += "Background, ";
    if (ebi.IsSubsystemProcess)
        flags += "WSL, ";
    if (ebi.IsStronglyNamed)
        flags += "Strong Name, ";
    if (ebi.IsProcessDeleting)
        flags += "Deleting, ";
    if (ebi.IsWow64Process)
        flags += "Wow64, ";

    if (!flags.empty())
        return flags.substr(0, flags.length() - 2);
    return "";
}

void DisplayInfo(HANDLE hProcess) {
    PROCESS_EXTENDED_BASIC_INFORMATION ebi;
    if (NT_SUCCESS(NtQueryInformationProcess(hProcess,
        ProcessBasicInformation, &ebi, sizeof(ebi), nullptr))) {
        auto& bi = ebi.BasicInfo;
        printf("PID:  %6u  PPID:  %6u  Pri:  %2u  PEB:  0x%p  %s\n ",
            HandleToULong(bi.UniqueProcessId),
            HandleToULong(bi.InheritedFromUniqueProcessId),
            bi.BasePriority, bi.PebBaseAddress,
            ProcessFlagsToString(ebi).c_str());
    }
}
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

# 5.2.2 ProcessIoCounters

此信息类使用以下结构(在 WinNt.h 中定义)返回进程的一些 I/O 统计信息:

typedef struct _IO_COUNTERS {
    ULONGLONG ReadOperationCount;
    ULONGLONG WriteOperationCount;
    ULONGLONG OtherOperationCount;
    ULONGLONG ReadTransferCount;
    ULONGLONG WriteTransferCount;
    ULONGLONG OtherTransferCount;
} IO_COUNTERS;
1
2
3
4
5
6
7
8

# 5.2.3 ProcessVmCounters

此信息类提供进程的各种内存计数器(memory counters)。它接受以下任意一种结构:

typedef struct _VM_COUNTERS {
    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;
} VM_COUNTERS, *PVM_COUNTERS;

typedef struct _VM_COUNTERS_EX {
    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 PrivateUsage;         // 与 PagefileUsage 相同
} VM_COUNTERS_EX, *PVM_COUNTERS_EX;

typedef struct _VM_COUNTERS_EX2 {
    VM_COUNTERS_EX CountersEx;
    SIZE_T PrivateWorkingSetSize;
    SIZE_T SharedCommitUsage;
} VM_COUNTERS_EX2, *PVM_COUNTERS_EX2;
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

内存计数器(memory counters)中使用的各种术语已有官方文档说明,因此此处不再详细阐述。

# 5.2.4 ProcessTimes

此信息类使用以下结构提供进程的创建时间、退出时间(若已退出)、用户模式执行时间(user-time)和内核模式执行时间(kernel-time):

typedef struct _KERNEL_USER_TIMES {
    LARGE_INTEGER CreateTime;
    LARGE_INTEGER ExitTime;
    LARGE_INTEGER KernelTime;
    LARGE_INTEGER UserTime;
} KERNEL_USER_TIMES, *PKERNEL_USER_TIMES;
1
2
3
4
5
6

Windows API 提供了 GetProcessTimes 函数来检索相同的数据。

创建时间(CreateTime)和退出时间(ExitTime)以 100 纳秒为单位,起始时间为 1601 年 1 月 1 日。内核模式时间(KernelTime)和用户模式时间(UserTime)同样以 100 纳秒为单位。

# 5.2.5 ProcessBasePriority 和 ProcessPriorityClass

这些信息类允许修改进程的基准优先级(base priority),该优先级会影响进程内所有线程的优先级。这两个信息类都要求进程句柄(process handle)具有 PROCESS_SET_INFORMATION 访问掩码(access mask)位。

Windows API 提供了六种可能的优先级类别(Priority Classes)值:低(Low,4)、低于正常(Below Normal,6)、正常(Normal,8)、高于正常(Above Normal,10)、最高(Highest,13)和实时(Realtime,24)(参见 SetPriorityClass API 的文档)。ProcessPriorityClass 允许基于以下结构查询和设置优先级类别(Priority Class)值:

#define PROCESS_PRIORITY_CLASS_UNKNOWN 0
#define PROCESS_PRIORITY_CLASS_IDLE 1
#define PROCESS_PRIORITY_CLASS_NORMAL 2
#define PROCESS_PRIORITY_CLASS_HIGH 3
#define PROCESS_PRIORITY_CLASS_REALTIME 4
#define PROCESS_PRIORITY_CLASS_BELOW_NORMAL 5
#define PROCESS_PRIORITY_CLASS_ABOVE_NORMAL 6

typedef struct _PROCESS_PRIORITY_CLASS {
    BOOLEAN Foreground;
    UCHAR PriorityClass;
} PROCESS_PRIORITY_CLASS, *PPROCESS_PRIORITY_CLASS;
1
2
3
4
5
6
7
8
9
10
11
12

前台(Foreground)成员指示该进程应被视为前台进程(foreground process),这意味着在客户端机器上,该进程中线程的时间片(quantum)长度会变为原来的三倍(默认情况下约为 90 毫秒,而标准时间片约为 30 毫秒)。有关线程时间片(thread quantums)的更多信息,请参见第 6 章。

SystemBasePriority 提供了一种更灵活的方式来设置基准优先级(base priority),其值不必是上述六种之一。提供的值为 KPRIORITY 类型,本质上是一个长整数(LONG)。该数值直接对应基准优先级(base priority)。可以设置最高位(第 31 位)来指定前台(Foreground)位。

以下是将当前基准优先级(base priority)设置为 7 的示例:

KPRIORITY priority = 7;
NtSetInformationProcess(NtCurrentProcess(), ProcessBasePriority,
    &priority, sizeof(priority));
1
2
3

有趣的是,使用 ProcessBasePriority 提高进程基准优先级(base priority)(超出当前级别)时,目标进程的令牌(process token)中必须包含 SeIncreaseBasePriorityPrivilege 权限(默认授予管理员,标准用户无此权限);而使用 ProcessPriorityClass 时,除实时(Realtime)优先级类别外,设置其他所有优先级类别均无需特殊权限。

# 5.2.6 ProcessHandleCount

此信息返回进程中的句柄数量(handle count),并可选返回该进程中曾存在过的最大句柄数量(highest number of handles)。预期的完整结构如下:

typedef struct _PROCESS_HANDLE_INFORMATION {
    ULONG HandleCount;
    ULONG HandleCountHighWatermark;
} PROCESS_HANDLE_INFORMATION, *PPROCESS_HANDLE_INFORMATION;
1
2
3
4

你也可以提供一个指向单个无符号长整数(ULONG)的指针,此时返回的结果仅为当前句柄数量(current handle count)。

# 5.2.7 ProcessSessionInformation

此信息类返回该进程所附加的会话(session)(类型为 ULONG)。

# 5.2.8 ProcessImageFileName 和 ProcessImageFileNameWin32

这些信息类以 UNICODE_STRING 类型返回进程可执行映像文件名(process executable image file name)。调用者必须分配足够大的字符串来容纳路径。ProcessImageFileName 返回 NT 设备格式的字符串(如 \Device\HarddiskVolume…),而 ProcessImageFileNameWin32 返回 Win32 格式的字符串(如 C:\MyFolder\…)。以下是示例:

BYTE buffer[512];
NtQueryInformationProcess(hProcess, ProcessImageFileName,
    buffer, sizeof(buffer), nullptr);
auto exePath = (UNICODE_STRING*)buffer;
printf("NT:  %wZ\n ", exePath);
NtQueryInformationProcess(hProcess, ProcessImageFileNameWin32,
    buffer, sizeof(buffer), nullptr);
exePath = (UNICODE_STRING*)buffer;
printf("Win32:  %wZ\n ", exePath);
1
2
3
4
5
6
7
8
9

# 5.2.9 ProcessImageInformation

此信息类返回 SECTION_IMAGE_INFORMATION 类型的固定结构,该结构提供可移植可执行文件(Portable Executable,PE)的部分详细信息:

typedef struct _SECTION_IMAGE_INFORMATION {
    PVOID TransferAddress;
    ULONG ZeroBits;
    SIZE_T MaximumStackSize;
    SIZE_T CommittedStackSize;
    ULONG SubSystemType;
    union {
        struct {
            USHORT SubSystemMinorVersion;
            USHORT SubSystemMajorVersion;
        };
        ULONG SubSystemVersion;
    };
    union {
        struct {
            USHORT MajorOperatingSystemVersion;
            USHORT MinorOperatingSystemVersion;
        };
        ULONG OperatingSystemVersion;
    };
    USHORT ImageCharacteristics;
    USHORT DllCharacteristics;
    USHORT Machine;
    BOOLEAN ImageContainsCode;
    union {
        UCHAR ImageFlags;
        struct {
            UCHAR ComPlusNativeReady : 1;
            UCHAR ComPlusILOnly : 1;
            UCHAR ImageDynamicallyRelocated : 1;
            UCHAR ImageMappedFlat : 1;
            UCHAR BaseBelow4gb : 1;
            UCHAR ComPlusPrefer32bit : 1;
            UCHAR Reserved : 2;
        };
    };
    ULONG LoaderFlags;
    ULONG ImageFileSize;
    ULONG CheckSum;
} SECTION_IMAGE_INFORMATION, *PSECTION_IMAGE_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
38
39
40

各个成员对应 PE 头(PE header)的不同部分。完整说明可在网上查询。

上述结构中使用的“COMPlus”一词意为“.NET”。如今 COM+ 已有不同含义,但 .NET 的初始名称为“COM+”。

# 5.2.10 ProcessHandleInformation

此信息类返回指定进程中句柄的快照(snapshot),相关结构如下:

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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

该信息与我们在第 4 章中看到的、由 NtQuerySystemInformation 返回的系统级句柄(system-wide handles)类似。最显著的区别是缺少对象地址(object address)。

# 5.2.11 ProcessHandleTable

此信息类在 Windows 8.1 及更高版本中受支持,仅返回指定进程中的句柄值数组。每个值的类型为 ULONG。返回的句柄数量应通过返回的大小获取,将其除以 sizeof(ULONG) 即可得到返回的句柄数。

# 5.2.12 ProcessCommandLineInformation

此信息类以 UNICODE_STRING 类型返回用于启动指定进程的完整命令行(full command line)。以下是示例:

BYTE buffer[2048];    // 任意大小
NtQueryInformationProcess(hProcess, ProcessCommandLineInformation,
    buffer, sizeof(buffer), nullptr);
auto cmdLine = (UNICODE_STRING*)buffer;
1
2
3
4

最长的命令行可包含 32767 个字符(UNICODE_STRING 可描述的最长字符串长度)。

# 5.3 进程环境块(Process Environment Block,PEB)

进程环境块(PEB)是每个用户模式进程(user-mode process)都拥有的用户模式数据结构(user-mode data structure)。设置进程环境块(PEB)的一个原因是,当进程自身需要获取进程相关信息时,可减少用户模式到内核模式的切换(user to kernel transitions)。另一个原因是,从内核角度来看,若某些信息无法被滥用以损害内核,则这些信息并不具备太大“价值”。

可通过便捷函数 NtCurrentPeb 访问当前进程的进程环境块(PEB)。“便捷”意味着它并非实际的原生 API(native API),而是一个通过当前线程环境块(Thread Environment Block,TEB)定位进程环境块(PEB)的宏:

#define NtCurrentPeb() (NtCurrentTeb()->ProcessEnvironmentBlock)
1

当前线程环境块(TEB)(通过 NtCurrentTeb 获取)可通过访问 GS 寄存器(x64 架构)或 FS 寄存器(x86 架构)获取(类似地,在 ARM/ARM64 架构上可从特定寄存器获取)。NtCurrentPeb 和 NtCurrentTeb 均在 WinNt.h 中定义。

要访问另一个进程的进程环境块(PEB),需调用 NtQueryInformationProcess 并指定 ProcessBasicInformation。当然,检索到的进程环境块(PEB)指针仅在目标进程中有效。要获取该数据,需调用 ReadProcessMemory / WriteProcessMemory(Windows API)或 NtReadVirtualMemory / NtWriteVirtualMemory(原生 API)。后者将在第 8 章中描述。

进程环境块(PEB)是一个大型结构——在此完整展示会占用过多篇幅。建议你在阅读各成员描述时,查看 phnt 头文件中的该结构定义(微软公开符号中也提供了该结构)。

并非进程环境块(PEB)的所有成员都会被描述,因为有些成员已不再使用,且现有成员列表本身已相当长。

• 若进程是通过克隆(RtlCloneUserProcess)创建的,则 InheritedAddressSpace 为 TRUE,否则为 FALSE。

• ReadImageFileExecOptions 似乎始终为 0。

• 若进程正在调试器(debugger)下运行,则 BeingDebugged 为 TRUE。请注意,将此值改为 FALSE 并不会改变进程正在被调试的事实。这可用于“欺骗”代码,使其认为进程未被调试,而实际上调试仍在进行。Windows API 中的 IsDebuggerPresent 函数会检查此成员以报告进程是否正在被调试。不依赖进程环境块(PEB)进行检查的“正确”方法是调用 Windows API 中的 CheckRemoteDebuggerPresent 函数,该函数会调用 NtQueryInformationProcess 并指定 ProcessDebugPort,若进程正在被调试,则返回指向调试对象(Debug object)的有效句柄。

• 若 PE 映像(PE image)使用大页面(large pages)映射,则设置 ImageUsesLargePages。

• 若进程是受保护进程(protected process)(例如 Csrss.exe、Smss.exe),则设置 IsProtectedProcess。

• 若 PE 映像(PE image)可加载到任意地址(通常为 TRUE),则设置 IsImageDynamicallyRelocated。

• 对于“安全”的 CD/DVD 内容,会设置 SkipPatchingUser32Forwarders;这似乎是一个遗留设置(legacy setting)。

• 若进程是基于 AppX 包创建的,则设置 IsPackagedProcess。

• 若进程在应用容器(AppContainer)内运行,则设置 IsAppContainer。

• 若进程是轻量级受保护进程(PPL protected)(相较于传统保护),则设置 IsProtectedProcessLight。

• 若进程支持超过 MAX_PATH(260)个字符的长路径,则设置 IsLongPathAwareProcess。

• ImageBaseAddress 是可执行映像(executable image)被映射到的基地址(base address)。

• Ldr 是指向 PEB_LDR_DATA 结构的指针:

typedef struct _PEB_LDR_DATA {
    ULONG Length;
    BOOLEAN Initialized;
    HANDLE SsHandle;
    LIST_ENTRY InLoadOrderModuleList;
    LIST_ENTRY InMemoryOrderModuleList;
    LIST_ENTRY InInitializationOrderModuleList;
    PVOID EntryInProgress;
    BOOLEAN ShutdownInProgress;
    HANDLE ShutdownThreadId;
} PEB_LDR_DATA, *PPEB_LDR_DATA;
1
2
3
4
5
6
7
8
9
10
11

核心组成部分是三组链表,它们以三种不同方式存储进程中已加载模块的相关信息:

  • 加载顺序模块列表(InLoadOrderModuleList):按模块加载顺序存储列表。
  • 内存顺序模块列表(InMemoryOrderModuleList):所有模块验证完成后,按与加载顺序相同的顺序存储列表。
  • 初始化顺序模块列表(InInitializationOrderModuleList):按初始化顺序存储列表。自然地,这会排除可执行模块(executable module),因为它不像动态链接库(DLL)那样具有初始化过程(没有DllMain函数)。

每个模块条目均为加载程序数据表格项(LDR_DATA_TABLE_ENTRY)类型,包含该模块的多项详细信息:

typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    union {
        LIST_ENTRY InInitializationOrderLinks;
        LIST_ENTRY InProgressLinks;
    };
    PVOID DllBase;
    PLDR_INIT_ROUTINE EntryPoint;
    ULONG SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
    union {
        UCHAR FlagGroup[4];
        ULONG Flags;
        struct {
            ULONG PackagedBinary : 1;
            ULONG MarkedForRemoval : 1;
            ULONG ImageDll : 1;
            ULONG LoadNotificationsSent : 1;
            ULONG TelemetryEntryProcessed : 1;
            ULONG ProcessStaticImport : 1;
            ULONG InLegacyLists : 1;
            ULONG InIndexes : 1;
            ULONG ShimDll : 1;
            ULONG InExceptionTable : 1;
            ULONG ReservedFlags1 : 2;
            ULONG LoadInProgress : 1;
            ULONG LoadConfigProcessed : 1;
            ULONG EntryProcessed : 1;
            ULONG ProtectDelayLoad : 1;
            ULONG ReservedFlags3 : 2;
            ULONG DontCallForThreads : 1;
            ULONG ProcessAttachCalled : 1;
            ULONG ProcessAttachFailed : 1;
            ULONG CorDeferredValidate : 1;
            ULONG CorImage : 1;
            ULONG DontRelocate : 1;
            ULONG CorILOnly : 1;
            ULONG ChpeImage : 1;
            ULONG ChpeEmulatorImage : 1;
            ULONG ReservedFlags5 : 1;

            ULONG Redirected : 1;
            ULONG ReservedFlags6 : 2;
            ULONG CompatDatabaseProcessed : 1;
        };
    };
    USHORT ObsoleteLoadCount;
    USHORT TlsIndex;
    LIST_ENTRY HashLinks;
    ULONG TimeDateStamp;
    struct _ACTIVATION_CONTEXT *EntryPointActivationContext;
    PVOID Lock;                   // SRW Lock
    PLDR_DDAG_NODE DdagNode;
    LIST_ENTRY NodeModuleLink;
    struct _LDRP_LOAD_CONTEXT *LoadContext;
    PVOID ParentDllBase;
    PVOID SwitchBackContext;
    RTL_BALANCED_NODE BaseAddressIndexNode;
    RTL_BALANCED_NODE MappingInfoIndexNode;
    ULONG_PTR OriginalBase;
    LARGE_INTEGER LoadTime;
    ULONG BaseNameHashValue;
    LDR_DLL_LOAD_REASON LoadReason;
    ULONG ImplicitPathOptions;
    ULONG ReferenceCount;  // since WIN10
    ULONG DependentLoadFlags;
    UCHAR SigningLevel;          // since REDSTONE2
    ULONG CheckSum;                   // since 22H1
    PVOID ActivePatchImageBase;
    LDR_HOT_PATCH_STATE HotPatchState;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
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

以下示例展示了如何通过一个通用函数枚举这三组链表:

void ListModules(LIST_ENTRY* head, int linkOffset) {
    for (auto next = head->Flink; next != head; next = next->Flink) {
        auto data = (PLDR_DATA_TABLE_ENTRY)((PBYTE)next - linkOffset);
        printf("0x%p: %wZ \n", data->DllBase, &data->BaseDllName);
    }
}

int main(int argc, const char* argv[]) {
    auto peb = NtCurrentPeb();

    printf("Load Order\n");
    ListModules(&peb->Ldr->InLoadOrderModuleList,
        offsetof(LDR_DATA_TABLE_ENTRY, InLoadOrderLinks));

    printf("\nMemory Order\n");
    ListModules(&peb->Ldr->InMemoryOrderModuleList,
        offsetof(LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks));

    printf("\nInitialization Order\n");
    ListModules(&peb->Ldr->InInitializationOrderModuleList,
        offsetof(LDR_DATA_TABLE_ENTRY, InInitializationOrderLinks));

    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

关键在于传入嵌入在加载程序数据表格项(LDR_DATA_TABLE_ENTRY)结构中的对应链表项(LIST_ENTRY)的偏移量,从而获取正确的指针。此代码会遍历当前进程,输出如下(调试版本(Debug build)):

Load Order
0x00007FF7696F0000: ModList.exe
0x00007FFC832D0000: ntdll.dll
0x00007FFC82590000: KERNEL32.DLL
0x00007FFC81030000: KERNELBASE.dll
0x00007FFC69000000: VCRUNTIME140D.dll
0x00007FFBD9C20000: ucrtbased.dll

Memory Order
0x00007FF7696F0000: ModList.exe
0x00007FFC832D0000: ntdll.dll
0x00007FFC82590000: KERNEL32.DLL
0x00007FFC81030000: KERNELBASE.dll
0x00007FFC69000000: VCRUNTIME140D.dll
0x00007FFBD9C20000: ucrtbased.dll

Initialization Order
0x00007FFC832D0000: ntdll.dll
0x00007FFC81030000: KERNELBASE.dll
0x00007FFC82590000: KERNEL32.DLL
0x00007FFBD9C20000: ucrtbased.dll
0x00007FFC69000000: VCRUNTIME140D.dll
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

要遍历另一个进程中的模块则更为复杂,因为需要读取属于该进程的内存。这部分内容留给感兴趣的读者自行探索,第8章将提供完整代码。

Windows API 提供了枚举进程模块函数(EnumProcessModules API),只要拥有足够权限的句柄,便可遍历任意进程中已加载的模块。该函数通过读取进程的进程环境块(PEB)中的相关详情实现功能,但它仅返回模块的基地址,因此调用者必须进一步调用其他 API(如获取模块基名函数(GetModuleBaseName)和获取模块信息函数(GetModuleInformation))以获取更多详情,且返回的详情不如完整的加载程序数据表格项(LDR_DATA_TABLE_ENTRY)丰富。

为节省篇幅,此处不再详细描述加载程序数据表格项(LDR_DATA_TABLE_ENTRY)的所有成员。

回到进程环境块(PEB)结构的成员:

  • 进程堆(ProcessHeap):默认进程堆句柄,可通过宏RtlProcessHeap或 Windows API 中的获取进程堆函数(GetProcessHeap)获取。有关堆的更多详情,请参见第8章。
  • 快速进程环境块锁(FastPebLock):指向临界区(RTL_CRITICAL_SECTION)的指针,用于同步对进程环境块(PEB)各成员的访问。原生 API 中的获取进程环境块锁函数(RtlAcquirePebLock)和释放进程环境块锁函数(RtlReleasePebLock)可用于获取和释放该锁。

接下来是存储在一个联合(union)中的多个标志,其中创建进程标志(CreateProcessFlags)是总览性成员:

  • 进程在作业中(ProcessInJob):如果进程属于某个作业(job),则设置该标志。
  • 进程初始化中(ProcessInitializing):在加载程序(loader)初始化进程期间设置该标志。
  • 进程使用向量异常处理程序(ProcessUsingVEH):如果进程使用向量异常处理程序(Vectored Exception Handlers,VEH),则设置该标志。
  • 进程使用向量继续处理程序(ProcessUsingVCH):如果进程使用向量继续处理程序(Vectored Continuation Handlers,VCH),则设置该标志。

有关向量异常处理程序(VEH)和向量继续处理程序(VCH)的更多信息,请参见 SDK 文档。

  • 内核回调表(KernelCallbackTable):指向回调函数数组的指针,这些回调函数由User32.Dll提供,在相关系统调用被调用后执行。可在 WinDbg 中附加到使用User32.Dll的进程(如记事本(Notepad.exe)),并尝试执行以下命令:
0:014> dt nt!_peb @$peb
ntdll!_PEB
+0x000 InheritedAddressSpace : 0 ''
+0x001 ReadImageFileExecOptions : 0 ''
+0x002 BeingDebugged : 0x1 ''
...
+0x058 KernelCallbackTable : 0x00007ffc`82f4f070 Void
...
0:014> dqs 0x00007ffc`82f4f070 L30
00007ffc`82f4f070 00007ffc`82ee2780 USER32!_fnCOPYDATA
00007ffc`82f4f078 00007ffc`82f47ea0 USER32!_fnCOPYGLOBALDATA
00007ffc`82f4f080 00007ffc`82ee0c00 USER32!_fnDWORD
00007ffc`82f4f088 00007ffc`82ee6a60 USER32!_fnNCDESTROY
00007ffc`82f4f090 00007ffc`82eedac0 USER32!_fnDWORDOPTINLPMSG
00007ffc`82f4f098 00007ffc`82f486d0 USER32!_fnINOUTDRAG
00007ffc`82f4f0a0 00007ffc`82ee7f90 USER32!_fnGETTEXTLENGTHS
00007ffc`82f4f0a8 00007ffc`82f48370 USER32!_fnINCNTOUTSTRING
00007ffc`82f4f0b0 00007ffc`82f48430 USER32!_fnINCNTOUTSTRINGNULL
00007ffc`82f4f0b8 00007ffc`82ee9700 USER32!_fnINLPCOMPAREITEMSTRUCT
00007ffc`82f4f0c0 00007ffc`82ee2be0 USER32!__fnINLPCREATESTRUCT
00007ffc`82f4f0c8 00007ffc`82f484f0 USER32!_fnINLPDELETEITEMSTRUCT
00007ffc`82f4f0d0 00007ffc`82eefe50 USER32!__fnINLPDRAWITEMSTRUCT
00007ffc`82f4f0d8 00007ffc`82f48550 USER32!_fnINLPHELPINFOSTRUCT
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

每个回调函数都具有简单通用的原型:

NTSTATUS KernelToUserCallback(
    PVOID InputBuffer,
    ULONG InputLength);
1
2
3
  • API 集映射(ApiSetMap):指向 API 集映射(API Set mappings)的指针,用于将 API 集名称映射到当前系统上的实现。API 集的完整讨论超出了本专栏的范围,请参考《Windows 内部原理(第一部分)》(Windows Internals, part 1)一专栏。其核心思想是:API 集允许将函数集(可将每个集合视为一个接口)与实际的实现二进制文件分离。

以下示例列出了该映射关系(详见 ApiSets 示例):

#define API_SET_SCHEMA_ENTRY_FLAGS_SEALED 1

auto apiSetMap = NtCurrentPeb()->ApiSetMap;
auto apiSetMapAsNumber = ULONG_PTR(apiSetMap);
auto nsEntry = PAPI_SET_NAMESPACE_ENTRY(apiSetMap->EntryOffset + apiSetMapAsNumber);

for (ULONG i = 0; i < apiSetMap->Count; i++) {
    auto isSealed = (nsEntry->Flags & API_SET_SCHEMA_ENTRY_FLAGS_SEALED) != 0;

    std::wstring name(PWCHAR(apiSetMapAsNumber + nsEntry->NameOffset),
        nsEntry->NameLength / sizeof(WCHAR));
    printf("%56ws.dll -> %s{", name.c_str(), (isSealed ? "s" : ""));

    auto valueEntry = PAPI_SET_VALUE_ENTRY(apiSetMapAsNumber +
        nsEntry->ValueOffset);
    for (ULONG j = 0; j < nsEntry->ValueCount; j++) {
        //
        // 主机名(host name)
        //
        name.assign(PWCHAR(apiSetMapAsNumber + valueEntry->ValueOffset),
            valueEntry->ValueLength / sizeof(WCHAR));
        printf("%ws", name.c_str());

        if ((j + 1) != nsEntry->ValueCount)
            printf(", ");

        //
        // 如果存在别名(alias)
        //
        if (valueEntry->NameLength != 0) {
            name.assign(PWCHAR(apiSetMapAsNumber + valueEntry->NameOffset),
                valueEntry->NameLength / sizeof(WCHAR));
            printf(" [%ws]", name.c_str());
        }

        valueEntry++;
    }
    printf("}\n");
    nsEntry++;
}
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
  • 线程本地存储位图(TlsBitmap):指向 RTL_BITMAP(有关位图的更多信息,请参见第2章)的指针,用于存储与线程本地存储(Thread Local Storage,TLS)相关的索引信息。一个进程保证至少提供 64 个线程本地存储(TLS)索引,存储在后续成员线程本地存储位图位(TlsBitmapBits)中(一个包含 2 个 32 位值的数组)。如果需要超过 64 个线程本地存储(TLS)索引,可通过线程本地存储扩展位图(TlsExpansionBitmap)成员实现,其对应的位图存储在后续成员线程本地存储扩展位图位(TlsExpansionBitmapBits)中(一个包含 32 个无符号长整数(ULONG)值的数组,支持 1024(32 * 8)个位(索引))。

线程本地存储(TLS)的详细描述超出了本专栏的范围,但其用法在 Windows API 中有完整文档说明。可参考函数线程本地存储分配函数(TlsAlloc)、线程本地存储释放函数(TlsFree)、线程本地存储设置值函数(TlsSetValue)和线程本地存储获取值函数(TlsGetValue)。

  • NT 全局标志(NtGlobalFlags):存储从注册表中读取的映像文件执行选项(Image File Execution Options)标志,这些标志根据可执行文件名称应用于当前进程。对应的注册表项为HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\exename,值名称为GlobalFlag。

以下成员与堆相关:

  • 堆段保留大小(HeapSegmentReserve)和堆段提交大小(HeapSegmentCommit):如果未指定覆盖值,则表示新堆的初始默认保留内存大小(以字节为单位)和提交内存大小(以字节为单位)。
  • 堆释放总空闲阈值(HeapDeCommitTotalFreeThreshold)和堆释放空闲块阈值(HeapDeCommitFreeBlockThreshold):用作堆中块被释放时,从堆中释放内存的阈值。

上述默认值从注册表项HKLM\System\CurrentControlSet\Control\Session Manager读取。

  • 堆数量(NumberOfHeaps):存储进程中当前的堆数量。
  • 最大堆数量(MaximumNumberOfHeaps):进程可拥有的最大堆数量。
  • 进程堆数组(ProcessHeaps):指向堆指针数组的指针,数组长度为堆数量(NumberOfHeaps)。获取进程堆函数(RtlGetProcessHeaps API)会返回此堆数组。

有关堆的更多详情,请参见第8章。

  • GDI 共享句柄表(GdiSharedHandleTable):当前会话(而非仅当前进程)的图形设备接口(GDI)对象共享句柄表。
  • 加载程序锁(LoaderLock):一个临界区,用于防止并发访问某些加载程序(loader)操作。

至此,我已介绍完当前要涵盖的进程环境块(PEB)成员。本专栏的更新版本可能会涵盖更多成员。

# 5.4 挂起和恢复进程(Suspending and Resuming Processes)

原生 API 提供了用于挂起和恢复进程的函数:

NTSTATUS NtSuspendProcess(_In_ HANDLE ProcessHandle);

NTSTATUS NtResumeProcess(_In_ HANDLE ProcessHandle);
1
2
3

Windows API 中没有这些 API 的直接对应函数。挂起一个进程意味着挂起该进程中的所有线程,因为线程才是实际执行的实体。

要使这些函数调用成功,提供的句柄必须具有进程挂起/恢复访问权限(PROCESS_SUSPEND_RESUME)。

# 5.5 枚举进程(第二种方法)

第4章介绍了如何使用NtQuerySystemInformation结合多个信息类(information classes)来枚举进程,这些信息类可提供不同详细程度的进程信息。还有另一种枚举进程的方法,能够获取调用者可访问的进程打开句柄(open handles)。

相关的应用程序编程接口(API)是NtGetNextProcess:

NTSTATUS  NtGetNextProcess(
    _In_opt_  HANDLE  ProcessHandle,
    _In_  ACCESS_MASK  DesiredAccess,
    _In_  ULONG  HandleAttributes,
    _In_  ULONG  Flags,
    _Out_  PHANDLE  NewProcessHandle);
1
2
3
4
5
6

该函数的设计思路是获取“下一个”进程的句柄,该句柄可通过请求的访问掩码(access mask)获取。如果序列中的下一个进程无法通过调用者指定的访问掩码访问,则会跳过该进程。此函数通常在循环中调用,直到执行失败(表示没有更多可通过请求的访问权限获取的进程)。

开始迭代时,输入参数ProcessHandle需设为NULL。执行成功后,输出句柄将存储在NewProcessHandle中。该句柄应作为下一次迭代的ProcessHandle参数传入。重要的是,使用完每个句柄后切勿忘记关闭——该应用程序编程接口(API)会打开句柄,客户端(client)需在适当的时候(通常是使用完返回的句柄后)关闭这些句柄。

ObjectAttributes可以设为0,或设为第2章讨论的OBJECT_ATTRIBUTES结构中定义的一组标志(flags);通常设为0。

Flags参数目前仅支持值为1的标志,若指定该标志,将按反向顺序遍历进程。

以下示例展示了如何使用特定的访问掩码遍历进程(完整代码见ProcList示例):

int main()
{
    HANDLE hProcess = nullptr;
    for (;;)
    {
        HANDLE hNewProcess;
        auto status = NtGetNextProcess(hProcess,
            PROCESS_QUERY_LIMITED_INFORMATION, 0, 0, &hNewProcess);

        //
        //  close previous handle
        //
        if (hProcess)
            NtClose(hProcess);

        if (!NT_SUCCESS(status))
            break;

        PROCESS_EXTENDED_BASIC_INFORMATION ebi;
        if (NT_SUCCESS(NtQueryInformationProcess(hNewProcess,
            ProcessBasicInformation, &ebi, sizeof(ebi), nullptr)))
        {
            auto& bi = ebi.BasicInfo;

            printf("PID:  %6u  PPID:  %6u  Pri:  %2u  PEB:  0x%p  %s\n ",
                HandleToULong(bi.UniqueProcessId),
                HandleToULong(bi.InheritedFromUniqueProcessId),
                bi.BasePriority, bi.PebBaseAddress,
                ProcessFlagsToString(ebi).c_str());
        }

        hProcess = hNewProcess;
    }
    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

ProcessFlagsToString是一个简单函数,用于返回PROCESS_BASIC_INFORMATION中提供的标志的文本描述:

std::string ProcessFlagsToString(PROCESS_EXTENDED_BASIC_INFORMATION const& ebi)
{
    std::string flags;
    if (ebi.IsProtectedProcess)
        flags += "Protected,  ";
    if (ebi.IsFrozen)
        flags += "Frozen,  ";
    if (ebi.IsSecureProcess)
        flags += "Secure,  ";
    if (ebi.IsCrossSessionCreate)
        flags += "Cross  Session,  ";
    if (ebi.IsBackground)
        flags += "Background,  ";
    if (ebi.IsSubsystemProcess)
        flags += "WSL,  ";
    if (ebi.IsStronglyNamed)
        flags += "Strong  Name,  ";
    if (ebi.IsProcessDeleting)
        flags += "Deleting,  ";
    if (ebi.IsWow64Process)
        flags += "Wow64,  ";

    if (!flags.empty())
        return flags.substr(0, flags.length() - 2);
    return "";
}
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

# 5.6 总结

本章介绍了多个与进程相关的原生 API。下一章,我们将探讨与线程相关的原生 API。

第4章:系统信息
第6章:线程

← 第4章:系统信息 第6章:线程→

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