欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【内核原理与实现 009】句柄结构体,PspCidTable,PID/CID的由来

程序员文章站 2022-07-13 10:52:06
...

一、句柄结构体

typedef struct _HANDLE_TABLE_ENTRY {
	union {
		PVOID Object;                          // 指向句柄所代表的对象
		ULONG ObAttributes;                    // 最低三位有特别含义,参见
		                                       // OBJ_HANDLE_ATTRIBUTES 宏定义
		PHANDLE_TABLE_ENTRY_INFO InfoTable;    // 各个句柄表页面的第一个表项
		                                       // 使用此成员指向一张表
		ULONG_PTR Value;
	};
	union {
		union {
			ACCESS_MASK GrantedAccess;         // 访问掩码
			struct {                           // 当NtGlobalFlag 中包含
			                                   // FLG_KERNEL_STACK_TRACE_DB 标记时使用
				USHORT GrantedAccessIndex;
				USHORT CreatorBackTraceIndex;
			};
		};
		LONG NextFreeTableEntry;               // 空闲时表示下一个空闲句柄索引
	};
} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;

低32位:

Object & ~7 是句柄对应的内核对象的地址,Object 的低3位有特殊用途:第0位 OBJ_PROTECT_CLOSE 表示是否允许关闭句柄,但通过宏 ObpEncodeProtectClose 和 ObpGetHandleAttributes 将此位转移到 GrantedAccess 成员中,然后第0位被用作表示句柄表项锁,置1表示此句柄表项被锁住;第1位 OBJ_INHERIT 表示是否拷贝父进程的句柄表;第2位 OBJ_AUDIT_OBJECT_CLOSE 表示关闭该对象时是否产生审计事件。

高32位:

当句柄不指向内核对象时,NextFreeTableEntry 表示句柄表中下一个空闲句柄位置,也可以理解为该进程创建新句柄时的句柄值;当句柄指向内核对象时,GrantedAccess 表示该句柄的访问掩码。

上一篇博客研究句柄表初始化时,已经分析过 ExpAllocateHandleTable 函数,最低层句柄表第0项是特殊项,其 NextFreeTableEntry 成员的值是 EX_ADDITIONAL_INFO_SIGNATURE,即−2,而在wrk源码中,第0项的 InfoTable 成员初始化为0(书上说第0项的 InfoTable 指向 HANDLE_TABLE_ENTRY_INFO,我没找到依据).

二、通过句柄找内核对象

此部分内容来自书上P133页,书上内容有错,我已纠正。

有效句柄有四种情况:

  • -1,表示当前进程
  • -2,表示当前线程
  • 负值,其绝对值为 System 进程的句柄表(ObpKernelHandleTable)中的索引
  • 不超过 226 的正值,表示在进程句柄表中的索引

解释下第四种(书上错印成226),为什么是2的26次方?因为一个进程最多只能有 224 个句柄,而句柄值需要乘以4,所以最大的句柄值就是 226 了。

解析句柄的函数是 ObReferenceObjectByHandle ,它可能会调用我在上一篇博客分析过的 ExpLookupHandleTableEntry 函数。

ObReferenceObjectByHandle

NTSTATUS
ObReferenceObjectByHandle (
    __in HANDLE Handle,
    __in ACCESS_MASK DesiredAccess,
    __in_opt POBJECT_TYPE ObjectType,
    __in KPROCESSOR_MODE AccessMode,
    __out PVOID *Object,
    __out_opt POBJECT_HANDLE_INFORMATION HandleInformation
    )

/*++

Routine Description:

    Given a handle to an object this routine returns a pointer
    to the body of the object with proper ref counts
    通过句柄找内核对象

Arguments:

    Handle - Supplies a handle to the object being referenced.  It can
        also be the result of NtCurrentProcess or NtCurrentThread
    句柄,用于查找内核对象

    DesiredAccess - Supplies the access being requested by the caller
    调用者指定的内核对象访问权限

    ObjectType - Optionally supplies the type of the object we
        are expecting
    可选地指定内核对象类型

    AccessMode - Supplies the processor mode of the access
    内核/用户

    Object - Receives a pointer to the object body if the operation
        is successful
    内核对象输出参数

    HandleInformation - Optionally receives information regarding the
        input handle.

Return Value:

    An appropriate NTSTATUS value

--*/

{
    ACCESS_MASK GrantedAccess;
    PHANDLE_TABLE HandleTable;
    POBJECT_HEADER ObjectHeader;
    PHANDLE_TABLE_ENTRY ObjectTableEntry;
    PEPROCESS Process;
    NTSTATUS Status;
    PETHREAD Thread;

    ObpValidateIrql("ObReferenceObjectByHandle");

    Thread = PsGetCurrentThread ();
    *Object = NULL;

    //
    // Check is this handle is a kernel handle or one of the two builtin pseudo handles
    // 检查是不是-1或-2
    //
    if ((LONG)(ULONG_PTR) Handle < 0) {
        //
        //  If the handle is equal to the current process handle and the object
        //  type is NULL or type process, then attempt to translate a handle to
        //  the current process. Otherwise, check if the handle is the current
        //  thread handle.
        // 
        
        if (Handle == NtCurrentProcess()) {
            // 如果句柄值是-1,即当前进程

            // 检查类型是否合法
            if ((ObjectType == PsProcessType) || (ObjectType == NULL)) {                

                // 获取当前进程内核对象
                Process = PsGetCurrentProcessByThread(Thread);
                GrantedAccess = Process->GrantedAccess;

                // SeComputeDeniedAccesses 检查权限,返回0表示权限检查通过;
                // 如果是内核调用,则不需要检查权限
                if ((SeComputeDeniedAccesses(GrantedAccess, DesiredAccess) == 0) ||
                    (AccessMode == KernelMode)) {

                    // 获取对象头
                    ObjectHeader = OBJECT_TO_OBJECT_HEADER(Process);

                    // 如果指定了输出参数 HandleInformation ,就填入一些信息
                    if (ARGUMENT_PRESENT(HandleInformation)) {

                        HandleInformation->GrantedAccess = GrantedAccess;
                        HandleInformation->HandleAttributes = 0;
                    }

                    // 指针计数加一
                    ObpIncrPointerCount(ObjectHeader);

                    // 返回进程内核对象
                    *Object = Process;

                    ASSERT( *Object != NULL );

                    Status = STATUS_SUCCESS;

                } else {

                    Status = STATUS_ACCESS_DENIED;
                }

            } else {

                Status = STATUS_OBJECT_TYPE_MISMATCH;
            }

            return Status;

        //
        //  If the handle is equal to the current thread handle and the object
        //  type is NULL or type thread, then attempt to translate a handle to
        //  the current thread. Otherwise, the we'll try and translate the
        //  handle
        //

        } else if (Handle == NtCurrentThread()) {
            // 如果句柄值是-2,即当前线程,和进程的做法类似
            if ((ObjectType == PsThreadType) || (ObjectType == NULL)) {

                GrantedAccess = Thread->GrantedAccess;

                if ((SeComputeDeniedAccesses(GrantedAccess, DesiredAccess) == 0) ||
                    (AccessMode == KernelMode)) {

                    ObjectHeader = OBJECT_TO_OBJECT_HEADER(Thread);

                    if (ARGUMENT_PRESENT(HandleInformation)) {

                        HandleInformation->GrantedAccess = GrantedAccess;
                        HandleInformation->HandleAttributes = 0;
                    }

                    ObpIncrPointerCount(ObjectHeader);
                    *Object = Thread;

                    ASSERT( *Object != NULL );

                    Status = STATUS_SUCCESS;

                } else {

                    Status = STATUS_ACCESS_DENIED;
                }

            } else {

                Status = STATUS_OBJECT_TYPE_MISMATCH;
            }

            return Status;

        } else if (AccessMode == KernelMode) {
            //
            //  Make the handle look like a regular handle
            //

            // 如果句柄是负数且是内核模式,就取句柄绝对值
            Handle = DecodeKernelHandle( Handle );

            //
            //  The global kernel handle table
            // 句柄值是负数,已经改成其绝对值了,从内核句柄表 ObpKernelHandleTable 里找

            HandleTable = ObpKernelHandleTable;
        } else {
            //
            // The previous mode was user for this kernel handle value. Reject it here.
            //

            // 用户模式不能访问系统进程句柄,返回非法句柄错误
            return STATUS_INVALID_HANDLE;
        }

    } else {
        // 获取当前进程的句柄表
        HandleTable = PsGetCurrentProcessByThread(Thread)->ObjectTable;
    }
    
    // 此时 HandleTable 可能是普通进程句柄表,也可能是系统进程句柄表(ObpKernelHandleTable)

    ASSERT(HandleTable != NULL);

    //
    // Protect this thread from being suspended while we hold the handle table entry lock
    // 加锁的目的是防止我们操作句柄表时线程被挂起
    // 

    KeEnterCriticalRegionThread(&Thread->Tcb);

    //
    //  Translate the specified handle to an object table index.
    // 通过句柄表找句柄表项,ExMapHandleToPointerEx 底层调用了 ExpLookupHandleTableEntry
    //

    ObjectTableEntry = ExMapHandleToPointerEx ( HandleTable, Handle, AccessMode );

    //
    //  Make sure the object table entry really does exist
    // 确保句柄表项确实存在

    if (ObjectTableEntry != NULL) {

        // 获取内核对象头
        ObjectHeader = (POBJECT_HEADER)(((ULONG_PTR)(ObjectTableEntry->Object)) & ~OBJ_HANDLE_ATTRIBUTES);

        //
        // Optimize for a successful reference by bringing the object header
        // into the cache exclusive.
        // 优化:将内核对象弄到 cache 里,在wrk里,这个宏没啥用,但在后续windows里是一种优化手段
        // 参考 _m_prefetch

        ReadForWriteAccess(ObjectHeader);

        // 检查类型是否合法
        if ((ObjectHeader->Type == ObjectType) || (ObjectType == NULL)) {

            // NtGlobalFlag 和调试有关,我不懂,2020年12月8日23:32:49
#if i386 
            if (NtGlobalFlag & FLG_KERNEL_STACK_TRACE_DB) {

                GrantedAccess = ObpTranslateGrantedAccessIndex( ObjectTableEntry->GrantedAccessIndex );

            } else {

                GrantedAccess = ObpDecodeGrantedAccess(ObjectTableEntry->GrantedAccess);
            }
#else
            GrantedAccess = ObpDecodeGrantedAccess(ObjectTableEntry->GrantedAccess);

#endif // i386 

            // SeComputeDeniedAccesses 返回0表示权限检查通过
            if ((SeComputeDeniedAccesses(GrantedAccess, DesiredAccess) == 0) ||
                (AccessMode == KernelMode)) {

                PHANDLE_TABLE_ENTRY_INFO ObjectInfo;

                ObjectInfo = ExGetHandleInfo(HandleTable, Handle, TRUE);

                //
                //  Access to the object is allowed. Return the handle
                //  information is requested, increment the object
                //  pointer count, unlock the handle table and return
                //  a success status.
                // 
                // 允许访问内核对象。如果需要,返回句柄信息。增加指针引用计数,
                // 解锁句柄表并返回成功。
                //
                //  Note that this is the only successful return path
                //  out of this routine if the user did not specify
                //  the current process or current thread in the input
                //  handle.
                // 
                // 除了获取当前进程线程以外,这是唯一的成功返回执行路径。
                //

                // 可选地返回句柄信息
                if (ARGUMENT_PRESENT(HandleInformation)) {

                    HandleInformation->GrantedAccess = GrantedAccess;
                    HandleInformation->HandleAttributes = ObpGetHandleAttributes(ObjectTableEntry);
                }

                //
                //  If this object was audited when it was opened, it may
                //  be necessary to generate an audit now.  Check the audit
                //  mask that was saved when the handle was created.
                // 
                // 如果内核对象审计选项开启,可能需要生成审计。检查句柄创建时保存的审计掩码
                //
                //  It is safe to do this check in a non-atomic fashion,
                //  because bits will never be added to this mask once it is
                //  created.
                //
                // 这里无须原子操作,因为掩码从初始化后就不变了
                //

                if ( (ObjectTableEntry->ObAttributes & OBJ_AUDIT_OBJECT_CLOSE) &&
                     (ObjectInfo != NULL) &&
                     (ObjectInfo->AuditMask != 0) &&
                     (DesiredAccess != 0)) {

                      
                      ObpAuditObjectAccess( Handle, ObjectInfo, &ObjectHeader->Type->Name, DesiredAccess );
                }

                ObpIncrPointerCount(ObjectHeader);

                ExUnlockHandleTableEntry( HandleTable, ObjectTableEntry );

                KeLeaveCriticalRegionThread(&Thread->Tcb);

                *Object = &ObjectHeader->Body;

                ASSERT( *Object != NULL );

                return STATUS_SUCCESS;

            } else {

                Status = STATUS_ACCESS_DENIED;
            }

        } else {

            Status = STATUS_OBJECT_TYPE_MISMATCH;
        }

        ExUnlockHandleTableEntry( HandleTable, ObjectTableEntry );

    } else {

        Status = STATUS_INVALID_HANDLE;
    }

    KeLeaveCriticalRegionThread(&Thread->Tcb);


    return Status;
}

三、插入句柄表

将一个对象插入到句柄表中的函数是ObInsertObject,其代码位于base\ntos\ob
obinsert.c 文件中。它首先对参数做各种检查,然后调用ObpCreateHandle 函数,为要插入
的对象创建一个句柄。ObpCreateHandle 函数又调用ExCreateHandle 来创建一个句柄表项,
并填充新建的表项。这些函数的代码比较直截了当,这里不再赘述。

四、对象计数

另外值得一提的是对象的引用计数。正如2.5.1 节的最后部分所讲,对象的引用有两
种来源:第一,在内核中直接通过对象地址来引用,这是通过ObReferenceObjectByPointer
来记录一次新的引用;第二,通过句柄来引用对象,这是由ObpIncrementHandleCount 函
数来检查并记录一次句柄引用。对于一个句柄,它的生命周期从被插入到句柄表中开始,
一直到它被关闭,在此过程中,对象的引用计数中包含有它的一份引用。在WRK 代码中,
我们可以看到,ObpCreateHandle 调用了ObpIncrementHandleCount,标志着新创建的句柄
引用了该对象;ObpCloseHandle 函数调用了ObpCloseHandleTableEntry,而后者又进一步
调用了ObpDecrementHandleCount,标志着一个句柄结束了相应对象的引用。另外,在一
个句柄上调用了ObReferenceObjectByHandle 函数以后,若该对象指针不再使用,则必须
调用ObDereferenceObject 函数。

五、进程ID、线程ID

创建进程和线程时,会调用 ExCreateHandle 在全局句柄表 PspCidTable 中创建一个句柄,句柄会指向相应的线程或进程内核对象,而句柄值就是进程ID或线程ID,这也就解释了为什么PID都是4的倍数,也确保了PID/CID的唯一性。

之前提到过,PspCidTable 是唯一一张遵循 FIFO 规则的句柄表,举例说明,PID=100的线程被杀死后,要等后续的空闲句柄项都用过一遍,它才能重新使用,就像一个队列一样。

PsLookupProcessThreadByCid、PsLookupProcessByProcessId 和PsLookupThreadByThreadId 函数可以很方便的从 PspCidTable 里找到进程/线程。