Windows内核对象(1) -- 内核对象与句柄

举报
winsoft666 发表于 2019/01/23 11:52:55 2019/01/23
【摘要】 一、什么是内核对象我们在windows开发中经常会遇到内核对象,如事件(Event),管道(Pipe),互斥量(Mutex),完成端口(IOCP),进程(Process),线程(Thread)等,他们都是内核对象。这些内核对象虽然通过不同的系统API来创建,但这些API都有一个共同特点,就是都需要传入SECURITY_ATTRIBUTES安全描述符结构体指针,并且返回句柄(HANDLE)。依...

一、什么是内核对象

我们在windows开发中经常会遇到内核对象,如事件(Event),管道(Pipe),互斥量(Mutex),完成端口(IOCP),进程(Process),线程(Thread)等,他们都是内核对象。这些内核对象虽然通过不同的系统API来创建,但这些API都有一个共同特点,就是都需要传入SECURITY_ATTRIBUTES安全描述符结构体指针,并且返回句柄(HANDLE)。依据这个特点,我们有一个简单方法来判断对象是否是内核对象,就是看创建它的函数是否允许传入SECURITY_ATTRIBUTES安全描述符。


二、内核对象的创建

大多数创建内核对象的系统API函数,如CreateEvent, CreateMutex, CreateThread, CreateProcess, CreatePipe, CreateNamedPipe等都会返回一个HANDLE(无论是以返回值的形式,还是以指针参数的形式返回),创建内核对象成功时HANDLE为非NULL,我们可以通过将HANDLE的值与NULL进行比较,来判断函数是否执行成功。但是有些函数比较例外,如CreateFile,这些函数执行失败时,返回的HANDLE的值为INVALID_HANDLE_VALUIE。



三、内核对象的访问

虽然内核对象属于系统内核,但创建函数返回的HANDLE句柄却只和当前进程有关,离开了当前进程这个句柄也就失去了意义。 

内核对象属于系统内核级别,为了系统安全性,Windows不允许我们直接访问内核对象的内存区域,只允许我们通过Windows提供的一系列API来访问内核对象,如SetEvent, ResetEvent等等,使用这些函数时我们都会用到HANDLE,windows头文件中HANDLE的定义如下:


typedef void *HANDLE;


虽然定义为void*类型,但很显然这个HANDLE不是指向内核对象的指针。



如何证明HANDLE不是指向内核对象的指针? 

一方面直接执行内核对象毫无安全性可言; 

另一方面内核对象保存在内核地址空间(32位系统是0x80000000 到 0xFFFFFFFF,64位系统是0x00000040 00000000到0xFFFFFFFF FFFFFFFF),我们可以调用类似CreateEvent的函数创建一个内核对象,观察其返回的HANDLE,明显不在内核地址空间的范围内,且值一般比较小。


那么这个HANDLE句柄是如何与内核对象关联起来的了?答案是:进程的句柄表。 
每个进程在初始化的时候,系统都会为它分配一个句柄表(Windows没有提供官方的文档来介绍句柄表),参考《Windows核心编程》得知句柄表的结构,如图:



索引 指向内核对象内存块的指针 访问掩码 标志

1 0x???????? 0x???????? 0x????????

2 0x???????? 0x???????? 0x????????

如我们调用类似CreateEvent的函数返回的句柄HANDLE就是句柄表中的索引。因为是索引,所以它的值一般比较小。我们向windows API函数传入这个索引,API再通过索引找到对应的内核对象指针。


四、内核对象的销毁

4.1 引用计数

内核对象的所有者是操作系统内核,而不是创建它的进程。


多个进程可以引用(使用)同一个内核对象,操作系统使用了计数器的方式来管理内核对象(这个和C++中的std::shared_ptr智能指针类似),一个内核对象其实有两个计数器:一个是给用户态(Ring3)用的句柄计数;另一个是指针计数,也叫引用计数,因为核心态程序(Ring0)也经常用到内核对象,为了使用方便,在核心态的代码用指针直接访问对象,所以内核对象的管理器也维护了这个指针引用计数。只有在内核对象的句柄计数和引用计数都为0时,该内核对象才被释放。一般而言,指针引用计数值比句柄计数值大。


4.2 正确的销毁方式

当程序不再使用内核对象时,需要调用CloseHandle将内核对象的计数减1,这样系统内核在该对象计数为0时(也就是没有被任何东西引用时)将销毁该对象。 并且在调用CloseHandle之后,程序还应该将HANDLE置为NULL。


如果CloseHandle之后不将HANDLE置为NULL,反而再次使用该HANDLE,就会出现2种情况:


进程句柄表中该HANDLE所在的索引项的记录已经被清除,且没有别的线程再次在该索引创建记录项,若此时使用这个过期的HANDLE调用Win32 API函数,Windows会返回无效参数错误。这种情况还比较好调试。


进程句柄表中该HANDLE所在的索引项的记录同样也已经被清除,但已经有别的线程(该进程中的其他线程)在该索引位置创建了记录项,若此时使用这个过期的HANDLE调用Win32 API函数,该HANDLE就会引用到其他线程新建的那个内核对象,从而出现一些难以预料的错误。这种错误很难调试。


4.3 获取内核对象的引用计数

虽然windows没有提供API让用户在用户态(Ring3)查询一个内核对象的句柄计数和引用计数,但我们可以从Ntdll.dll导出NtQueryObject函数来实现查询内核对象的当前状态(该函数没有被文档化)。


NtQueryObject函数声明如下:

// 返回值:如果成功则返回0
//
DWORD WINAPI NtQueryObject(
HANDLE   handle,       // 待查询的句柄
DWORD   nQueryIndex,   // 0为查询对象的当前状态,包括句柄计数,引用计数等等。
VOID*   pOutBuffer,    // 存放查询结果
DWORD   cbInBufferSize,   // pOutBuffer的大小,如果nQueryIndex为0,这里为sizeof(SYSTEM_HANDLE_STATE)
VOID*   cbOutBufferSize   // 实际大小
);


NtQueryObject函数调用的细节封装到GetKernelObjectRefCount函数中,方便使用:

bool GetKernelObjectRefCount(HANDLE handle, DWORD &handle_count, DWORD &point_count) {
    typedef struct _SYSTEM_HANDLE_STATE {
        DWORD   r1;
        DWORD   GrantedAccess;
        DWORD   HandleCount;      // 减1为句柄计数 
        DWORD   ReferenceCount;   // 减1为指针引用计数 
        DWORD   r5;
        DWORD   r6;
        DWORD   r7;
        DWORD   r8;
        DWORD   r9;
        DWORD   r10;
        DWORD   r11;
        DWORD   r12;
        DWORD   r13;
        DWORD   r14;
    }SYSTEM_HANDLE_STATE, *PSYSTEM_HANDLE_STATE;

    typedef DWORD(WINAPI *PFN_NtQueryObject)(HANDLE handle,
        DWORD nQueryIndex,
        VOID* pOutBuffer,
        DWORD cbInBufferSize,
        VOID* cbOutBufferSize);

    static PFN_NtQueryObject pfnNtQueryObject = NULL;
    bool ret = false;

    do {
        if (pfnNtQueryObject == NULL) {
            HMODULE ntdll = GetModuleHandle(TEXT("Ntdll.dll"));
            if (ntdll == NULL)
                break;

            pfnNtQueryObject = (PFN_NtQueryObject)GetProcAddress(ntdll, "NtQueryObject");
            if (pfnNtQueryObject == NULL)
                break;
        }

        SYSTEM_HANDLE_STATE sys_handle_state;
        memset(&sys_handle_state, 0, sizeof(SYSTEM_HANDLE_STATE));

        DWORD out_buf_size = 0;
        ret = (pfnNtQueryObject(handle, 0, &sys_handle_state, sizeof(SYSTEM_HANDLE_STATE), &out_buf_size) == 0);
        if (ret) {
            handle_count = sys_handle_state.HandleCount - 1;
            point_count = sys_handle_state.ReferenceCount - 1;
        }
    } while (false);

    return ret;
}


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。