14、进程与线程管理:进程与线程结构、枚举进程、线程注入检测、进程回调

进程和线程,是驱动开发里绕不开的核心话题。说实话,我刚入行那会儿,总觉得用户态和内核态里的进程概念差不多,结果第一次写枚举进程的驱动就蓝屏了。嗯,这里面的坑,我今天带你一个个踩一遍。

14.1 进程与线程的内核结构

在Windows内核里,每个进程对应一个EPROCESS结构体。这个结构体非常大,不同Windows版本里字段偏移还不一样。我习惯用WinDbgdt _EPROCESS命令来查看当前版本的具体布局。

线程对应的结构是ETHREAD。每个线程都隶属于一个进程,通过ETHREAD里的ThreadsProcess字段可以找到所属进程的EPROCESS

关键字段速查表:

结构体关键字段说明
EPROCESSImageFileName进程名,16字节
EPROCESSUniqueProcessId进程PID
EPROCESSActiveProcessLinks进程链表,用于枚举
ETHREADThreadsProcess指向所属EPROCESS
ETHREADCid.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

注意:直接遍历链表时,一定要检查FLINKBLINK的有效性。我曾经因为没做校验,结果遍历到一个被部分销毁的进程节点,直接蓝屏。血的教训。

14.3 线程注入检测

线程注入,是恶意软件常用的手段。说白了,就是在一个正常进程里创建恶意线程。检测思路其实不复杂:检查线程的入口点是否在合法模块内。

具体怎么做?我一般这么干:

  1. 遍历目标进程的所有线程
  2. 获取每个线程的StartAddress(起始地址)
  3. 检查这个地址是否落在已加载的DLL或EXE范围内
  4. 如果不在,大概率是注入的
// 伪代码:检测线程注入
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);
    }
}

小技巧:有些高级注入会使用APCSetWindowsHookEx,入口点可能是内核API地址。这时候光检查入口点不够,还得结合线程的Win32StartAddressCreateTime做综合判断。我习惯把检测逻辑做成一个状态机,分多个维度打分。

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);
    }
}

警告:回调函数里不要调用IoGetCurrentProcessPsGetCurrentProcess来获取当前进程——因为回调触发时,当前进程可能不是目标进程。要用传进来的ProcessId去查。我刚开始写驱动时就犯过这个错,查出来的进程名永远是系统进程。

14.5 知识体系总览

下面这张图,是我整理的本章节核心逻辑。你看一眼,心里就有数了。

进程与线程管理知识体系 进程与线程管理 内核结构 EPROCESS / ETHREAD ImageFileName / PID 枚举进程 PsGetNextProcess ActiveProcessLinks 线程注入检测 StartAddress检查 模块合法性验证 进程回调 (PsSetCreateProcessNotifyRoutine) 回调函数:ParentId, ProcessId, Create 轻量处理 + 工作线程卸载

这张图把四个核心知识点串起来了。内核结构是基础,枚举进程和线程注入检测是实战应用,进程回调是事件驱动机制。你写驱动时,这四个点经常要配合使用。

举个例子:你要做一个进程监控工具。先用进程回调捕获进程创建事件,然后在回调里枚举该进程的所有线程,检查是否有注入。整个过程,就是这张图的完整落地。

避坑指南:我曾经在回调函数里直接调用PsLookupProcessByProcessId,结果因为IRQL太高导致死锁。后来我改用PsGetCurrentProcess配合CompareObjectHandles,才把问题解决。记住:回调里尽量用ProcessId做标识,不要频繁查对象。

好了,进程与线程管理这块,核心内容就这些。你写驱动时,多想想这几个结构之间的关系,很多问题就能迎刃而解。


公众号:蓝海资料掘金营,微信deep3321