句柄
今天来介绍一个熟悉的概念,句柄。早在Win32编程时,海哥曾提到,我们不必去理解句柄是什么,只要记住它是一个4字节的DWORD数即可。但是到了内核层面,就有必要深入了解一下这个概念了。
一直以来,我们接触到的句柄主要分为两种:
1)窗口句柄
1 | HWND hwnd = FindWindow(NULL,"计算器"); |
例如上面这个代码,就是从桌面获取到一个已经打开了的窗口的句柄,这个句柄相当于这个窗口的编号,通过这个编号,便可以快速访问到这个窗口。
2)内核句柄
1 | HANDLE g_hMutex = CreateMutex(NULL,FALSE, "XYZ"); |
窗口句柄是作用于窗口对象的,内核句柄则是对应了一个内核对象,这也是通常而言我们所说的句柄。正如上面例子所示,当一个进程创建或者打开一个内核对象时,将获得一个句柄(相当于内核对象的一个编号),通过这个句柄可以访问内核对象(例如进程,线程,互斥体等)。
为什么要有句柄
前面提到过,句柄相当于内核对象的编号,3环的程序可以根据这个编号,来使用这个内核对象。这样就避免了3环程序直接访问到内核对象;由于内核对象位于0环,如果3环的程序可以直接访问内核对象,一旦访问到无效的内核地址,就会导致蓝屏
句柄表
在认识了句柄之后,来了解一下句柄表。前面提到了,当一个进程创建或者打开一个内核对象时,将获得一个句柄;那么当这个进程,打开或创建多个内核对象时,就会获得多个句柄,这些句柄,就会存到一个叫做句柄表的结构里。先来看看句柄表这个结构在哪。
句柄表的位置
在_EPROCESS结构体+0xC4偏移处,有一个字段ObjectTable,其指向了一个HANDLE_TABLE的结构体,这就是句柄表结构体。
各个字段具体含义如下:
1 | +0x000 TableCode :ULONG_PTR //指向句柄表的存储结构 |
本篇主要关注TableCode这个字段,其余字段在用到时会再作分析。TableCode它指向了一个表,这个表存储了当前进程所打开的所有句柄。下面来看一个例子:
在虚拟机中,先打开应用程序计算器,然后运行如下代码:
1 |
|
在这个代码中,我们会在程序中重复10次打开计算器 根据打印的结果来看,可以发现,尽管是打开的是同一个进程,但是每次打开时的句柄都不一样。下面我们来根据句柄表的位置来查找一下这些句柄:
首先,在Windbg中,根据进程结构体地址,确定句柄表结构体所在位置
最后,根据句柄表存储结构的地址,找到当前进程所打开的别的进程的句柄
这里有一点需要说明一下,在应用层,句柄是一个四字节的数,但是在内核存储时,句柄表的每个成员是8字节,因此在查询的时候,3环得到句柄的值需要先除以4再乘8,才能在句柄表中找到对应的句柄(3环句柄相当于是一个查询的编号)。现在我们知道如何通过进程的句柄表找到其打开进程的句柄。现在我们来看看0环中这个句柄的结构。
句柄的结构
先来看看句柄的结构是如何定义的(取自潘爱民老师的Windows内核原理与实现)
1 | typedef struct _HANDLE_TABLE_ENTRY |
直接看结构定义的话,有太多的联合体,看着比较费劲,下面用一张图来解释一下(图片取自张嘉杰的笔记)
句柄共8字节,64位,主要分为4个部分,接下来按照标记依次介绍:
- (bit48~bit63)这一块共计两个字节,16位;高位字节是给SetHandleInformation这个函数用的,例如当执行如下语句:
1 | SetHandleInformation(Handle, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE); |
这个位置将会被写入0x02
(bit32~bit47)这一块也是两个字节,16位;这块是访问掩码,是给OpenProcess这个函数用的,即OpenProcess的第一个参数 DWORD dwDesiredAccess的值
(bit0~bit31)这两块共计四个字节,32位,各个位主要含义如下:
- bit0:OBJ_PROTECT_CLOSE,表示调用者是否允许关闭该句柄;默认值为1
- bit1:OBJ_INHERIT,指示该进程创建的子进程是否可以继承该句柄,即是否将该句柄项拷贝到它们的 句柄表中
- bit2:OBJ_AUDIT_OBJECT_CLOSE,指示关闭该对象时是否产生一个审计事件;默认值为0
- bit3~31:存放该内核对象在内核中的具体地址
内核对象
_OBJECT_HEADER
通过上文的学习了,我们了解到句柄的低32位可以获取到内核对象的地址。之前的文章学习过,例如进程,线程,互斥体都属于内核对象,并且它们都有各自的结构体,那是不是句柄指向的地址就是这些结构体的地址呢?其实不然,拿进程来举例,内核对象在开头都有一个0x18字节的_OBJECT_HEADER结构,这是内核对象的头部,也就是说从0x18字节开始, 才是进程结构体开始的位置。_OBJECT_HEADER结构如下
这个_Object_Header包含一些对内核对象的描述信息,其中一个字段Type,指明了内核对象的类型,并包含更多的描述信息
进程查找
在进程与线程相关篇章中,我们学习了很多种查找进程的方式,现在又多了一种,就是通过句柄表。有了上面的基础后,可以总结出这么一个思路:
已知当前进程EPROCESS -> _HANDLE_TABLE(EPROCESS+0xc4)-> TableCode -> _OBJECT_HEADER(TableCode + 3环句柄/4*8)-> 打开的进程EPROCESS(OBJECT_HEADER+0x18)
下面简要演示一下这个过程:
- 第一步:获取3环句柄
- 第二步:EPROCESS -> _HANDLE_TABLE(EPROCESS+0xc4)
- 第三步: _HANDLE_TABLE(EPROCESS+0xc4)-> TableCode
- 第四步:TableCode -> _OBJECT_HEADER(TableCode + 3环句柄/48,*这里注意查询时要将后3bit清零**) -> 打开的进程EPROCESS(OBJECT_HEADER+0x18)
关于反调试
句柄表和反调试,进程遍历也是息息相关的,进程遍历会在下一篇全局句柄表中再作分析。这里简单介绍反调试。一个进程加载进内存后,可以起一个线程,专门去遍历其他所有进程的句柄表,如果发现,某个进程的句柄表中有自己进程的句柄,说明自己的这个进程可能正在被调试,就算没有在被调试,也至少被打开了,这时就可以强行关闭自己的程序,不被调试,达到反调试的目的。当然反调试有很多种手段,在调试章节会再作分析。
总结
- 一个进程可以创建、打开很多内核对象,这些内核对象的地址存储在当前进程的句柄表中。我们在应用层得到的句柄值,实际上就是当前进程句柄表的索引。
- 同一个内核对象可以被不同的进程所引用,但句柄的值可能一样也可能不一样。
- 通过句柄表可以找到当前进程所打开或创建的其它内核对象。
参考资料
参考书籍:《Windows内核原理与实现》——潘爱民
参考教程:https://www.bilibili.com/video/BV1NJ411M7aE?p=67
参考文章:
- https://blog.csdn.net/weixin_42052102/article/details/83476572
- https://blog.csdn.net/qq_41988448/article/details/104945311
- https://www.cnblogs.com/joneyyana/p/12595525.html
参考笔记:张嘉杰,时间刺客.