-
-
[原创]驱动隐藏进程(1.不蓝屏。2.多种方法。)
-
发表于: 2天前 653
-
最近在复盘关注的B站UP主的视频,看到有一个视频是驱动隐藏进程不蓝屏的视频,视频地址如下:【【ACZR】驱动隐藏进程,断链不蓝屏效果演示,仅演示非教学】 7b8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2T1K9h3I4A6j5X3W2D9K9g2)9J5k6h3y4G2L8g2)9J5c8Y4k6A6k6r3g2G2i4K6u0r3b7W2j5I4z5f1Z5@1L8e0q4%4y4$3N6j5i4K6u0r3i4K6y4r3M7$3S2S2M7X3g2Q4y4h3k6K6L8%4g2J5j5$3g2Q4x3@1c8U0L8%4m8&6i4K6g2X3N6$3g2T1i4K6t1$3j5h3#2H3i4K6y4n7N6X3c8Q4y4h3k6K6L8%4g2J5j5$3g2Q4x3@1b7&6x3r3u0U0y4e0W2S2y4X3q4X3y4U0V1#2x3o6y4U0k6h3b7J5k6U0l9$3z5o6j5&6k6X3g2V1x3X3j5I4j5b7`.`.
遂想自己动手亲自试试。于是搜索了一大堆方法整理和整合。
方法大概如下:
一、DKOM 实现进程隐藏
DKOM 就是直接内核对象操作技术(Direct Kernel Object Modification 即直接内核对象修改。它的核心思想是不通过任何 Windows API,而是直接通过驱动程序修改内存中的内核结构体体数据(如 EPROCESS、KTHREAD、TOKEN 等),我们所有的操作都会被系统记录在内存中,而驱动进程隐藏的做旧就是操作进程的EPROCESS结构与线程的ETHREAD结构、链表,要实现进程的隐藏我们只需要将某个进程中的信息,在系统EPROCESS链表中摘除即可实现进程隐藏。
DKOM 隐藏进程的本质是操作EPROCESS结构体,EPROCESS结构体中包含了系统中的所有进程相关信息,还有很多指向其他结构的指针,首先我们可以通过WinDBG在内核调试模式下输入dt_eprocess 即可查看到当前的EPROCESS结构体的偏移信息,结构较多。

但常用的就下面这几个,我这里给摘出来了(可以使用Ai快速摘出来):
1: kd> dt _EPROCESS nt!_EPROCESS +0x000 Pcb : _KPROCESS +0x2e8 UniqueProcessId : Ptr64 Void // 进程PID +0x2f0 ActiveProcessLinks : _LIST_ENTRY // 活动进程双向链表 +0x310 CreateTime : _LARGE_INTEGER // 进程创建时间 +0x360 Token : _EX_FAST_REF // 进程令牌(权限) +0x3f8 Peb : Ptr64 _PEB // 指向用户态PEB +0x418 ObjectTable : Ptr64 _HANDLE_TABLE // 句柄表 +0x428 WoW64Process : Ptr64 _EWOW64PROCESS // 32位进程标识 +0x450 ImageFileName : [15] UChar // 进程名(如notepad.exe) +0x488 ThreadListHead : _LIST_ENTRY // 线程链表头 +0x498 ActiveThreads : Uint4B // 活动线程数 +0x4a4 LastThreadExitStatus : Int4B // 最后线程退出状态 +0x500 Vm : _MMSUPPORT_FULL // 内存管理相关 +0x654 ExitStatus : Int4B // 进程退出状态 +0x658 VadRoot : _RTL_AVL_TREE // 虚拟地址描述符树 +0x6c0 ExitTime : _LARGE_INTEGER // 进程退出时间 +0x6f8 SignatureLevel : UChar // 签名级别 +0x6fa Protection : _PS_PROTECTION // 进程保护级别 +0x850 MitigationFlags : Uint4B // 缓解机制标志
要实现进程的隐藏,我们需要关注结构中的 ActiveProcessLinks:
ActiveProcessLinks 核心速记 +0x188 ActiveProcessLinks : _LIST_ENTRY // 活动进程双向链表 (DKOM 隐藏进程的“手术位置”) - 类型: _LIST_ENTRY (包含 Flink 和 Blink 两个指针) - 作用: Windows 内核通过该链表串联所有活跃进程,供任务管理器、Tasklist 等工具遍历显示。 - 断链原理: 令目标进程的前驱节点指向后继节点,使目标节点在系统遍历路径上“消失”。 - 风险点: 1. PatchGuard 检测: 触发频率极高,需配合其他手段绕过。 2. 进程退出: 若不还原链表直接关闭进程,内核删除节点时会导致 BSoD (蓝屏)。 3. 交叉检测: 无法躲过针对 PspCidTable 或会话链表 (SessionProcessLinks) 的深度扫描。
该指针把每个进程的EPROCESS结构体连接成了双向链表,我们可以使用 ZwQuerySystemInformation 这个函数来遍历出所有的进程信息,要实现进程的隐藏,只需要将某个进程的EPROCESS从结构体中摘除,那么通过ZwQuerySystemInformation函数就无法遍历出被摘链的进程了,从而实现了进程的隐藏。
在实现进程隐藏之前,我们需要通过代码的方式获取到当前系统中所有进程的EPROCESS信息,我们可以通过 PsLookupProcessByProcessId函数获取到指定进程的ID,然后通过 PsGetProcessImageFileName 函数取出结构名称,并通过 _stricmp 判断是否是我们想要隐藏的程序。
#include <ntifs.h>
// 声明未在头文件中导出的内核函数
NTKERNELAPI NTSTATUS PsLookupProcessByProcessId(HANDLE ProcessId, PEPROCESS *Process);
NTKERNELAPI CHAR* PsGetProcessImageFileName(PEPROCESS Process);
// --- 函数定义 ---
/**
* @brief 根据进程名称获取 EPROCESS 对象
* @param TargetName 目标进程名 (例如 "calc.exe")
* @return PEPROCESS 成功返回对象指针(需手动释放引用),失败返回 NULL
*/
PEPROCESS FindProcessObjectByName(const char* TargetName)
{
// Windows PID 总是 4 的倍数,起始通常从 4 (System) 开始
// 遍历范围可以根据需要调整,30000 是一个常见的安全上限
for (ULONG_PTR CurrentPid = 4; CurrentPid < 30000; CurrentPid += 4)
{
PEPROCESS ProcessBody = NULL;
NTSTATUS Status = PsLookupProcessByProcessId((HANDLE)CurrentPid, &ProcessBody);
if (NT_SUCCESS(Status))
{
// 获取进程映像名称
CHAR* ImageName = PsGetProcessImageFileName(ProcessBody);
// 不区分大小写比较字符串
if (_stricmp(ImageName, TargetName) == 0)
{
// 命中目标!注意:由于找到了目标,我们返回该指针
// 调用者在使用完该指针后,必须调用 ObDereferenceObject(ProcessBody)
return ProcessBody;
}
// 如果不是目标进程,必须释放引用计数,否则会导致对象无法销毁
ObDereferenceObject(ProcessBody);
}
}
return NULL;
}
/**
* @brief 驱动卸载回调
*/
VOID OnDriverUnload(PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);
DbgPrint("[MyDriver] 驱动程序已成功卸载\n");
}
/**
* @brief 驱动入口
*/
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DbgPrint("[MyDriver] 驱动加载中...\n");
const char* TargetProcess = "calc.exe";
PEPROCESS FoundProcess = FindProcessObjectByName(TargetProcess);
if (FoundProcess)
{
DbgPrint("[MyDriver] 成功找到进程: %s, 对象地址: %p\n", TargetProcess, FoundProcess);
// 演示完毕,记得平衡引用计数
ObDereferenceObject(FoundProcess);
}
else
{
DbgPrint("[MyDriver] 未找到进程: %s\n", TargetProcess);
}
// 设置卸载函数
DriverObject->DriverUnload = OnDriverUnload;
return STATUS_SUCCESS;
}然后得到句柄以后直接摘除进程的结构即可实现隐藏,这种摘除方式比较草率,如果关闭驱动后没有手工还原的话可能会导致蓝屏。
#include <ntifs.h>
// --- 宏与外部声明 ---
// 建议通过动态获取,这里仅作为逻辑演示。根据之前的内容可以得知 ActiveProcessLinks 偏移为 0x2f0
#define DEFAULT_PROCESS_LINKS_OFFSET 0x2f0
NTKERNELAPI NTSTATUS PsLookupProcessByProcessId(HANDLE ProcessId, PEPROCESS *Process);
NTKERNELAPI CHAR* PsGetProcessImageFileName(PEPROCESS Process);
// --- 核心逻辑函数 ---
/**
* @brief 安全地从双向链表中摘除指定的节点
* @param Entry 待摘除的链表节点指针
*/
VOID SafeRemoveProcessListEntry(PLIST_ENTRY Entry)
{
KIRQL OldIrql;
// 提升 IRQL 确保操作的原子性,防止在摘链时发生上下文切换
OldIrql = KeRaiseIrqlToDpcLevel();
// 检查链表完整性(简单的安全验证)
if (Entry->Flink->Blink == Entry && Entry->Blink->Flink == Entry)
{
PLIST_ENTRY Previous = Entry->Blink;
PLIST_ENTRY Next = Entry->Flink;
Previous->Flink = Next;
Next->Blink = Previous;
// 将被摘除节点的指针指向自身,防止重复删除导致的崩溃
Entry->Flink = Entry;
Entry->Blink = Entry;
}
KeLowerIrql(OldIrql);
}
/**
* @brief 根据进程名称获取 EPROCESS 对象
* @param TargetName 目标进程名 (例如 "calc.exe")
* @return PEPROCESS 成功返回对象指针(需手动释放引用),失败返回 NULL
*/
PEPROCESS FindProcessObjectByName(const char* TargetName)
{
// Windows PID 总是 4 的倍数,起始通常从 4 (System) 开始
// 遍历范围可以根据需要调整,30000 是一个常见的安全上限
for (ULONG_PTR CurrentPid = 4; CurrentPid < 30000; CurrentPid += 4)
{
PEPROCESS ProcessBody = NULL;
NTSTATUS Status = PsLookupProcessByProcessId((HANDLE)CurrentPid, &ProcessBody);
if (NT_SUCCESS(Status))
{
// 获取进程映像名称
CHAR* ImageName = PsGetProcessImageFileName(ProcessBody);
// 不区分大小写比较字符串
if (_stricmp(ImageName, TargetName) == 0)
{
// 命中目标!注意:由于找到了目标,我们返回该指针
// 调用者在使用完该指针后,必须调用 ObDereferenceObject(ProcessBody)
return ProcessBody;
}
// 如果不是目标进程,必须释放引用计数,否则会导致对象无法销毁
ObDereferenceObject(ProcessBody);
}
}
return NULL;
}
/**
* @brief 执行隐藏操作
*/
BOOLEAN PerformProcessHide(const char* ProcessName)
{
PEPROCESS TargetProcess = FindProcessObjectByName(ProcessName);
if (TargetProcess == NULL)
{
DbgPrint("[Guard] 未找到目标进程: %s\n", ProcessName);
return FALSE;
}
// 在实际开发中,ulActiveProcessLinksOffset 应该根据系统版本动态计算,这里为0x2f0
ULONG ulActiveProcessLinksOffset = DEFAULT_PROCESS_LINKS_OFFSET;
// 计算对应进程的 ActiveProcessLinks 在对应进程的 EPROCESS 中的位置
PLIST_ENTRY ProcessLinks = (PLIST_ENTRY)((PUCHAR)TargetProcess + ulActiveProcessLinksOffset);
DbgPrint("[Guard] 正在摘除进程链表: %s (Address: %p)\n", ProcessName, TargetProcess);
// 执行断链
SafeRemoveProcessListEntry(ProcessLinks);
// 摘链完成后,平衡 FindTargetProcessObject 中产生的引用计数
ObDereferenceObject(TargetProcess);
return TRUE;
}
// --- 驱动入口与卸载 ---
VOID OnDriverUnload(PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);
DbgPrint("[Guard] 隐藏驱动已卸载。\n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DbgPrint("[Guard] 进程隐藏模块加载...\n");
// 尝试隐藏计算器进程
if (PerformProcessHide("calc.exe"))
{
DbgPrint("[Guard] 进程隐藏操作执行完毕。\n");
}
DriverObject->DriverUnload = OnDriverUnload;
return STATUS_SUCCESS;
}二、PspCidTable 句柄表抹除
(1)前置知识补充
什么是句柄?当一个进程创建或者打开一个内核对象时,将获得一个句柄,通过这个句柄可以访问内核对象。为什么要有句柄?句柄存在的目的是为了避免在应用层直接修改内核对象。
我们假设一个场景,如果直接返回内核地址给应用层,我们可以在应用层随意修改内核地址,当我们修改的地址没有访问权限的时候,操作系统就会蓝屏,所以为了安全起见,只给应用层一个句柄,再通过这个句柄去找到真实的内核地址,就可以有效防止蓝屏的情况出现
句柄表项每个占8字节,一个页4KB,所以一个页能存储512个句柄表项,当进程中的句柄数量超过512,句柄表就会以分级形式存储,最多三级,句柄表的结构如下:

我们可以编写一个程序去寻找句柄,这里举个例子:
#include <windows.h>
#include <stdio.h>
/**
* @brief 核心功能函数:针对指定标题的窗口进程生成大量句柄
* @param WindowTitle 目标窗口的标题(如 "计算器")
* @param HandleCount 想要生成的句柄数量
* @return 成功生成的句柄总数
*/
int GenerateProcessHandles(const char* WindowTitle, int HandleCount)
{
DWORD TargetProcessId = 0;
HWND hTargetWindow = NULL;
// 1. 定位目标窗口
hTargetWindow = FindWindowA(NULL, WindowTitle);
if (hTargetWindow == NULL)
{
printf("[!] 错误:未找到标题为 '%s' 的窗口\n", WindowTitle);
return 0;
}
// 2. 获取对应的 PID
GetWindowThreadProcessId(hTargetWindow, &TargetProcessId);
printf("[+] 目标进程 PID: %u\n", TargetProcessId);
// 3. 定义访问权限(读写、线程创建等)
const DWORD AccessMask = PROCESS_CREATE_THREAD |
PROCESS_VM_OPERATION |
PROCESS_VM_WRITE |
PROCESS_VM_READ;
int SuccessCount = 0;
HANDLE hLastOpened = NULL;
// 4. 循环开启句柄
for (int i = 0; i < HandleCount; i++)
{
HANDLE hCurrent = OpenProcess(AccessMask, TRUE, TargetProcessId);
if (hCurrent != NULL)
{
SuccessCount++;
hLastOpened = hCurrent;
// 每获取 100 个句柄打印一次进度
if (SuccessCount % 100 == 0)
{
printf("[*] 已成功在当前句柄表中创建 %d 个条目\n", SuccessCount);
}
}
}
// 5. 对最后一个获取到的句柄设置保护,防止被 CloseHandle 关闭
if (hLastOpened != NULL)
{
SetHandleInformation(hLastOpened, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE);
printf("[+] 已对最后一个句柄 (0x%p) 开启关闭保护\n", hLastOpened);
}
return SuccessCount;
}
int main()
{
printf("=== 进程句柄压力测试工具 ===\n\n");
// 调用函数:尝试为计算器生成 600 个句柄
int Total = GenerateProcessHandles("计算器", 600);
if (Total > 0)
{
printf("\n[OK] 操作完成。共持有 %d 个目标进程句柄。\n", Total);
printf("[*] 现在你可以去 WinDbg 中查看该进程的 ObjectTable (+0x418) 了。\n");
printf("[*] 按回车键退出程序...\n");
getchar();
}
return 0;
}首先说一下如何定位到句柄表,首先找到_EPROCESS的0x0c4偏移有一个_HANDLE_TABLE结构(依旧使用gpt去找)
+0x418 ObjectTable : Ptr64 _HANDLE_TABLE
通过_HANDLE_TABLE结构的地址找到句柄表
1: kd> dt _HANDLE_TABLE nt!_HANDLE_TABLE +0x000 NextHandleNeedingPool : Uint4B +0x004 ExtraInfoPages : Int4B +0x008 TableCode : Uint8B +0x010 QuotaProcess : Ptr64 _EPROCESS +0x018 HandleTableList : _LIST_ENTRY +0x028 UniqueProcessId : Uint4B +0x02c Flags : Uint4B +0x02c StrictFIFO : Pos 0, 1 Bit +0x02c EnableHandleExceptions : Pos 1, 1 Bit +0x02c Rundown : Pos 2, 1 Bit +0x02c Duplicated : Pos 3, 1 Bit +0x02c RaiseUMExceptionOnInvalidHandleClose : Pos 4, 1 Bit +0x030 HandleContentionEvent : _EX_PUSH_LOCK +0x038 HandleTableLock : _EX_PUSH_LOCK +0x040 FreeLists : [1] _HANDLE_TABLE_FREE_LIST +0x040 ActualEntry : [32] UChar +0x060 DebugInfo : Ptr64 _HANDLE_TRACE_DEBUG_INFO

然后我们通过dd打印一下TableCode的地址就行。这里不做举例(因为没有拿相关进程做实验,看不到地址)。
之后通过之前的句柄寻找程序去找最后一个对应地址。也就是句柄最后一个的对应地址。假设句柄最后一个是640,那么用640/4得到190偏移,这里因为inter设置句柄表的储存是8个字节一组,所以这里需要*8。我们就使用dd和dq分别打印出来“TableCode的地址+190*8”的数值,我们就可以我们得到句柄表里面的值(根据qd),将最后一个字节的后四位拆分,后四位中的后三位清零,可以得到一个新的值。
在 64 位 Windows 中,_HANDLE_TABLE_ENTRY 的低 8 字节(LowValue)结构如下:
低 3 位:标志位(如
GrantedAccess相关或对象属性)。剩余高位:指向对象的指针(加密后的)。
最后三位(
011)是标志位,对于寻找对象地址来说,它们是“杂质”。我们要把这 3 位清零。
之后因为每个链表之前都有一个OBJECT_HEADER结构,所以需要加上0x030才能定位到真正的链表(这里的是Body)
1: kd> dt _OBJECT_HEADER nt!_OBJECT_HEADER +0x000 PointerCount : Int8B +0x008 HandleCount : Int8B +0x008 NextToFree : Ptr64 Void +0x010 Lock : _EX_PUSH_LOCK +0x018 TypeIndex : UChar +0x019 TraceFlags : UChar +0x019 DbgRefTrace : Pos 0, 1 Bit +0x019 DbgTracePermanent : Pos 1, 1 Bit +0x01a InfoMask : UChar +0x01b Flags : UChar +0x01b NewObject : Pos 0, 1 Bit +0x01b KernelObject : Pos 1, 1 Bit +0x01b KernelOnlyAccess : Pos 2, 1 Bit +0x01b ExclusiveObject : Pos 3, 1 Bit +0x01b PermanentObject : Pos 4, 1 Bit +0x01b DefaultSecurityQuota : Pos 5, 1 Bit +0x01b SingleHandleEntry : Pos 6, 1 Bit +0x01b DeletedInline : Pos 7, 1 Bit +0x01c Reserved : Uint4B +0x020 ObjectCreateInfo : Ptr64 _OBJECT_CREATE_INFORMATION +0x020 QuotaBlockCharged : Ptr64 Void +0x028 SecurityDescriptor : Ptr64 Void +0x030 Body : _QUAD

所以使用dt _EPROCESS <进程地址+0x030>,通过句柄表后4字节的值即可定位到当前程序的EPROCESS(注意这里用的是0x18,实际上自己的程序还是0x030)
(2)
特别留意 TableCode的第2位,它表明了句柄表的结构,如果第2位是01,表示现在句柄表有两级, TableCode指向的表存储了 4KB / 4 = 1024 个句柄表的地址,每个地址指向一个句柄表。
(
在 64 位 Windows 中,一个句柄条目(_HANDLE_TABLE_ENTRY)占用 16 字节。内核为一个句柄表页分配的大小通常是 4KB(即一个标准页)。
一级表(Level 0):一个 4KB 页面可以存放 4096 / 16 = 256$个条目。
注意: 实际上由于句柄号是从 4 开始步进的,且包含一些对齐因素,当句柄数量超过一定阈值(通常是 256 或 512 个,取决于具体系统版本对页面的利用率)时,一个页面就装不下了。
二级表(Level 1):当一级表满了,内核会创建一个“索引页”。
TableCode的低 2 位变为01,指向这个索引页。索引页里存的是指针,每个指针指向一个 4KB 的一级表页。实验目的:我们提供的代码通过申请 600 个句柄,故意撑破一级表的容量,迫使内核将句柄表升级为二级结构。此时你在 WinDbg 里看
TableCode,它的最后一位就会从0变成1。
)
我们构造超过512个句柄,看看 TableCode 的低2位是否是01
#include <windows.h>
#include <stdio.h>
/**
* @brief 压力测试函数:通过大量申请句柄迫使内核句柄表分级升级
* @param ProcessName 目标进程窗口名
* @param TargetCount 申请数量(建议 600 以上以触发二级表)
*/
void TriggerHandleTableExpansion(const char* WindowTitle, int TargetCount)
{
DWORD Pid = 0;
HWND hWindow = FindWindowA(NULL, WindowTitle);
if (!hWindow)
{
printf("[!] 未找到窗口: %s\n", WindowTitle);
return;
}
GetWindowThreadProcessId(hWindow, &Pid);
printf("[+] 目标进程 PID: %u\n", Pid);
// 存储句柄,防止被系统回收
HANDLE* HandlePool = (HANDLE*)malloc(sizeof(HANDLE) * TargetCount);
if (!HandlePool) return;
int ActualCreated = 0;
for (int i = 0; i < TargetCount; i++)
{
// 申请权限丰富的句柄
HANDLE hTmp = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, Pid);
if (hTmp != NULL)
{
HandlePool[ActualCreated] = hTmp;
ActualCreated++;
// 打印关键节点,方便在 WinDbg 中同步观察
if (ActualCreated == 255 || ActualCreated == 511)
{
printf("[*] 已创建 %d 个句柄,接近单页上限,准备观察 TableCode 变化...\n", ActualCreated);
}
}
}
printf("[+] 成功创建 %d 个句柄。\n", ActualCreated);
printf("[*] 提示:请在 WinDbg 中查看本进程的 EPROCESS -> ObjectTable -> TableCode。\n");
printf("[*] 如果低 2 位为 01 (十六进制末尾为 1, 5, 9, D...),说明已升级为二级表。\n");
printf("\n按任意键退出并释放资源...");
getchar();
// 释放句柄和内存
for (int i = 0; i < ActualCreated; i++)
{
CloseHandle(HandlePool[i]);
}
free(HandlePool);
}
int main()
{
// 执行实验
TriggerHandleTableExpansion("计算器", 600);
return 0;
}图中箭头指向的值是 0xe2c68001。
低 2 位是
01:这正是我们之前预言的变化!末尾的1代表这个句柄表目前是 二级结构。基地址:将低位清零后得到的
0xe2c68000是**一级索引页(一级目录)**的地址。这个页面里存放的不是具体的句柄条目,而是一系列指向“真正的句柄存储页”的指针。
PspCidTable概述
PspCidTable也是一个句柄表,其格式与普通的句柄表是完全一样的,但它与每个进程私有的句柄表有以下不同:
1.PspCidTable中存放的对象是系统中所有的进程线程对象,其索引就是PID和TID。
2.PspCidTable中存放的直接是对象体(EPROCESS和ETHREAD),而每个进程私有的句柄表则存放的是对象头(OBJECT_HEADER)。
3.PspCidTable是一个独立的句柄表,而每个进程私有的句柄表以一个双链连接起来。
注意访问对象时要掩掉低三位,每个进程私有的句柄表是双链连接起来的,实际上ZwQuerySystemInformation枚举系统句柄时就是走的这条双链,隐藏进程的话,这条链也是要断掉的~~在遍历进程活动链表(ActiveProcessLinks)、DKOM隐藏进程时,还要把隐藏进程的句柄表从链表中摘去。
全局变量PspCidTable存储了全局句柄表 _HANDLE_TABLE的地址
全局句柄表存储了所有 EPROCESS和 ETHREAD 和进程的句柄表不同,全局句柄表项低32位指向的就是内核对象,而非 OBJECT_HEADER
除此之外,和进程句柄表就没什么不同了,结构也是可以分为1、2、3级。
这里可以打开任务管理器,找到想要保护的进程的pid,然后除以4从而转为16进制。之后dd PspCidTable从而得到表,之后dt _HANDLE_TABLE <这个表的地址> 从而得到TableCode。
之后dq <TableCode>,得到ReadVirtual的值,然后dt _EPROCESS <ReadVirtual>,从而可以在值里面找到ImageFileName的值,并且发现这个值与我们的进程的名字是一致的。
遍历PsdCidTable,
这里我们了解了原理之后就可以编写程序来遍历所有的进程,首先要解决的一个问题就是该如何找到全局句柄表,在查阅资料后发现,有三个函数调用了PsdCidTable
PsLookupProcessByProcessId() PsLookupProcessThreadByCid() PsLookupThreadByThreadId()
这里我们直接去看一下PsLookupProcessByProcessId的反汇编,可以看到有一个push PspCidTable地址的操作,那么这里我们就直接通过定位PsLookupProcessByProcessId加偏移的方法去定位PsdCidTable,这里因为系统的原因可能结构会有所不同,所以更完美的方法就是通过特征码去定位,这里我就使用偏移的方法定位。

通过计算偏移为26(十进制的26,十六进制是1A),
那么就可以定位到PspCidTable结构
PspCidTable = **(PULONG*)((ULONG)PsLookupProcessByProcessId + 26);DbgPrint("PspCidTable = %x\n", PspCidTable);定位到结构之后我们取出对应地址里面的值
TableCode = *(PULONG)PspCidTable;
DbgPrint("TableCode = %x\n", TableCode);我们首先要判断是几级句柄表,就是通过最后一位是0、1、2来判断,那么这里就可以与0x03相与,然后消除标志位
TableLevel = TableCode & 0x03; // 句柄表等级 TableCode = TableCode & ~0x03; // 清除等级标志位
我们知道一级句柄表的范围是0-512,那么这里就可以写一个for循环来进行遍历操作
for (i = 0; i < 512; i++)
首先通过MmIsAddressValid判断一下地址是否可用,否则会发生蓝屏的风险,如果可用就继续遍历
if (MmIsAddressValid(TableLevel1[i].Object))
然后用RtlCompareUnicodeString进行判断,如果为EPROCESS结构则直接打印出进程名,如果为ETHREAD结构则打印出地址和进程名
RtlInitUnicodeString(&ProcessString, L"Process");RtlInitUnicodeString(&ThreadString, L"Thread"); HandleAddr = ((ULONG)(TableLevel1[i].Object) & ~0x03);pObjectHeader = (POBJECT_HEADER)(HandleAddr - 0x18);if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ProcessString, TRUE) == 0){
pEprocess = (PEPROCESS)HandleAddr;
ImageFileName = (PCHAR)pEprocess + 0x174;
DbgPrint("进程名:%s\n", ImageFileName);}else if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ThreadString, TRUE) == 0){
pEprocess = (PEPROCESS) * (PULONG)(HandleAddr + 0x220);
ImageFileName = (PCHAR)pEprocess + 0x174;
DbgPrint("ETHREAD: %x, 所属进程:%s\n", HandleAddr, ImageFileName);}else{
DbgPrint("既不是线程也不是进程 0x%x\n", HandleAddr); }如果是二层句柄表则再加一个for循环即可
for (i = 0; i < 1024; i++)
{
if (MmIsAddressValid((PVOID)((PULONG)TableLevel2)[i]))
{
for (j = 0; j < 512; j++)
{
if (MmIsAddressValid(TableLevel2[i][j].Object))三层句柄表的话使用三个for循环进行遍历
for (i = 0; i < 1024; i++)
{
if (MmIsAddressValid((PVOID)((PULONG)TableLevel3)[i]))
{
for (j = 0; j < 1024; j++)
{
if (MmIsAddressValid((PVOID)((PULONG*)TableLevel3)[i][j]))
{
for (k = 0; k < 512; k++)
{
if (MmIsAddressValid(TableLevel3[i][j][k].Object))完整代码如下
#include <ntifs.h>typedef struct _LDR_DATA_TABLE_ENTRY{
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
UINT16 LoadCount;
UINT16 TlsIndex;
LIST_ENTRY HashLinks;
PVOID SectionPointer;
ULONG CheckSum;
ULONG TimeDateStamp;
PVOID LoadedImports;
PVOID EntryPointActivationContext;
PVOID PatchInformation;} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;typedef struct _HANDLE_TABLE_ENTRY {
// // The pointer to the object overloaded with three ob attributes bits in // the lower order and the high bit to denote locked or unlocked entries //
union {
PVOID Object;
ULONG ObAttributes;
PHANDLE_TABLE_ENTRY_INFO InfoTable;
ULONG_PTR Value;
};
// // This field either contains the granted access mask for the handle or an // ob variation that also stores the same information. Or in the case of // a free entry the field stores the index for the next free entry in the // free list. This is like a FAT chain, and is used instead of pointers // to make table duplication easier, because the entries can just be // copied without needing to modify pointers. //
union {
union {
ACCESS_MASK GrantedAccess;
struct {
USHORT GrantedAccessIndex;
USHORT CreatorBackTraceIndex;
};
};
LONG NextFreeTableEntry;
};} HANDLE_TABLE_ENTRY, * PHANDLE_TABLE_ENTRY;typedef struct _OBJECT_TYPE {
ERESOURCE Mutex;
LIST_ENTRY TypeList;
UNICODE_STRING Name;
PVOID DefaultObject;
ULONG Index;
ULONG TotalNumberOfObjects;
ULONG TotalNumberOfHandles;
ULONG HighWaterNumberOfObjects;
ULONG HighWaterNumberOfHandles;
OBJECT_TYPE_INITIALIZER TypeInfo;#ifdef POOL_TAGGING ULONG Key;#endif //POOL_TAGGING ERESOURCE ObjectLocks[ OBJECT_LOCK_COUNT ];} OBJECT_TYPE, * POBJECT_TYPE;typedef struct _OBJECT_HEADER {
LONG PointerCount;
union {
LONG HandleCount;
PVOID NextToFree;
};
POBJECT_TYPE Type;
UCHAR NameInfoOffset;
UCHAR HandleInfoOffset;
UCHAR QuotaInfoOffset;
UCHAR Flags;
union {
POBJECT_CREATE_INFORMATION ObjectCreateInfo;
PVOID ObjectCreateInfo;
PVOID QuotaBlockCharged;
};
PSECURITY_DESCRIPTOR SecurityDescriptor;
QUAD Body;} OBJECT_HEADER, * POBJECT_HEADER;VOID DriverUnload(PDRIVER_OBJECT pDriver);NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING reg_path);ULONG PspCidTable;NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING reg_path){
typedef HANDLE_TABLE_ENTRY* L1P;
typedef volatile L1P* L2P;
typedef volatile L2P* L3P;
int i, j, k;
ULONG TableCode, TableLevel;
L1P TableLevel1;
L2P TableLevel2;
L3P TableLevel3;
UNICODE_STRING ProcessString, ThreadString;
ULONG HandleAddr;
PEPROCESS pEprocess;
PCHAR ImageFileName;
POBJECT_HEADER pObjectHeader;
PspCidTable = **(PULONG*)((ULONG)PsLookupProcessByProcessId + 26); // 找到PspCidTable的地址 DbgPrint("PspCidTable = %x\n", PspCidTable);
TableCode = *(PULONG)PspCidTable;
DbgPrint("TableCode = %x\n", TableCode);
TableLevel = TableCode & 0x03; // 句柄表等级 TableCode = TableCode & ~0x03; // 清除等级标志位 DbgPrint("TableLevel = %x\n", TableLevel);
DbgPrint("New_TableCode = %x\n", TableCode);
RtlInitUnicodeString(&ProcessString, L"Process");
RtlInitUnicodeString(&ThreadString, L"Thread");
switch (TableLevel)
{
case 0:
{
DbgPrint("\n一级句柄表\n");
TableLevel1 = (L1P)TableCode;
for (i = 0; i < 512; i++)
{
if (MmIsAddressValid(TableLevel1[i].Object))
{
HandleAddr = ((ULONG)(TableLevel1[i].Object) & ~0x03);
pObjectHeader = (POBJECT_HEADER)(HandleAddr - 0x18);
if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ProcessString, TRUE) == 0)
{
pEprocess = (PEPROCESS)HandleAddr;
ImageFileName = (PCHAR)pEprocess + 0x174;
DbgPrint("进程名:%s\n", ImageFileName);
}
else if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ThreadString, TRUE) == 0)
{
pEprocess = (PEPROCESS) * (PULONG)(HandleAddr + 0x220);
ImageFileName = (PCHAR)pEprocess + 0x174;
DbgPrint("ETHREAD: %x, 所属进程:%s\n", HandleAddr, ImageFileName);
}
else
{
DbgPrint("既不是线程也不是进程 0x%x\n", HandleAddr);
}
}
}
break;
}
case 1:
{
DbgPrint("\n二级句柄表\n");
TableLevel2 = (L2P)TableCode;
for (i = 0; i < 1024; i++)
{
if (MmIsAddressValid((PVOID)((PULONG)TableLevel2)[i]))
{
for (j = 0; j < 512; j++)
{
if (MmIsAddressValid(TableLevel2[i][j].Object))
{
HandleAddr = ((ULONG)(TableLevel2[i][j].Object) & ~0x03);
pObjectHeader = (POBJECT_HEADER)(HandleAddr - 0x18);
if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ProcessString, TRUE) == 0)
{
pEprocess = (PEPROCESS)HandleAddr;
ImageFileName = (PCHAR)pEprocess + 0x174;
DbgPrint("进程名:%s\n", ImageFileName);
}
else if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ThreadString, TRUE) == 0)
{
pEprocess = (PEPROCESS) * (PULONG)(HandleAddr + 0x220);
ImageFileName = (PCHAR)pEprocess + 0x174;
DbgPrint("ETHREAD: %x, 所属进程:%s\n", HandleAddr, ImageFileName);
}
else
{
DbgPrint("既不是线程也不是进程 0x%x\n", HandleAddr);
}
}
}
}
}
break;
}
case 2:
{
DbgPrint("\n三级句柄表\n");
TableLevel3 = (L3P)TableCode;
for (i = 0; i < 1024; i++)
{
if (MmIsAddressValid((PVOID)((PULONG)TableLevel3)[i]))
{
for (j = 0; j < 1024; j++)
{
if (MmIsAddressValid((PVOID)((PULONG*)TableLevel3)[i][j]))
{
for (k = 0; k < 512; k++)
{
if (MmIsAddressValid(TableLevel3[i][j][k].Object))
{
HandleAddr = ((ULONG)(TableLevel3[i][j][k].Object) & ~0x03);
pObjectHeader = (POBJECT_HEADER)(HandleAddr - 0x18);
if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ProcessString, TRUE) == 0)
{
pEprocess = (PEPROCESS)HandleAddr;
ImageFileName = (PCHAR)pEprocess + 0x174;
DbgPrint("进程名:%s\n", ImageFileName);
}
else if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ThreadString, TRUE) == 0)
{
pEprocess = (PEPROCESS) * (PULONG)(HandleAddr + 0x220);
ImageFileName = (PCHAR)pEprocess + 0x174;
DbgPrint("ETHREAD: %x, 所属进程:%s\n", HandleAddr, ImageFileName);
}
else
{
DbgPrint("既不是线程也不是进程 0x%x\n", HandleAddr);
}
}
}
}
}
}
}
break;
}
}
pDriver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;}VOID DriverUnload(PDRIVER_OBJECT pDriver){
DbgPrint("DriverUnload successfully!\n");}这样的代码就能发现我们用PEB断链的
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

