8. 进程与线程管理:深入内核对象
进程和线程,是驱动开发里绕不开的核心话题。很多刚入行的朋友,总觉得写驱动就是操作硬件、处理IO,进程管理那是应用层的事。其实不然。你想想看,驱动里要保护某个进程、要挂起某个线程、要遍历系统里所有进程,这些操作都得跟内核里的EPROCESS和ETHREAD打交道。
我个人习惯,把进程和线程比作「容器」和「执行流」。进程是个大容器,装着地址空间、句柄表、安全令牌;线程才是真正干活的。今天我们就来拆开看看,这两个结构体到底长什么样,怎么用。
核心知识点速览
- EPROCESS结构体:进程的内核表示,包含进程ID、父进程、映像路径等
- ETHREAD结构体:线程的内核表示,包含线程ID、所属进程、TEB指针等
- 进程遍历:从PsActiveProcessHead链表出发,遍历所有进程
- 线程创建与挂起:PsCreateSystemThread、KeSuspendThread等API
- 进程保护:通过修改EPROCESS中的保护标志位实现
8.1 EPROCESS:进程的内核身份证
每个进程在Windows内核里都有一个EPROCESS结构体。这个结构体非常大,不同Windows版本里字段偏移都不一样。我刚开始做驱动时,最头疼的就是这个——Win7和Win10的EPROCESS布局完全不同,硬编码偏移量简直是给自己挖坑。
EPROCESS里我们最关心的几个字段:
| 字段(相对偏移) | 含义 | 备注 |
|---|---|---|
| ImageFileName | 进程映像文件名(如notepad.exe) | 最多15个字符+'\0' |
| UniqueProcessId | 进程PID | 内核句柄,非应用层PID |
| ActiveProcessLinks | 进程链表节点 | 用于遍历所有进程 |
| Protection | 进程保护标志(Win8+) | 用于PPL保护 |
| SectionObject | 进程地址空间描述 | 指向文件映射对象 |
获取当前进程的EPROCESS,最简单的方式是调用 IoGetCurrentProcess()。这个函数返回当前线程所在进程的EPROCESS指针。我在项目中经常用它来判断当前操作是在哪个进程上下文中执行的。
// 获取当前进程的EPROCESS
PEPROCESS CurrentProcess = IoGetCurrentProcess();
// 获取进程PID(不同Windows版本偏移不同,建议用API)
HANDLE pid = PsGetProcessId(CurrentProcess);
// 获取进程映像名
PUCHAR imageName = PsGetProcessImageFileName(CurrentProcess);
DbgPrint("当前进程: %s, PID: %p\n", imageName, pid);
我的经验:千万别直接通过偏移量访问EPROCESS字段!不同Windows版本、甚至同一个版本的不同补丁,偏移量都可能变。用微软提供的API(如PsGetProcessId、PsGetProcessImageFileName)才是正道。我曾经因为硬编码偏移量,导致驱动在Win10 1903上蓝屏,排查了一整天。
8.2 ETHREAD:线程的执行上下文
ETHREAD是线程的内核对象。每个线程都有一个对应的ETHREAD结构体。它比EPROCESS小一些,但同样包含大量信息。
ETHREAD里关键的字段:
| 字段 | 含义 |
|---|---|
| Tcb(KTHREAD) | 线程的内核调度块,包含优先级、状态等 |
| ThreadsProcess | 指向所属进程的EPROCESS |
| Cid(CLIENT_ID) | 包含线程ID和进程ID |
| Teb | 线程环境块(用户态地址) |
| Alertable | 线程是否可报警等待 |
获取当前线程的ETHREAD,可以用 PsGetCurrentThread()。这个函数返回当前CPU上正在执行的线程的ETHREAD指针。
// 获取当前线程的ETHREAD
PETHREAD CurrentThread = PsGetCurrentThread();
// 获取线程ID
HANDLE tid = PsGetThreadId(CurrentThread);
// 获取所属进程ID
HANDLE pid = PsGetThreadProcessId(CurrentThread);
DbgPrint("当前线程TID: %p, 所属进程PID: %p\n", tid, pid);
注意:ETHREAD和KTHREAD的关系要搞清楚。ETHREAD是线程对象,KTHREAD(即Tcb字段)是调度器使用的结构体。很多调度相关的操作(如挂起、恢复)实际上操作的是KTHREAD。但API层面,我们通常传入ETHREAD指针,内核会自动转换。
8.3 进程遍历:从链表到进程信息
遍历系统所有进程,是驱动开发的常见需求。比如你要保护某个进程不被结束,或者要监控特定进程的行为。
Windows内核维护了一个全局链表头 PsActiveProcessHead,所有活动进程的EPROCESS都通过ActiveProcessLinks字段挂在这个链表上。遍历方法如下:
// 遍历所有进程
void EnumProcesses() {
PEPROCESS CurrentProcess = NULL;
PLIST_ENTRY ProcessListHead = &PsActiveProcessHead;
PLIST_ENTRY Entry = ProcessListHead->Flink;
while (Entry != ProcessListHead) {
// 从链表节点获取EPROCESS基址
CurrentProcess = (PEPROCESS)((PUCHAR)Entry -
FIELD_OFFSET(EPROCESS, ActiveProcessLinks));
HANDLE pid = PsGetProcessId(CurrentProcess);
PUCHAR name = PsGetProcessImageFileName(CurrentProcess);
DbgPrint("进程: %s, PID: %p\n", name, pid);
Entry = Entry->Flink;
}
}
这里有个关键点:PsActiveProcessHead 在Windows不同版本里导出情况不同。Win7及之前版本可以直接引用,Win8之后可能要用 MmGetSystemRoutineAddress 动态获取。我建议用 ZwQuerySystemInformation 配合 SystemProcessInformation 来遍历,更稳定。
另一种更推荐的方式:使用 ZwQuerySystemInformation(SystemProcessInformation, ...)。这个API返回一个SYSTEM_PROCESS_INFORMATION结构体数组,包含所有进程的详细信息。虽然它是个用户态API,但在驱动里也能用(需要声明)。
8.4 线程创建与挂起:控制执行流
在驱动里创建线程,最常用的API是 PsCreateSystemThread。这个函数创建一个内核线程,运行在System进程上下文中。如果你想让线程运行在特定进程上下文,需要配合 KeStackAttachProcess。
// 创建系统线程
HANDLE hThread = NULL;
NTSTATUS status = PsCreateSystemThread(
&hThread, // 输出:线程句柄
THREAD_ALL_ACCESS, // 访问权限
NULL, // 对象属性(可选)
NULL, // 进程句柄(NULL表示System进程)
NULL, // 客户端ID(可选)
MyThreadFunction, // 线程函数
NULL // 线程函数参数
);
if (NT_SUCCESS(status)) {
// 获取线程的ETHREAD对象
PETHREAD pThread = NULL;
ObReferenceObjectByHandle(
hThread,
THREAD_ALL_ACCESS,
*PsThreadType,
KernelMode,
(PVOID*)&pThread,
NULL
);
// 使用完后释放引用
ObDereferenceObject(pThread);
ZwClose(hThread);
}
挂起线程用 KeSuspendThread,恢复用 KeResumeThread。这两个函数操作的是KTHREAD中的SuspendCount字段。每挂起一次,计数加1;恢复一次,计数减1。只有计数为0时,线程才会被调度。
// 挂起线程
NTSTATUS SuspendThread(PETHREAD Thread) {
// KeSuspendThread返回之前的挂起计数
ULONG PreviousCount = KeSuspendThread(Thread);
if (PreviousCount < 0) {
// 线程已经终止,挂起失败
return STATUS_THREAD_IS_TERMINATING;
}
return STATUS_SUCCESS;
}
// 恢复线程
NTSTATUS ResumeThread(PETHREAD Thread) {
ULONG PreviousCount = KeResumeThread(Thread);
if (PreviousCount <= 0) {
// 线程没有被挂起,恢复失败
return STATUS_UNSUCCESSFUL;
}
return STATUS_SUCCESS;
}
避坑指南:我曾经在挂起一个线程后,忘记检查返回值。结果那个线程实际上已经终止了,挂起操作返回了负数。后续恢复时,计数变成负数,线程永远无法恢复调度。嗯,从那以后我每次调用KeSuspendThread都会检查返回值。
8.5 进程保护:让进程「杀不死」
进程保护,说白了就是让某些进程不能被用户结束。比如杀毒软件的保护进程、系统关键进程。Windows从Win8.1开始引入了PPL(Protected Process Light)机制。
PPL的原理是在EPROCESS中设置一个保护级别。保护级别分为多个等级:
| 保护级别 | 含义 | 典型进程 |
|---|---|---|
| PsProtectedNone | 无保护 | 普通用户进程 |
| PsProtectedLight | 轻度保护(PPL) | Windows Defender、杀毒软件 |
| PsProtectedProtected | 完全保护(PP) | 系统关键进程 |
在驱动里设置进程保护,需要修改EPROCESS中的Protection字段。但注意,这个操作需要高权限,而且微软不推荐普通驱动修改保护级别。
// 设置进程保护(仅用于演示,实际使用需谨慎)
NTSTATUS SetProcessProtection(PEPROCESS Process, UCHAR Level) {
// 获取Protection字段的偏移(不同版本不同)
// 这里假设已经通过某种方式获取了偏移量
PUCHAR ProtectionField = (PUCHAR)Process + ProtectionOffset;
// 设置保护级别
*ProtectionField = Level;
return STATUS_SUCCESS;
}
// 检查进程是否受保护
BOOLEAN IsProcessProtected(PEPROCESS Process) {
// 使用API获取保护级别(Win8+)
PS_PROTECTION Protection = PsGetProcessProtection(Process);
return Protection.Type != PsProtectedNone;
}
重要警告:直接修改EPROCESS的Protection字段是非常危险的操作。微软在Win10 1803之后加强了对这个字段的保护,随意修改可能导致系统崩溃。我建议使用 PsSetProcessProtection 这个未文档化API(如果可用),或者通过正规的PPL签名机制来实现。
8.6 知识体系总览
说了这么多,我们来画一张图,把进程线程管理的核心逻辑串起来。这张图展示了EPROCESS、ETHREAD、链表遍历、线程操作之间的关系。
这张图把今天讲的内容都串起来了。左边是EPROCESS,右边是ETHREAD,中间通过链表和指针关联。下面分别是进程遍历和线程操作。你想想看,掌握了这些,你就能在内核里自由地管理进程和线程了。
总结一下:
- EPROCESS是进程的内核对象,ETHREAD是线程的内核对象
- 遍历进程可以通过PsActiveProcessHead链表,或者用ZwQuerySystemInformation
- 创建线程用PsCreateSystemThread,挂起/恢复用KeSuspendThread/KeResumeThread
- 进程保护通过修改EPROCESS的Protection字段实现,但需谨慎操作
- 始终使用API访问结构体字段,不要硬编码偏移量
好了,进程线程管理这块就讲到这里。记住,内核编程里,结构体就是你的地图,API就是你的工具。把EPROCESS和ETHREAD摸透了,很多问题都能迎刃而解。
公众号:蓝海资料掘金营,微信deep3321