判断进程状态
进程由线程组成,所有线程都挂起时,进程就属于挂起了。这就是判断方法。怎么判断线程是否挂起了呢?
SuspendThread查询挂起计数法
每个线程都有个挂起计数,一般地,挂起计数是0时线程属于工作中,而大于0就是属于挂起。PCHunter类软件查询底层的_KTHREAD.SuspendCount
,原理都是查询线程挂起计数。应用层直接查询挂起计数,据我所知只有SuspendThread()
和ResumeThread()
。返回线程之前的挂起计数。我们从SuspendThread()
入手,挂起线程取得它挂起计数,然后立刻恢复。这样对目标线程没什么影响。而我们也拿到了它的挂起计数。
由此我们可以写出判断进程状态的函数。创建线程快照,把目标进程所有线程的挂起计数拿到了。只要还有一个线程存活(挂起数0)就视为进程还在运行。
//获取进程的状态
//返回-1,表示发生异常
//返回0,表示进程没有被挂起
//返回1,表示进程处于挂起状态
int GetProcessState(DWORD dwProcessID) {
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, dwProcessID);
if (hSnapshot != INVALID_HANDLE_VALUE) {
DWORD state = 1;//先置1,一旦有线程还在运行就置0
THREADENTRY32 te = {sizeof(te)};
BOOL fOk = Thread32First(hSnapshot, &te);
for (; fOk; fOk = Thread32Next(hSnapshot, &te)) {
if (te.th32OwnerProcessID == dwProcessID) {
HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, te.th32ThreadID);
DWORD suspendCount = SuspendThread(hThread);//返回之前的挂起数,大于0表示已挂起
ResumeThread(hThread);//马上恢复,这样不会对目标程序造成影响
CloseHandle(hThread);
if (suspendCount == 0) state = 0; //是个判断所有线程都挂起的好方法
}
}
CloseHandle(hSnapshot);
return state;
}
return -1;
}
虽然,你挂起人家线程似乎“不道德”,但是在不深入系统底层的情况下,这应该是最简单的办法了。
NtQuerySystemInformation查询(推荐)
如果我们对要查询的线程没有挂起权限(很常见,如权限不足、被Hook),就无法使用上面的办法了。另一种用得比较广泛的方法是调用NtQuerySystemInformation()
,它功能非常强大,据我所知Toolhelp API底层实现便是调用了它。
函数原型:
typedef enum _SYSTEM_INFORMATION_CLASS {
SystemBasicInformation = 0,
SystemProcessorInformation = 1,
SystemPerformanceInformation = 2,
SystemTimeOfDayInformation = 3,
SystemProcessInformation = 5,
SystemProcessorPerformanceInformation = 8,
SystemHandleInformation = 16,
SystemPagefileInformation = 18,
SystemInterruptInformation = 23,
SystemExceptionInformation = 33,
SystemRegistryQuotaInformation = 37,
SystemLookasideInformation = 45
} SYSTEM_INFORMATION_CLASS;
NTSTATUS NTAPI NtQuerySystemInformation(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
IN OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength OPTIONAL);
其定义在winternl.h
头文件中,但是不包含在windows.h
中,需另行导入。虽然其中定义了NtQuerySystemInformation()
函数原型,但是并不会被静态链接到dll中,需自己用GetProcAddress()
获取地址来调用。
参数1表示希望查询的数据种类,SystemProcessInformation
(值为5)意为查询系统所有的进程和线程的信息;
参数2指向接收数据的缓冲区;
参数3是缓冲区大小;
参数4指向所需数据的大小,可以先调用一次获取数据大小再开辟缓冲区。
当查询系统进程和线程信息时,接收的缓冲区包含所有的进程信息(SYSTEM_PROCESS_INFORMATION
结构),每个进程信息结构后面紧跟着该进程所有的线程信息(SYSTEM_THREAD_INFORMATION
结构)。它们在内存中的结构如下所示:
进程1 | 进程2 | 进程3 | ||||||
---|---|---|---|---|---|---|---|---|
进程1信息 | 线程1信息 | 进程2信息 | 线程1信息 | 线程2信息 | 线程3信息 | 进程3信息 | 线程1信息 | 线程2信息 |
这两个结构的定义(都包含在winternl.h
中):
typedef LONG KPRIORITY;
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;//下一个进程信息结构地址与当前结构地址的偏移
ULONG NumberOfThreads;//线程数量,也决定了后面跟着的线程信息的数量
LARGE_INTEGER Reserved[3];
LARGE_INTEGER CreateTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER KernelTime;
UNICODE_STRING ImageName;//映像名称
KPRIORITY BasePriority;
HANDLE UniqueProcessId;//进程PID
HANDLE InheritedFromUniqueProcessId;
ULONG HandleCount;
ULONG SessionId;
ULONG PageDirectoryBase;
VM_COUNTERS VirtualMemoryCounters;
SIZE_T PrivatePageCount;
IO_COUNTERS IoCounters;
} SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;
typedef struct _SYSTEM_THREAD_INFORMATION {
LARGE_INTEGER Reserved1[3];
ULONG Reserved2;
PVOID StartAddress;
CLIENT_ID ClientId;//一个结构,包含了所在进程ID和线程ID
KPRIORITY Priority;
LONG BasePriority;
ULONG Reserved3;
ULONG ThreadState;//线程状态
ULONG WaitReason;//线程处于等待状态的原因
} SYSTEM_THREAD_INFORMATION, *PSYSTEM_THREAD_INFORMATION;
typedef enum _THREAD_STATE {//线程状态
StateInitialized = 0,
StateReady, StateRunning/*工作中*/, StateStandby, StateTerminated,
StateWait/*等待中*/, StateTransition,
StateUnknown
} THREAD_STATE;
typedef enum _KWAIT_REASON {//线程处于等待状态的原因
Executive = 0,
FreePage, PageIn, PoolAllocation, DelayExecution,
Suspended/*挂起*/, UserRequest, WrExecutive, WrFreePage, WrPageIn,
WrPoolAllocation, WrDelayExecution, WrSuspended,
WrUserRequest, WrEventPair, WrQueue, WrLpcReceive,
WrLpcReply, WrVirtualMemory, WrPageOut, WrRendezvous,
Spare2, Spare3, Spare4, Spare5, Spare6, WrKernel,
MaximumWaitReason
} KWAIT_REASON;
可以看到SYSTEM_PROCESS_INFORMATION
结构中并没有关联其后紧随的SYSTEM_THREAD_INFORMATION
。我们可以用地址访问它后面紧随的线程信息,但是还有更好的办法,我们对SYSTEM_PROCESS_INFORMATION
作一个改写:
//在原结构之后加上不影响结构大小的线程数组,巧妙运用越界带来的跨结构访问后面的线程结构
typedef struct _MYSYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
LARGE_INTEGER Reserved[3];
LARGE_INTEGER CreateTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER KernelTime;
UNICODE_STRING ImageName;
KPRIORITY BasePriority;
HANDLE UniqueProcessId;
HANDLE InheritedFromUniqueProcessId;
ULONG HandleCount;
ULONG SessionId;
ULONG PageDirectoryBase;
VM_COUNTERS VirtualMemoryCounters;
SIZE_T PrivatePageCount;
IO_COUNTERS IoCounters;
//以上为原结构内容
SYSTEM_THREAD_INFORMATION Threads[0];
} MYSYSTEM_PROCESS_INFORMATION, *PMYSYSTEM_PROCESS_INFORMATION;
//覆盖原定义
#define SYSTEM_PROCESS_INFORMATION MYSYSTEM_PROCESS_INFORMATION
#define PSYSTEM_PROCESS_INFORMATION PMYSYSTEM_PROCESS_INFORMATION
改写之后SYSTEM_PROCESS_INFORMATION
结构的大小并没有改变,而且多了一个数组供我们方便地访问后面的线程。毫无疑问,访问任何元素都会越界,但是越界后访问的地址是这个数组所在地址加上sizeof(SYSTEM_THREAD_INFORMATION)
乘以访问的下标的积,正好是内存中那一个或多个线程信息结构的地址。
由此我们有如下的思路(流程图做得难看,请见谅):
这是代码:
#include <winternl.h>
//在原结构之后加上不影响结构大小的线程数组,巧妙运用越界带来的跨结构访问后面的线程结构
typedef struct _MYSYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
LARGE_INTEGER Reserved[3];
LARGE_INTEGER CreateTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER KernelTime;
UNICODE_STRING ImageName;
KPRIORITY BasePriority;
HANDLE UniqueProcessId;
HANDLE InheritedFromUniqueProcessId;
ULONG HandleCount;
ULONG SessionId;
ULONG PageDirectoryBase;
VM_COUNTERS VirtualMemoryCounters;
SIZE_T PrivatePageCount;
IO_COUNTERS IoCounters;
//以上为原结构内容
SYSTEM_THREAD_INFORMATION Threads[0];
} MYSYSTEM_PROCESS_INFORMATION, *PMYSYSTEM_PROCESS_INFORMATION;
//覆盖原定义
#define SYSTEM_PROCESS_INFORMATION MYSYSTEM_PROCESS_INFORMATION
#define PSYSTEM_PROCESS_INFORMATION PMYSYSTEM_PROCESS_INFORMATION
//定义函数原型
typedef NTSTATUS(NTSYSAPI NTAPI *FunNtQuerySystemInformation)
(IN SYSTEM_INFORMATION_CLASS SystemInformationClass, IN OUT PVOID SystemInformation,
IN ULONG SystemInformationLength, OUT PULONG ReturnLength OPTIONAL);
//获取进程的状态
//返回-1,表示发生异常
//返回0,表示进程没有被挂起
//返回1,表示进程处于挂起状态
int GetProcessState(DWORD dwProcessID) {
int nStatus = -1;
//取函数地址
FunNtQuerySystemInformation mNtQuerySystemInformation = FunNtQuerySystemInformation(GetProcAddress(GetModuleHandle("ntdll.dll"), "NtQuerySystemInformation"));
//先调用一次,获取所需缓冲区大小
DWORD dwSize;
mNtQuerySystemInformation(SystemProcessInformation, NULL, 0, &dwSize);
//申请缓冲区
HGLOBAL hBuffer = GlobalAlloc(LPTR, dwSize);
if (hBuffer == NULL)
return nStatus;
PSYSTEM_PROCESS_INFORMATION pInfo = PSYSTEM_PROCESS_INFORMATION(hBuffer);
//查询
NTSTATUS lStatus = mNtQuerySystemInformation(SystemProcessInformation, pInfo, dwSize, 0);
if (!NT_SUCCESS(lStatus)) {
GlobalFree(hBuffer);
return nStatus;
}
//遍历进程
while (true) {
//判断是否是目标进程
if (((DWORD)(ULONG_PTR) pInfo->UniqueProcessId) == dwProcessID) {
nStatus = 1;
//遍历线程
for (ULONG i = 0; i < pInfo->NumberOfThreads; i++) {
//如果不是在挂起,就表明程序存活,可以返回(堵塞、无响应不算挂起)
if (pInfo->Threads[i].WaitReason != Suspended) {
nStatus = 0;
break;
}
}
break;
}
//遍历进程完成
if (pInfo->NextEntryOffset == 0)
break;
//移动到下一个进程信息结构的地址
pInfo = PSYSTEM_PROCESS_INFORMATION(PBYTE(pInfo) + pInfo->NextEntryOffset);
}
//释放缓冲区
GlobalFree(hBuffer);
return nStatus;
}
这里我们对线程“挂起”的判断是狭义的挂起,不包括GetMessage()
、SendMessage()
造成的堵塞、无响应等;对进程挂起的定义是需要进程的所有线程都是挂起(因为NtSuspendProcess()
就是挂起了进程的所有线程)。事实上,当我们没有操作一个Win32 GUI程序时,它的主线程就是等待状态,因此我们不能拿SYSTEM_THREAD_INFORMATION.ThreadState == StateWait
作为判断线程挂起的依据。
还有一个有一点概率发生的情况:在第一次调用NtQuerySystemInformation()
取得缓冲区大小后,正好有新进程/线程创建了,第二次调用会出错(微软在创建进程快照的文档也提及过),解决方法就是再调用一次获取新缓冲区;对于如上示例中给的函数,重复调用即可。
挂起/恢复进程
知道了进程挂起的“定义”,我们可以创建个线程快照,把目标进程所有线程给挂起了,那它就算挂起了(就像上面用挂起线程取挂起计数的例子一样,只要去掉其中的ResumeThread()
就是挂起进程了)。
比较合适的办法呢?我们用软件(如ExeScope)查看ntdll.dll的导出函数,可以发现函数NtSuspendProcess()
还有配套NtResumeProcess()
。
它们的原型是:
NTSYSAPI NTSTATUS NTAPI NtSuspendProcess(
IN HANDLE Process);
NTSYSAPI NTSTATUS NTAPI NtResumeProcess(
IN HANDLE Process);
由此我们可以动态从ntdll.dll中获取它们地址并调用它们。
//挂起进程,调用未公开函数NtSuspendProcess。suspend参数决定挂起/恢复
typedef NTSTATUS(NTSYSAPI NTAPI *NtSuspendProcess)(IN HANDLE Process);
typedef NTSTATUS(NTSYSAPI NTAPI *NtResumeProcess)(IN HANDLE Process);
BOOL SuspendProcess(DWORD dwProcessID, BOOL suspend) {
NtSuspendProcess mNtSuspendProcess;
NtResumeProcess mNtResumeProcess;
HMODULE ntdll = GetModuleHandle("ntdll.dll");
HANDLE handle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, dwProcessID);
if (suspend) {
mNtSuspendProcess = (NtSuspendProcess)GetProcAddress(ntdll, "NtSuspendProcess");
return mNtSuspendProcess(handle) == 0;
} else {
mNtResumeProcess = (NtResumeProcess)GetProcAddress(ntdll, "NtResumeProcess");
return mNtResumeProcess(handle) == 0;
}
}
我这里想吐槽的一点是,网上很多文章对于ntdll中函数调用前,都用的LoadLibrary()
加载ntdll。程序必然会已加载它,直接GetModuleHandle()
就能拿到它句柄了呀。
参考
- https://docs.microsoft.com/zh-cn/windows/win32/api/processthreadsapi/nf-processthreadsapi-suspendthread
- https://docs.microsoft.com/zh-cn/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation
- http://undocumented.ntinternals.net/,
SYSTEM_PROCESS_INFORMATION
结构
还参考了以下文章:
上面这个并不是合规的办法(但能用),因为其默认每个进程的线程数都是5,在进程信息结构体定义末尾加上了大小为5的线程数组,很别扭
上面这个只是查询了第一个线程的挂起状态,不准,而且对缓冲区大小的获取非常随意,是判断大小够不够,不够再把大小乘2