Windows内核驱动中枚举进程的几种方式
Windows内核驱动中枚举进程的几种方式遍历所有可能的PID这种方式很简单先给定一个PID最大值从PID0开始使用PsLookupProcessByProcessId获取PID对应的EPROCESS结构。示例代码/* 通过暴力枚举所有可能的PID,并调用PsLookupProcessByProcessId函数 来获取进程的EPROCESS 这种方法性能堪忧,PID并非连续整数;进程退出后,其PID可能会被复用,这 可能会让我们得到过时的EPROCESS */#includentifs.h// 未文档化的函数EXTERN_C NTKERNELAPI UCHAR*PsGetProcessImageFileName(IN PEPROCESS pEprocess);// 未文档化的函数EXTERN_C NTKERNELAPI HANDLEPsGetProcessInheritedFromUniqueProcessId(IN PEPROCESS pEprocess);PEPROCESSLookupProcess(HANDLE Pid){PEPROCESS pEprocessnullptr;NTSTATUS statusPsLookupProcessByProcessId(Pid,pEprocess);if(NT_SUCCESS(status)){returnpEprocess;}returnnullptr;}voidEnumProcess(){PEPROCESS pEprocessNULL;for(inti0;i1000000;i4){pEprocessLookupProcess((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-DriverUnloadDriverUnload;EnumProcess();returnSTATUS_SUCCESS;}注意PID是4递增的。在Windows系统中PID应该都是4的倍数。ZwQuerySystemInformation对于这个函数官方文档中说[ZwQuerySystemInformation 自Windows 8起不再可用。 请改用本主题中列出的备用函数。]经过实验发现对于枚举进程这个函数还是可以使用的下面是示例代码#includentddk.h// 定义 SYSTEM_INFORMATION_CLASS 枚举typedefenum_SYSTEM_INFORMATION_CLASS{SystemProcessInformation5,// 进程信息SystemProcessesAndThreadsInformation5// 与上面等价不同文档可能命名不同[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 bufferSize0;PVOID bufferNULL;PSYSTEM_PROCESS_INFORMATION pInfoNULL;// 第一步先调用一次获取所需的缓冲区大小// 传入 NULL 和 0函数会返回 STATUS_INFO_LENGTH_MISMATCH 并填充所需大小[reference:5][reference:6]statusZwQuerySystemInformation(SystemProcessInformation,NULL,0,bufferSize);if(status!STATUS_INFO_LENGTH_MISMATCH){KdPrint((ZwQuerySystemInformation 获取大小失败: 0x%X\n,status));return;}// 第二步分配足够的内存// 内核驱动中使用 ExAllocatePoolWithTag 分配内存bufferExAllocatePoolWithTag(PagedPool,bufferSize,mEnP// 自定义标签用于内存调试);if(!buffer){KdPrint((内存分配失败\n));return;}RtlZeroMemory(buffer,bufferSize);// 第三步再次调用获取实际的进程信息[reference:8][reference:9]statusZwQuerySystemInformation(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 processCount0;while(TRUE){processCount;// 进程ID为0的是 System Idle Process[reference:12]if(pInfo-UniqueProcessId0){KdPrint(([%6d] System Idle Process\n,pInfo-UniqueProcessId));}else{KdPrint(([%6d] %wZ\n,pInfo-UniqueProcessId,pInfo-ImageName));}// 如果 NextEntryOffset 为 0说明已到达链表末尾[reference:13]if(pInfo-NextEntryOffset0){break;}// 移动到下一个结构体[reference:14][reference:15]pInfo(PSYSTEM_PROCESS_INFORMATION)((PUCHAR)pInfopInfo-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在内核中的实现逻辑大致如下锁定数据源它首先会获取内核全局的PsActiveProcessHead双向链表头的锁。遍历链表从链表头开始沿着每个EPROCESS结构中的ActiveProcessLinks成员逐个向后遍历。收集并拷贝每遍历到一个EPROCESS就从中摘取进程ID、名称、线程数等信息填充到SYSTEM_PROCESS_INFORMATION结构中并拷贝到调用者提供的缓冲区。返回结果直到遍历完整个链表将组装好的信息返回给调用者。这个API本质上就是PsActiveProcessHead链表的“官方只读查看器”。它不依赖任何其他索引链表里有什么它就返回什么。被欺骗的原因“断链”隐藏DKOM既然它的数据完全来自链表攻击手段就非常清晰了——直接操作内核内存中的链表指针这种技术被称为直接内核对象操作DKOMDirect Kernel Object Manipulation。恶意驱动如Rootkit的典型操作如下找到目标进程的EPROCESS结构例如通过PsLookupProcessByProcessId。修改该结构中的ActiveProcessLinks指针将前一个进程的Blink向后指针指向后一个进程。将后一个进程的Flink向前指针指向前一个进程。相当于把目标进程的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找到该进程会导致OpenProcess、TerminateProcess等操作失败极易引发系统蓝屏或崩溃。因此恶意程序通常只敢断链不敢删表。所以枚举PspCidTable能发现那些“从链表消失”但“仍在句柄表”中的进程。PsActiveProcessHead这个和使用ZwQuerySystemInformation原理是一样的就不多说了。