https://www.vergiliusproject.com/
在 Windows 操作系统中,句柄(Handle) 是一种 用于标识和管理系统对象的抽象引用。它本质上是一个 索引值或小整数,由内核分配给用户态程序,用户通过它间接访问和操作内核对象。
在内核中,句柄是通过句柄表(Handle Table)管理的:
- 用户得到的是一个 整数句柄值(HANDLE)。
- 该值是句柄表的一个索引,通过
TableCode
分层结构定位到 HANDLE_TABLE_ENTRY
。
- 每个表项指向一个真正的 内核对象(如 EPROCESS、ETHREAD 等)。
通常我们研究的 Windows 的句柄表包括私有句柄表和全局句柄表:
另外 Windows 系统中存在一些特殊句柄(pseudo handles),它们不是从句柄表中分配来的,但可以在大多数 API 中当作常规句柄使用。最常见的包括:
句柄值 |
含义 |
等价函数调用 |
-1 或 ((HANDLE)-1) |
当前进程句柄(Current Process) |
GetCurrentProcess() |
-2 或 ((HANDLE)-2) |
当前线程句柄(Current Thread) |
GetCurrentThread() |
-3 或 ((HANDLE)-3) |
当前进程的调试对象(Current Debug Object) |
NtQueryInformationProcess 可用 |
-4 或 ((HANDLE)-4) |
当前进程的目录句柄(Current Directory Handle) |
内部用(部分 API 支持) |
句柄表相关结构
EXHANDLE
句柄 HANDLE
的实际结构是 EXHANDLE
,该结构定义如下:
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
|
typedef struct _EXHANDLE {
union {
struct {
ULONG TagBits : 2;
ULONG Index : 30; };
HANDLE GenericHandleOverlay;
#define HANDLE_VALUE_INC 4
ULONG_PTR Value; };
} EXHANDLE, *PEXHANDLE;
|
可以看到,实际上句柄的低 2 位是保留位(通常置 0),真正有效的是高 30 位,是句柄对应在句柄表中的索引。
我们常说的进程 ID(pid
) 实际上也是句柄,由于句柄低 2 位是保留位,因此我们看到的 pid
都是 4 的倍数。
HANDLE_TABLE
Windows 的句柄表的是一个 HANDLE_TABLE
类型的结构体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| struct _HANDLE_TABLE { ULONG TableCode; struct _EPROCESS* QuotaProcess; VOID* UniqueProcessId; struct _EX_PUSH_LOCK HandleLock; struct _LIST_ENTRY HandleTableList; struct _EX_PUSH_LOCK HandleContentionEvent; struct _HANDLE_TRACE_DEBUG_INFO* DebugInfo; LONG ExtraInfoPages; union { ULONG Flags; UCHAR StrictFIFO:1; }; ULONG FirstFreeHandle; struct _HANDLE_TABLE_ENTRY* LastFreeHandleEntry; ULONG HandleCount; ULONG NextHandleNeedingPool; ULONG HandleCountHighWatermark; };
|
由于需要支持句柄的高效管理,Windows 内核采用了分层句柄表机制(Layered Handle Table),类似于多级页表。其核心结构是 _HANDLE_TABLE
,其字段 TableCode
决定了整个表的层级结构和入口地址。

TableCode
是一个编码指针(Encoded Pointer):
- 低 2 位(bit0 和 bit1):表示句柄表的层级数,取值范围为 0~2。
- 清除低 2 位后 :是句柄表根节点的实际地址(即指向第 1 层的目录页)。
句柄表的每一层结构单位是一个内存页(通常为 4KB),其具体内容取决于该层的作用:
- 如果该层用于目录索引(即存放指针),那么这个内存页中会包含 1024 个指针项(每个指针大小为 4 字节 → 4 × 1024 = 4096 字节),64位系统则是 512 个指针项。
- 如果是最底层的句柄项页,则该页用于存放实际的
HANDLE_TABLE_ENTRY
数组。每个句柄项大小为 8 字节(32 位系统),一页可容纳 512 个句柄项(512 × 8 = 4096 字节),64位系统则是 256 个指针项。
2 层结构对于 32 位系统可支持 1024 × 1024 × 512 = 超过 5 亿个句柄项;对于 64 位系统可支持 512 × 512 × 256 = 超过 6 千万个句柄项。
这里提到的内存页指的是句柄表的每一层结构单位的大小是 4KB,相当于内存页的大小,但实际上并不是真正对应一个内存页,有些情况下句柄表的结构单位地址并不关于 0x1000 对齐。
HANDLE_TABLE_ENTRY
句柄项是一个 HANDLE_TABLE_ENTRY
类型的结构体,该类型定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| struct _HANDLE_TABLE_ENTRY { union { VOID* Object; ULONG ObAttributes; struct _HANDLE_TABLE_ENTRY_INFO* InfoTable; ULONG Value; }; union { ULONG GrantedAccess; struct { USHORT GrantedAccessIndex; USHORT CreatorBackTraceIndex; }; ULONG NextFreeTableEntry; }; };
|
对于 Windows 10 开始的 64 位系统,在获取到 HANDLE_TABLE_ENTRY
的前 16 位是 RefCnt
是句柄的引用计数值。
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
| union _HANDLE_TABLE_ENTRY { volatile LONGLONG VolatileLowValue; LONGLONG LowValue; struct { struct _HANDLE_TABLE_ENTRY_INFO* volatile InfoTable; LONGLONG HighValue; union _HANDLE_TABLE_ENTRY* NextFreeHandleEntry; struct _EXHANDLE LeafHandleValue; }; LONGLONG RefCountField; ULONGLONG Unlocked:1; ULONGLONG RefCnt:16; ULONGLONG Attributes:3; struct { ULONGLONG ObjectPointerBits:44; ULONG GrantedAccessBits:25; ULONG NoRightsUpgrade:1; ULONG Spare1:6; }; ULONG Spare2; };
|
因此我们需要通过 ExGetHandlePointer
函数将 HANDLE_TABLE_ENTRY
的前 8 字节带符号右移 16 位得到对象的地址。
1 2 3 4 5 6 7
| PVOID ExGetHandlePointer(PLONGLONG EncodedHandle) { return (PVOID)((*EncodedHandle >> 16) & ~0xFuLL); }
PHANDLE_TABLE_ENTRY CidEntry = ExMapHandleToPointer(PspCidTable, ProcessId);
PEOROCESS lProcess = ExGetHandlePointer(&CidEntry->LowValue)
|
句柄表相关代码分析
句柄查找过程
ExMapHandleToPointer
ExMapHandleToPointer
函数可以从指定句柄表中根据句柄查找到对应的句柄表项。
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
|
NTKERNELAPI PHANDLE_TABLE_ENTRY ExMapHandleToPointer ( __in PHANDLE_TABLE HandleTable, __in HANDLE Handle )
{ EXHANDLE LocalHandle; PHANDLE_TABLE_ENTRY HandleTableEntry;
PAGED_CODE();
LocalHandle.GenericHandleOverlay = Handle;
if ((LocalHandle.Index & (LOWLEVEL_COUNT - 1)) == 0) { return NULL; }
HandleTableEntry = ExpLookupHandleTableEntry(HandleTable, LocalHandle);
if ((HandleTableEntry == NULL) || !ExpLockHandleTableEntry(HandleTable, HandleTableEntry)) {
if (HandleTable->DebugInfo != NULL) { ExpUpdateDebugInfo( HandleTable, PsGetCurrentThread(), Handle, HANDLE_TRACE_DB_BADREF ); }
return NULL; }
return HandleTableEntry; }
|
ExMapHandleToPointer
主要是对用户传入的句柄做了一些类型转换和检查,另外还有一些加锁的操作。
ExMapHandleToPointer
首先会对句柄索引进行检查:
1 2 3 4 5 6 7
| #define PAGE_SIZE 0x1000 #define TABLE_PAGE_SIZE PAGE_SIZE #define LOWLEVEL_COUNT (TABLE_PAGE_SIZE / sizeof(HANDLE_TABLE_ENTRY))
if ((LocalHandle.Index & (LOWLEVEL_COUNT - 1)) == 0) { return NULL; }
|
这个判断要求句柄的索引值不能是「一个内存页中 HANDLE_TABLE_ENTRY
的数量」的倍数,也就是说每个存放 HANDLE_TABLE_ENTRY
的内存页中的第一个 HANDLE_TABLE_ENTRY
是被视为保留或特殊用途,系统拒绝使用这些特定位置的句柄项。
基于这个特性,我们可以通过修改 EPROCESS
的 UniqueProcessId
字段为 LOWLEVEL_COUNT<<2
的整数倍,使得系统无法通过 ExMapHandleToPointer
查询到我们的进程。
之后会调用 ExpLookupHandleTableEntry
函数进行真正的句柄表查询操作。这个函数会根据我们传入的句柄表和句柄表索引找到对应的句柄表项。
1 2 3 4 5
|
HandleTableEntry = ExpLookupHandleTableEntry(HandleTable, LocalHandle);
|
在获取到句柄表项的后需要通过 ExpLockHandleTableEntry
函数给句柄表项加锁。
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
|
BOOLEAN FORCEINLINE ExpLockHandleTableEntry ( PHANDLE_TABLE HandleTable, PHANDLE_TABLE_ENTRY HandleTableEntry ) { LONG_PTR NewValue; LONG_PTR CurrentValue;
ASSERT((KeGetCurrentThread()->CombinedApcDisable != 0) || (KeGetCurrentIrql() == APC_LEVEL));
while (TRUE) { CurrentValue = ReadForWriteAccess((volatile LONG_PTR *)&HandleTableEntry->Object);
if (CurrentValue & EXHANDLE_TABLE_ENTRY_LOCK_BIT) { NewValue = CurrentValue - EXHANDLE_TABLE_ENTRY_LOCK_BIT;
if ((LONG_PTR)(InterlockedCompareExchangePointer( &HandleTableEntry->Object, (PVOID)NewValue, (PVOID)CurrentValue)) == CurrentValue) { return TRUE; } } else { if (CurrentValue == 0) { return FALSE; } }
ExpBlockOnLockedHandleEntry(HandleTable, HandleTableEntry); } }
|
这个函数的具体操作是给 HandleTableEntry->Object
的最低位置 0。
1 2
| #define EXHANDLE_TABLE_ENTRY_LOCK_BIT 1
|
因此对于私有句柄表如果我们清空了句柄表表项的 Object
的 EXHANDLE_TABLE_ENTRY_LOCK_BIT
位那么就表示这个句柄被加锁了,这就意味着如果我们退出进程或者关闭这个句柄都要等待这个句柄的锁,也就会把进程卡死。
ExpLookupHandleTableEntry
实际进行句柄查找的核心函数是 ExpLookupHandleTableEntry
。
ExpLookupHandleTableEntry
会将句柄值的高 30 位作为下标在句柄表中查找对应的句柄表项,查找过程类似页表的地址转换,只不过我们要根据句柄表指针的低 2 位确定句柄表层数。
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
|
PHANDLE_TABLE_ENTRY ExpLookupHandleTableEntry ( IN PHANDLE_TABLE HandleTable, IN EXHANDLE tHandle )
{ ULONG_PTR i, j, k; ULONG_PTR CapturedTable; ULONG TableLevel; PHANDLE_TABLE_ENTRY Entry = NULL; EXHANDLE Handle;
PUCHAR TableLevel1; PUCHAR TableLevel2; PUCHAR TableLevel3;
ULONG_PTR MaxHandle;
PAGED_CODE();
Handle = tHandle; Handle.TagBits = 0;
MaxHandle = *(volatile ULONG *) &HandleTable->NextHandleNeedingPool;
if (Handle.Value >= MaxHandle) { return NULL; }
CapturedTable = *(volatile ULONG_PTR *) &HandleTable->TableCode;
TableLevel = (ULONG)(CapturedTable & LEVEL_CODE_MASK);
CapturedTable = CapturedTable - TableLevel;
switch (TableLevel) {
case 0:
TableLevel1 = (PUCHAR) CapturedTable;
Entry = (PHANDLE_TABLE_ENTRY) &TableLevel1[ Handle.Value * (sizeof(HANDLE_TABLE_ENTRY) / HANDLE_VALUE_INC) ];
break;
case 1:
TableLevel2 = (PUCHAR) CapturedTable;
i = Handle.Value % (LOWLEVEL_COUNT * HANDLE_VALUE_INC);
Handle.Value -= i;
j = Handle.Value / ((LOWLEVEL_COUNT * HANDLE_VALUE_INC) / sizeof(PHANDLE_TABLE_ENTRY));
TableLevel1 = (PUCHAR) *(PHANDLE_TABLE_ENTRY *) &TableLevel2[j];
Entry = (PHANDLE_TABLE_ENTRY) &TableLevel1[ i * (sizeof(HANDLE_TABLE_ENTRY) / HANDLE_VALUE_INC) ];
break;
case 2:
TableLevel3 = (PUCHAR) CapturedTable;
i = Handle.Value % (LOWLEVEL_COUNT * HANDLE_VALUE_INC);
Handle.Value -= i;
k = Handle.Value / ((LOWLEVEL_COUNT * HANDLE_VALUE_INC) / sizeof(PHANDLE_TABLE_ENTRY));
j = k % (MIDLEVEL_COUNT * sizeof(PHANDLE_TABLE_ENTRY));
k -= j; k /= MIDLEVEL_COUNT;
TableLevel2 = (PUCHAR) *(PHANDLE_TABLE_ENTRY *) &TableLevel3[k]; TableLevel1 = (PUCHAR) *(PHANDLE_TABLE_ENTRY *) &TableLevel2[j];
Entry = (PHANDLE_TABLE_ENTRY) &TableLevel1[ i * (sizeof(HANDLE_TABLE_ENTRY) / HANDLE_VALUE_INC) ];
break;
default: _assume(0); }
return Entry; }
|
句柄表遍历
Windows 导出了一个 ExEnumHandleTable
函数用于遍历句柄表。这个函数本质上就是枚举句柄值,然后调用 ExpLookupHandleTableEntry
在句柄表中查找对应的句柄表项,然后调用回调函数,直到 ExpLookupHandleTableEntry
返回为 NULL。
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
| NTKERNELAPI BOOLEAN ExEnumHandleTable ( __in PHANDLE_TABLE HandleTable, __in EX_ENUMERATE_HANDLE_ROUTINE EnumHandleProcedure, __in PVOID EnumParameter, __out_opt PHANDLE Handle )
{ PKTHREAD CurrentThread; BOOLEAN ResultValue; EXHANDLE LocalHandle; PHANDLE_TABLE_ENTRY HandleTableEntry;
PAGED_CODE();
CurrentThread = KeGetCurrentThread();
ResultValue = FALSE;
KeEnterCriticalRegionThread(CurrentThread);
for (LocalHandle.Value = 0; (HandleTableEntry = ExpLookupHandleTableEntry(HandleTable, LocalHandle)) != NULL; LocalHandle.Value += HANDLE_VALUE_INC) {
if (ExpIsValidObjectEntry(HandleTableEntry)) {
if (ExpLockHandleTableEntry(HandleTable, HandleTableEntry)) {
ResultValue = (*EnumHandleProcedure)(HandleTableEntry, LocalHandle.GenericHandleOverlay, EnumParameter);
ExUnlockHandleTableEntry(HandleTable, HandleTableEntry);
if (ResultValue) { if (ARGUMENT_PRESENT(Handle)) { *Handle = LocalHandle.GenericHandleOverlay; } break; } } } }
KeLeaveCriticalRegionThread(CurrentThread);
return ResultValue; }
|
其中 ExpIsValidObjectEntry
宏用于检查句柄条目是否有效。
1 2 3 4
| #define EX_ADDITIONAL_INFO_SIGNATURE (-2)
#define ExpIsValidObjectEntry(Entry) \ ( (Entry != NULL) && (Entry->Object != NULL) && (Entry->NextFreeTableEntry != EX_ADDITIONAL_INFO_SIGNATURE) )
|
它检查以下三个条件,当这三个条件都成立时,句柄条目才被认为是有效的。
Entry != NULL
:检查句柄条目是否为空。
Entry->Object != NULL
:检查句柄条目是否有有效的对象。
Entry->NextFreeTableEntry != EX_ADDITIONAL_INFO_SIGNATURE
:检查句柄条目的 NextFreeTableEntry
是否不等于 EX_ADDITIONAL_INFO_SIGNATURE(-2)
,这是一个特殊的标识符,用于表示已释放的句柄条目。
如果句柄有效则先锁定句柄表项,然后调用用户传入的回调函数 EnumHandleProcedure
,解锁句柄表项,最后根据回调函数的返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| if (ExpLockHandleTableEntry(HandleTable, HandleTableEntry)) {
ResultValue = (*EnumHandleProcedure)(HandleTableEntry, LocalHandle.GenericHandleOverlay, EnumParameter);
ExUnlockHandleTableEntry(HandleTable, HandleTableEntry);
if (ResultValue) { if (ARGUMENT_PRESENT(Handle)) { *Handle = LocalHandle.GenericHandleOverlay; } break; } }
|
其中用户传入的回调函数的类型 EX_ENUMERATE_HANDLE_ROUTINE
定义如下:
1 2 3 4 5
| typedef BOOLEAN (*EX_ENUMERATE_HANDLE_ROUTINE)( IN PHANDLE_TABLE_ENTRY HandleTableEntry, IN HANDLE Handle, IN PVOID EnumParameter );
|
回调函数需要返回一个布尔值。如果返回 TRUE
,则表示枚举停止;如果返回 FALSE
,则继续枚举下一个句柄。
全局句柄表
全局句柄表 PspCidTable
是一个 HANDLE_TABLE
类型的全局指针,指向一个句柄表管理结构。这个表中存储着所有进程和线程对象。
1
| PHANDLE_TABLE PspCidTable;
|
句柄表项添加
Windows 在创建进程(PspAllocateProcess
)和线程(PspAllocateThread
)的时候都会把进程对象和线程对象加入到这个全局句柄表中,并得到进程 ID 和线程 ID。
以 PspAllocateProcess
为例:
1 2 3 4 5 6 7 8 9 10
|
CidEntry.Object = Process; CidEntry.GrantedAccess = 0;
Process->UniqueProcessId = ExCreateHandle(PspCidTable, &CidEntry);
|
- 全局句柄表的表项的
Object
指向的是进程或线程对象,不指向前面的 OBJECT_HEADER
。
- 全局句柄表的表项的的
GrantedAccess
为 0,即全局句柄表的表项没有句柄属性。
ExCreateHandle
会根据传入的 HandleTableEntry
在句柄表中分配一个空闲的句柄表项 NewHandleTableEntry
。之后会调用 ExUnlockHandleTableEntry
解锁句柄表项,这里会将句柄表项的 Object
的最低位置 1。因此全局句柄表项的 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
| NTKERNELAPI HANDLE ExCreateHandle ( __inout PHANDLE_TABLE HandleTable, __in PHANDLE_TABLE_ENTRY HandleTableEntry ) { EXHANDLE Handle; PETHREAD CurrentThread; PHANDLE_TABLE_ENTRY NewHandleTableEntry;
PAGED_CODE();
Handle.GenericHandleOverlay = NULL;
NewHandleTableEntry = ExpAllocateHandleTableEntry(HandleTable, &Handle);
if (NewHandleTableEntry != NULL) {
CurrentThread = PsGetCurrentThread();
KeEnterCriticalRegionThread(&CurrentThread->Tcb);
*NewHandleTableEntry = *HandleTableEntry;
if (HandleTable->DebugInfo != NULL) { ExpUpdateDebugInfo( HandleTable, CurrentThread, Handle.GenericHandleOverlay, HANDLE_TRACE_DB_OPEN ); }
ExUnlockHandleTableEntry(HandleTable, NewHandleTableEntry);
KeLeaveCriticalRegionThread(&CurrentThread->Tcb); }
return Handle.GenericHandleOverlay; }
|
全局句柄表查询
获取 PspCidTable
GetPspCidTable
函数通过例如 PsLookupProcessByProcessId
的特征码来定位 PspCidTable
的地址。
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
|
PVOID GetPspCidTable( VOID ) { static PVOID g_PspCidTable = NULL;
if (g_PspCidTable) return g_PspCidTable;
MEMORY_REGION region = { 0 };
if (!GetKernelModuleRegion("ntoskrnl.exe", ®ion)) { DbgPrint("[-] 无法获取 ntoskrnl.exe 范围\n"); return NULL; }
SIGNATURE_PATTERN sig = { 0 }; if (!InitSignaturePattern(&sig, "8B*****E8****8BF885FF74*8B1F", 2)) { DbgPrint("[-] InitSignaturePattern 失败\n"); return NULL; }
PUCHAR addr = FindMultiplePatternsInRange( region.BaseAddress, region.RegionSize, &sig, 1 ); if (!addr) { DbgPrint("[-] 未能定位到 PspCidTable 特征码位置\n"); return NULL; }
g_PspCidTable = **(PVOID**)addr;
if (!MmIsAddressValid(g_PspCidTable)) { DbgPrint("[-] 解引用 PspCidTable 值无效: %p\n", g_PspCidTable); g_PspCidTable = NULL; return NULL; }
DbgPrint("[+] 成功获取 PspCidTable = %p (from 指令地址 = %p)\n", g_PspCidTable, addr); return g_PspCidTable; }
|
获取 ExpLookupHandleTableEntry
由于句柄表查询函数 ExpLookupHandleTableEntry
在不同版本操作系统的实现不太一样,因此我们既不方便自己实现一个句柄表查询函数,也不方便直接通过搜索 ExpLookupHandleTableEntry
的特征码定位该函数。
我们可以通过 ExEnumHandleTable
函数来间接定位 ExpLookupHandleTableEntry
函数。
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
|
PVOID GetExpLookupHandleTableEntry( VOID ) { static PVOID g_ExpLookupHandleTableEntry = NULL;
if (g_ExpLookupHandleTableEntry) return g_ExpLookupHandleTableEntry;
UNICODE_STRING uName = RTL_CONSTANT_STRING(L"ExEnumHandleTable"); PVOID pExEnum = MmGetSystemRoutineAddress(&uName); if (!pExEnum) { DbgPrint("[-] 无法获取 ExEnumHandleTable 地址\n"); return NULL; }
SIGNATURE_PATTERN sig = { 0 }; if (!InitSignaturePattern(&sig, "FF75*8B4D*E8****8BF085F675*EB*", -7)) { DbgPrint("[-] InitSignaturePattern 初始化失败\n"); return NULL; }
PUCHAR base = (PUCHAR)pExEnum; SIZE_T searchRange = 0x100;
PUCHAR match = FindMultiplePatternsInRange(base, searchRange, &sig, 1); if (!match) { DbgPrint("[-] 未找到 ExpLookupHandleTableEntry 特征码匹配位置\n"); return NULL; }
PUCHAR relAddr = match; INT32 rel = *(INT32*)relAddr;
PUCHAR target = relAddr + 4 + rel; g_ExpLookupHandleTableEntry = (PVOID)target;
DbgPrint("[+] 成功定位 ExpLookupHandleTableEntry = %p\n", g_ExpLookupHandleTableEntry); return g_ExpLookupHandleTableEntry; }
|
全局句柄表查询函数实现
基于前面获取的 PspCidTable
和 ExpLookupHandleTableEntry
我们可以实现全局句柄表查询。
在 32 位下 ExpLookupHandleTableEntry
的调用约定是 thiscall
,也就是说第一个参数放在 ECX
中,剩余参数放在堆栈上。
由于 C 语言不支持 thiscall
调用约定,除非内联汇编进行模拟,因此这里使用 fastcall
来模拟 thiscall
调用约定。而 fastcall
的前两个参数放在 ECX
和 EDX
中,剩余参数放在堆栈上,因此我们需要多传入一个参数占位 EDX
。
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
|
PVOID QueryCidHandleEntry( _In_ HANDLE UniqueId ) { PHANDLE_TABLE cidTable = (PHANDLE_TABLE)GetPspCidTable(); if (!cidTable) { DbgPrint("[-] 获取 PspCidTable 失败\n"); return NULL; }
typedef PVOID (__fastcall *EXP_LOOKUP_HANDLE_TABLE_ENTRY)( PHANDLE_TABLE HandleTable, PVOID Dummy, HANDLE Handle );
EXP_LOOKUP_HANDLE_TABLE_ENTRY fnLookup = (EXP_LOOKUP_HANDLE_TABLE_ENTRY)GetExpLookupHandleTableEntry();
if (!fnLookup) { DbgPrint("[-] 获取 ExpLookupHandleTableEntry 失败\n"); return NULL; }
PVOID object = fnLookup(cidTable, NULL, UniqueId); if (!object) { DbgPrint("[-] 未找到句柄项(Handle = %p)\n", UniqueId); return NULL; }
DbgPrint("[+] 成功查询 CID 对象地址 = %p (Handle = %p)\n", object, UniqueId); return object; }
|
进程保护
我们可以使用 PsLookupProcessByProcessId
和 PsLookupThreadByThreadId
根据进程 ID 和线程 ID 在全局句柄表中查找对应的进程对象和线程对象。
另外很多 API 例如 NtOpenProcess
内部也是通过 PsLookupProcessByProcessId
等 API 查询全局句柄表来找到进程的。
因此如果我们能够将自身进程在全局句柄表中对应的句柄表项清空,那么就可以让很多查找进程的 API 无法定位到自身进程从而达到进程保护的目的。
1 2 3
| PVOID oldObject = InterlockedExchangePointer(&entry->Object, NULL); DbgPrint("[+] 成功隐藏进程,PID=%p,原对象地址=%p\n", pid, oldObject);
|
PsLookupProcessByProcessId
实际上是根据进程 ID 查询出对应的全局句柄表项,因此如果将全局句柄表项置空,则会导致后续出现内存访问错误。
1 2 3 4
| CidEntry = ExMapHandleToPointer(PspCidTable, ProcessId); if (CidEntry != NULL) { lProcess = (PEPROCESS)CidEntry->Object; if (lProcess->Pcb.Header.Type == ProcessObject &&
|
但是实际上这种事情并没有发生,这是因为我们清空全局句柄表项实际上会对句柄表项加锁,这就导致 ExMapHandleToPointer
调用的 ExpLockHandleTableEntry
函数加锁失败返回 FALSE
。
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
|
BOOLEAN FORCEINLINE ExpLockHandleTableEntry ( PHANDLE_TABLE HandleTable, PHANDLE_TABLE_ENTRY HandleTableEntry ) { LONG_PTR NewValue; LONG_PTR CurrentValue;
while (TRUE) { CurrentValue = ReadForWriteAccess((volatile LONG_PTR *)&HandleTableEntry->Object);
if (CurrentValue & EXHANDLE_TABLE_ENTRY_LOCK_BIT) { } else { if (CurrentValue == 0) { return FALSE; } }
} }
|
进而导致 ExMapHandleToPointer
返回 NULL 导致进程查询失败。
1 2 3 4 5 6 7 8
| if ((HandleTableEntry == NULL) || !ExpLockHandleTableEntry(HandleTable, HandleTableEntry)) {
return NULL; }
|
然而这种方法会导致被保护进程退出时蓝屏,这是因为在 PspProcessDelete
函数中 ExDestroyHandle
查询不到我们隐藏的进程。
1 2 3 4 5
| if (Process->UniqueProcessId) { if (!(ExDestroyHandle (PspCidTable, Process->UniqueProcessId, NULL))) { KeBugCheck (CID_HANDLE_DELETION); } }
|
解决方法也很简单,只要将 Process->UniqueProcessId
置 0 即可。完整代码如下:
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
|
LONG GetEprocessUniqueProcessIdOffset( VOID ) { static LONG g_UniquePidOffset = -2;
if (g_UniquePidOffset != -2) return g_UniquePidOffset;
UNICODE_STRING uName = RTL_CONSTANT_STRING(L"PsGetProcessId"); PVOID func = MmGetSystemRoutineAddress(&uName); if (!func) { DbgPrint("[-] 无法获取 PsGetProcessId 地址\n"); g_UniquePidOffset = -1; return -1; }
SIGNATURE_PATTERN sig = { 0 }; if (!InitSignaturePattern(&sig, "8B45*8B80****5D", -5)) { DbgPrint("[-] InitSignaturePattern 失败\n"); g_UniquePidOffset = -1; return -1; }
PUCHAR base = (PUCHAR)func; SIZE_T range = 0x40; PUCHAR match = FindMultiplePatternsInRange(base, range, &sig, 1); if (!match) { DbgPrint("[-] 未找到 UniqueProcessId 偏移匹配特征\n"); g_UniquePidOffset = -1; return -1; }
g_UniquePidOffset = *(LONG*)match; DbgPrint("[+] _EPROCESS.UniqueProcessId 偏移 = 0x%X\n", g_UniquePidOffset); return g_UniquePidOffset; }
VOID ProtectProcess( _In_ HANDLE pid ) { DbgPrint("[*] 尝试隐藏 PID = %p 的进程对象\n", pid);
PHANDLE_TABLE_ENTRY entry = (PHANDLE_TABLE_ENTRY)QueryCidHandleEntry(pid); if (!entry) { DbgPrint("[-] 查询 CID 表失败,PID=%p\n", pid); return; }
PVOID oldObject = InterlockedExchangePointer(&entry->Object, NULL); if (!oldObject) { DbgPrint("[-] 指定 PID 的句柄表项对象已为空\n"); return; }
DbgPrint("[+] 已清空句柄表项,PID=%p,原对象地址=%p\n", pid, oldObject);
LONG offset = GetEprocessUniqueProcessIdOffset(); if (offset < 0) { DbgPrint("[-] 获取 UniqueProcessId 偏移失败,无法清零 PID 字段\n"); return; }
*(PHANDLE)((PUCHAR)oldObject + offset) = NULL; DbgPrint("[+] 成功清除 EPROCESS.UniqueProcessId,PID=%p\n", pid); }
|
这个方法在虚拟机中依然会造成蓝屏,这是由虚拟机的驱动 vm3dmp.sys
注册的进程退出回调造成的。
私有句柄表
EPROCESS
的 ObjectTable
指针指向改进程的私有句柄表。
与全局句柄表不同的是,私有句柄表中的句柄表项指向的对象是包含 OBJECT_HEADER
的,我们需要加上 sizeof(_OBJECT_HEADER)
偏移才能找到句柄表项实际对应的对象。
获取对象类型
私有句柄表中存储的是进程打开的所有句柄,因此我们在遍历其中的句柄表项时需要根据句柄表项指向的 OBJECT_HEADER
来确定对象的类型。 OBJECT_HEADER
的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| struct _OBJECT_HEADER { LONG PointerCount; union { LONG HandleCount; VOID* NextToFree; }; struct _EX_PUSH_LOCK Lock; UCHAR TypeIndex; UCHAR TraceFlags; UCHAR InfoMask; UCHAR Flags; union { struct _OBJECT_CREATE_INFORMATION* ObjectCreateInfo; VOID* QuotaBlockCharged; }; VOID* SecurityDescriptor; struct _QUAD Body; };
|
OBJECT_HEADER
中的 Body
只是个占位的成员,用来表示对象本体,也就是说这里 OBJECT_HEADER
的实际大小为 0x18。这么定义的好处是方便我们使用 CONTAINING_RECORD
从对象指针找到 OBJECT_HEADER
。
1 2 3
|
POBJECT_HEADER Header = CONTAINING_RECORD(Object, OBJECT_HEADER, Body);
|
其中的 TypeIndex
字段表示的是对象的类型索引,这里的索引是在 OBJECT_TYPE
指针数组 ObTypeIndexTable
中的索引。
我们可以通过 OBJECT_TYPE
的 Name
确定对象的具体类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| struct _OBJECT_TYPE { struct _LIST_ENTRY TypeList; struct _UNICODE_STRING Name; VOID* DefaultObject; UCHAR Index; ULONG TotalNumberOfObjects; ULONG TotalNumberOfHandles; ULONG HighWaterNumberOfObjects; ULONG HighWaterNumberOfHandles; struct _OBJECT_TYPE_INITIALIZER TypeInfo; struct _EX_PUSH_LOCK TypeLock; ULONG Key; struct _LIST_ENTRY CallbackList; };
|
然而 ObTypeIndexTable
在内核中并不导出,不过我们可以通过一些内核函数例如 ObGetObjectType
函数来定位:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
POBJECT_TYPE ObGetObjectType ( IN PVOID Object ) { POBJECT_HEADER Header;
Header = CONTAINING_RECORD(Object, OBJECT_HEADER, Body);
return ObTypeIndexTable[Header->TypeIndex]; }
|
从 Windows 10 64 位开始,OBJECT_HEADER
中的 TypeIndex
被加密了,我们可以直接调用 ObGetObjectType
函数来获取对象对应的 OBJECT_TYPE
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| POBJECT_TYPE ObGetObjectType ( IN PVOID Object ) { POBJECT_HEADER Header; UCHAR EncryptedTypeIndex; UCHAR DecodedIndex;
Header = CONTAINING_RECORD(Object, OBJECT_HEADER, Body);
EncryptedTypeIndex = Header->TypeIndex;
DecodedIndex = ObHeaderCookie ^ EncryptedTypeIndex ^ (UCHAR)(((ULONG_PTR)Header >> 8) & 0xFF);
return ObTypeIndexTable[DecodedIndex]; }
|
另外如果是判断进程类型的句柄,我们可以直接使用 Windows 导出的 PsProcessType
进行判断。
句柄防降权
句柄降权是保护进程的一个常见方法,这个方法的思路是每隔一段时间就扫描一遍所有进程的私有句柄表,一旦发现有受保护进程的句柄,就修改该句柄的权限。
示例代码没有处理受保护进程自身退出的情况,关闭受保护进程可能会导致蓝屏。
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
| #include <ntifs.h> #include <ntddk.h> #include <windef.h>
extern POBJECT_TYPE *PsProcessType;
PKEVENT g_StopEvent = NULL;
BOOLEAN HandleEnumCallback( HANDLE Handle, PHANDLE_TABLE_ENTRY HandleEntry, PVOID Context ) { PEPROCESS targetProcess = (PEPROCESS)Context; PVOID rawObject = HandleEntry->Object;
POBJECT_HEADER header = (POBJECT_HEADER)((PUCHAR)rawObject & ~7); PEPROCESS actualObject = (PEPROCESS)((PUCHAR)header + 0x18);
if (actualObject == targetProcess) { HandleEntry->GrantedAccessBits &= ~(PROCESS_VM_READ | PROCESS_VM_WRITE); DbgPrint("[+] 移除句柄 0x%p 权限,来自进程 PID=%p\n", Handle, PsGetCurrentProcessId()); }
return FALSE; }
VOID ProtectProcessHandle( PEPROCESS targetProcess ) { for (ULONG_PTR pid = 4; pid < 0x100000; pid += 4) { PEPROCESS process = NULL; if (!NT_SUCCESS(PsLookupProcessByProcessId((HANDLE)pid, &process))) continue;
PHANDLE_TABLE handleTable = *(PHANDLE_TABLE*)((PUCHAR)process + 0xF4); if (handleTable) { ExEnumHandleTable( handleTable, HandleEnumCallback, (PVOID)targetProcess, NULL ); }
ObDereferenceObject(process); } }
VOID ThreadFunction( PVOID StartContext ) { PEPROCESS targetProcess = (PEPROCESS)StartContext;
while (TRUE) { if (KeWaitForSingleObject(g_StopEvent, Executive, KernelMode, FALSE, NULL) == STATUS_WAIT_0) { DbgPrint("[+] 驱动停止,退出保护线程\n"); return; }
ProtectProcessHandle(targetProcess); KeDelayExecutionThread(KernelMode, FALSE, &((LARGE_INTEGER) { -5 * 1000 * 1000 })); } }
VOID StartProtectingProcess( PEPROCESS targetProcess ) { PKEVENT event; PETHREAD thread;
g_StopEvent = ExAllocatePool(NonPagedPool, sizeof(KEVENT)); if (g_StopEvent == NULL) { DbgPrint("[-] 无法创建停止事件\n"); return; } KeInitializeEvent(g_StopEvent, NotificationEvent, FALSE);
PsCreateSystemThread( &thread, THREAD_ALL_ACCESS, NULL, NULL, NULL, (PKSTART_ROUTINE)ThreadFunction, (PVOID)targetProcess ); }
VOID UnloadDriver( PDRIVER_OBJECT DriverObject ) { if (g_StopEvent) { KeSetEvent(g_StopEvent, 0, FALSE); ExFreePool(g_StopEvent); }
DbgPrint("[+] 驱动卸载\n"); }
NTSTATUS DriverEntry( PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath ) { UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = UnloadDriver;
PEPROCESS targetProcess = NULL; if (!NT_SUCCESS(PsLookupProcessByProcessId((HANDLE)1234, &targetProcess))) { DbgPrint("[-] 无法找到进程\n"); return STATUS_UNSUCCESSFUL; }
StartProtectingProcess(targetProcess);
return STATUS_SUCCESS; }
|
句柄表断链
通常这种降权手段是通过遍历私有句柄表链表实现的,即遍历 HANDLE_TABLE
的 HandleTableList
。如果我们将自身进程的私有句柄表从句柄表链表中断链则可以避免句柄被降权。不过这种方法会导致蓝屏,好在蓝屏需要很长一段时间才会触发。
伪造进程
另一种思路是创建一个”影子进程”,欺骗保护机制,同时保持对目标进程的完全访问权限。其对抗过程可分为以下关键步骤:
- 创建伪造进程对象:分配内存并复制目标进程的完整
OBJECT_HEADER
和 EPROCESS
结构。修改关键字段:例如将 PID 设为 0,清空进程名称,进一步对抗私有句柄表扫描。
- 复制页表:复制目标进程的
CR3
页表到新的物理内存,更新伪造进程对象的 CR3
指向新复制的页表
- 重定向句柄:遍历所有进程的句柄表,将指向被保护的目标进程的句柄重定向到伪造对象,保留句柄表项的低 3 位标志位并恢复完整访问权限(
PROCESS_ALL_ACCESS
)。
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
| #include <ntifs.h> #include <ntddk.h>
#define EPROCESS_UNIQUE_PROCESS_ID_OFFSET 0x140 #define EPROCESS_IMAGE_FILE_NAME_OFFSET 0x16C #define EPROCESS_DIRECTORY_TABLE_BASE_OFFSET 0x18 #define OBJECT_HEADER_BODY_OFFSET 0x18
PEPROCESS g_targetProcess = NULL; PVOID g_fakeHeader = NULL; PHYSICAL_ADDRESS g_fakeCr3 = { 0 }; PVOID g_fakePageTable = NULL; ULONG g_protectedPid = 1234;
PVOID AllocatePhysicalMemory(SIZE_T size, PHYSICAL_ADDRESS* physicalAddress) { PVOID virtualAddress = MmAllocateContiguousMemory( size, { MAXULONG_PTR } ); if (virtualAddress) { *physicalAddress = MmGetPhysicalAddress(virtualAddress); } return virtualAddress; }
SIZE_T GetPageTableSize() { SIZE_T tableSize = PAGE_SIZE; if (IsProcessorFeaturePresent(PF_PAE_ENABLED)) { tableSize = 32; } return tableSize; }
NTSTATUS CopyPageTable(PHYSICAL_ADDRESS sourceCr3, PVOID* destination, PHYSICAL_ADDRESS* destCr3) { SIZE_T tableSize = GetPageTableSize(); PVOID newPageTable = AllocatePhysicalMemory(PAGE_SIZE, destCr3); if (!newPageTable) { return STATUS_INSUFFICIENT_RESOURCES; } SIZE_T mapSize = (sourceCr3.LowPart & 0xFFF) + tableSize > PAGE_SIZE ? PAGE_SIZE : tableSize; PVOID sourcePageTable = MmMapIoSpace(sourceCr3, mapSize, MmNonCached); if (!sourcePageTable) { MmFreeContiguousMemory(newPageTable); return STATUS_UNSUCCESSFUL; } RtlCopyMemory(newPageTable, sourcePageTable, tableSize); MmUnmapIoSpace(sourcePageTable, mapSize); *destination = newPageTable; return STATUS_SUCCESS; }
NTSTATUS CreateFakeProcessObject() { NTSTATUS status = STATUS_SUCCESS; POBJECT_HEADER targetHeader = (POBJECT_HEADER)((PUCHAR)g_targetProcess - OBJECT_HEADER_BODY_OFFSET); g_fakeHeader = ExAllocatePool(NonPagedPool, PAGE_SIZE); if (!g_fakeHeader) { return STATUS_INSUFFICIENT_RESOURCES; } RtlCopyMemory(g_fakeHeader, targetHeader, PAGE_SIZE); PEPROCESS fakeProcess = (PEPROCESS)((PUCHAR)g_fakeHeader + OBJECT_HEADER_BODY_OFFSET); *(HANDLE*)((PUCHAR)fakeProcess + EPROCESS_UNIQUE_PROCESS_ID_OFFSET) = 0; PUCHAR imageFileName = (PUCHAR)fakeProcess + EPROCESS_IMAGE_FILE_NAME_OFFSET; RtlZeroMemory(imageFileName, 15); PHYSICAL_ADDRESS originalCr3; originalCr3.QuadPart = *(ULONG_PTR*)((PUCHAR)g_targetProcess + EPROCESS_DIRECTORY_TABLE_BASE_OFFSET); status = CopyPageTable(originalCr3, &g_fakePageTable, &g_fakeCr3); if (!NT_SUCCESS(status)) { ExFreePool(g_fakeHeader); g_fakeHeader = NULL; return status; } *(ULONG_PTR*)((PUCHAR)fakeProcess + EPROCESS_DIRECTORY_TABLE_BASE_OFFSET) = g_fakeCr3.QuadPart; return status; }
VOID RedirectProcessHandles(PEPROCESS process) { PHANDLE_TABLE handleTable = *(PHANDLE_TABLE*)((PUCHAR)process + 0xF4); if (!handleTable) { return; }
ExEnumHandleTable( handleTable, [](HANDLE, PHANDLE_TABLE_ENTRY HandleEntry, PVOID) -> BOOLEAN { PVOID object = HandleEntry->Object; POBJECT_HEADER header = (POBJECT_HEADER)((ULONG_PTR)object & ~7); PVOID body = (PUCHAR)header + OBJECT_HEADER_BODY_OFFSET; if (body == g_targetProcess) { ULONG_PTR fakeObject = (ULONG_PTR)g_fakeHeader; fakeObject |= (ULONG_PTR)object & 7; HandleEntry->Object = (PVOID)fakeObject; HandleEntry->GrantedAccessBits |= PROCESS_ALL_ACCESS; DbgPrint("[+] 重定向句柄到伪造对象 (PID=%d)\n", PsGetProcessId(PsGetCurrentProcess())); } return TRUE; }, NULL, NULL ); }
VOID RedirectSpecificProcessHandles() { for (ULONG_PTR pid = 4; pid < 0x100000; pid += 4) { PEPROCESS process = NULL; if (!NT_SUCCESS(PsLookupProcessByProcessId((HANDLE)pid, &process))) { continue; } RedirectProcessHandles(process); ObDereferenceObject(process); } }
VOID CleanupFakeProcess() { if (g_fakePageTable) { MmFreeContiguousMemory(g_fakePageTable); g_fakePageTable = NULL; } if (g_fakeHeader) { ExFreePool(g_fakeHeader); g_fakeHeader = NULL; } }
VOID UnloadDriver(PDRIVER_OBJECT DriverObject) { UNREFERENCED_PARAMETER(DriverObject); CleanupFakeProcess(); DbgPrint("[+] 驱动卸载完成\n"); }
NTSTATUS DriverEntry( PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath ) { UNREFERENCED_PARAMETER(RegistryPath); DriverObject->DriverUnload = UnloadDriver; NTSTATUS status = PsLookupProcessByProcessId((HANDLE)g_protectedPid, &g_targetProcess); if (!NT_SUCCESS(status)) { DbgPrint("[-] 无法找到目标进程 (PID=%d)\n", g_protectedPid); return status; } status = CreateFakeProcessObject(); if (!NT_SUCCESS(status)) { DbgPrint("[-] 创建伪造进程对象失败: 0x%X\n", status); ObDereferenceObject(g_targetProcess); return status; } RedirectSpecificProcessHandles(); DbgPrint("[+] 进程保护已启用! 真实进程: 0x%p, 伪造对象: 0x%p\n", g_targetProcess, g_fakeHeader); return STATUS_SUCCESS; }
|