14、进程与线程管理:进程与线程结构、枚举进程、线程注入检测、进程回调
进程和线程,是驱动开发里绕不开的核心话题。说实话,我刚入行那会儿,总觉得用户态和内核态里的进程概念差不多,结果第一次写枚举进程的驱动就蓝屏了。嗯,这里面的坑,我今天带你一个个踩一遍。
14.1 进程与线程的内核结构
在Windows内核里,每个进程对应一个EPROCESS结构体。这个结构体非常大,不同Windows版本里字段偏移还不一样。我习惯用WinDbg的dt _EPROCESS命令来查看当前版本的具体布局。
线程对应的结构是ETHREAD。每个线程都隶属于一个进程,通过ETHREAD里的ThreadsProcess字段可以找到所属进程的EPROCESS。
关键字段速查表:
| 结构体 | 关键字段 | 说明 |
|---|---|---|
| EPROCESS | ImageFileName | 进程名,16字节 |
| EPROCESS | UniqueProcessId | 进程PID |
| EPROCESS | ActiveProcessLinks | 进程链表,用于枚举 |
| ETHREAD | ThreadsProcess | 指向所属EPROCESS |
| ETHREAD | Cid.UniqueThread | 线程TID |
你想想看,为什么驱动里要直接操作这些结构?因为用户态API会被hook,而内核态直接读内存,更底层也更可靠。不过要小心——不同Windows版本里字段偏移可能不同,我建议用RTL_CONSTANT或者运行时动态获取偏移。
14.2 枚举进程的两种方式
枚举进程,说白了就是遍历系统里的所有EPROCESS。我个人最常用的是ZwQuerySystemInformation,但驱动里更推荐用PsGetProcessNextProcess这种内核API。
先看第一种:用PsGetProcessNextProcess。
// 枚举所有进程
PEPROCESS CurrentProcess = PsGetCurrentProcess();
PEPROCESS NextProcess = NULL;
do {
HANDLE pid = PsGetProcessId(CurrentProcess);
// 处理进程...
NextProcess = PsGetNextProcess(CurrentProcess);
ObDereferenceObject(CurrentProcess);
CurrentProcess = NextProcess;
} while (CurrentProcess != NULL);
第二种方式:直接遍历ActiveProcessLinks链表。这种方式更底层,但需要自己处理同步。我在项目中遇到过一个问题:遍历过程中进程被创建或销毁,链表指针就变了。解决办法是加锁,或者用ExAcquireRundownProtection。
注意:直接遍历链表时,一定要检查FLINK和BLINK的有效性。我曾经因为没做校验,结果遍历到一个被部分销毁的进程节点,直接蓝屏。血的教训。
14.3 线程注入检测
线程注入,是恶意软件常用的手段。说白了,就是在一个正常进程里创建恶意线程。检测思路其实不复杂:检查线程的入口点是否在合法模块内。
具体怎么做?我一般这么干:
- 遍历目标进程的所有线程
- 获取每个线程的
StartAddress(起始地址) - 检查这个地址是否落在已加载的DLL或EXE范围内
- 如果不在,大概率是注入的
// 伪代码:检测线程注入
VOID CheckThreadInjection(PEPROCESS TargetProcess) {
PETHREAD Thread = PsGetFirstProcessThread(TargetProcess);
while (Thread != NULL) {
PVOID StartAddr = PsGetThreadStartAddress(Thread);
// 检查StartAddr是否在合法模块内
if (!IsAddressInValidModule(TargetProcess, StartAddr)) {
// 可疑!记录日志
DbgPrint("可疑线程注入: 进程 %p, 线程 %p, 入口 %p",
TargetProcess, Thread, StartAddr);
}
Thread = PsGetNextProcessThread(TargetProcess, Thread);
}
}
小技巧:有些高级注入会使用APC或SetWindowsHookEx,入口点可能是内核API地址。这时候光检查入口点不够,还得结合线程的Win32StartAddress和CreateTime做综合判断。我习惯把检测逻辑做成一个状态机,分多个维度打分。
14.4 进程回调
进程回调,是Windows提供的一种通知机制。当进程创建、退出、加载DLL时,系统会调用你注册的回调函数。这个功能在安全软件里用得特别多。
注册回调用PsSetCreateProcessNotifyRoutine,原型如下:
NTSTATUS PsSetCreateProcessNotifyRoutine(
PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine,
BOOLEAN Remove
);
回调函数的签名:
VOID ProcessNotifyRoutine(
HANDLE ParentId,
HANDLE ProcessId,
BOOLEAN Create
);
嗯,这里要注意:回调函数运行在任意线程上下文中,不能做太重的操作。我建议只做记录和快速判断,复杂逻辑扔到工作线程里处理。
举个例子,监控notepad.exe的启动:
VOID MyProcessNotify(
HANDLE ParentId,
HANDLE ProcessId,
BOOLEAN Create
) {
if (Create) {
PEPROCESS Process;
PsLookupProcessByProcessId(ProcessId, &Process);
// 获取进程名
PCHAR ImageName = PsGetProcessImageFileName(Process);
if (ImageName && strstr(ImageName, "notepad.exe")) {
DbgPrint("notepad.exe 启动了, PID: %d", ProcessId);
// 可以做拦截或记录
}
ObDereferenceObject(Process);
}
}
警告:回调函数里不要调用IoGetCurrentProcess或PsGetCurrentProcess来获取当前进程——因为回调触发时,当前进程可能不是目标进程。要用传进来的ProcessId去查。我刚开始写驱动时就犯过这个错,查出来的进程名永远是系统进程。
14.5 知识体系总览
下面这张图,是我整理的本章节核心逻辑。你看一眼,心里就有数了。
这张图把四个核心知识点串起来了。内核结构是基础,枚举进程和线程注入检测是实战应用,进程回调是事件驱动机制。你写驱动时,这四个点经常要配合使用。
举个例子:你要做一个进程监控工具。先用进程回调捕获进程创建事件,然后在回调里枚举该进程的所有线程,检查是否有注入。整个过程,就是这张图的完整落地。
避坑指南:我曾经在回调函数里直接调用PsLookupProcessByProcessId,结果因为IRQL太高导致死锁。后来我改用PsGetCurrentProcess配合CompareObjectHandles,才把问题解决。记住:回调里尽量用ProcessId做标识,不要频繁查对象。
好了,进程与线程管理这块,核心内容就这些。你写驱动时,多想想这几个结构之间的关系,很多问题就能迎刃而解。
公众号:蓝海资料掘金营,微信deep3321