news 2026/7/20 13:20:46

Windows内核驱动中枚举进程的几种方式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Windows内核驱动中枚举进程的几种方式

Windows内核驱动中枚举进程的几种方式

遍历所有可能的PID

这种方式很简单,先给定一个PID最大值,从PID=0开始,使用PsLookupProcessByProcessId获取PID对应的EPROCESS结构。

示例代码:

/* 通过暴力枚举所有可能的PID,并调用PsLookupProcessByProcessId函数 来获取进程的EPROCESS 这种方法性能堪忧,PID并非连续整数;进程退出后,其PID可能会被复用,这 可能会让我们得到过时的EPROCESS */#include<ntifs.h>// 未文档化的函数EXTERN_C NTKERNELAPI UCHAR*PsGetProcessImageFileName(IN PEPROCESS pEprocess);// 未文档化的函数EXTERN_C NTKERNELAPI HANDLEPsGetProcessInheritedFromUniqueProcessId(IN PEPROCESS pEprocess);PEPROCESSLookupProcess(HANDLE Pid){PEPROCESS pEprocess=nullptr;NTSTATUS status=PsLookupProcessByProcessId(Pid,&pEprocess);if(NT_SUCCESS(status)){returnpEprocess;}returnnullptr;}voidEnumProcess(){PEPROCESS pEprocess=NULL;for(inti=0;i<1000000;i+=4){pEprocess=LookupProcess((HANDLE)i);if(pEprocess!=nullptr){DbgPrintEx(DPFLTR_IHVDRIVER_ID,DPFLTR_INFO_LEVEL,"Process Name: %s, PID: %d, PPID: %d\r\n",PsGetProcessImageFileName(pEprocess),PsGetProcessId(pEprocess),PsGetProcessInheritedFromUniqueProcessId(pEprocess));ObDereferenceObject(pEprocess);}}}voidDriverUnload(PDRIVER_OBJECT pDrvObj){DbgPrintEx(DPFLTR_IHVDRIVER_ID,DPFLTR_INFO_LEVEL,"Uninstall Driver Is Ok\n");}EXTERN_C NTSTATUSDriverEntry(PDRIVER_OBJECT pDrvObj,PUNICODE_STRING pRegPath){UNREFERENCED_PARAMETER(pRegPath);pDrvObj->DriverUnload=DriverUnload;EnumProcess();returnSTATUS_SUCCESS;}

注意,PID是+4递增的。在Windows系统中,PID应该都是4的倍数。

ZwQuerySystemInformation

对于这个函数,官方文档中说:

[ZwQuerySystemInformation 自Windows 8起不再可用。 请改用本主题中列出的备用函数。]

经过实验发现,对于枚举进程,这个函数还是可以使用的:

下面是示例代码:

#include<ntddk.h>// 定义 SYSTEM_INFORMATION_CLASS 枚举typedefenum_SYSTEM_INFORMATION_CLASS{SystemProcessInformation=5,// 进程信息SystemProcessesAndThreadsInformation=5// 与上面等价,不同文档可能命名不同[reference:1][reference:2]}SYSTEM_INFORMATION_CLASS;// 定义 SYSTEM_PROCESS_INFORMATION 结构[reference:3][reference:4]typedefstruct_SYSTEM_PROCESS_INFORMATION{ULONG NextEntryOffset;// 下一个结构体的偏移量ULONG NumberOfThreads;ULONG Reserved[6];LARGE_INTEGER CreateTime;LARGE_INTEGER UserTime;LARGE_INTEGER KernelTime;UNICODE_STRING ImageName;// 进程名称KPRIORITY BasePriority;HANDLE UniqueProcessId;// 进程IDHANDLE InheritedFromUniqueProcessId;// 父进程IDULONG HandleCount;ULONG SessionId;ULONG_PTR PageDirectoryBase;VM_COUNTERS VmCounters;SIZE_T PrivatePageCount;IO_COUNTERS IoCounters;// 后面还有线程信息数组,此处省略}SYSTEM_PROCESS_INFORMATION,*PSYSTEM_PROCESS_INFORMATION;// 声明 ZwQuerySystemInformation 函数EXTERN_C NTKERNELAPI NTSTATUSZwQuerySystemInformation(IN SYSTEM_INFORMATION_CLASS SystemInformationClass,IN OUT PVOID SystemInformation,IN ULONG SystemInformationLength,OUT PULONG ReturnLength OPTIONAL);// 枚举进程的核心函数VOIDEnumProcesses(){NTSTATUS status;ULONG bufferSize=0;PVOID buffer=NULL;PSYSTEM_PROCESS_INFORMATION pInfo=NULL;// 第一步:先调用一次,获取所需的缓冲区大小// 传入 NULL 和 0,函数会返回 STATUS_INFO_LENGTH_MISMATCH 并填充所需大小[reference:5][reference:6]status=ZwQuerySystemInformation(SystemProcessInformation,NULL,0,&bufferSize);if(status!=STATUS_INFO_LENGTH_MISMATCH){KdPrint(("ZwQuerySystemInformation 获取大小失败: 0x%X\n",status));return;}// 第二步:分配足够的内存// 内核驱动中使用 ExAllocatePoolWithTag 分配内存buffer=ExAllocatePoolWithTag(PagedPool,bufferSize,'mEnP'// 自定义标签,用于内存调试);if(!buffer){KdPrint(("内存分配失败\n"));return;}RtlZeroMemory(buffer,bufferSize);// 第三步:再次调用,获取实际的进程信息[reference:8][reference:9]status=ZwQuerySystemInformation(SystemProcessInformation,buffer,bufferSize,NULL);if(!NT_SUCCESS(status)){KdPrint(("ZwQuerySystemInformation 查询失败: 0x%X\n",status));ExFreePoolWithTag(buffer,'mEnP');return;}// 第四步:遍历链表,输出每个进程的信息[reference:10][reference:11]pInfo=(PSYSTEM_PROCESS_INFORMATION)buffer;ULONG processCount=0;while(TRUE){processCount++;// 进程ID为0的是 System Idle Process[reference:12]if(pInfo->UniqueProcessId==0){KdPrint(("[%6d] System Idle Process\n",pInfo->UniqueProcessId));}else{KdPrint(("[%6d] %wZ\n",pInfo->UniqueProcessId,&pInfo->ImageName));}// 如果 NextEntryOffset 为 0,说明已到达链表末尾[reference:13]if(pInfo->NextEntryOffset==0){break;}// 移动到下一个结构体[reference:14][reference:15]pInfo=(PSYSTEM_PROCESS_INFORMATION)((PUCHAR)pInfo+pInfo->NextEntryOffset);}KdPrint(("进程总数: %d\n",processCount));// 第五步:释放内存[reference:16]ExFreePoolWithTag(buffer,'mEnP');}// 驱动入口点EXTERN_C NTSTATUSDriverEntry(IN PDRIVER_OBJECT DriverObject,IN PUNICODE_STRING RegistryPath){UNREFERENCED_PARAMETER(DriverObject);UNREFERENCED_PARAMETER(RegistryPath);KdPrint(("=== 驱动加载,开始枚举进程 ===\n"));EnumProcesses();KdPrint(("=== 进程枚举完成 ===\n"));// 注意:此示例在 DriverEntry 中直接执行后返回// 实际驱动通常需要创建设备对象,在分发函数中调用returnSTATUS_SUCCESS;}

原理:它读取的是活动进程链表

ZwQuerySystemInformation(信息类SystemProcessInformation)在内核中的实现逻辑大致如下:

  1. 锁定数据源:它首先会获取内核全局的PsActiveProcessHead双向链表头的锁。
  2. 遍历链表:从链表头开始,沿着每个EPROCESS结构中的ActiveProcessLinks成员,逐个向后遍历。
  3. 收集并拷贝:每遍历到一个EPROCESS,就从中摘取进程ID、名称、线程数等信息,填充到SYSTEM_PROCESS_INFORMATION结构中,并拷贝到调用者提供的缓冲区。
  4. 返回结果:直到遍历完整个链表,将组装好的信息返回给调用者。

这个API本质上就是PsActiveProcessHead链表的“官方只读查看器”。它不依赖任何其他索引,链表里有什么,它就返回什么


被欺骗的原因:“断链”隐藏(DKOM)

既然它的数据完全来自链表,攻击手段就非常清晰了——直接操作内核内存中的链表指针,这种技术被称为直接内核对象操作(DKOM,Direct Kernel Object Manipulation)

恶意驱动(如Rootkit)的典型操作如下:

  1. 找到目标进程的EPROCESS结构(例如通过PsLookupProcessByProcessId)。
  2. 修改该结构中的ActiveProcessLinks指针:
    • 前一个进程Blink(向后指针)指向后一个进程
    • 后一个进程Flink(向前指针)指向前一个进程
  3. 相当于把目标进程的EPROCESS节点,从双向链表的“环”中摘除(Unlink)

结果

  • 目标进程依然在内存中正常运行,CPU调度、内存管理都正常,PID也有效。
  • PsActiveProcessHead链表里完全没有它的痕迹

此时,ZwQuerySystemInformation去遍历链表,自然就会直接跳过这个被摘除的节点,因此任务管理器、Process Explorer(如果也依赖此API)或其他安全软件都无法看到它。


补充:NtGetNextProcess呢?

Windows 10引入的NtGetNextProcess底层依然依赖PsActiveProcessHead链表遍历,因此同样会被传统的“断链”技术欺骗。它只是解决了遍历时的竞争条件(比如枚举过程中进程退出导致的崩溃),并未解决数据源本身的局限性。


总结

ZwQuerySystemInformation枚举进程,本质上是在遍历系统公开的链表索引;而进程隐藏技术做的,正是从该索引中把自己移除——所以它天然受到欺骗。想要在驱动中做对抗检测,就必须绕过链表,直接从内核更底层的句柄表(PspCidTable)或内存池中扫描EPROCESS特征。

PspCidTable句柄表

这个符号是没有被导出的,但是我们可以使用PsLookupProcessByProcessId中的特征码,来找到该符号的地址:

我们可以使用MmGetSystemRoutineAddress得到PsLookupProcessByProcessId地址,然后搜索特征码即可,不依赖特定符号,通用性强,能适配大部分Windows版本。

为什么PspCidTable能对抗DKOM隐藏?

  • 链表(PsActiveProcessHead:是为了方便枚举而维护的“索引目录”,摘掉节点不影响进程运行,适合隐藏。
  • 句柄表(PspCidTable:是内核用于通过PID查找对象的“核心哈希表”。如果从这张表里删除条目,系统将无法通过PID找到该进程,会导致OpenProcessTerminateProcess等操作失败,极易引发系统蓝屏或崩溃。

因此,恶意程序通常只敢断链,不敢删表。所以枚举PspCidTable能发现那些“从链表消失”但“仍在句柄表”中的进程。

PsActiveProcessHead

这个和使用ZwQuerySystemInformation原理是一样的,就不多说了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/20 13:20:17

苹果iMessage虚拟机群发系统的实现与原理?

随着企业与众多个人对高效沟通和大规模信息传递的需求不断增加&#xff0c;传统的高效短信、邮件等方式逐渐不够灵活和灵活。苹果公司推出的iMessage作为一款即时通讯 工具&#xff0c;凭借其端对端加密、增长的消息传递方式以及跨设备的优势&#xff0c;逐渐成为人际沟通的重要…

作者头像 李华
网站建设 2026/7/20 13:20:10

你的桌面影视管家:如何用zyfun实现跨平台视频播放一站式体验

你的桌面影视管家&#xff1a;如何用zyfun实现跨平台视频播放一站式体验 【免费下载链接】zyfun 跨平台桌面端视频资源播放器,免费高颜值. 项目地址: https://gitcode.com/gh_mirrors/zy/zyfun 你是否曾为寻找一个能同时满足本地播放、在线资源聚合、直播观看的播放器而…

作者头像 李华
网站建设 2026/7/20 13:19:22

终极跨平台DLNA投屏解决方案:Macast深度体验与实战指南

终极跨平台DLNA投屏解决方案&#xff1a;Macast深度体验与实战指南 【免费下载链接】Macast Macast is a cross-platform application which using mpv as DLNA Media Renderer. 项目地址: https://gitcode.com/gh_mirrors/ma/Macast 想要在电脑上流畅播放手机视频&…

作者头像 李华
网站建设 2026/7/20 13:18:42

笔记九:提示工程与模型压缩——从入门到实战

摘要:本文系统性地介绍了提示工程(Prompt Engineering)与模型压缩(Model Compression)两大核心主题。提示工程部分涵盖上下文学习、零样本/少样本提示、指令遵循、结构化输出、思维链等十大技术,从大白话解释到深度原理剖析,配合大量代码示例和反直觉洞察。模型压缩部分…

作者头像 李华
网站建设 2026/7/20 13:17:46

Spring Boot Redis自动配置与优化实践

1. RedisAutoConfiguration 核心机制解析Spring Boot 的 RedisAutoConfiguration 是 Spring Data Redis 的自动配置类&#xff0c;其核心作用是在检测到类路径下存在 RedisOperations 类时&#xff0c;自动配置 Redis 连接工厂和模板类。这个自动配置过程主要包含以下几个关键环…

作者头像 李华
网站建设 2026/7/20 13:17:03

告别手速焦虑:大麦智能抢票工具让你轻松锁定心仪演出门票

告别手速焦虑&#xff1a;大麦智能抢票工具让你轻松锁定心仪演出门票 【免费下载链接】ticket-purchase 大麦自动抢票&#xff0c;支持人员、城市、日期场次、价格选择 项目地址: https://gitcode.com/GitHub_Trending/ti/ticket-purchase 还记得那些令人心跳加速的抢票…

作者头像 李华