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 注意事项
- PID通常从8开始,步长为4
- 每次成功获取EPROCESS后必须调用
ObDereferenceObject释放引用 - 这种方法效率较低,因为需要遍历大量可能的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 注意事项
PsGetNextProcess是未导出函数,需要动态定位- 遍历时从当前进程开始,直到回到起点完成遍历
- 特征码可能随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 注意事项
ActiveProcessLinks的偏移需要根据Windows版本调整- 遍历时需要从链表项地址计算出EPROCESS结构地址
- 这是一种高效且稳定的遍历方式
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 注意事项
- 需要先获取全局句柄表PspCidTable的地址
- 进程对象的TypeIndex通常为7
- 通过OBJECT_HEADER可以定位到EPROCESS结构
- 这种方法可以绕过一些进程隐藏技术
6. 总结对比
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 暴力枚举PID | 实现简单,无需获取未导出函数 | 效率低,可能遗漏进程 | 简单场景,不追求效率 |
| PsGetNextProcess | 效率较高,代码简洁 | 需要定位未导出函数 | 需要高效遍历的场景 |
| ActiveProcessLinks | 高效稳定,不易被绕过 | 需要知道结构偏移 | 大多数场景的首选 |
| 句柄表遍历 | 可以绕过某些隐藏技术 | 实现复杂,需要更多内核知识 | 对抗隐藏进程的场景 |
7. 补充说明
- 在实际开发中,ActiveProcessLinks偏移和TypeIndex可能随Windows版本变化,需要动态获取或根据版本调整
- 遍历进程时应注意同步问题,避免在遍历过程中进程被创建或终止
- 所有方法获取的EPROCESS都需要适当管理引用计数
- 在生产环境中使用时,应添加更多的错误检查和边界条件处理
通过掌握这些进程遍历技术,可以开发出更强大的内核级工具和驱动程序,用于系统监控、安全防护等多种场景。