windows内核进程遍历常见方式
字数 1645 2025-08-22 12:22:42

Windows内核进程遍历技术详解

1. 概述

在Windows内核开发中,进程遍历是一项基础但重要的技术。本文将详细介绍四种常见的内核进程遍历方式,包括暴力枚举PID、使用PsGetNextProcess函数、手动遍历ActiveProcessLinks链表以及通过句柄表遍历进程。

2. 暴力枚举PID方式

2.1 基本原理

通过循环枚举可能的PID值,使用PsLookupProcessByProcessId函数获取每个PID对应的EPROCESS结构,然后获取进程名进行比较。

2.2 关键函数

  • PsLookupProcessByProcessId: 根据PID获取EPROCESS结构
  • PsGetProcessImageFileName: 获取进程名(未文档化)

2.3 代码实现

NTKERNELAPI PCHAR PsGetProcessImageFileName(PEPROCESS Process);

PEPROCESS GetProcessByName(PUCHAR TargetName)
{
    PEPROCESS pEprocess = NULL;
    for(ULONG i = 8; i <= 100000; i += 4)
    {
        NTSTATUS status = PsLookupProcessByProcessId(i, &pEprocess);
        if(NT_SUCCESS(status))
        {
            PCHAR processName = PsGetProcessImageFileName(pEprocess);
            if(_stricmp(processName, TargetName) == 0)
            {
                return pEprocess;
            }
            ObDereferenceObject(pEprocess);
        }
    }
    return NULL;
}

2.4 注意事项

  1. PID通常从8开始,步长为4
  2. 每次成功获取EPROCESS后必须调用ObDereferenceObject释放引用
  3. 这种方法效率较低,因为需要遍历大量可能的PID

3. 使用PsGetNextProcess函数

3.1 基本原理

PsGetNextProcess是未导出的内核函数,可以通过特征码搜索获取其地址。该函数可以遍历系统中的所有进程。

3.2 特征码定位

PVOID FuzzFunc(PUCHAR startAddress, ULONG length, PUCHAR signature, ULONG signatureLength)
{
    PUCHAR endAddress = startAddress + length - signatureLength;
    PUCHAR i = NULL;
    ULONG j = 0;
    
    if(!startAddress || !signature || signatureLength == 0 || length < signatureLength)
    {
        return NULL;
    }
    
    for(i = startAddress; i <= endAddress; i++)
    {
        for(j = 0; j < signatureLength; j++)
        {
            if(*(PUCHAR)(i + j) != signature[j])
            {
                break;
            }
        }
        if(j >= signatureLength)
        {
            return (PVOID)i;
        }
    }
    return NULL;
}

// 在DriverEntry中定位函数
PUCHAR pStartaddr = IoCreateStreamFileObjectLite;
UCHAR signature[] = {0x00, 0x00, 0x66, 0xFF, 0x8E, 0x86};
PUCHAR temAddr = FuzzFunc(pStartaddr, 0x1000, signature, 6);
PUCHAR funcAddr = temAddr - 22;

3.3 使用PsGetNextProcess遍历

typedef PEPROCESS (NTAPI *pPsGetNextProcess)(IN PEPROCESS OldProcess);

pPsGetNextProcess PsGetNextProcess = NULL;

PEPROCESS GetProcessByName2(PUCHAR TargetName)
{
    if(!PsGetNextProcess)
    {
        return NULL;
    }
    
    PEPROCESS currentProcess = PsGetCurrentProcess();
    PEPROCESS nextProcess = NULL;
    
    nextProcess = PsGetNextProcess(currentProcess);
    while(nextProcess != NULL && nextProcess != currentProcess)
    {
        PCHAR processName = PsGetProcessImageFileName(nextProcess);
        nextProcess = PsGetNextProcess(nextProcess);
        if(_stricmp(processName, TargetName) == 0)
        {
            return nextProcess;
        }
    }
    return NULL;
}

3.4 注意事项

  1. PsGetNextProcess是未导出函数,需要动态定位
  2. 遍历时从当前进程开始,直到回到起点完成遍历
  3. 特征码可能随Windows版本变化而变化

4. 手动遍历ActiveProcessLinks链表

4.1 基本原理

每个EPROCESS结构中包含一个ActiveProcessLinks成员,它是一个双向链表,连接系统中的所有进程。通过遍历这个链表可以枚举所有进程。

4.2 关键偏移

  • ActiveProcessLinks在EPROCESS结构中的偏移:0x0b8(可能随Windows版本变化)

4.3 代码实现

PEPROCESS GetProcessByName3(PUCHAR TargetName)
{
    PLIST_ENTRY CurrentEntry = NULL;
    PLIST_ENTRY StartEntry = NULL;
    
    PEPROCESS CurrentProcess = PsGetCurrentProcess();
    StartEntry = (PLIST_ENTRY)((ULONG_PTR)CurrentProcess + 0x0b8); //ActiveProcessLinks
    CurrentEntry = StartEntry->Flink;
    
    while(CurrentEntry != StartEntry)
    {
        PEPROCESS Process = (PEPROCESS)((ULONG_PTR)CurrentEntry - 0x0b8);
        PCHAR processName = PsGetProcessImageFileName(Process);
        
        if(_stricmp(processName, TargetName) == 0)
        {
            return Process;
        }
        CurrentEntry = CurrentEntry->Flink;
    }
    return NULL;
}

4.4 注意事项

  1. ActiveProcessLinks的偏移需要根据Windows版本调整
  2. 遍历时需要从链表项地址计算出EPROCESS结构地址
  3. 这是一种高效且稳定的遍历方式

5. 通过句柄表遍历进程

5.1 基本原理

Windows内核维护一个全局句柄表(PspCidTable),其中包含所有进程和线程的句柄。通过遍历这个表可以枚举所有进程。

5.2 获取PspCidTable

PULONG pHandleTable = NULL;

PULONG GetPspCidTable()
{
    if(!pHandleTable)
    {
        PULONG pPspCidTable = (ULONG_PTR)PsLookupThreadByThreadId + 0x1e + 2;
        PULONG PspCidTable = *(PULONG)pPspCidTable;
        pHandleTable = *(PULONG)PspCidTable;
    }
    return pHandleTable;
}

5.3 枚举句柄表

pEPROCESS targetProcess = NULL;

BOOLEAN NTAPI getProcessRoutine(
    _In_ struct _HANDLE_TABLE_ENTRY *HandleTableEntry,
    _In_ HANDLE Handle,
    _In_ PVOID Context)
{
    if(HandleTableEntry)
    {
        PUCHAR TargetName = (PUCHAR)Context;
        POBJECT_HEADER objHead = (HandleTableEntry->Value & 0xfffffff8) - 0x18;
        
        if(objHead->TypeIndex == 7) // 7表示进程对象
        {
            pEPROCESS pEprocess = (pEPROCESS)((ULONG_PTR)objHead + 0x18);
            if(_stricmp(pEprocess->ImageFileName, TargetName) == 0)
            {
                targetProcess = pEprocess;
            }
        }
    }
    return FALSE;
}

PEPROCESS GetProcessByName4(PUCHAR TargetName)
{
    PULONG table = GetPspCidTable();
    ExEnumHandleTable(table, getProcessRoutine, TargetName, NULL);
    return targetProcess;
}

5.4 注意事项

  1. 需要先获取全局句柄表PspCidTable的地址
  2. 进程对象的TypeIndex通常为7
  3. 通过OBJECT_HEADER可以定位到EPROCESS结构
  4. 这种方法可以绕过一些进程隐藏技术

6. 总结对比

方法 优点 缺点 适用场景
暴力枚举PID 实现简单,无需获取未导出函数 效率低,可能遗漏进程 简单场景,不追求效率
PsGetNextProcess 效率较高,代码简洁 需要定位未导出函数 需要高效遍历的场景
ActiveProcessLinks 高效稳定,不易被绕过 需要知道结构偏移 大多数场景的首选
句柄表遍历 可以绕过某些隐藏技术 实现复杂,需要更多内核知识 对抗隐藏进程的场景

7. 补充说明

  1. 在实际开发中,ActiveProcessLinks偏移和TypeIndex可能随Windows版本变化,需要动态获取或根据版本调整
  2. 遍历进程时应注意同步问题,避免在遍历过程中进程被创建或终止
  3. 所有方法获取的EPROCESS都需要适当管理引用计数
  4. 在生产环境中使用时,应添加更多的错误检查和边界条件处理

通过掌握这些进程遍历技术,可以开发出更强大的内核级工具和驱动程序,用于系统监控、安全防护等多种场景。

Windows内核进程遍历技术详解 1. 概述 在Windows内核开发中,进程遍历是一项基础但重要的技术。本文将详细介绍四种常见的内核进程遍历方式,包括暴力枚举PID、使用PsGetNextProcess函数、手动遍历ActiveProcessLinks链表以及通过句柄表遍历进程。 2. 暴力枚举PID方式 2.1 基本原理 通过循环枚举可能的PID值,使用 PsLookupProcessByProcessId 函数获取每个PID对应的EPROCESS结构,然后获取进程名进行比较。 2.2 关键函数 PsLookupProcessByProcessId : 根据PID获取EPROCESS结构 PsGetProcessImageFileName : 获取进程名(未文档化) 2.3 代码实现 2.4 注意事项 PID通常从8开始,步长为4 每次成功获取EPROCESS后必须调用 ObDereferenceObject 释放引用 这种方法效率较低,因为需要遍历大量可能的PID 3. 使用PsGetNextProcess函数 3.1 基本原理 PsGetNextProcess 是未导出的内核函数,可以通过特征码搜索获取其地址。该函数可以遍历系统中的所有进程。 3.2 特征码定位 3.3 使用PsGetNextProcess遍历 3.4 注意事项 PsGetNextProcess 是未导出函数,需要动态定位 遍历时从当前进程开始,直到回到起点完成遍历 特征码可能随Windows版本变化而变化 4. 手动遍历ActiveProcessLinks链表 4.1 基本原理 每个EPROCESS结构中包含一个 ActiveProcessLinks 成员,它是一个双向链表,连接系统中的所有进程。通过遍历这个链表可以枚举所有进程。 4.2 关键偏移 ActiveProcessLinks 在EPROCESS结构中的偏移:0x0b8(可能随Windows版本变化) 4.3 代码实现 4.4 注意事项 ActiveProcessLinks 的偏移需要根据Windows版本调整 遍历时需要从链表项地址计算出EPROCESS结构地址 这是一种高效且稳定的遍历方式 5. 通过句柄表遍历进程 5.1 基本原理 Windows内核维护一个全局句柄表(PspCidTable),其中包含所有进程和线程的句柄。通过遍历这个表可以枚举所有进程。 5.2 获取PspCidTable 5.3 枚举句柄表 5.4 注意事项 需要先获取全局句柄表PspCidTable的地址 进程对象的TypeIndex通常为7 通过OBJECT_ HEADER可以定位到EPROCESS结构 这种方法可以绕过一些进程隐藏技术 6. 总结对比 | 方法 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | 暴力枚举PID | 实现简单,无需获取未导出函数 | 效率低,可能遗漏进程 | 简单场景,不追求效率 | | PsGetNextProcess | 效率较高,代码简洁 | 需要定位未导出函数 | 需要高效遍历的场景 | | ActiveProcessLinks | 高效稳定,不易被绕过 | 需要知道结构偏移 | 大多数场景的首选 | | 句柄表遍历 | 可以绕过某些隐藏技术 | 实现复杂,需要更多内核知识 | 对抗隐藏进程的场景 | 7. 补充说明 在实际开发中,ActiveProcessLinks偏移和TypeIndex可能随Windows版本变化,需要动态获取或根据版本调整 遍历进程时应注意同步问题,避免在遍历过程中进程被创建或终止 所有方法获取的EPROCESS都需要适当管理引用计数 在生产环境中使用时,应添加更多的错误检查和边界条件处理 通过掌握这些进程遍历技术,可以开发出更强大的内核级工具和驱动程序,用于系统监控、安全防护等多种场景。