为什么要有句柄
在3环可以通过句柄访问到内核对象,如果windows不提供句柄,而是直接提供内核对象的指针给用户,那么是容易出错的。
例如:CloseHandle使用了错误的指针,在0环访问到了错误的地址,就会发生蓝屏。
因此提供了句柄给3环使用,进入0环时,在句柄表通过句柄为索引找到对应的内核对象。
句柄表里面有哪些内核对象
所有在自己进程的线程创建或者打开的内核对象,进程本身是父进程创建的,所以进程的句柄在父进程的句柄表里。
使用OpenProcess后,句柄表的变化
连续多次打开相同对象后,返回给三环的句柄的值是不同的,句柄表里面也会添加相应的值。
通过句柄寻找内核对象
第一步 :WinDbg使用命令 !Process 0 0 遍历所有进程
第二步 :dt _EPROCESS +指定进程的地址
第三步 :_EPROCESS+c4里面的值 就是句柄表位置的指针
kd> dt _HANDLE_TABLE 0xe2775e80
nt!_HANDLE_TABLE
+0x000 TableCode : 0xe2de3000 //句柄表的指针
+0x004 QuotaProcess : 0x8a12f448 _EPROCESS
+0x008 UniqueProcessId : 0x00000d00 Void
+0x00c HandleTableLock : [4] _EX_PUSH_LOCK
+0x01c HandleTableList : _LIST_ENTRY [ 0x80565ba8 - 0xe11dbebc ]
+0x024 HandleContentionEvent : _EX_PUSH_LOCK
+0x028 DebugInfo : (null)
+0x02c ExtraInfoPages : 0n0
+0x030 FirstFree : 0x7a0
+0x034 LastFree : 0
+0x038 NextHandleNeedingPool : 0x800 在分配下一层句柄表之前可以容纳的最大句柄的值 这个值/4就是这一层句柄表最大的索引
+0x03c HandleCount : 0n28
+0x040 Flags : 0
+0x040 StrictFIFO : 0y0
第四步 : kd> dq e2de3000(句柄表位置的指针) 句柄表每一项是8字节,前四字节是 句柄 ,后四字节是内核对象地址
第五步:找句柄7a4对应的内核对象
7a4为索引,该值/4*8就是要找的位置 /4表示在句柄表的第几项,*8 因为每一项的大小是8字节
句柄表里面的值,低两位清0 最后一位固定为0. 第二位表示是否可继承(9->8),然后加上0x18就是该内核对象的位置。(+18是_OBJECT_HEADER的大小)
因为每一个内核对象开始都是_OBJECT_HEADER这个结构体。然后才是_EPROCESS的开始位置
kd> dt _OBJECT_HEADER
nt!_OBJECT_HEADER
+0x000 PointerCount : Int4B
+0x004 HandleCount : Int4B
+0x004 NextToFree : Ptr32 Void
+0x008 Type : Ptr32 _OBJECT_TYPE
+0x00c NameInfoOffset : UChar
+0x00d HandleInfoOffset : UChar
+0x00e QuotaInfoOffset : UChar
+0x00f Flags : UChar
+0x010 ObjectCreateInfo : Ptr32 _OBJECT_CREATE_INFORMATION
+0x010 QuotaBlockCharged : Ptr32 Void
+0x014 SecurityDescriptor : Ptr32 Void
+0x018 Body : _QUAD
kd> dt _EPROCESS 8a1413b8+18
nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x06c ProcessLock : _EX_PUSH_LOCK
+0x070 CreateTime : _LARGE_INTEGER 0x1d51d70`0b380ae4
+0x078 ExitTime : _LARGE_INTEGER 0x0
+0x080 RundownProtect : _EX_RUNDOWN_REF
+0x084 UniqueProcessId : 0x00000980 Void
+0x088 ActiveProcessLinks : _LIST_ENTRY [ 0x8a0fe498 - 0x8a134c68 ]
+0x090 QuotaUsage : [3] 0xa00
+0x09c QuotaPeak : [3] 0xc10
+0x0a8 CommitCharge : 0x17d
+0x0ac PeakVirtualSize : 0x22ef000
+0x0b0 VirtualSize : 0x1f9d000
+0x0b4 SessionProcessLinks : _LIST_ENTRY [ 0x8a0fe4c4 - 0x8a134c94 ]
+0x0bc DebugPort : (null)
+0x0c0 ExceptionPort : 0xe15d3bb8 Void
+0x0c4 ObjectTable : 0xe10cd258 _HANDLE_TABLE
+0x0c8 Token : _EX_FAST_REF
+0x0cc WorkingSetLock : _FAST_MUTEX
+0x0ec WorkingSetPage : 0x46763
+0x0f0 AddressCreationLock : _FAST_MUTEX
+0x110 HyperSpaceLock : 0
+0x114 ForkInProgress : (null)
+0x118 HardwareTrigger : 0
+0x11c VadRoot : 0x8a139268 Void
+0x120 VadHint : 0x8a1391d8 Void
+0x124 CloneRoot : (null)
+0x128 NumberOfPrivatePages : 0xcb
+0x12c NumberOfLockedPages : 0
+0x130 Win32Process : 0xe123e188 Void
+0x134 Job : (null)
+0x138 SectionObject : 0xe12955f8 Void
+0x13c SectionBaseAddress : 0x01000000 Void
+0x140 QuotaBlock : 0x8a1d56d8 _EPROCESS_QUOTA_BLOCK
+0x144 WorkingSetWatch : (null)
+0x148 Win32WindowStation : 0x00000038 Void
+0x14c InheritedFromUniqueProcessId : 0x00000644 Void
+0x150 LdtInformation : (null)
+0x154 VadFreeHint : (null)
+0x158 VdmObjects : (null)
+0x15c DeviceMap : 0xe1af54d8 Void
+0x160 PhysicalVadList : _LIST_ENTRY [ 0x8a141530 - 0x8a141530 ]
+0x168 PageDirectoryPte : _HARDWARE_PTE
+0x168 Filler : 0
+0x170 Session : 0xba5da000 Void
+0x174 ImageFileName : [16] “calc.exe”
+0x184 JobLinks : _LIST_ENTRY [ 0x0 - 0x0
第三位 :第0位 为1 第1位:跟OpenProcess有关 第2位:为0
句柄表的一项是8字节,一页可以存512项句柄。
当句柄超过512时,句柄表的结构会发生变化,TableCode存的是句柄表地址,一页存了1024个句柄表地址。也就是最多可以存1024*512个句柄。
把TableCode的值拆开,最后两位决定了句柄表有多少级。
总结:
通过进程结构体_EPROCESS +C4可以指定进程的句柄表 _HANDLE_TABLE结构 该结构第一个成员为TableCode
如果TableCode的低两位为 00 说明句柄表结构只有1级,通过TableCode 里面的句柄表地址+句柄/4*8 (8字节的最低两位要清0)就可以找到对应的内核结构体_OBJECT_HEADER,+0x18即为真正的内核对象结构体 例如:_EPROCESS _ETHREAD.
句柄表有一级的情况下:HANDLE的 0-9位 用于找具体项
句柄表有两级的情况下:HANDLE的 10-19用于找第一级偏移 0-9位 用于找具体项
句柄表有三级的情况下:HANDLE的 20-29 用于找第一级的偏移 10-19用于找第二级偏移 0-9位 用于找具体项
如果TableCode的低两位为 01 说明句柄表结构只有2级 ,TableCode 低两位清0,里面存的是句柄表指针,再次读出里面的值才是句柄表的位置,然后也是句柄表地址+句柄*4/8跟上面一样
以此类推 10 11 的情况。
##练习:根据handle查找进程的内核对象
ULONG FindKernelObjectByHandle(ULONG Index)
{
PULONG pThread=NULL;
PULONG pProcess=NULL;
PULONG ObjectTable=NULL;
ULONG TableCode=0;
//句柄表有几层
ULONG Tier=0;
PVOID KeObject=NULL;
PULONG pFirstTableBase=NULL;
GetCurrentThread(&pThread);
pProcess=(PULONG)(*(pThread+0x44/4));
ObjectTable=(PULONG)(*(pProcess+0xc4/4));
TableCode=*ObjectTable;
Tier=TableCode&3;
switch (Tier)
{
case 0:
//句柄表只有一级,PspCidTable低两位清0 然后加上索引*4/8 低4字节的低2位清0 就是内核对象的头_OBJECT_HEADER +0x18就是真正的内核对象_ETHREAD 或者_EPROCESS
KeObject=(PVOID)(TableCode+Index*2);
break;
case 1:
//找在第一级表的位置 一个表最大的HANDLE 是 512*4=2048 索引、2048 就找出第一级表中的偏移
pFirstTableBase=(PULONG)(TableCode&0xFFFFFFFC)+Index/512/4;
//在表中的偏移等于 e1003000 e1fc0000
//低10位 是在一级表中的偏移
KeObject=(PVOID)((*pFirstTableBase)+(Index%2048*2));
break;
}
//进程私有句柄表存的是_OBJECT_HEADER的指针 +0x18才是真正的内核结构体
return ((*(PULONG)KeObject)&0xfffffffe)+0x18;
}