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(进程对象) ImageFileName → "notepad.exe" UniqueProcessId → 0x1234 ActiveProcessLinks → 链表节点 Protection → 保护级别 ThreadListHead → 线程链表头 ETHREAD(线程对象) Tcb (KTHREAD) → 调度信息 ThreadsProcess → 指向EPROCESS Cid → {PID, TID} Teb → 用户态TEB地址 Alertable → 是否可报警 ThreadListHead 进程遍历:PsActiveProcessHead 链表 EPROCESS1.ActiveProcessLinks ↔ EPROCESS2.ActiveProcessLinks ↔ ... ↔ EPROCESSn.ActiveProcessLinks 通过 Flink/Blink 双向链表遍历所有活动进程 线程操作 API PsCreateSystemThread → 创建内核线程 KeSuspendThread → 挂起线程 KeResumeThread → 恢复线程

这张图把今天讲的内容都串起来了。左边是EPROCESS,右边是ETHREAD,中间通过链表和指针关联。下面分别是进程遍历和线程操作。你想想看,掌握了这些,你就能在内核里自由地管理进程和线程了。

总结一下:

  • EPROCESS是进程的内核对象,ETHREAD是线程的内核对象
  • 遍历进程可以通过PsActiveProcessHead链表,或者用ZwQuerySystemInformation
  • 创建线程用PsCreateSystemThread,挂起/恢复用KeSuspendThread/KeResumeThread
  • 进程保护通过修改EPROCESS的Protection字段实现,但需谨慎操作
  • 始终使用API访问结构体字段,不要硬编码偏移量

好了,进程线程管理这块就讲到这里。记住,内核编程里,结构体就是你的地图,API就是你的工具。把EPROCESS和ETHREAD摸透了,很多问题都能迎刃而解。


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