扫描系统句柄表(WIN7 x86)(附录源码)
PspCidTable存放着系统中所有的进程和线程对象,其索引也就是进程ID(PID)或线程ID(TID).先通过它来看看windbg里的HANDLE_TABLE结构:
可以看到地址 0x83f41bc4中存放的内容是 0x 8da010a8,这是系统的_HANDLE_TABLE的结构。
好了,现在windbg是得到HANDLE_TABLE结构了,还是要代码实现的。这里只简单用一下加偏移:
//system进程的eprocess地址
PEPROCESS EProcess = (PEPROCESS)0x86aee798;
#define _HANDLE_TABLE_OFFSET_EPROCESS 0x0f4
HandleTable = (PHANDLE_TABLE)(*((ULONG*)((UINT8*)EProcess + _HANDLE_TABLE_OFFSET_EPROCESS)));
HANDLE_TABLE这里有两个需要关注的内容,第一个是TableCode,另一个是NextHandleNeedingPool.
TableCode成员,可以认为它是一个指向句柄表的地址,其中这个数值的低2位表示的是句柄表的层数,所以我们实际得到的句柄表的地址是要掩掉低2位的,也就是 TableCode&& 0xFFFFFFF8(TableCode&~0x3) ,其中低两位 为0时表示 1层索引,为1时表示2层索引,为2时表示3层索引,最后我们索引到的是一个_HANDLE_TABLE_ENTRY的结构,这个结构里面有我们要的_EPROCESS对象地址。
借某位博主图一用(图片源自:http://www.cnblogs.com/ck1020/p/5897460.html)
这个图可以说是非常清晰了(有一处小错误,TableCode低两位为2是三级索引,不过瑕不掩瑜呐)
对于每一个索引表大小都为1页 4KB,其中一级表存放的是8Byte的_HANDLE_TABLE_ENTRY的结构,所以每一个1级表就只能存放512个项;
2级表存放的是1级表的地址(4Byte)那么每一个2级表能够存放4KB/4B = 1024个1级表的地址,如果存在3级表的话,相当庞大的数目了。
回到上面可以看到TableCode为0x8f71001, 低2位为01 ,可以知道当前是2层索引结构,其中句柄表的地址为0x8f71000,通过命令 dd 0x0x8f71000看到对应的2级索引表:
可以看到就只有两项,也就是说有两个1级表,那么当前的句柄表能够容纳512*2=1024个句柄.
通过地址0x8da04000访问到第一个1级索引表来看看 :
里面是很多8字节的_HANDLE_TABLE_ENTRY,查看WRK的源码看它的结构:
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 _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;
通过这个结构可以看出HANDLE_TABLE_ENTRY 8Byte的前4Byte是一个Object对象,这也就是我们要找的_EPROCESS指针,但是需要注意的是,句柄表中的Object指针的低3位则是有另外意义的:
①第0位OBJ_PROTECT_CLOSE,表示调用者是否允许关闭该句柄;
②第1位OBJ_ INHERIT,指示该进程所创建的子进程是否可以继承该句柄,即是否将该句柄项拷贝到子进程的句柄表中;
③第2位OBJ_ AUDIT_OBJECT_ CLOSE。指示关闭该对象时是否产生一个审计事件。
所以我们在使用该指针的时候要掩掉低3Bit
也就是说对于Object=86ae88a9应该变为ObjectHeader=0x86aee799& 0xFFFFFFF8 (0x86aee799&~0x07)&~0x07 = 0x86aee798,这才是需要的_EPROCESS 的地址;
说了这么多,我们的代码也来实现实现:
TableCode = HandleTable->TableCode;
TableCode = (ULONG)TableCode & 0xFFFFFFFC; //去掉低两位
#define _SPECIAL_PURPOSE 8
//越过特殊用途8字节到第一个HandleTableEntry
HandleTableEntry = (PHANDLE_TABLE_ENTRY)((ULONG*)((UINT8*)TableCode + _SPECIAL_PURPOSE));
//去掉低3位掩码标志,转换为对象(体)指针
//EPROCESS地址
PVOID ObjectHeader = (PVOID)((ULONG)(HandleTableEntry->Object) & 0xFFFFFFF8);
Windbg继续开进,验证一下0x86aee798,这个地址的对象类型:
(在写这个程序代码的时候,就已经WINDBG查看了system进程的EPROCESS地址写死到了代码中,回头看看,正是这个0x86aee798!!!!)
一个Process对象,
Bingo!
代码无误,代接下来循环读取HandleTableEntry结构,打印出对象头和对象体:
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 | NTSTATUS EnumTable0(PVOID TableCode) { PHANDLE_TABLE_ENTRY HandleTableEntry = NULL; ULONG i = 0; //越过特殊用途8字节到第一个HandleTableEntry HandleTableEntry = (PHANDLE_TABLE_ENTRY)((ULONG*)((UINT8*)TableCode + _SPECIAL_PURPOSE)); for (i = 0; i<_MAX; i++) { if (MmIsAddressValid((PVOID)HandleTableEntry)) //判断该虚拟内存是否合法 { //去掉低3位掩码标志,转换为对象(体)指针 //EPROCESS地址 PVOID ObjectHeader = (PVOID)((ULONG)(HandleTableEntry->Object) & 0xFFFFFFF8); if (MmIsAddressValid(ObjectHeader)) { DbgPrint( "ObjectHeader:%p\r\n" ,ObjectHeader); PVOID ObjectBody = (PVOID)((UINT8*)ObjectHeader + _BODY_OFFSET_OBJECT_HEADER); if (MmIsAddressValid(ObjectBody)) //这里应当判断对象是否合法 { DbgPrint( "Object:%p\r\n" , ObjectBody); __ObjectCount++; } } } HandleTableEntry++; //结构体指针++ 一加一个结构体 } return STATUS_SUCCESS; } |
接下来回头谈谈_HANDLE_TABLE中的一个叫做NextHandleNeedingPool的成员:
这个成员描述了下次句柄表增长的时,起始的句柄值(别忘了句柄是以4为步长增长的),上面的分析我们知道系统有2个2级索引那么最多能描述512*2=1024个_HANDLE_ENTRY,也就是说最大能表示的句柄值为1024*4=4096=0x1000,因为是从0x00开始的,所以当前的索引表状态能够描述的最大句柄上限为0x1000,这个值也就是下次句柄表扩展的起始句柄值.
最后来验证一下通过PspCidTable和进程的PID找到进程对应的_EPROCESS:
以audiodg.exe作为检验对象。
它的PID为1048(十进制),应当位于第1048/4=262 个表项,我们每一个1级索引表能容纳512个表项,PID为1048应该在第1个1级索引的第262=0x106个表项(每个表项8Byte)
BINGO!
最后的最后总结一下流程:
(1)(WINDBG )获取到PspCidTable的地址,根据Tablecode低2位判断句柄表的层数。
(2)遍历句柄表:只有一级句柄表才是_HANDLE_TABLE_ENTRY(8字节),二级和三级都是指针(4字节),每一个表都是1页(4KB)大小,
(3)获取到Object之后,可以通过ObjectHeader的TypeIndex看看是不是Process.
(4)HANDLE_TABLE_ENTRY 8Byte的前4Byte是一个Object对象,去掉低三位才是才是需要的_EPROCESS 对象的地址,_EPROCESS 对象偏移0x18处是相对于对象头的对象体。
源代码:
1 | ScanProcessHandleTable.c |
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 | #include "ScanProcessHandleTable.h" #define _HANDLE_TABLE_OFFSET_EPROCESS 0x0f4 #define _SPECIAL_PURPOSE 8 #define _MAX 511 #define _BODY_OFFSET_OBJECT_HEADER 0x18 ULONG64 __ObjectCount = 0; NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegisterPath) { UNREFERENCED_PARAMETER(RegisterPath); NTSTATUS Status = STATUS_SUCCESS; //system进程的eprocess地址 PEPROCESS EProcess = (PEPROCESS)0x86aee798; PDEVICE_OBJECT DeviceObject = NULL; DriverObject->DriverUnload = DriverUnload; SeScanProcessHandleTable(EProcess); return Status; } NTSTATUS SeScanProcessHandleTable(PEPROCESS EProcess) { NTSTATUS Status = STATUS_UNSUCCESSFUL; PHANDLE_TABLE HandleTable = NULL; PVOID TableCode = NULL; ULONG Flag = 0; if (EProcess==NULL) { return Status; } HandleTable = (PHANDLE_TABLE)(*(( ULONG *)((UINT8*)EProcess + _HANDLE_TABLE_OFFSET_EPROCESS))); if (HandleTable==NULL) { return Status; } TableCode = HandleTable->TableCode; TableCode = ( ULONG )TableCode & 0xFFFFFFFC; //去掉低两位 Flag = ( ULONG )(HandleTable->TableCode) & 0x03; //00 01 10 11 switch (Flag) { case 0: { EnumTable0(TableCode); break ; } case 1: { EnumTable1(TableCode); break ; } case 2: { EnumTable2(TableCode); break ; } case 3: { EnumTable3(TableCode); break ; } } } NTSTATUS EnumTable0( PVOID TableCode) { PHANDLE_TABLE_ENTRY HandleTableEntry = NULL; ULONG i = 0; //越过特殊用途8字节到第一个HandleTableEntry HandleTableEntry = (PHANDLE_TABLE_ENTRY)(( ULONG *)((UINT8*)TableCode + _SPECIAL_PURPOSE)); for (i = 0; i<_MAX; i++) { if (MmIsAddressValid(( PVOID )HandleTableEntry)) //判断该虚拟内存是否合法 { //去掉低3位掩码标志,转换为对象(体)指针 //EPROCESS地址 PVOID ObjectHeader = ( PVOID )(( ULONG )(HandleTableEntry->Object) & 0xFFFFFFF8); if (MmIsAddressValid(ObjectHeader)) { DbgPrint( "ObjectHeader:%p\r\n" ,ObjectHeader); PVOID ObjectBody = ( PVOID )((UINT8*)ObjectHeader + _BODY_OFFSET_OBJECT_HEADER); if (MmIsAddressValid(ObjectBody)) //这里应当判断对象是否合法 { DbgPrint( "Object:%p\r\n" , ObjectBody); __ObjectCount++; } } } HandleTableEntry++; //结构体指针++ 一加一个结构体 } return STATUS_SUCCESS; } NTSTATUS EnumTable1( PVOID TableCode) { do { EnumTable0(*( ULONG *)TableCode); (UINT8*)TableCode += sizeof ( ULONG ); } while (*( ULONG *)TableCode != 0 && MmIsAddressValid(*( ULONG *)TableCode)); return STATUS_SUCCESS; } NTSTATUS EnumTable2( PVOID TableCode) { do { EnumTable1(*( ULONG *)TableCode); (UINT8*)TableCode += sizeof ( ULONG ); } while (*( ULONG *)TableCode != 0 && MmIsAddressValid(*( ULONG *)TableCode)); return STATUS_SUCCESS; } NTSTATUS EnumTable3( PVOID TableCode) { do { EnumTable2(*( ULONG *)TableCode); (UINT8*)TableCode += sizeof ( ULONG ); } while (*( ULONG *)TableCode != 0 && MmIsAddressValid(*( ULONG *)TableCode)); return STATUS_SUCCESS; } VOID DriverUnload(PDRIVER_OBJECT DriverObject) { UNREFERENCED_PARAMETER(DriverObject); DbgPrint( "DriverUnload()\r\n" ); } |
1 | ScanProcessHandleTable.h |
#pragma once #include <ntifs.h> typedef struct _HANDLE_TABLE { PVOID TableCode; PEPROCESS QuotaProcess; HANDLE UniqueProcessId; ULONG HandleTableLock; LIST_ENTRY HandleTableList; ULONG HandleContentionEvent; PVOID DebugInfo; LONG ExtraInfoPages; ULONG Flags; ULONG FirstFreeHandle; PVOID LastFreeHandleEntry; ULONG HandleCount; ULONG NextHandleNeedingPool; ULONG HandleCountHighWatermark; } HANDLE_TABLE, *PHANDLE_TABLE; typedef struct _HANDLE_TABLE_ENTRY { union { PVOID Object; ULONG ObAttributes; PVOID InfoTable; PVOID Value; }; union { union { ULONG GrantedAccess; struct { USHORT GrantedAccessIndex; USHORT CreatorBackTraceIndex; }; }; ULONG NextFreeTableEntry; }; } HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY; NTSTATUS EnumTable0(PVOID TableCode); NTSTATUS EnumTable1(PVOID TableCode); NTSTATUS EnumTable2(PVOID TableCode); NTSTATUS EnumTable3(PVOID TableCode); NTSTATUS SeScanProcessHandleTable(PEPROCESS EProcess); VOID DriverUnload(PDRIVER_OBJECT DriverObject);
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗