Windows句柄表

句柄是什么

windows内核中万物皆对象,每个对象都有其对应的内核对象。因为应用层是不能直接读写内核空间的,windows系统通过为进程分配与内核对象对应的句柄并赋予一定的权限,使应用层通过这些句柄间接访问对应的内核对象并对权限进行严格的控制。

句柄表

windows为进程分配的句柄都存放在句柄表中,其中句柄表分为私有句柄表和全局句柄表。

  • 私有句柄表是每个进程私有的,这个句柄表中保存了当前进程拥有的全部句柄,当然对于私有句柄表中的句柄而言只对当前进程有意义,其他进程使用当前进程的句柄都是无意义的。私有句柄的值一般都是4的倍数,句柄值/4 == 对应在私有句柄表中的索引。
  • 全局句柄表中主要保存了所有的进程和线程句柄信息。进程的pid和线程的tid也都是句柄,只不过其句柄对应的句柄信息保存在全局句柄表中。同样pid和tid的值也都是4的倍数,句柄值/4 == 对应在全局句柄表中的索引。

句柄表结构


句柄表是分层结构,层数最多为3层。EPROCESS.ObjectTable.TableCode的低两位的值表示句柄表的层数。无论有几层只有最后一层保存的是真正的句柄表项,其他层保存的都是下一层的地址。

私有句柄表

进程的私有句柄表保存在进程内核对象_EPROCESS.ObjcetTable中。

0: kd> dt _HANDLE_TABLE
ntdll!_HANDLE_TABLE
   +0x000 NextHandleNeedingPool : Uint4B
   +0x004 ExtraInfoPages   : Int4B
   +0x008 TableCode        : Uint8B    //低2位置0后指向HandleTable
   +0x010 QuotaProcess     : Ptr64 _EPROCESS
   +0x018 HandleTableList  : _LIST_ENTRY    //所有的HandleTable形成一个List
   +0x028 UniqueProcessId  : Uint4B
   +0x02c Flags            : Uint4B
   +0x02c StrictFIFO       : Pos 0, 1 Bit
   +0x02c EnableHandleExceptions : Pos 1, 1 Bit
   +0x02c Rundown          : Pos 2, 1 Bit
   +0x02c Duplicated       : Pos 3, 1 Bit
   +0x02c RaiseUMExceptionOnInvalidHandleClose : Pos 4, 1 Bit
   +0x030 HandleContentionEvent : _EX_PUSH_LOCK
   +0x038 HandleTableLock  : _EX_PUSH_LOCK
   +0x040 FreeLists        : [1] _HANDLE_TABLE_FREE_LIST
   +0x040 ActualEntry      : [32] UChar
   +0x060 DebugInfo        : Ptr64 _HANDLE_TRACE_DEBUG_INFO

_EPROCESS.ObjcetTable对应的就是_HANDLE_TABLE。其中TableCode的低2位表示句柄表层数,低二位置空后经过(1-3层)索引后最后会指向最后一层,最后一层对应的就是一个句柄项_HANDLE_TABLE_ENTRY指针数组。指针数组的大小为4096个字节,第一项无意义,其余项都是一个_HANDLE_TABLE_ENTRY类型的指针,所以最多保存0x99个_HANDLE_TABLE_ENTRY指针。

0: kd> dt _HANDLE_TABLE_ENTRY
ntdll!_HANDLE_TABLE_ENTRY
   +0x000 VolatileLowValue : Int8B
   +0x000 LowValue         : Int8B
   +0x000 InfoTable        : Ptr64 _HANDLE_TABLE_ENTRY_INFO
   +0x008 HighValue        : Int8B
   +0x008 NextFreeHandleEntry : Ptr64 _HANDLE_TABLE_ENTRY
   +0x008 LeafHandleValue  : _EXHANDLE
   +0x000 RefCountField    : Int8B
   +0x000 Unlocked         : Pos 0, 1 Bit
   +0x000 RefCnt           : Pos 1, 16 Bits
   +0x000 Attributes       : Pos 17, 3 Bits     //句柄属性
   +0x000 ObjectPointerBits : Pos 20, 44 Bits   //最后会指向句柄对应的内核对象的_OBJECT_HEADER
   +0x008 GrantedAccessBits : Pos 0, 25 Bits    //句柄权限
   +0x008 NoRightsUpgrade  : Pos 25, 1 Bit
   +0x008 Spare1           : Pos 26, 6 Bits
   +0x00c Spare2           : Uint4B

进程的每一个句柄值都对应一个句柄项_HANDLE_TABLE_ENTRY,句柄项描述了句柄的属性,具有的权限,以及此句柄所指向的内核对象。win7的_HANDLE_TABLE_ENTRY好像有个Object字段直接指向句柄对应的内核对象,_OBJECT_HEADER.TypeIndex对应的就是此对象在ObTypeIndexTable中的索引,win10就略有不同。

查看ObpReferenceObjectByHandleWithTag函数,此函数能够获取句柄对应的内核对象。可以看到通过句柄表项最后得到_OBJECT_HEADER和通过_OBJECT_HEADER.TypeIndex得到对象对应的OBJECT_TYPE的方法。

  • _OBJECT_HEADER = (_HANDLE_TABLE_ENTRY.ObjectPointerBits << 4) ^ 0xFFFF000000000000;
  • OBJECT_TYPE = ObTypeIndexTable[ ObHeaderCookie ^ _OBJECT_HEADER.TypeIndex ^ _OBJECT_HEADER地址的第二个字节 ]

手动从handle找到其对应的_OBJECT_HEADER和_OBJECT_TYPE


测试程序会通过一个进程pid打开一个进程并获得进程句柄并输出。


测试程序的pid为5164(0x142c),输入explorer.exe进程的pid,得到一个进程句柄值为164(0xa4)。

windbg查看其handle对应的_OBJECT_HEADER。

windbg查看此_OBJECT_HEADER对应的_OBJECT_TYPE,name为Process,所以此句柄对应的是一个进程内核对象。

全局句柄表

全局句柄表保存在全局变量PsCidTable中,指向一个_HANDLE_TABLE类型。其保存的是所有进程和线程的句柄,进程id和线程id作为句柄值在全局句柄表中得到对应的句柄项_HANDLE_TABLE_ENTRY。

通过windbg查看上述测试程序DebugTest.exe(pid为0x142c)的进程句柄_HANDLE_TABLE_ENTRY。

通过pid: 0x142c对应的进程句柄项_HANDLE_TABLE_ENTRY.ObjectPointerBits的值为0xd58f221c028,得到此句柄对应的进程内核对象地址为 (0xd58f221c028 << 4) & 0xFFFF000000000000 == 0xffffd58f221c0280。查看对应的内核对象dt _EPROCESS 0xffffd58f221c0280,_EPROCESS.ImageFileName == “DebugTest.exe”

所以全局句柄表和私有句柄表的区别就是,私有句柄表项
_HANDLE_TABLE_ENTRY.ObjectPointerBits指向_OBJECT_HEADER,而全局句柄表是指向Object本省。

内核句柄表

System进程的句柄表就是内核句柄表,所谓的内核句柄表就是内核中使用的句柄。通过windbg的!handle可以查看当前进程上下文中所有的句柄信息(包括句柄对应的内核对象的信息)。

应用层查看句柄

可以通过process exploere查看某个进程的句柄,如查看DebugTest.exe的句柄可以发现其通过OpenProcess返回的句柄信息。

也可以通过microsoft的sysinternals工具包里的handle64.exe,它显示所有进程的句柄信息。大佬通过仿照handle64.exe实现了用户层进程获取其他进程的所有句柄信息,GitHub ( https://github.com/SinaKarvandi/Process-Magics/tree/master/EnumAllHandles

利用句柄表保护进程

针对进程私有句柄表

上图使用OpenProcess打开进程的时候使用了PROCESS_ALL_ACCESS权限,得到的句柄对应在内核中的句柄项_HANDLE_TABLE_ENTRY的GrantedAccessBits 值为0x1fffff。在使用此句柄去操作进程时操作系统都会判断此权限的值,如果将指向被保护进程的所有句柄的GrantedAccessBits都置零(句柄降权)就可以达到保护进程的目的。可以手动查找,也可以通过windows内部导出但未文档化的ExEnumHandleTable函数可以枚举EPROCESS中的所有句柄。

针对全局句柄表

OpenProcess通过pid打开一个进程时的调用流程为:

OpenProcess()
 ├─ NtOpenProcess(进入内核层)
 │  └─ PsOpenProcess
 │      └─ PsLookupProcessByProcessId­(从全局句柄表PspCidTable获取对应的进程内核对象EPROCESS地址)
 |            ObOpenObjectByPointer
 │          └─ ObpCreateHandle
 │               └─ ObpIncrementHandleCountEx
 │                   └─ _OBJECT_TYPE->TypeInfo.OpenProcedure(PspProcessOpen)  
 │                    ObpPreInterceptHandleCreate
 |                     创建句柄项并设置句柄的值
 │____________________ObpPostInterceptHandleCreate

其中PsLookupProcessByProcessId­会调用PspReferenceCidTableEntry。反编译分析PspReferenceCidTableEntry函数发现其首先会调用ExpLookupHandleTableEntry通过pid从PspCidTable全局句柄表中得到句柄项_HANDLE_TYPE_ENTRY,然后得到_HANDLE_TYPE_ENTRY的低8个字节。


最后ExpLookupHandleTableEntry会得到_HANDLE_TYPE_ENTRY的GrantedAccessBits的值并通过运算 (GrantedAccessBits << 4) ^ 0xFFFF000000000000 得到对应的EPROCESS进程内核对象地址。

接着回到PsOpenProcess函数中,其会继续调用ObOpenObjectByPointer函数会调用ObpCreateHandle并根据刚才获取到的进程内核对象EPROCESS创建与之相关联的进程私有句柄。然后最后返回到应用进程中,利用此私有句柄操作被保护进程。所以如果将全局句柄表PspCidTable中被保护进程pid对应的句柄项抹去,那么其他进程通过pid调用OpenProcess时就无法从全局句柄表中得到对应的句柄项以及与之相关联的进程内核对象,自然OpenProcess无法得到一个私有句柄也无法操作被保护进程。

参考:
https://rayanfam.com/topics/reversing-windows-internals-part1/#objecttypes-in-windows
https://github.com/SinaKarvandi/Process-Magics
https://tttang.com/archive/1682/#toc__3
https://www.modb.pro/db/184529
https://bbs.pediy.com/thread-272049.htm#msg_header_h2_5

posted @ 2022-11-27 21:00  怎么可以吃突突  阅读(917)  评论(1编辑  收藏  举报