前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于全局句柄表发现隐藏进程

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

原创
作者头像
红队蓝军
发布2022-03-29 11:10:52
9450
发布2022-03-29 11:10:52
举报
文章被收录于专栏:红队蓝军

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

句柄表

什么是句柄?

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

为什么要有句柄?

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

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

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

句柄表的结构如下:

image-20220316100523106.png
image-20220316100523106.png

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

代码语言:c++
复制
// 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("句柄:%x\n", hPro);
	}
	SetHandleInformation(hPro, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE);
	getchar();
	return 0;
}

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

image-20220316100747765.png
image-20220316100747765.png

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

image-20220316100816814.png
image-20220316100816814.png

查看一下

image-20220316100829319.png
image-20220316100829319.png

这里找最后一个对应地址

image-20220316100840995.png
image-20220316100840995.png

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

image-20220316100907603.png
image-20220316100907603.png

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

image-20220316101137251.png
image-20220316101137251.png

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

image-20220316101220991.png
image-20220316101220991.png

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

image-20220316101234632.png
image-20220316101234632.png

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

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

代码语言:c++
复制
// 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("句柄:%x\n", hPro);
	}
	SetHandleInformation(hPro, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE);
	getchar();
	return 0;
}
image-20220316100929466.png
image-20220316100929466.png

全局句柄表

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

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

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

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

image-20220316101319177.png
image-20220316101319177.png

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

image-20220316101303811.png
image-20220316101303811.png

得到当前进程

image-20220316101330600.png
image-20220316101330600.png

遍历PsdCidTable

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

代码语言:c++
复制
PsLookupProcessByProcessId()
PsLookupProcessThreadByCid()
PsLookupThreadByThreadId()

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

image-20220316101546218.png
image-20220316101546218.png

通过计算偏移为26

image-20220316101720840.png
image-20220316101720840.png

那么就可以定位到PspCidTable结构

代码语言:c++
复制
	PspCidTable = **(PULONG*)((ULONG)PsLookupProcessByProcessId + 26);
	DbgPrint("PspCidTable = %x\n", PspCidTable);

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

代码语言:c++
复制
	TableCode = *(PULONG)PspCidTable;
	DbgPrint("TableCode = %x\n", TableCode);

我们首先要判断是几级句柄表,就是通过最后一位是0、1、2来判断,那么这里就可以与0x03相与,然后消除标志位

代码语言:c++
复制
	TableLevel = TableCode & 0x03; // 句柄表等级
	TableCode = TableCode & ~0x03; // 清除等级标志位

我们知道一级句柄表的范围是0-512,那么这里就可以写一个for循环来进行遍历操作

代码语言:c++
复制
for (i = 0; i < 512; i++)

首先通过MmIsAddressValid判断一下地址是否可用,否则会发生蓝屏的风险,如果可用就继续遍历

代码语言:c++
复制
if (MmIsAddressValid(TableLevel1[i].Object))

然后用RtlCompareUnicodeString进行判断,如果为EPROCESS结构则直接打印出进程名,如果为ETHREAD结构则打印出地址和进程名

代码语言:c++
复制
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循环即可

代码语言:c++
复制
			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循环进行遍历

代码语言:c++
复制
			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))

完整代码如下

代码语言:c++
复制
#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");
}

实现效果

首先安装驱动

image-20220316105344582.png
image-20220316105344582.png

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

image-20220316105532085.png
image-20220316105532085.png

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

image-20220316110021797.png
image-20220316110021797.png

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

image-20220316105813025.png
image-20220316105813025.png

启动驱动断链成功

image-20220316110106266.png
image-20220316110106266.png

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

image-20220316110135787.png
image-20220316110135787.png

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

image-20220316110207457.png
image-20220316110207457.png

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

image-20220316110235182.png
image-20220316110235182.png

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 句柄表
  • 全局句柄表
  • 遍历PsdCidTable
  • 实现效果
相关产品与服务
网站渗透测试
网站渗透测试(Website Penetration Test,WPT)是完全模拟黑客可能使用的攻击技术和漏洞发现技术,对目标系统的安全做深入的探测,发现系统最脆弱的环节。渗透测试和黑客入侵最大区别在于渗透测试是经过客户授权,采用可控制、非破坏性质的方法和手段发现目标和网络设备中存在弱点,帮助管理者知道自己网络所面临的问题,同时提供安全加固意见帮助客户提升系统的安全性。腾讯云网站渗透测试由腾讯安全实验室安全专家进行,我们提供黑盒、白盒、灰盒多种测试方案,更全面更深入的发现客户的潜在风险。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档