背景
在特定场景下,一些进程运行单纯的浪费资源,但又不能杀掉进程,所以需要通过挂起的方式,暂停进程运行。以释放资源给关键进程运行。
方法对比
方法详解
1. NtSuspendProcess
通过直接调用 NtSuspendProcess
来对进程进行挂起,通过 NtResumeProcess
来恢复进程。
此 API 是 ntdll.dll
导出但未文档化的接口。也是最常用到的接口。其中 ProcessHacker 、SystemInfomations 和 Sandboxie 等都使用的这种方式。
但也存在一定的缺陷,一些情况下还是会存在无法挂起的情况。
例如:
[Plus v1.11.4] Process Suspend/Resume issues · Issue #3375 · sandboxie-plus/Sandboxie
devenv, msedge, discord, skype and other processes cannot be suspended · Issue #856 · winsiderss/sys
下面我们来看看它是怎么做的:
1.1 原理
通过 https://github.com/reactos/reactos 进去(这里也可以通过 windbg 来看,是一样的),可以看到其调用链:
NtSuspendProcess
->PsSuspendProcess
->PsGetNextProcessThread
+PsSuspendThread
->KeSuspendThread
->KiSuspendThread
->KiInsertQueueApc
那这个我们就突然熟悉了,这就很明显了,就是通过在内核模式下,通过遍历线程,然后依次塞入 APC 来进行挂起。具体可以看 线程挂起和恢复 里面有详细的分析。
1.2 问题
- 不能暂停内核代码。由于是塞了个 APC 到线程中去,所以是需要等内核代码执行完成后才会执行到 APC 中,才有可能被暂停(加之 APC 队列中可能还存在其他 APC)
- 中断的影响不可恢复。如果在遍历线程的过程中进程意外退出,可能导致部分线程被挂起的状态,可能导致进程部分功能异常。且无法恢复。
1.3 示例
void ByNtSuspendProcess(DWORD dwProcessId)
{
HANDLE hProcess = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, dwProcessId);
if (hProcess == NULL)
{
std::cout << "OpenProcess failed" << std::endl;
return;
}
if (NtSuspendProcess(hProcess) != STATUS_SUCCESS)
{
std::cout << "NtSuspendProcess failed" << std::endl;
CloseHandle(hProcess);
return;
}
std::cout << "Process suspended" << std::endl;
CloseHandle(hProcess);
}
void ByNtResumeProcess(DWORD dwProcessId) {
HANDLE hProcess = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, dwProcessId);
if (hProcess == NULL)
{
std::cout << "OpenProcess failed" << std::endl;
return;
}
if (NtResumeProcess(hProcess) != STATUS_SUCCESS)
{
std::cout << "NtResumeProcess failed" << std::endl;
CloseHandle(hProcess);
return;
}
std::cout << "Process resumed" << std::endl;
CloseHandle(hProcess);
}
2. SuspendThread
通过遍历当前进程的所有线程,然后依次在用户模式下调用 SuspendThread
来实现进程的挂起,通过 ResumeThread
来恢复进程。
特别地:对于 WOW64 线程,需要调用 Wow64SuspendThread
和 ResumeThread
来进行挂起和恢复。
API 参考:
- SuspendThread function (processthreadsapi.h) - Win32 apps
- Wow64SuspendThread - Win32 apps
- ResumeThread function (processthreadsapi.h) - Win32 apps
这里不同的遍历线程方式,可能会产生不同的效果。
主要分为两种不同的遍历方式:
- CreateToolhelp32Snapshot function (tlhelp32.h) - Win32 apps:通过创建快照的方式,依次进行挂起
- 只记录调用时刻的线程列表,调用后创建的进程没法获取
- 进程快照会带来较大的开销
- 快照中记录的是线程 ID,如果该线程退出后,快速被分配其他线程,则有可能挂起错误的线程(可能性很少,但存在可能)
NtGetNextThread
(undocument):依次枚举线程,再挂起。由于其是挂起一个线程,获取下一个线程,所以不存在上述问题。
2.1 原理
此方式的原理在 线程挂起和恢复 有较详细的分析,这里不再赘述。与 NtSuspendProcess
几乎一致。
2.2 问题
- 额外的开销。由于在用户模式下遍历线程,并调用
SuspendThread
,这里需要每次在获取目标线程的句柄时,都有额外的检查。 - 不能暂停内核代码。由于是塞了个 APC 到线程中去,所以是需要等内核代码执行完成后才会执行到 APC 中,才有可能被暂停(加之 APC 队列中可能还存在其他 APC)
- 中断的影响不可恢复。如果在遍历线程的过程中进程意外退出,可能导致部分线程被挂起的状态,可能导致进程部分功能异常。且无法恢复。
2.3 示例
- Snapshot & SuspendThread
void BySnapshotAndSuspendThread(DWORD dwProcessId)
{
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if (hProcess == NULL)
{
std::cout << "OpenProcess failed" << std::endl;
return;
}
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, dwProcessId);
if (hSnapshot == INVALID_HANDLE_VALUE)
{
std::cout << "CreateToolhelp32Snapshot failed" << std::endl;
CloseHandle(hProcess);
return;
}
THREADENTRY32 te32;
te32.dwSize = sizeof(THREADENTRY32);
if (!Thread32First(hSnapshot, &te32))
{
std::cout << "Thread32First failed" << std::endl;
CloseHandle(hSnapshot);
CloseHandle(hProcess);
return;
}
do
{
if (te32.th32OwnerProcessID == dwProcessId)
{
HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, te32.th32ThreadID);
if (hThread == NULL)
{
std::cout << "OpenThread failed" << std::endl;
CloseHandle(hSnapshot);
CloseHandle(hProcess);
return;
}
if (SuspendThread(hThread) == -1)
{
std::cout << "SuspendThread failed" << std::endl;
CloseHandle(hThread);
CloseHandle(hSnapshot);
CloseHandle(hProcess);
return;
}
std::cout << "Thread " << te32.th32ThreadID << " suspended" << std::endl;
CloseHandle(hThread);
}
} while (Thread32Next(hSnapshot, &te32));
CloseHandle(hSnapshot);
CloseHandle(hProcess);
}
void
标签:std,遍历,cout,Windows,汇总,进程,线程,暂停,APC
From: https://blog.csdn.net/Frendguo/article/details/139292288