背景
在特定场景下,一些进程运行单纯的浪费资源,但又不能杀掉进程,所以需要通过挂起的方式,暂停进程运行。以释放资源给关键进程运行。
方法对比
方案 | 方案描述 | 优势 | 不足 |
NtSuspendProcess | 在内核模式下遍历线程,并通过挂起线程来完成进程的挂起 | 1. 内核态遍历,性能高 2. 新创建的线程也能挂起 | 1. 不能中断内核代码 2. 中断影响不可恢复 3. 存在部分兼容问题 |
SuspendThread | 在用户模式下遍历线程,再通过挂起线程来完成进程的挂起 | - | 1. 不能中断内核代码 2. 中断影响不可恢复 3. 用户态遍历,性能较低 4. 可能存在部分新线程未挂起 5. 存在部分兼容问题 |
NtDebugActiveProcess | 利用调试的机制,创建一个调试端口附加到特定进程,以实现挂起应用(实际也是在内核模式下遍历线程,并挂起) | 1. 内核态遍历,性能高 2. 可恢复性好 3. 兼容性好 | 1. 不能中断内核代码 |
JobObjectFreezeInfomation | 利用 Job 从进程级别冻结 | 1. 进程级别冻结,不存在线程之间竞争 | 1. 可恢复性差 |
NtChangeProcessState | 通过修改进程状态来冻结进程 | 可恢复性好 | 不能中断内核代码 |
方法详解
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 BySnapshotAndResumeThread(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 (ResumeThread(hThread) == -1)
{
std::cout << "ResumeThread failed" << std::endl;
CloseHandle(hThread);
CloseHandle(hSnapshot);
CloseHandle(hProcess);
return;
}
std::cout << "Thread " << te32.th32ThreadID << " resumed" << std::endl;
CloseHandle(hThread);
}
} while (Thread32Next(hSnapshot, &te32));
CloseHandle(hSnapshot);
CloseHandle(hProcess);
}
- NtGetNextThread & SuspendThread
void ByNtGetNextThreadAndSuspendThread(DWORD dwProcessId)
{
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if (hProcess == NULL)
{
std::cout << "OpenProcess failed" << std::endl;
return;
}
HANDLE hThread = NULL;
THREAD_BASIC_INFORMATION tbi;
NTSTATUS status = STATUS_SUCCESS;
while (NtGetNextThread(hProcess, hThread, THREAD_SUSPEND_RESUME, 0, 0, &hThread) == STATUS_SUCCESS)
{
if (SuspendThread(hThread) == -1)
{
std::cout << "NtSuspendThread failed, error code: " << GetLastError() << std::endl;
}
}
CloseHandle(hProcess);
}
void ByNtGetNextThreadAndResumeThread(DWORD dwProcessId) {
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if (hProcess == NULL)
{
std::cout << "OpenProcess failed" << std::endl;
return;
}
HANDLE hThread = nullptr;
THREAD_BASIC_INFORMATION tbi;
NTSTATUS status = STATUS_SUCCESS;
while (NtGetNextThread(hProcess, hThread, THREAD_SUSPEND_RESUME, 0, 0, &hThread) == STATUS_SUCCESS)
{
if (ResumeThread(hThread) == -1)
{
std::cout << "NtResumeThread failed, error code: " << GetLastError() << std::endl;
break;
}
}
CloseHandle(hProcess);
}
3. NtDebugActiveProcess
通过调用 NtCreateDebugObject
来创建 Debug Object,然后通过 NtDebugActiveProcess
将 Debug Object 附加到进程上,以此实现进程的暂停。
通过 NtRemoveProcessDebug
移除对应的 Debug Object 即可恢复执行。
3.1 原理
这种方式主要依赖 Windows 中的调试机制,其可以兼容几乎所有的应用。
首先,通过 NtCreateDebugObject
来创建一个调试对象(也称:调试端口),然后将其连接到目标进程(NtDebugActiveProcess
)。然后等待目标进程发生感兴趣的事件(NtWaitForDebugEvent
),然后再进行对应处理(NtDebugContinue
)。
这里讨论的是用户模式下的调试,内核模式机制不太一样。这里不详述
这里如果关闭了调试对象的 handle(或者进程崩溃)会自动取消暂停
这里我们不是真的调试,所以不需要后续的操作,只需要连接到目标进程即可。
那问题来了,NtDebugActiveProcess
连接进程之后怎么就停下来了呢?是怎么做到的呢?
- ReactOS
我们看看 ReactOS 里面是怎么实现 NtDebugActiveProcess
的:
reactos/ntoskrnl/dbgk/dbgkobj.c at 1a162375f9946088d503d586aff73e40ff2c143a · reactos/reactos
总结一下调用链:
NtDebugActiveProcess
->DbgkpPostFakeProcessCreateMessages
->DbgkpPostFakeThreadMessages
->PsGetNextProcessThread
+PsSuspendThread
于是我们就熟悉了,在连接到目标进程的时候,在内核模式下,通过发送进程/线程的创建事件,遍历线程并通过 PsSuspendThread
来将线程挂起。
- Windbg 来看
在 Win10 22H2 上内核调试,追踪 NtDebugActiveProcess
可以看到与 ReactOS 基本一致:
堆栈如下:
nt!PsSuspendThread
nt!DbgkpPostFakeThreadMessages+0x16a
nt!DbgkpPostFakeProcessCreateMessages+0x59
nt!NtDebugActiveProcess+0x174
nt!KiSystemServiceCopyEnd+0x25
ntdll!NtDebugActiveProcess+0x14
结合 NtDebugActiveProcess
的反汇编代码(中间神略部分):
3.2 问题
- 不能暂停内核代码。由于是塞了个 APC 到线程中去,所以是需要等内核代码执行完成后才会执行到 APC 中,才有可能被暂停(加之 APC 队列中可能还存在其他 APC)
3.3 示例
HANDLE ByNtCreateDebugObjectSuspendProcess(DWORD dwProcessId)
{
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if (hProcess == NULL)
{
std::cout << "OpenProcess failed" << std::endl;
return nullptr;
}
HANDLE hDebugObject = nullptr;
OBJECT_ATTRIBUTES objAttr;
InitializeObjectAttributes(&objAttr, NULL, 0, NULL, NULL);
if (NtCreateDebugObject(&hDebugObject, DEBUG_ALL_ACCESS, &objAttr, 0) != STATUS_SUCCESS)
{
std::cout << "NtCreateDebugObject failed" << std::endl;
CloseHandle(hProcess);
hProcess = nullptr;
return nullptr;
}
if (NtDebugActiveProcess(hProcess, hDebugObject) != STATUS_SUCCESS)
{
std::cout << "NtDebugActiveProcess failed" << std::endl;
CloseHandle(hDebugObject);
CloseHandle(hProcess);
return nullptr;
}
std::cout << "Process suspended" << std::endl;
CloseHandle(hProcess);
return hDebugObject;
}
void ByNtRemoveProcessDebugResumeProcess(DWORD dwProcessId, HANDLE hDebugObject)
{
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if (hProcess == NULL)
{
std::cout << "OpenProcess failed" << std::endl;
return;
}
if (NtRemoveProcessDebug(hProcess, hDebugObject) != STATUS_SUCCESS)
{
std::cout << "NtRemoveProcessDebug failed" << std::endl;
CloseHandle(hProcess);
return;
}
std::cout << "Process resumed" << std::endl;
CloseHandle(hProcess);
}
4. JobObjectFreezeInformation
通过 CreateJobObject
新建一个 job 对象,通过 NtSetInformationJobObject
修改 job 对象的属性 JobObjectFreezeInformation
中的 Freeze
为 true,再通过 AssignProcessToJobObject
将目标进程指定到此 job 中,即可实现对进程的冻结。
通过 NtSetInformationJobObject
修改 job 对象属性 Freeze
为 false,即可取消对进程的冻结。
此方案,被用于现代应用(UWP 应用)的挂起操作中。
4.1 原理
Windows 提供对的一种对进程的冻结机制。
可以在内核进程对象 KPROCESS 中找到对应的属性 DeepFreeze
:
2: kd> dt nt!_KPROCESS
+0x000 Header : _DISPATCHER_HEADER
+0x018 ProfileListHead : _LIST_ENTRY
+0x028 DirectoryTableBase : Uint8B
+0x030 ThreadListHead : _LIST_ENTRY
+0x040 ProcessLock : Uint4B
+0x044 ProcessTimerDelay : Uint4B
+0x048 DeepFreezeStartTime : Uint8B
+0x050 Affinity : _KAFFINITY_EX
+0x0f8 AffinityPadding : [12] Uint8B
+0x158 ReadyListHead : _LIST_ENTRY
+0x168 SwapListEntry : _SINGLE_LIST_ENTRY
+0x170 ActiveProcessors : _KAFFINITY_EX
+0x218 ActiveProcessorsPadding : [12] Uint8B
+0x278 AutoAlignment : Pos 0, 1 Bit
+0x278 DisableBoost : Pos 1, 1 Bit
+0x278 DisableQuantum : Pos 2, 1 Bit
+0x278 DeepFreeze : Pos 3, 1 Bit
+0x278 TimerVirtualization : Pos 4, 1 Bit
+0x278 CheckStackExtents : Pos 5, 1 Bit
+0x278 CacheIsolationEnabled : Pos 6, 1 Bit
+0x278 PpmPolicy : Pos 7, 3 Bits
+0x278 VaSpaceDeleted : Pos 10, 1 Bit
+0x278 ReservedFlags : Pos 11, 21 Bits
+0x278 ProcessFlags : Int4B
+0x27c ActiveGroupsMask : Uint4B
+0x280 BasePriority : Char
+0x281 QuantumReset : Char
+0x282 Visited : Char
+0x283 Flags : _KEXECUTE_OPTIONS
+0x284 ThreadSeed : [20] Uint2B
+0x2ac ThreadSeedPadding : [12] Uint2B
+0x2c4 IdealProcessor : [20] Uint2B
+0x2ec IdealProcessorPadding : [12] Uint2B
+0x304 IdealNode : [20] Uint2B
+0x32c IdealNodePadding : [12] Uint2B
+0x344 IdealGlobalNode : Uint2B
+0x346 Spare1 : Uint2B
+0x348 StackCount : _KSTACK_COUNT
+0x350 ProcessListEntry : _LIST_ENTRY
+0x360 CycleTime : Uint8B
+0x368 ContextSwitches : Uint8B
+0x370 SchedulingGroup : Ptr64 _KSCHEDULING_GROUP
+0x378 FreezeCount : Uint4B
+0x37c KernelTime : Uint4B
+0x380 UserTime : Uint4B
+0x384 ReadyTime : Uint4B
+0x388 UserDirectoryTableBase : Uint8B
+0x390 AddressPolicy : UChar
+0x391 Spare2 : [71] UChar
+0x3d8 InstrumentationCallback : Ptr64 Void
+0x3e0 SecureState : <anonymous-tag>
+0x3e8 KernelWaitTime : Uint8B
+0x3f0 UserWaitTime : Uint8B
+0x3f8 EndPadding : [8] Uint8B
4.2 问题
- 如果在冻结后,丢失了 job 的句柄,则无法恢复
4.3 示例
#define JOB_OBJECT_OPERATION_FREEZE 1
HANDLE ByJobObjectFreeze(DWORD dwProcessId)
{
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if (hProcess == NULL)
{
std::cout << "OpenProcess failed" << std::endl;
return nullptr;
}
HANDLE hJob = CreateJobObject(NULL, NULL);
if (hJob == NULL)
{
std::cout << "CreateJobObject failed" << std::endl;
CloseHandle(hProcess);
return nullptr;
}
JOBOBJECT_FREEZE_INFORMATION jfi;
jfi.Flags = JOB_OBJECT_OPERATION_FREEZE;
jfi.Freeze = true;
// (JOBOBJECTINFOCLASS)18 <-> JobObjectFreezeInformation
if (!NT_SUCCESS(NtSetInformationJobObject(hJob, (JOBOBJECTINFOCLASS)18, &jfi, sizeof(jfi))))
{
std::cout << "SetInformationJobObject failed" << std::endl;
CloseHandle(hJob);
CloseHandle(hProcess);
return nullptr;
}
if (!AssignProcessToJobObject(hJob, hProcess))
{
std::cout << "AssignProcessToJobObject failed" << std::endl;
CloseHandle(hJob);
CloseHandle(hProcess);
return nullptr;
}
std::cout << "Process freeze" << std::endl;
CloseHandle(hProcess);
return hJob;
}
void ByJobObjectUnFreeze(DWORD dwProcessId, HANDLE hObj) {
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if (hProcess == NULL)
{
std::cout << "OpenProcess failed" << std::endl;
return;
}
JOBOBJECT_FREEZE_INFORMATION jfi;
jfi.Flags = JOB_OBJECT_OPERATION_FREEZE;
jfi.Freeze = false;
// (JOBOBJECTINFOCLASS)18 <-> JobObjectFreezeInformation
if (!NT_SUCCESS(NtSetInformationJobObject(hObj, (JOBOBJECTINFOCLASS)18, &jfi, sizeof(jfi))))
{
std::cout << "SetInformationJobObject failed" << std::endl;
CloseHandle(hObj);
CloseHandle(hProcess);
return;
}
std::cout << "Process unfreeze" << std::endl;
CloseHandle(hProcess);
}
5. NtChangeProcessState
此 API 只能在 Win11 以上使用
首先通过 NtCreateProcessStateChange
创建一个句柄,然后通过 NtChangeProcessState
修改进程状态
如果释放了 NtCreateProcessStateChange
创建的句柄,则会自动恢复
5.1 原理
这里直接看看调用堆栈:
Call Site
nt!KiInsertQueueApc
nt!KiSuspendThread+0x6f
nt!KiFreezeSingleThread+0x71
nt!KeFreezeProcess+0x9f
nt!PsFreezeProcess+0x2c
nt!NtChangeProcessState+0x179
nt!KiSystemServiceCopyEnd+0x25
ntdll!NtChangeProcessState+0x14
基本上可以看出,其本质上都是通过插入 apc 来实现进程挂起。
5.2 问题
- 不能暂停内核代码。由于是塞了个 APC 到线程中去,所以是需要等内核代码执行完成后才会执行到 APC 中,才有可能被暂停(加之 APC 队列中可能还存在其他 APC)
5.3 示例
#define PROCESS_STATE_CHANGE_STATE (0x0001)
#define PROCESS_STATE_ALL_ACCESS STANDARD_RIGHTS_ALL | PROCESS_STATE_CHANGE_STATE
// 此方法只在 win11 以后的版本才有
HANDLE ByProcessStateChangeFreeze(DWORD processId) {
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId);
if (hProcess == NULL)
{
std::cout << "OpenProcess failed" << std::endl;
return nullptr;
}
HANDLE hProcessStateChange = nullptr;
NTSTATUS status = NtCreateProcessStateChange(&hProcessStateChange, PROCESS_SET_INFORMATION | PROCESS_STATE_ALL_ACCESS, nullptr, hProcess, NULL);
if (!NT_SUCCESS(status)) {
std::cout << "NtCreateProcessStateChange failed, status is : " << status << std::endl;
CloseHandle(hProcess);
return nullptr;
}
status = NtChangeProcessState(hProcessStateChange, hProcess, PROCESS_STATE_CHANGE_TYPE::ProcessStateChangeSuspend, NULL, 0, 0);
if (!NT_SUCCESS(status)) {
std::cout << "NtChangeProcessState failed, status is : " << status << std::endl;
CloseHandle(hProcessStateChange);
CloseHandle(hProcess);
return nullptr;
}
std::cout << "Process freeze" << std::endl;
CloseHandle(hProcess);
}
void ByProcessStateChangeResume(DWORD processId, HANDLE hProcessStateChange) {
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId);
if (hProcess == NULL)
{
std::cout << "OpenProcess failed" << std::endl;
return;
}
NTSTATUS status = NtChangeProcessState(hProcessStateChange, hProcess, PROCESS_STATE_CHANGE_TYPE::ProcessStateChangeResume, NULL, 0, 0);
if (!NT_SUCCESS(status)) {
std::cout << "NtChangeProcessState failed, status is: " << status << std::endl;
CloseHandle(hProcessStateChange);
CloseHandle(hProcess);
return;
}
std::cout << "Process unfreeze" << std::endl;
CloseHandle(hProcessStateChange);
CloseHandle(hProcess);
}
0