我们知道在0环进行PEB断链可以达到隐藏进程的效果,但是这只是作为权限维持的一种方法,如果要想完美的隐藏进程几乎是不可能的,本文就基于全局句柄表PsdCidTable
,来找到隐藏进程的效果。
句柄表 什么是句柄?
当一个进程创建或者打开一个内核对象时,将获得一个句柄,通过这个句柄可以访问内核对象。
为什么要有句柄?
句柄存在的目的是为了避免在应用层直接修改内核对象。
我们假设一个场景,如果直接返回内核地址给应用层,我们可以在应用层随意修改内核地址,当我们修改的地址没有访问权限的时候,操作系统就会蓝屏,所以为了安全起见,只给应用层一个句柄,再通过这个句柄去找到真实的内核地址,就可以有效防止蓝屏的情况出现
句柄表项每个占8字节,一个页4KB,所以一个页能存储512个句柄表项,当进程中的句柄数量超过512,句柄表就会以分级形式存储,最多三级
句柄表的结构如下:
我们编写一个程序,得到一些句柄
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #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
结构
通过_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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #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 ; }
全局句柄表 全局变量 PspCidTable
存储了全局句柄表 _HANDLE_TABLE
的地址
全局句柄表存储了所有 EPROCESS
和 ETHREAD
和进程的句柄表不同,全局句柄表项低32位指向的就是内核对象,而非 OBJECT_HEADER
除此之外,和进程句柄表就没什么不同了,结构也是可以分为1、2、3级。
1464/4转十六进制得到16E
这里 0xe1000cc0
低位是0,就只有一级
得到当前进程
遍历PsdCidTable 这里我们了解了原理之后就可以编写程序来遍历所有的进程,首先要解决的一个问题就是该如何找到全局句柄表,在查阅资料后发现,有三个函数调用了PsdCidTable
1 2 3 PsLookupProcessByProcessId ()PsLookupProcessThreadByCid ()PsLookupThreadByThreadId ()
这里我们直接去看一下PsLookupProcessByProcessId
的反汇编,可以看到有一个push PspCidTable
地址的操作,那么这里我们就直接通过定位PsLookupProcessByProcessId
加偏移的方法去定位PsdCidTable
,这里因为系统的原因可能结构会有所不同,所以更完美的方法就是通过特征码去定位,这里我就使用偏移的方法定位
通过计算偏移为26
那么就可以定位到PspCidTable
结构
1 2 PspCidTable = **(PULONG*)((ULONG)PsLookupProcessByProcessId + 26 ); DbgPrint ("PspCidTable = %x\n" , PspCidTable);
定位到结构之后我们取出对应地址里面的值
1 2 TableCode = *(PULONG)PspCidTable; DbgPrint ("TableCode = %x\n" , TableCode);
我们首先要判断是几级句柄表,就是通过最后一位是0、1、2来判断,那么这里就可以与0x03
相与,然后消除标志位
1 2 TableLevel = TableCode & 0x03 ; TableCode = TableCode & ~0x03 ;
我们知道一级句柄表的范围是0-512,那么这里就可以写一个for循环来进行遍历操作
1 for (i = 0 ; i < 512 ; i++)
首先通过MmIsAddressValid
判断一下地址是否可用,否则会发生蓝屏的风险,如果可用就继续遍历
1 if (MmIsAddressValid (TableLevel1[i].Object))
然后用RtlCompareUnicodeString
进行判断,如果为EPROCESS
结构则直接打印出进程名,如果为ETHREAD
结构则打印出地址和进程名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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循环即可
1 2 3 4 5 6 7 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循环进行遍历
1 2 3 4 5 6 7 8 9 10 11 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))
完整代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 #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 { union { PVOID Object; ULONG ObAttributes; PHANDLE_TABLE_ENTRY_INFO InfoTable; ULONG_PTR Value; }; 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 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 ); 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" ); }
实现效果 首先安装驱动
然后启动即可遍历全局句柄表
这里我们可以看到notepad.exe
这个进程
这里为了看一下效果,使用PEB断链隐藏一下notepad进程
启动驱动断链成功
然后在任务管理器和cmd里面都看不到notepad.exe
这个进程
然后再启动遍历全局句柄表的驱动
可以看到notepad.exe
进程,那么这里就可以证明,系统并不是通过PEB找双向链表去定位到进程的,而是通过全局句柄表来寻找进程,也就是说我们通过PEB断链进行进程隐藏只能进行表面上的隐藏,要实现真正的隐藏就需要将某个进程从全局句柄表里面摘除,但是这里如果将进程从全局句柄表里面摘除就有可能发生不稳定的情况,这又是另外一个知识点了,这里就不拓展延伸了。