win32 判断进程状态(挂起/运行中)、用API挂起/恢复进程

判断进程状态

进程由线程组成,所有线程都挂起时,进程就属于挂起了。这就是判断方法。怎么判断线程是否挂起了呢?

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)乘以访问的下标的积,正好是内存中那一个或多个线程信息结构的地址。

由此我们有如下的思路(流程图做得难看,请见谅):

Created with Raphaël 2.3.0 传入进程PID 调用NtQuerySystemInformation() 传入SystemProcessInformation(5), 缓冲区指针传NULL,取得缓冲区大小 申请对应大小的内存 再次调用函数,传入缓冲区指针,取得数据 遍历(下一个)进程信息SYSTEM_PROCESS_INFORMATION结构 进程遍历完 出错(进程不存在) 判断是否是目标进程 (对比PID) 遍历(下一个)线程信息SYSTEM_THREAD_INFORMATION结构 线程遍历完 进程已挂起 判断线程是否在挂起 进程未挂起 yes no yes no yes no yes no

这是代码:

#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()就能拿到它句柄了呀。

参考

还参考了以下文章:

上面这个并不是合规的办法(但能用),因为其默认每个进程的线程数都是5,在进程信息结构体定义末尾加上了大小为5的线程数组,很别扭

上面这个只是查询了第一个线程的挂起状态,不准,而且对缓冲区大小的获取非常随意,是判断大小够不够,不够再把大小乘2

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用 C# 调用 Win32 API 创建新进程并返回进程句柄的详细方法如下: 1. 首先需要设置一个 StartInfo 对象,用于存储启动进程的相关信息,如进程名称、命令行参数等。 ```csharp ProcessStartInfo startInfo = new ProcessStartInfo(); startInfo.FileName = "notepad.exe"; // 进程名称 startInfo.Arguments = "test.txt"; // 命令行参数 ``` 2. 创建一个 Process 对象,并将 StartInfo 对象赋值给它。 ```csharp Process process = new Process(); process.StartInfo = startInfo; ``` 3. 调用 Win32 API 的 CreateProcess 方法,创建新进程并返回进程句柄。 ```csharp bool success = CreateProcess( startInfo.FileName, startInfo.Arguments, IntPtr.Zero, IntPtr.Zero, false, 0, IntPtr.Zero, null, ref startupInfo, out PROCESS_INFORMATION processInfo); if (success) { process.Handle = processInfo.hProcess; process.Id = processInfo.dwProcessId; } ``` 其,CreateProcess 方法的参数含义如下: - lpApplicationName:要执行的可执行文件的名称,如果指定了完整路径,则 lpCommandLine 参数可以为 null。 - lpCommandLine:要传递给可执行文件的命令行参数。 - lpProcessAttributes:进程句柄的安全属性,通常设置为 IntPtr.Zero。 - lpThreadAttributes:线程句柄的安全属性,通常设置为 IntPtr.Zero。 - bInheritHandles:指定是否从父进程继承句柄,通常设置为 false。 - dwCreationFlags:指定进程的创建标志,通常设置为 0。 - lpEnvironment:进程的环境变量,通常设置为 IntPtr.Zero。 - lpCurrentDirectory:进程的当前工作目录,通常设置为 null。 - lpStartupInfo:启动信息,包括窗口大小、标题等。 - lpProcessInformation:输出参数,包括进程句柄和进程 ID。 4. 最后可以使用 Process 对象的方法来操作新创建的进程,如 WaitForExit() 等。 ```csharp process.WaitForExit(); ``` 完整的代码示例: ```csharp using System; using System.Diagnostics; using System.Runtime.InteropServices; namespace CreateProcessDemo { class Program { static void Main(string[] args) { // 设置启动信息 ProcessStartInfo startInfo = new ProcessStartInfo(); startInfo.FileName = "notepad.exe"; startInfo.Arguments = "test.txt"; // 创建进程并返回进程句柄 Process process = new Process(); process.StartInfo = startInfo; bool success = CreateProcess( startInfo.FileName, startInfo.Arguments, IntPtr.Zero, IntPtr.Zero, false, 0, IntPtr.Zero, null, ref startupInfo, out PROCESS_INFORMATION processInfo); if (success) { process.Handle = processInfo.hProcess; process.Id = processInfo.dwProcessId; // 等待进程结束 process.WaitForExit(); } } // 声明 CreateProcess 方法 [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] static extern bool CreateProcess( string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); // 定义 STARTUPINFO 结构体 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct STARTUPINFO { public uint cb; public string lpReserved; public string lpDesktop; public string lpTitle; public uint dwX; public uint dwY; public uint dwXSize; public uint dwYSize; public uint dwXCountChars; public uint dwYCountChars; public uint dwFillAttribute; public uint dwFlags; public short wShowWindow; public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } // 定义 PROCESS_INFORMATION 结构体 [StructLayout(LayoutKind.Sequential)] struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public uint dwProcessId; public uint dwThreadId; } } } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值