强制结束进程的两种方式
一、引言
在 Windows 内核开发中,进程和线程的终止是一个核心的操作场景。PspTerminateProcess和PspTerminateThreadByPointer是 Windows 内核中未导出的核心函数,它们分别负责进程的终止和线程的终止操作。这两个函数由于未被微软正式文档化,通常需要通过逆向工程的方式来获取其地址并使用,在驱动开发、安全软件和恶意软件中都有广泛的应用场景。
二、函数定义与作用
1.1 PspTerminateProcess
PspTerminateProcess是 Windows 内核中用于终止指定进程的核心函数,它的定义通常如下(不同 Windows 版本可能有细微差异):
cpp
typedef NTSTATUS(__stdcall* _PspTerminateProcess)(PEPROCESS TargetProcess, PEPROCESS SourceProcess, NTSTATUS ExitCode);
该函数的作用是终止指定的进程,同时会终止该进程下的所有线程。
参数列表:
TargetProcess:目标进程的EPROCESS指针。SourceProcess:发起进程的EPROCESS指针。ExitCode:进程退出代码。
1.2 PspTerminateThreadByPointer
PspTerminateThreadByPointer是用于终止指定线程的核心函数,它的定义通常如下:
cpp
typedef NTSTATUS(__stdcall* _PsTerminateThreadByPointer)(PETHREAD pThread, NTSTATUS exitCode, BOOLEAN bDirectTerminate);
该函数接收线程的ETHREAD结构体指针作为参数,以及线程的退出码,还有一个是否等待线程终止完成的标志位。该函数会直接终止指定的线程,是内核中线程终止操作的底层实现之一。
三、未导出函数地址的获取
3.1 通过特征码获取起始地址
-
由于
PspTerminateProcess和PspTerminateThreadByPointer是未导出函数,无法直接通过MmGetSystemRoutineAddress等常规方式获取其地址,通常需要通过特征码搜索导出函数的调用链获取。我们将使用此函数来搜索特征码:cppPVOID SearchPattern(PVOID baseAddr, SIZE_T size, UCHAR* pattern, const char* mask) { PVOID result = NULL; __int64 patternLength = strlen(mask); BOOLEAN found = FALSE; for (__int64 i = 0; i <= size - patternLength; i++) { found = TRUE; for (__int64 j = 0; j < patternLength; j++) { if (mask[j] != '?' && *(UCHAR*)((DWORD_PTR)baseAddr + i + j) != pattern[j]) { found = FALSE; break; } } if (found) { result = (PVOID)((DWORD_PTR)baseAddr + i); break; } } return result; } -
一条指令(如
call指令)的最后四个字节通常为下一条指令起始地址相对目标地址的偏移,这个偏移的数据类型为INT32。
3.1 PspTerminateThreadByPointer的获取方法
-
打开Ghidra,搜索导出函数
PsTerminateSystemThread,此函数在内部会调用PspTerminateThreadByPointer。值得庆幸的是,PspTerminateThreadByPointer相对于PsTerminateSystemThread始终是第一个call指令,所以我们直接寻找PsTerminateSystemThread的第一个call指令的起始地址。

-
通过特征码搜索第一个
call指令的起始地址,从而获取PspTerminateThreadByPointer的真实地址:cppPVOID FindPspTerminateThreadByPointer() { PVOID PspTerminateThreadByPointerAddr = NULL; UNICODE_STRING funcName = RTL_CONSTANT_STRING(L"PsTerminateSystemThread"); PVOID pPsTerminateSystemThread = MmGetSystemRoutineAddress(&funcName); if (!pPsTerminateSystemThread) { return NULL; } BYTE pattern[] = { 0xe8,0x00,0x00,0x00,0x00 }; // '?'为通配符 char mask[] = "x????"; DWORD_PTR uValueA = 0; INT32 iValueA = 0; uValueA = (DWORD_PTR)SearchPattern(pPsTerminateSystemThread, 0x2F, pattern, mask); if (!uValueA) { return NULL; } iValueA = *(INT32*)(uValueA + 1); uValueA += 5; PspTerminateThreadByPointerAddr = (PVOID)(uValueA + iValueA); return PspTerminateThreadByPointerAddr; }
3.2 PspTerminateProcess的获取方法
与PspTerminateThreadByPointer不同的是,PspTerminateProcess的情况要稍复杂一些:前者的 call指令直接存在于导出函数 PsTerminateSystemThread内部,而 PspTerminateProcess则是被内核未导出的 NtTerminateProcess函数所调用,其对应的call指令也因此存在于这个未导出函数中。
NtTerminateProcess并非被其他内核函数直接调用,而是作为内核系统调用的实现函数,由导出函数ZwTerminateProcess通过 SSDT(系统服务描述符表)中对应的服务编号发起间接调用。
-
获取
ZwTerminateProcess地址:cppUNICODE_STRING funcName = RTL_CONSTANT_STRING(L"ZwTerminateProcess"); PUCHAR pZwTerminateProcess = MmGetSystemRoutineAddress(&funcName); if (!pZwTerminateProcess) { DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "获取ZwTerminateProcess地址失败\n"); return NULL; } -
获取
KeServiceDescriptorTable的地址:在 64 位 Windows 内核中,
KeServiceDescriptorTable(SSDT 的核心描述表)是未导出的全局变量,需要通过内核指令特征匹配 +MSR寄存器读取的方式逆向定位,其中syscall指令触发后会跳转到内核的KiSystemCall64函数地址存储在MSR寄存器0xC0000082(LSTAR)中,可通过__readmsr(0xC0000082)读取。其中KeServiceDescriptorTable的结构定义和具体代码实现如下:cpptypedef struct _KSERVICE_DESCRIPTOR_TABLE { PVOID KiServiceTable; // SSDT表基址 PULONG ServiceCounterTableBase; // 服务调用计数表基址(通常为NULL) ULONG NumberOfServices; // SSDT中服务函数的个数(4字节) PVOID ParamTableBase; // 系统服务参数表基址(8字节) } KSERVICE_DESCRIPTOR_TABLE, * PKSERVICE_DESCRIPTOR_TABLE; NTSTATUS GetKeServiceDescriptorTableAddr(PKSERVICE_DESCRIPTOR_TABLE* pKeServiceDescriptorTable) { if (!pKeServiceDescriptorTable) { return STATUS_INVALID_PARAMETER; } PUCHAR KiSystemCall64_Address = SYSCALL_ADDRESS; PUCHAR uValueA = SearchPattern(KiSystemCall64_Address, 0x6080DC, (PUCHAR)"\x4c\x8d\x15\x00\x00\x00\x00", "xxx????"); DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "uValue Address:%p\n", uValueA); if (!MmIsAddressValid(uValueA)) { DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "uValueA is no memory"); return STATUS_NO_MEMORY; } INT32 offset = *(INT32*)(uValueA + 3); *pKeServiceDescriptorTable = (PKSERVICE_DESCRIPTOR_TABLE)(uValueA + 7 + offset); return STATUS_SUCCESS; } -
获取
NtTerminateProcess的地址打开 WinDbg 内核调试模式,输入
uf ZwTerminateProcess进行反汇编,输出如下:js0: kd> uf ZwTerminateProcess nt!ZwTerminateProcess: fffff802`da29de70 488bc4 mov rax,rsp fffff802`da29de73 fa cli fffff802`da29de74 4883ec10 sub rsp,10h fffff802`da29de78 50 push rax fffff802`da29de79 9c pushfq fffff802`da29de7a 6a10 push 10h fffff802`da29de7c 488d05dd5d0000 lea rax,[nt!KiServiceLinkage (fffff802`da2a3c60)] fffff802`da29de83 50 push rax fffff802`da29de84 b82c000000 mov eax,2Ch fffff802`da29de89 e932730100 jmp nt!KiServiceInternal (fffff802`da2b51c0) Branch nt!KiServiceInternal: fffff802`da2b51c0 4883ec08 sub rsp,8 fffff802`da2b51c4 55 push rbp fffff802`da2b51c5 4881ec58010000 sub rsp,158h fffff802`da2b51cc 488dac2480000000 lea rbp,[rsp+80h] fffff802`da2b51d4 48899dc0000000 mov qword ptr [rbp+0C0h],rbx fffff802`da2b51db 4889bdc8000000 mov qword ptr [rbp+0C8h],rdi fffff802`da2b51e2 4889b5d0000000 mov qword ptr [rbp+0D0h],rsi fffff802`da2b51e9 48c7455800000000 mov qword ptr [rbp+58h],0 fffff802`da2b51f1 fb sti fffff802`da2b51f2 65488b1c2588010000 mov rbx,qword ptr gs:[188h] fffff802`da2b51fb 0f0d8b90000000 prefetchw [rbx+90h] fffff802`da2b5202 0fb6bb32020000 movzx edi,byte ptr [rbx+232h] fffff802`da2b5209 40887da8 mov byte ptr [rbp-58h],dil fffff802`da2b520d c6833202000000 mov byte ptr [rbx+232h],0 fffff802`da2b5214 4c8b9390000000 mov r10,qword ptr [rbx+90h] fffff802`da2b521b 4c8995b8000000 mov qword ptr [rbp+0B8h],r10 fffff802`da2b5222 4c8d1d97030000 lea r11,[nt!KiSystemServiceStart (fffff802`da2b55c0)] fffff802`da2b5229 e8f2ef4f00 call nt!_guard_retpoline_switchtable_jump_r11 (fffff802`da7b4220) fffff802`da2b522e cc int 3 fffff802`da2b522f c3 retb82c000000 mov eax,2Ch是ZwTerminateProcess向内核传递NtTerminateProcess的 SSDT 系统服务编号,也是 Windows 内核系统调用的标准流程。我们继续反汇编
KiSystemServiceRepeat,输出结果如下:jsnt!KiSystemServiceRepeat: fffff802`da2b55d4 4c8d15e5c2b400 lea r10,[nt!KeServiceDescriptorTable (fffff802`dae018c0)] fffff802`da2b55db 4c8d1d9e0c9100 lea r11,[nt!KeServiceDescriptorTableShadow (fffff802`dabc6280)] fffff802`da2b55e2 f7437880000000 test dword ptr [rbx+78h],80h fffff802`da2b55e9 7413 je nt!KiSystemServiceRepeat+0x2a (fffff802`da2b55fe) Branch nt!KiSystemServiceRepeat+0x17: fffff802`da2b55eb f7437800002000 test dword ptr [rbx+78h],200000h fffff802`da2b55f2 7407 je nt!KiSystemServiceRepeat+0x27 (fffff802`da2b55fb) Branch nt!KiSystemServiceRepeat+0x20: fffff802`da2b55f4 4c8d1d850d9100 lea r11,[nt!KeServiceDescriptorTableFilter (fffff802`dabc6380)] nt!KiSystemServiceRepeat+0x27: fffff802`da2b55fb 4d8bd3 mov r10,r11 nt!KiSystemServiceRepeat+0x2a: fffff802`da2b55fe 413b443a10 cmp eax,dword ptr [r10+rdi+10h] fffff802`da2b5603 0f83be080000 jae nt!KiSystemServiceExitPico+0x31c (fffff802`da2b5ec7) Branch nt!KiSystemServiceRepeat+0x35: fffff802`da2b5609 4d8b143a mov r10,qword ptr [r10+rdi] fffff802`da2b560d 4d631c82 movsxd r11,dword ptr [r10+rax*4] fffff802`da2b5611 498bc3 mov rax,r11 fffff802`da2b5614 49c1fb04 sar r11,4 fffff802`da2b5618 4d03d3 add r10,r11 fffff802`da2b561b 83ff20 cmp edi,20h fffff802`da2b561e 7550 jne nt!KiSystemServiceGdiTebAccess+0x49 (fffff802`da2b5670) Branch在得到
49c1fb04 sar r11,4这串指令之后,我们就可以编写如下代码来计算系统调用函数的地址:cpp/* 4d631c82 movsxd r11,dword ptr [r10+rax*4] 498bc3 mov rax,r11 49c1fb04 sar r11,4 4d03d3 add r10,r11 */ // syscallIndex = 2Ch PVOID GetSyscallAddress(PKSERVICE_DESCRIPTOR_TABLE table, ULONG syscallIndex) { LONG offset = ((PLONG)table->KiServiceTable)[syscallIndex]; return (PVOID)((ULONG_PTR)table->KiServiceTable + (offset >> 4)); }- 首先
movsxd r11,dword ptr [r10+rax*4]:从KiServiceTable(r10存储的是KiServiceTable的基地址)中取出对应索引rax的偏移量,转换为 64 位有符号数存储到r11中,这对应 C 代码中的((PLONG)table->KiServiceTable)[syscallIndex]。 - 然后
mov rax,r11:将偏移量复制到rax中,暂时保存。 - 接下来
sar r11,4:将r11中的偏移量算术右移 4 位,还原真实的偏移量,对应 C 代码中的offset >> 4。 - 最后
add r10,r11:将KiServiceTable的基地址r10加上还原后的偏移量r11,得到系统调用函数的地址,对应 C 代码中的(ULONG_PTR)table->KiServiceTable + (offset >> 4)。
结合上述代码,我们可以通过ZwTerminateProcess的地址获取NtTerminateProcess的地址:
cppNTSTATUS GetSSDTNtFunc(PKSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorTable, PUCHAR pZwFuncAddr, PVOID* pNtFuncAddress) { PUCHAR uValueA = SearchPattern(pZwFuncAddr, 0x1E, (PUCHAR)"\xb8\x00\x00\x00\x00", "x??xx"); if (!MmIsAddressValid(uValueA)) { return STATUS_NO_MEMORY; } ULONG index = *(PULONG)(uValueA + 1); *pNtFuncAddress = GetSyscallAddress(KeServiceDescriptorTable, index); return STATUS_SUCCESS; }这样,我们就有能力获取到
NtTerminateProcess了。- 通过
NtTerminateProcess获取PspTerminateProcess
我们使用 WinDbg 反汇编
NtTerminateProcess后输出结果如下:js0: kd> uf nt!NtTerminateProcess nt!NtTerminateProcess: fffff802`d8d31500 4c8bdc mov r11,rsp fffff802`d8d31503 49895b10 mov qword ptr [r11+10h],rbx fffff802`d8d31507 55 push rbp fffff802`d8d31508 56 push rsi fffff802`d8d31509 57 push rdi fffff802`d8d3150a 4154 push r12 fffff802`d8d3150c 4155 push r13 fffff802`d8d3150e 4156 push r14 fffff802`d8d31510 4157 push r15 fffff802`d8d31512 4883ec40 sub rsp,40h fffff802`d8d31516 65488b342588010000 mov rsi,qword ptr gs:[188h] fffff802`d8d3151f 4533f6 xor r14d,r14d fffff802`d8d31522 4d897308 mov qword ptr [r11+8],r14 fffff802`d8d31526 448be2 mov r12d,edx fffff802`d8d31529 488bbeb8000000 mov rdi,qword ptr [rsi+0B8h] fffff802`d8d31530 418d6e01 lea ebp,[r14+1] fffff802`d8d31534 448abe32020000 mov r15b,byte ptr [rsi+232h] fffff802`d8d3153b 4885c9 test rcx,rcx fffff802`d8d3153e 0f84de000000 je nt!NtTerminateProcess+0x122 (fffff802`d8d31622) Branch nt!NtTerminateProcess+0x44: ... fffff802`d8d31568 e82303f1ff call nt!ObpReferenceObjectByHandleWithTag (fffff802`d8c41890) nt!NtTerminateProcess+0x75: ... nt!NtTerminateProcess+0x7e: ... fffff802`d8d31590 e8cb2eb2ff call nt!EtwpGetProcessStartKey (fffff802`d8854460) ... fffff802`d8d315a0 e89b0bb2ff call nt!PsGetProcessCreateTimeQuadPart (fffff802`d8852140) ... fffff802`d8d315c5 e806e6ffff call nt!PspTerminateProcess (fffff802`d8d2fbd0) ...可以看出
e806e6ffff call nt!PspTerminateProcess (fffff802d8d2fbd0)是NtTerminateProcess的第四个call指令,我们就能写出如下代码:cpp// 在Windows 10中,NtTerminsteProcess的第二个call指令才是PspTerminateProcess // 为了保持兼容性,我们在这加以判断 BOOLEAN IsWindows11() { OSVERSIONINFOW osInfo = { 0 }; osInfo.dwOSVersionInfoSize = sizeof(osInfo); NTSTATUS status = RtlGetVersion(&osInfo); if (status) { return FALSE; } return osInfo.dwBuildNumber >= 22000; } PVOID FindPspTerminateProcess() { UCHAR pattern[] = { 0xe8,0x00,0x00,0x00,0x00 }; PUCHAR uValueA = NULL; //KdBreakPoint(); UNICODE_STRING funcName = RTL_CONSTANT_STRING(L"ZwTerminateProcess"); PUCHAR pZwTerminateProcess = MmGetSystemRoutineAddress(&funcName); NTSTATUS status = GetSSDTNtFunc(gKeServiceDescriptorTable, pZwTerminateProcess, &uValueA); if (status) { DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "Failed get SSDT NtFunc.\n"); return NULL; } //KdBreakPoint(); DWORD count = 2; if (IsWindows11()) { count = 4; } for (DWORD i = 0; i < count; i++) { uValueA = SearchPattern(uValueA, 0x106, pattern, "x????"); uValueA += 1; } return (PVOID)((uValueA + 4) + *(INT32*)uValueA); } - 首先
四、使用方法
4.1 调用PspTerminateThreadByPointer结束进程
cpp
typedef NTSTATUS(__stdcall* _PsTerminateThreadByPointer)(PETHREAD pThread, NTSTATUS exitCode, BOOLEAN bDirectTerminate);
_PsTerminateThreadByPointer gPsTerminateThreadByPointer = NULL;
VOID Init()
{
gPsTerminateThreadByPointer=(_PsTerminateThreadByPointer)FindPspTerminateThreadByPointer();
}
NTSTATUS PsTerminateThreadByPointer(PETHREAD pThread, NTSTATUS exitCode, BOOLEAN bDirectTerminate)
{
_PsTerminateThreadByPointer pPsTerminateThreadByPointer = gPsTerminateThreadByPointer;
if (!pPsTerminateThreadByPointer)
{
return STATUS_NO_MEMORY;
}
return pPsTerminateThreadByPointer(pThread, exitCode, bDirectTerminate);
}
NTSTATUS PsTerminateProcessByPid(DWORD64 pid)
{
NTSTATUS funcStatus = STATUS_SUCCESS;
PETHREAD ethrd = NULL;
PEPROCESS pProcess = NULL;
for (DWORD64 tid = 4; tid < 262144; tid += 4)
{
NTSTATUS status = PsLookupThreadByThreadId((HANDLE)tid, ðrd);
if (!NT_SUCCESS(status))
{
continue;
}
pProcess = IoThreadToProcess(ethrd);
if (!pProcess)
{
ObDereferenceObject(ethrd);
ethrd = NULL;
continue;
}
DWORD64 processId = (DWORD64)PsGetProcessId(pProcess);
if (processId == pid)
{
status = PsTerminateThreadByPointer(ethrd, STATUS_SUCCESS, 0);
if (status != STATUS_SUCCESS)
{
funcStatus = status;
}
}
ObDereferenceObject(ethrd);
ethrd = NULL;
pProcess = NULL;
}
if (funcStatus == STATUS_SUCCESS)
{
//DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "TerminateProcess Success!");
}
return funcStatus;
}
4.2 调用PspTerminateProcess结束进程
-
代码实现 :
cpp#include <ntifs.h> #include <ntstrsafe.h> #include <intrin.h> typedef NTSTATUS(__stdcall* _PspTerminateProcess)(PEPROCESS TargetProcess, PEPROCESS AttackProcess, NTSTATUS ExitCode); #define SYSCALL_ADDRESS (PUCHAR)__readmsr(0xC0000082) _PspTerminateProcess gPspTerminateProcess = NULL; NTSTATUS PspTerminateProcessId(HANDLE pid) { PEPROCESS targetProcess = NULL; NTSTATUS status = PsLookupProcessByProcessId(pid, &targetProcess); if (status != STATUS_SUCCESS) { DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "Failed get EPROCESS by PID\n"); return status; } status = gPspTerminateProcess(targetProcess, IoGetCurrentProcess(), 1); DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "EPROCESS Address:%p\n",targetProcess); //__debugbreak(); ObDereferenceObject(targetProcess); return status; } NTSTATUS DriverUnload(PDRIVER_OBJECT DriverObject) { UNREFERENCED_PARAMETER(DriverObject); return STATUS_SUCCESS; } NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { UNREFERENCED_PARAMETER(RegistryPath); NTSTATUS status = GetKeServiceDescriptorTableAddr(&gKeServiceDescriptorTable); if (status != STATUS_SUCCESS) { DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "Failed Get KeServiceDescriptorTable.\n"); return status; } DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "KeServiceDescriptorTable Address:%p\n", gKeServiceDescriptorTable); //return STATUS_ACCESS_DENIED; PVOID pPspTerminateProcess = FindPspTerminateProcess(); if (!pPspTerminateProcess) { DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "Failed Get PspTerminateProcess.\n"); return STATUS_NO_MEMORY; } DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "PspTerminateProcess Address:%p\n", pPspTerminateProcess); gPspTerminateProcess = (_PspTerminateProcess)pPspTerminateProcess; // 这里要把1234改成你想要结束的进程ID status=PspTerminateProcessId((HANDLE)1234); DriverObject->DriverUnload = DriverUnload; return status; } -
最终效果 :
js0: kd> g uValue Address:FFFFF802D8AB55D4 KeServiceDescriptorTable Address:FFFFF802D96018C0 PspTerminateProcess Address:FFFFF802D8D2FBD0 EPROCESS Address:FFFF808CBF7F9080 5: kd> x nt!KeServiceDescriptorTable fffff802`d96018c0 nt!KeServiceDescriptorTable = <no type information> 5: kd> x nt!PspTerminateProcess fffff802`d8d2fbd0 nt!PspTerminateProcess (void) 5: kd> dt nt!_EPROCESS FFFF808CBF7F9080 ExitStatus +0x554 ExitStatus : 0n1从 WinDbg 的输出结果可以看出:
- 调试信息的
PspTerminateProcess地址正好是x nt!PspTerminateProcess。 - 其中
+0x554 ExitStatus : 0n1也正好是gPspTerminateProcess(targetProcess, IoGetCurrentProcess(), 1)中第三个参数的值。
- 调试信息的
五、总结
通过特征码搜索、SSDT 系统服务表解析、MSR 寄存器读取,成功实现了不同 Windows 版本下两个函数地址的精准获取,并针对性编写了两种内核层强制结束指定 PID 进程的完整代码方案,分别为遍历终止目标进程所有线程、直接调用进程终止函数。经 WinDbg 内核调试验证,所实现的地址获取方法精准有效,进程终止功能可正常执行,退出码参数也按预期生效,最终完成了 Windows 内核层强制结束进程的技术实现与验证。