Windows11_22H2下的句柄与句柄表分析

简述

Windows在内核使用对象,而在用户模式下,为了保证安全,使用句柄。
进程创建或打开一个内核对象时,会获得一个句柄,通过这个句柄可以查找到对应的内核对象。

用户态下可以通过ntdll!NtQuerySystemInformation遍历句柄,也可以查到句柄对应的内核对象地址。

私有句柄表

私有句柄表是基于进程的,每个进程都有私有句柄表。

位于_EPROCESS+0x570处。

nt!_EPROCESS
   ...
   +0x570 ObjectTable      : Ptr64 _HANDLE_TABLE
   ...

类型_HANDLE_TABLE

//0x80 bytes (sizeof)
struct _HANDLE_TABLE
{
    ULONG NextHandleNeedingPool;                                            //0x0
    LONG ExtraInfoPages;                                                    //0x4
    volatile ULONGLONG TableCode;                                           //0x8
    struct _EPROCESS* QuotaProcess;                                         //0x10
    struct _LIST_ENTRY HandleTableList;                                     //0x18
    ULONG UniqueProcessId;                                                  //0x28
    union
    {
        ULONG Flags;                                                        //0x2c
        struct
        {
            UCHAR StrictFIFO:1;                                             //0x2c
            UCHAR EnableHandleExceptions:1;                                 //0x2c
            UCHAR Rundown:1;                                                //0x2c
            UCHAR Duplicated:1;                                             //0x2c
            UCHAR RaiseUMExceptionOnInvalidHandleClose:1;                   //0x2c
        };
    };
    struct _EX_PUSH_LOCK HandleContentionEvent;                             //0x30
    struct _EX_PUSH_LOCK HandleTableLock;                                   //0x38
    union
    {
        struct _HANDLE_TABLE_FREE_LIST FreeLists[1];                        //0x40
        struct
        {
            UCHAR ActualEntry[32];                                          //0x40
            struct _HANDLE_TRACE_DEBUG_INFO* DebugInfo;                     //0x60
        };
    };
}; 

在windbg中定位句柄表

句柄1
句柄2
句柄3

_HANDLE_TABLE.TableCode 低两位清零后指向句柄表(低两位是判断几级句柄表的,下面会有解释)。

句柄表中每个项(联合体_HANDLE_TABLE_ENTRY)大小为0x10,一个表可以放0x1000 / 0x10 = 0x100 即256个项。那么如果进程中句柄的数量大于256个会怎么办呢? Windows采用了多级指针的方法,根据_HANDLE_TABLE.TableCode 的前两位判断,如果为0代表直接指向句柄表,第0位为1代表二级句柄表,前两位为1代表三级句柄表。
二级句柄表的意思是_HANDLE_TABLE.TableCode (低两位清零后)指向的地址上的值是句柄表地址数组,句柄表地址再指向含有句柄表项的地址。三级句柄表同理,只不过是多了一级。
句柄4

句柄5

句柄除4,是在句柄表中的索引。

句柄表项是一个联合体_HANDLE_TABLE_ENTRY,结构为:

//0x10 bytes (sizeof)
union _HANDLE_TABLE_ENTRY
{
    volatile LONGLONG VolatileLowValue;                                     //0x0
    LONGLONG LowValue;                                                      //0x0
    struct
    {
        struct _HANDLE_TABLE_ENTRY_INFO* volatile InfoTable;                //0x0
    LONGLONG HighValue;                                                     //0x8
    union _HANDLE_TABLE_ENTRY* NextFreeHandleEntry;                         //0x8
        struct _EXHANDLE LeafHandleValue;                                   //0x8
    };
    LONGLONG RefCountField;                                                 //0x0
    ULONGLONG Unlocked:1;                                                   //0x0
    ULONGLONG RefCnt:16;                                                    //0x0
    ULONGLONG Attributes:3;                                                 //0x0
    struct
    {
        ULONGLONG ObjectPointerBits:44;                                     //0x0
    ULONG GrantedAccessBits:25;                                             //0x8
    ULONG NoRightsUpgrade:1;                                                //0x8
        ULONG Spare1:6;                                                     //0x8
    };
    ULONG Spare2;                                                           //0xc
}; 

句柄表里的成员并不是内核对象,如果要得到内核对象还需要对其解密。
解密方法为对低八字节右移16位,然后&0xfffffffffffffff0,指向的是对象头(_OBJECT_HEADER),再加对象头的大小(0x30)就是内核对象的地址。

下面来用写的一个程序做验证。

代码如下:

#include <iostream>
#include <Windows.h>


int main() 
{

	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, GetCurrentProcessId());

	std::cout << "句柄:" << hProcess << std::endl;

	system("pause");
	return 0;
}

获取的句柄:10C。
句柄6

查找对应的内核对象:
句柄7

(0x98046a32d0500001>>0x10)&0xfffffffffffffff0=0xFFFF98046A32D050
FFFF98046A32D050+0x30=0xFFFF98046A32D080
句柄8
句柄9

No Problem!

除了可以通过句柄找到内核对象之外,还可以根据句柄表中的权限操作内核对象。
在句柄表项HANDLE_TABLE_ENTRY这个联合体中,修改对应的位(HANDLE_TABLE_ENTRY.GrantedAccessBits,高8字节的第0到第24位,第0位到第20位都为1就是PROCESS_ALL_ACCESS满权限。)就可以修改用户模式下句柄的权限。

内核对句柄权限的验证是通过ObReferenceObjectByHandle进行的。这个函数内部就是通过遍历句柄表实现的。

全局句柄表

如果把私有句柄表学明白了,那么学全局句柄表是很简单的。
全局句柄表中只有进程与线程的内核对象。
线程的TID以及进程的PID相当于“句柄”,可以理解为全局句柄表的索引。
全局句柄表位于全局变量PspCidTable中,也是一个_HANDLE_TABLE结构。
与私有句柄表不同的是,全局句柄表项解密后直接指向内核对象而不是对象头。

来查找一下Dbgiew.exe 进程。
句柄10
查找全局句柄表,并找到对应的表项。6852/4=1713,1713/256等于6余177,177转换为16进制为0xB1。
句柄11
句柄12

解密,(0xC38D32972080F703>>10)&0x&0xfffffffffffffff0=0xFFFFC38D32972080
句柄13

No Problem!

拓展

ObReferenceObjectByHandleWithTag分析

ObReferenceObjectByHandleWithTag 例程递增由指定句柄标识的对象的引用计数,并将四字节标记值写入对象以支持对象引用跟踪。

NTSTATUS ObReferenceObjectByHandleWithTag(
  [in]            HANDLE                     Handle,
  [in]            ACCESS_MASK                DesiredAccess,
  [in, optional]  POBJECT_TYPE               ObjectType,
  [in]            KPROCESSOR_MODE            AccessMode,
  [in]            ULONG                      Tag,
  [out]           PVOID                      *Object,
  [out, optional] POBJECT_HANDLE_INFORMATION HandleInformation
);

这个函数其实就是调用了ObpReferenceObjectByHandleWithTag,只不过加了层封装。
句柄14

ObpReferenceObjectByHandleWithTag分析

R3的ReadProcessMemory等函数进内核后会调用到ntoskrnl!MiReadWriteVirtualMemory,在内部操作内核对象时就调用了ObpReferenceObjectByHandleWithTag,判断权限是否充足,并接收到内核对象。
句柄15

_QWORD* WriteSize

首先局部变量赋值等操作。如果提供了writesize 则将writesize置0(可能中途退出,所以先设置成0,执行成功的话再设置对应值)。
判断传入的参数_Handle是否属于内核的句柄,如果属于内核的句柄,判断是否为当前进程句柄。
如果为_Handle是当前进程的句柄。 如果对象类型不是进程类型,则返回0xC0000024({类型错误}请求的操作所需的对象类型与请求中指定的对象类型不匹配。);如果是,则继续执行。如果(_DesiredAccessf&0xFFE00000)==0(没有内核的权限)或者_AccessMode为1(用户模式),则返回0xC0000022({访问被拒绝}进程请求访问对象,但未被授予这些访问权限。)。
句柄16

如果(_DesiredAccessf&0xFFE00000)!=0并且_AccessMode==0(内核模式),则继续执行。
如果参数_HandleInformation不为NULL时,将_HandleInformation->GrantedAccess设置为0x1FFFFF,将_HandleInformation->HandleAttributes设置为0。
如果全局变量ObpTraceFlags不为0,调用ObpPushStackInfo(ETW追踪对象操作,取调用栈中的所有返回地址,记录调用信息等。)。
随后,增加引用计数,如果引用计数小于等于1,也就是说之前小于等于0,那么会蓝屏(0x18,对象的引用计数对于对象的当前状态是非法的。);如果大于1,将参数_Object里的值设置为当前进程对象,然后返回0(成功状态)。
句柄17

判断参数_Handle是否是当前线程句柄。
如果_Handle是当前线程句柄。 如果对象类型不是线程类型,返回0xC0000024;如果是,继续执行。如果(_DesiredAccessf&0xFFE00000)==0(没有内核的权限)或者_AccessMode为1(用户模式),返回0xC0000022。
如果(_DesiredAccessf&0xFFE00000)!=0并且_AccessMode==0(内核模式),继续执行。如果参数_HandleInformation不为NULL时,将_HandleInformation->GrantedAccess设置为0x1FFFFF,将_HandleInformation->HandleAttributes设置为0。如果全局变量ObpTraceFlags不为0,调用ObpPushStackInfo。增加引用计数,如果引用计数小于等于1,也就是说之前小于等于0,那么会蓝屏(0x18);如果大于1,将参数_Object里的值设置为当前进程对象,然后返回0(成功状态)。这些操作与上面的基本相同
句柄18

再判断一下_AccessMode,如果为用户模式,返回0xC0000008(指定了无效的HANDLE。),因为在用户模式下是无法使用内核句柄的。
将句柄值跟0xFFFFFFFF80000000异或,开启内核APC,设置局部变量的值,随后跳转到LABEL_10处。
句柄19

接着来看,执行出了if判断(是否属于内核句柄)即不属于内核句柄的。如果MmVerifierData第八位为1并且为_AccessMode为内核模式,则会调用VfCheckUserHandle检查用户句柄。开启内核APC。判断当前进程是否为原始进程。
如果当前进程为原始进程,如果cr3的第58位是1,则跳转至LABEL_80处,调用KeLeaveCriticalRegionThread离开线程临界区,然后返回0xC0000008(指定了无效的HANDLE)。
如果当前进程不为原始进程,调用ObReferenceProcessHandleTable,进行了引用_EPROCESS.RundownProtect(可以简单的理解为:引用了_EPROCESS.RundownProtect后这个进程在没有释放前不能结束。对进程要进行什么操作的时候会先引用这个字段进行加保护,等操作结束后再进行解保护。看到有些资料解释为“停运保护”~ 也有点儿形象,哈哈。),获取当前进程的句柄表,判断获取的句柄表是否为0,如果为0,则返回0xC0000008。判断获得句柄表是否为内核句柄表,如果为内核句柄表,返回0xC0000008(如果正常用户模式的走到这儿,就不会等于内核句柄表,如果等于可能就是出了些非法情况)。
句柄20

随后,执行到LABEL_10处。不管是R0还是R3正常情况找到句柄表后都是执行到 LABEL_10。判断句柄是否是按四字节对齐的,调用ExpLookupHandleTableEntry(此函数比较重要,根据句柄表和句柄值来查询 _HANDLE_TABLE_ENTRY句柄表项,这里不作详细解释,建议自己去分析分析,挺有意思的。),判断返回值是否为0。
如果没有按四字节对齐或者返回值为0,如果句柄存在的话会调用一下ExHandleLogBadReference,然后返回0xC0000008。
句柄21

如果按四字节对齐并且返回了正常的句柄表项,预取句柄表项至缓存,将句柄表项的低八字节跟0x1FFFE做与运算,如果等于0,跳转至LABEL_45处;如果不等于0,则继续执行。
随后,进入while循环,while循环里面,判断句柄表项的Unlocked位 & 1 是否为0(是否被锁住),如果为0,被锁住,如果为1,则没有被锁住。 这个while循环,并不是那么重要,但讲起来吧,又比较麻烦,这个自己去看看吧…
句柄22

while(1)循环结束后,有个判断,这个判断个人感觉是判断的_HANDLE_TABLE_ENTRY.RefCnt成员,如果不等于0x10,会用(_HANDLE_TABLE_ENTRY.LowValue>> 16) & 0xFFFFFFFFFFFFFFF0这种解密算法(大多数情况下会走这里的),然后跳转到LABEL_17处;如果等于0x10,则会是另外一种解密方法,这种解密方法很少会走到这里,后面还进行了增加指针引用计数,刷新句柄表等操作,不详细讲解,感兴趣的话可以自己去分析分析,并不难。
句柄23

在LABEL_17处,判断全局变量ObpTraceFlags的值,检查Trace标志是否有效,不为0则调用ObpPushStackInfo(跟上面的这个操作类似)。
随后,对象类型索引解密。对象类型索引=全局变量ObHeaderCookie异或_OBJECT_HEADER.TypeIndex异或对象头地址的首个字节。操作系统把绝大部分的对象类型放在了ObTypeIndexTable表里,ObTypeIndexTable[Index] == 对应内核对象类型
之后,如果参数_ObjectType为空或者参数的索引与解密的索引不同,会根据索引在ObTypeIndexTable里获取对象类型,如果获取的对象类型为0或者是错误的指针值,会蓝屏(0x189,OBJECT_HEADER已损坏)。如果参数_ObjectType不为0(也就意味着数的索引与解密的索引不同),会进行解引用等操作,然后返回0xC0000024。
句柄24
随后,就是权限比较了。如果为内核模式,会直接跳转到LABEL_27处(也就是内核模式不会经过权限的验证);如果为用户模式,会根据句柄表项_HANDLE_TABLE_ENTRY.GrantedAccessBits去判断。先将_HANDLE_TABLE_ENTRY.HighValue & 0x1FFFFFF ,也就是得到句柄表项的GrantedAccessBits成员(高八字节低25位),将其取反后跟参数_DesiredAccess(渴望的权限)进行与操作,如果不为0(也就是权限不足),会进行解引用等操作后返回0xC0000022;如果等于0,则会继续执行。
句柄25

再然后,根据对象头的掩码信息做一些判断,说不重要吧,也挺有用,这里就不再讲了,建议自己去分析分析。
在分析本函数时,有些相对较不重要的地方省略没讲,强烈建议自己去分析分析!

  • 25
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值