跳转至

0x01. “一切皆对象”

类似于 NIX 系统中一切皆文件的哲学,在 Windows NT kernel 当中同样有一切都是一个对象(object)的说法,文件、设备、同步机制、注册表项等在内核中都表示为对象*,每个对象有一个 header 存储对象基本信息(如名字、类型、位置),以及一个 body 存储数据

NT kernel 中的对象可以分为两类:

  • 执行体对象(Executive Objects):执行体对外暴露的资源对象(如进程、线程、文件等),对用户态可见
  • 内核对象(Kernel Objects):最基本的资源对象(如物理设备等),对用户态不可见

注:纯用户态对象不在本文讨论范围内,例如归属不同用户态子系统如 GDI、User32 管理的 GDI 对象、窗口对象等

一、_OBJECT_HEADER:对象基本信息

在 NT kernel 中 _OBJECT_HEADER 结构体用来存储对象的基本信息,定义如下:

kd> dt nt!_OBJECT_HEADER
   +0x000 PointerCount     : Int8B
   +0x008 HandleCount      : Int8B
   +0x008 NextToFree       : Ptr64 Void
   +0x010 Lock             : _EX_PUSH_LOCK
   +0x018 TypeIndex        : UChar
   +0x019 TraceFlags       : UChar
   +0x019 DbgRefTrace      : Pos 0, 1 Bit
   +0x019 DbgTracePermanent : Pos 1, 1 Bit
   +0x01a InfoMask         : UChar
   +0x01b Flags            : UChar
   +0x01b NewObject        : Pos 0, 1 Bit
   +0x01b KernelObject     : Pos 1, 1 Bit
   +0x01b KernelOnlyAccess : Pos 2, 1 Bit
   +0x01b ExclusiveObject  : Pos 3, 1 Bit
   +0x01b PermanentObject  : Pos 4, 1 Bit
   +0x01b DefaultSecurityQuota : Pos 5, 1 Bit
   +0x01b SingleHandleEntry : Pos 6, 1 Bit
   +0x01b DeletedInline    : Pos 7, 1 Bit
   +0x01c Reserved         : Uint4B
   +0x020 ObjectCreateInfo : Ptr64 _OBJECT_CREATE_INFORMATION
   +0x020 QuotaBlockCharged : Ptr64 Void
   +0x028 SecurityDescriptor : Ptr64 Void
   +0x030 Body             : _QUAD

一个对象除了固定的 _OBJECT_HEADER 以外还可以有额外的 optional header 来存储额外的信息,对应的 optional header 是否存在由 _OBJECT_HEADER->InfoMask 掩码决定:

_OBJECT_HEADER->InfoMask 掩码中存在对应位时则表示 对应的 optional header 存在 ,由于 optional header 的存储顺序固定,NT kernel 可以很容易计算出不同 header 对应的偏移,掩码与 header type & size 对应关系如下:

Bit Type Size (on X86)
0x01 nt!_OBJECT_HEADER_CREATOR_INFO 0x10
0x02 nt!_OBJECT_HEADER_NAME_INFO 0x10
0x04 nt!_OBJECT_HEADER_HANDLE_INFO 0x08
0x08 nt!_OBJECT_HEADER_QUOTA_INFO 0x10
0x10 nt!_OBJECT_HEADER_PROCESS_INFO 0x08

NT kernel 19H1 版本以前,Windows 内核使用 Pool Allocator ,此时对于每个分配的内核池对象都有一个 _POOL_HEADER 结构体存储相应的信息:

kd> dt nt!_POOL_HEADER
   +0x000 PreviousSize     : Pos 0, 8 Bits
   +0x000 PoolIndex        : Pos 8, 8 Bits
   +0x002 BlockSize        : Pos 0, 8 Bits
   +0x002 PoolType         : Pos 8, 8 Bits
   +0x000 Ulong1           : Uint4B
   +0x004 PoolTag          : Uint4B
   +0x008 ProcessBilled    : Ptr64 _EPROCESS
   +0x008 AllocatorBackTraceIndex : Uint2B
   +0x00a PoolTagHash      : Uint2B

由此我们可以得到一个内核对象的基本结构如下图所示:

二、对象管理器与命名空间

NT kernel 中的对象通过对象管理器(Object Manager)进行管理,用户态对这些对象的访问通常需要通过对象管理子系统,对象管理器主要负责如下任务:

  • 管理对象的创建与销毁
  • 维护对象命名空间数据库
  • 追踪特定对象的访问权限
  • 追踪特定对象的引用计数
  • 管理对象的生命周期

对象可以归属于不同的命名空间(Namespace),NT Kernel 为这些 命名的内核对象 提供了多个命名空间:事件、信号量、互斥量、可等待计时器、文件映射对象、工作对象和符号链接对象

有一些内核对象不是命名的,例如线程对象通常是匿名的

不同的用户会话(user session)也是不同的命名空间,对象创建时默认归属于当前会话的命名空间,此外还有一个全局共享的全局命名空间,从而使得对象可以在不同会话间进行共享

创建对象时可以通过 "Global\" 前缀来将该对象创建于全局命名空间中,例如通过如下代码可以创建一个属于全局命名空间的名为 ARTTNBA3 的事件对象:

CreateEvent( NULL, FALSE, FALSE, "Global\\ARTTNBA3" );

也可以通过 “Local\” 前缀显式说明对象创建在会话命名空间中,需要注意的是全局命名空间不适用于 Windows Store 的应用

三、句柄(handler)

有点类似于 Linux 下文件描述符的概念,但是扩展到了大部分内核对象

非常不知所谓的翻译,笔者个人感觉翻译成 引用符 可能会更好一些

句柄(handler)是 Windows 下 用户态程序用来管理对应内核对象的一个对象描述符 ,表示形式为一个 值为 4 的倍数的 整数值(32/64 位系统中为 32/64 位整数),用户程序通过其拥有的句柄可以访问内核中相对应的内核对象

正如我们前面所言,NT Kernel 中有 “一切皆对象” 的概念,而这对于用户态应用程式而言实际上就意味着 “一切皆句柄”

在 WinDbg 当中,我们可以使用 !handle 命令查看当前进程所拥有的句柄:

在 NT Kernel 当中使用 _HANDLE_TABLE_ENTRY 结构体表示一个句柄,在 64 位系统中大小为 16 字节,定义如下(摘自 VERGILIUS - Windows 11 22H2 ):

//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
}; 

虽然是个大联合体,我们实际上使用到的是如下部分:

  • Unlocked:用来锁定/解锁句柄
  • RefCnt:句柄的引用计数的(注:这个字段据称是 反向计数 ,不过笔者尚未进行验证)
  • Attributes :句柄属性,分三个 bit Audit (关闭句柄时是否触发日志)、 Inherit (是否将句柄复制到 Inheritance==True 的子进程中) 、Protected (是否可以关闭句柄)
  • ObjectPointerBits :内核对象的地址
  • GrantedAccessBits:句柄的访问权限
  • NoRightsUpgrade
  • Spare1 && Spare2:仅用作填充

句柄通过 句柄表 进行管理,在 NT Kernel 当中使用 _HANDLE_TABLE 结构体进行表示,定义如下(摘自 VERGILIUS - Windows 11 22H2 ):

//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
        };
    };
}; 

在 Windows 中一共有以下几种存储对象信息的句柄表:

  • Windows NT kernel 中所有对象的句柄都存放在一个全局的句柄表当中
  • 每个进程有其独立的一个句柄表(其地址存放在进程控制块的 _EPROCESS->_HANDLER_TABLE->TableCode 中),进程所获取到的对象句柄值实际上是【该表对应项的下标索引值x4】

NT Kernel 中的句柄条目实际上以 结构体数组 形式存放在句柄表当中,每个句柄数组的大小应当为单张页面大小,在句柄表的 _HANDLE_TABLE::TableCode 字段中存放着句柄数组第一个条目的地址,其中 第 0 项为保留项 ,句柄表项为 _HANDLE_TABLE_ENTRY 结构,其低 44 位值左移 4 位加上 0xffff000000000000 便是对象头地址:

自己画的图

在 Windows 7 当中进程句柄表实际上仅简单存放对象的地址,但是到了高版本内核一切都开始变得复杂了起来:(

与此同时 TableCode 使用低 4 位标识句柄表的结构层次——即进程句柄表实际上可以有多层结构,类似于页表,仅有最后一层存放对象地址,其他层都用于存放表地址,每张表的大小为 4096 字节(一张内存页大小):

自己画的图