问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
基于全局句柄表发现隐藏进程
我们知道在0环进行PEB断链可以达到隐藏进程的效果,但是这只是作为权限维持的一种方法,如果要想完美的隐藏进程几乎是不可能的,本文就基于全局句柄表`PsdCidTable`,来找到隐藏进程的效果。
0x00 前言 ======= 我们知道在0环进行PEB断链可以达到隐藏进程的效果,但是这只是作为权限维持的一种方法,如果要想完美的隐藏进程几乎是不可能的,本文就基于全局句柄表`PsdCidTable`,来找到隐藏进程的效果。 0x01 句柄表 ======== 什么是句柄? > 当一个进程创建或者打开一个内核对象时,将获得一个句柄,通过这个句柄可以访问内核对象。 为什么要有句柄? > 句柄存在的目的是为了避免在应用层直接修改内核对象。 我们假设一个场景,如果直接返回内核地址给应用层,我们可以在应用层随意修改内核地址,当我们修改的地址没有访问权限的时候,操作系统就会蓝屏,所以为了安全起见,只给应用层一个句柄,再通过这个句柄去找到真实的内核地址,就可以有效防止蓝屏的情况出现 句柄表项每个占8字节,一个页4KB,所以一个页能存储512个句柄表项,当进程中的句柄数量超过512,句柄表就会以分级形式存储,最多三级 句柄表的结构如下: ![image-20220316100523106.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-0acba6a15e877b3df59c4ec75e45b0b8c7423b51.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 < 100; 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](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-35e79c151dc77f7e27be91467c5f2d22fd5bb0b2.png) 通过`_HANDLE_TABLE`结构的地址找到句柄表 ![image-20220316100816814.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-d52b0873ce674893d2ec50a91302f1bb231860c4.png) 这里找最后一个对应地址 ![image-20220316100840995.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-37a11b39d91fdf4e02d88eb4272f945a67cc6923.png) 用640/4得到190偏移,这里因为inter设置句柄表的储存是8个字节一组,所以这里需要\*8 ![image-20220316100907603.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-2d44bb870ad6703868f19afe4675d765d97867d8.png) 我们得到句柄表里面的值为02000002`85dc0d8b,这里b拆分开为1011,将后3位清0可以得到85dc0d88 ![image-20220316101137251.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-acf178dd7c3551ef93593d60b6c33c99eb20789e.png) 这里因为每个链表之前都有一个`OBJECT_HEADER`结构,所以需要加上0x18才能定位到真正的链表 ![image-20220316101220991.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-9d5b1753168c9f9404dd563f4b766b3af13417c8.png) 通过句柄表后4字节的值即可定位到当前程序的`EPROCESS` ![image-20220316101234632.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-6cf9ba898166b7997bedf24fb4a4c93b4654356f.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](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-4e5526a9da7f73782ba4d3369fabdbd623143b0e.png) 0x02 全局句柄表 ========== 全局变量 `PspCidTable`存储了全局句柄表 `_HANDLE_TABLE`的地址 全局句柄表存储了所有 `EPROCESS`和 `ETHREAD` 和进程的句柄表不同,全局句柄表项低32位指向的就是内核对象,而非 `OBJECT_HEADER` 除此之外,和进程句柄表就没什么不同了,结构也是可以分为1、2、3级。 1464/4转十六进制得到16E ![image-20220316101319177.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-b5369e34c97be52b437aa72ae7efac4afae12022.png) 这里 `0xe1000cc0` 低位是0,就只有一级 ![image-20220316101303811.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-823c5d77c806db2fcad41bd862ef953865c48ae5.png) 得到当前进程 ![image-20220316101330600.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-d99f7ef7b73f2b9108e099d50b983282a7ad4e44.png) 0x03 遍历PsdCidTable ================== 这里我们了解了原理之后就可以编写程序来遍历所有的进程,首先要解决的一个问题就是该如何找到全局句柄表,在查阅资料后发现,有三个函数调用了`PsdCidTable` PsLookupProcessByProcessId() PsLookupProcessThreadByCid() PsLookupThreadByThreadId() 这里我们直接去看一下`PsLookupProcessByProcessId`的反汇编,可以看到有一个`push PspCidTable`地址的操作,那么这里我们就直接通过定位`PsLookupProcessByProcessId`加偏移的方法去定位`PsdCidTable`,这里因为系统的原因可能结构会有所不同,所以更完美的方法就是通过特征码去定位,这里我就使用偏移的方法定位 ![image-20220316101546218.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-047a79d85bd40c11c13902a43834d224ca868c6a.png) 通过计算偏移为26 ![image-20220316101720840.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-30d7359b1322f3bc40f7723bda354d025d7e9c1d.png) 那么就可以定位到`PspCidTable`结构 ```php PspCidTable = **(PULONG*)((ULONG)PsLookupProcessByProcessId + 26); DbgPrint("PspCidTable = %x\n", PspCidTable); ``` 定位到结构之后我们取出对应地址里面的值 ```php TableCode = *(PULONG)PspCidTable; DbgPrint("TableCode = %x\n", TableCode); ``` 我们首先要判断是几级句柄表,就是通过最后一位是0、1、2来判断,那么这里就可以与`0x03`相与,然后消除标志位 ```php TableLevel = TableCode & 0x03; // 句柄表等级 TableCode = TableCode & ~0x03; // 清除等级标志位 ``` 我们知道一级句柄表的范围是0-512,那么这里就可以写一个for循环来进行遍历操作 `for (i = 0; i < 512; i++)` 首先通过`MmIsAddressValid`判断一下地址是否可用,否则会发生蓝屏的风险,如果可用就继续遍历 `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循环即可 ```php 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循环进行遍历 ```php 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"); } ``` 0x04 实现效果 ========= 首先安装驱动 ![image-20220316105344582.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-7abc0d3dcd7d25b30f6fcb0546ed92f9fbaf8dd1.png) 然后启动即可遍历全局句柄表 ![image-20220316105532085.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-d252f0950ec26c3fd8b1b3ea1b55b2a3328c3e9e.png) 这里我们可以看到`notepad.exe`这个进程 ![image-20220316110021797.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-567059fa812839382e24b81dd4a75559adece8bd.png) 这里为了看一下效果,使用PEB断链隐藏一下notepad进程 ![image-20220316105813025.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-011d26368739e1d86c6ad7c7689eec77e8b9a27f.png) 启动驱动断链成功 ![image-20220316110106266.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-f8bafe0f116f08a7707818501e009e79bc38aa01.png) 然后在任务管理器和cmd里面都看不到`notepad.exe`这个进程 ![image-20220316110135787.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-200626a65fa855864f13f8051d042aaa906e014c.png) 然后再启动遍历全局句柄表的驱动 ![image-20220316110207457.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-3b5fbb72ec909e9d07aaab20937b474d158da604.png) 可以看到`notepad.exe`进程,那么这里就可以证明,系统并不是通过PEB找双向链表去定位到进程的,而是通过全局句柄表来寻找进程,也就是说我们通过PEB断链进行进程隐藏只能进行表面上的隐藏,要实现真正的隐藏就需要将某个进程从全局句柄表里面摘除,但是这里如果将进程从全局句柄表里面摘除就有可能发生不稳定的情况,这又是另外一个知识点了,这里就不拓展延伸了。 ![image-20220316110235182.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-a353069a0687327584ba82e3c984a7d92094a7d9.png)
发表于 2022-03-23 14:04:40
阅读 ( 6229 )
分类:
漏洞分析
0 推荐
收藏
0 条评论
请先
登录
后评论
szbuffer
30 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!