第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);
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;
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;
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);
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);
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;
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
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);
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);
最重要的部分是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;
}
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);
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;
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());
}
}
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;
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;
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;
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;
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));
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;
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);
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;
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;
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;
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)
当前线程环境块(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;
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;
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;
}
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
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
...
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);
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++;
}
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);
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);
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;
}
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 "";
}
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。