首页
社区
课程
招聘
[原创]驱动隐藏进程(1.不蓝屏。2.多种方法。)
发表于: 2天前 653

[原创]驱动隐藏进程(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,而是直接通过驱动程序修改内存中的内核结构体体数据(如 EPROCESSKTHREADTOKEN 等),我们所有的操作都会被系统记录在内存中,而驱动进程隐藏的做旧就是操作进程的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实战!

最后于 1天前 被BOSC叛忍编辑 ,原因:
收藏
免费 1
支持
分享
最新回复 (1)
雪    币: 41
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
不好意思,由于时间原因,暂时没能把后续补全,晚点我会对内容进行补全。同时该帖会持续更新方法,以后有其他方法了也会更新到帖子里,希望大家可以收藏一下常来看看
2天前
0
游客
登录 | 注册 方可回帖
返回