基于全局句柄表发现隐藏进程

admin 2025年2月25日13:28:13评论16 views字数 13868阅读46分13秒阅读模式

首发于奇安信攻防社区:https://forum.butian.net/share/1416

前言

我们知道在0环进行PEB断链可以达到隐藏进程的效果,但是这只是作为权限维持的一种方法,如果要想完美的隐藏进程几乎是不可能的,本文就基于全局句柄表PsdCidTable,来找到隐藏进程的效果。

句柄表

什么是句柄?

当一个进程创建或者打开一个内核对象时,将获得一个句柄,通过这个句柄可以访问内核对象。

为什么要有句柄?

句柄存在的目的是为了避免在应用层直接修改内核对象。

我们假设一个场景,如果直接返回内核地址给应用层,我们可以在应用层随意修改内核地址,当我们修改的地址没有访问权限的时候,操作系统就会蓝屏,所以为了安全起见,只给应用层一个句柄,再通过这个句柄去找到真实的内核地址,就可以有效防止蓝屏的情况出现

句柄表项每个占8字节,一个页4KB,所以一个页能存储512个句柄表项,当进程中的句柄数量超过512,句柄表就会以分级形式存储,最多三级

句柄表的结构如下:

基于全局句柄表发现隐藏进程

我们编写一个程序,得到一些句柄

// Handle_Table.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>
#include <tchar.h>

int _tmain(int argc, _TCHAR* argv[])
{
    DWORD PID;
    HANDLE hPro = NULL;
    HWND hwnd = FindWindowA(NULL, "计算器");
    GetWindowThreadProcessId(hwnd, &PID);

    for (int i = 0; i < 600; i++)
    {
        hPro = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, TRUE, PID);
        printf("句柄:%xn", hPro);
    }
    SetHandleInformation(hPro, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE);
    getchar();
    return 0;
}

首先说一下如何定位到句柄表,首先找到_EPROCESS的0x0c4偏移有一个_HANDLE_TABLE结构

基于全局句柄表发现隐藏进程

通过_HANDLE_TABLE结构的地址找到句柄表

基于全局句柄表发现隐藏进程

查看一下

基于全局句柄表发现隐藏进程

这里找最后一个对应地址

基于全局句柄表发现隐藏进程

用640/4得到190偏移,这里因为inter设置句柄表的储存是8个字节一组,所以这里需要*8

基于全局句柄表发现隐藏进程

我们得到句柄表里面的值为02000002`85dc0d8b,这里b拆分开为1011,将后3位清0可以得到85dc0d88

基于全局句柄表发现隐藏进程

这里因为每个链表之前都有一个OBJECT_HEADER结构,所以需要加上0x18才能定位到真正的链表

基于全局句柄表发现隐藏进程

通过句柄表后4字节的值即可定位到当前程序的EPROCESS

基于全局句柄表发现隐藏进程

特别留意 TableCode 的第2位,它表明了句柄表的结构,如果第2位是01,表示现在句柄表有两级, TableCode 指向的表存储了 4KB / 4 = 1024 个句柄表的地址,每个地址指向一个句柄表。

我们构造超过512个句柄,看看 TableCode 的低2位是否是01

// Handle_Table2.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>
#include <tchar.h>

int _tmain(int argc, _TCHAR* argv[])
{
    DWORD PID;
    HANDLE hPro = NULL;
    HWND hwnd = FindWindowA(NULL, "计算器");
    GetWindowThreadProcessId(hwnd, &PID);

    for (int i = 0; i < 600; i++)
    {
        hPro = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, TRUE, PID);
        printf("句柄:%xn", hPro);
    }
    SetHandleInformation(hPro, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE);
    getchar();
    return 0;
}

基于全局句柄表发现隐藏进程

全局句柄表

全局变量 PspCidTable 存储了全局句柄表 _HANDLE_TABLE 的地址

全局句柄表存储了所有 EPROCESS 和 ETHREAD 和进程的句柄表不同,全局句柄表项低32位指向的就是内核对象,而非 OBJECT_HEADER

除此之外,和进程句柄表就没什么不同了,结构也是可以分为1、2、3级。

1464/4转十六进制得到16E

基于全局句柄表发现隐藏进程

这里 0xe1000cc0 低位是0,就只有一级

基于全局句柄表发现隐藏进程

得到当前进程

基于全局句柄表发现隐藏进程

遍历PsdCidTable

这里我们了解了原理之后就可以编写程序来遍历所有的进程,首先要解决的一个问题就是该如何找到全局句柄表,在查阅资料后发现,有三个函数调用了PsdCidTable

PsLookupProcessByProcessId()
PsLookupProcessThreadByCid()
PsLookupThreadByThreadId()

这里我们直接去看一下PsLookupProcessByProcessId的反汇编,可以看到有一个push PspCidTable地址的操作,那么这里我们就直接通过定位PsLookupProcessByProcessId加偏移的方法去定位PsdCidTable,这里因为系统的原因可能结构会有所不同,所以更完美的方法就是通过特征码去定位,这里我就使用偏移的方法定位

基于全局句柄表发现隐藏进程

通过计算偏移为26

基于全局句柄表发现隐藏进程

那么就可以定位到PspCidTable结构

    PspCidTable = **(PULONG*)((ULONG)PsLookupProcessByProcessId + 26);
    DbgPrint("PspCidTable = %xn", PspCidTable);

定位到结构之后我们取出对应地址里面的值

    TableCode = *(PULONG)PspCidTable;
    DbgPrint("TableCode = %xn", 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("进程名:%sn", ImageFileName);
}
else if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ThreadString, TRUE) == 0)
{
    pEprocess = (PEPROCESS) * (PULONG)(HandleAddr + 0x220);
    ImageFileName = (PCHAR)pEprocess + 0x174;
    DbgPrint("ETHREAD: %x, 所属进程:%sn", HandleAddr, ImageFileName);
}
else
{
    DbgPrint("既不是线程也不是进程 0x%xn", 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 = %xn", PspCidTable);

    TableCode = *(PULONG)PspCidTable;
    DbgPrint("TableCode = %xn", TableCode);

    TableLevel = TableCode & 0x03; // 句柄表等级
    TableCode = TableCode & ~0x03; // 清除等级标志位
    DbgPrint("TableLevel = %xn", TableLevel);
    DbgPrint("New_TableCode = %xn", 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("进程名:%sn", ImageFileName);
                    }
                    else if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ThreadString, TRUE) == 0)
                    {
                        pEprocess = (PEPROCESS) * (PULONG)(HandleAddr + 0x220);
                        ImageFileName = (PCHAR)pEprocess + 0x174;
                        DbgPrint("ETHREAD: %x, 所属进程:%sn", HandleAddr, ImageFileName);
                    }
                    else
                    {
                        DbgPrint("既不是线程也不是进程 0x%xn", 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("进程名:%sn", ImageFileName);
                            }
                            else if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ThreadString, TRUE) == 0)
                            {
                                pEprocess = (PEPROCESS) * (PULONG)(HandleAddr + 0x220);
                                ImageFileName = (PCHAR)pEprocess + 0x174;
                                DbgPrint("ETHREAD: %x, 所属进程:%sn", HandleAddr, ImageFileName);
                            }
                            else
                            {
                                DbgPrint("既不是线程也不是进程 0x%xn", 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("进程名:%sn", ImageFileName);
                                    }
                                    else if (RtlCompareUnicodeString(&pObjectHeader->Type->Name, &ThreadString, TRUE) == 0)
                                    {
                                        pEprocess = (PEPROCESS) * (PULONG)(HandleAddr + 0x220);
                                        ImageFileName = (PCHAR)pEprocess + 0x174;
                                        DbgPrint("ETHREAD: %x, 所属进程:%sn", HandleAddr, ImageFileName);
                                    }
                                    else
                                    {
                                        DbgPrint("既不是线程也不是进程 0x%xn", HandleAddr); 
                                    }
                                }
                            }
                        }
                    }

                }
            }
            break;
        }
        
    }

    pDriver->DriverUnload = DriverUnload;
    return STATUS_SUCCESS;
}

VOID DriverUnload(PDRIVER_OBJECT pDriver)
{
    DbgPrint("DriverUnload successfully!n");
}

实现效果

首先安装驱动

基于全局句柄表发现隐藏进程

然后启动即可遍历全局句柄表

基于全局句柄表发现隐藏进程

这里我们可以看到notepad.exe这个进程

基于全局句柄表发现隐藏进程

这里为了看一下效果,使用PEB断链隐藏一下notepad进程

基于全局句柄表发现隐藏进程

启动驱动断链成功

基于全局句柄表发现隐藏进程

然后在任务管理器和cmd里面都看不到notepad.exe这个进程

基于全局句柄表发现隐藏进程

然后再启动遍历全局句柄表的驱动

基于全局句柄表发现隐藏进程

可以看到notepad.exe进程,那么这里就可以证明,系统并不是通过PEB找双向链表去定位到进程的,而是通过全局句柄表来寻找进程,也就是说我们通过PEB断链进行进程隐藏只能进行表面上的隐藏,要实现真正的隐藏就需要将某个进程从全局句柄表里面摘除,但是这里如果将进程从全局句柄表里面摘除就有可能发生不稳定的情况,这又是另外一个知识点了,这里就不拓展延伸了。

基于全局句柄表发现隐藏进程

原文始发于微信公众号(红队蓝军):基于全局句柄表发现隐藏进程

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年2月25日13:28:13
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   基于全局句柄表发现隐藏进程https://cn-sec.com/archives/851191.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息