avatar

Catalog
句柄表

句柄

今天来介绍一个熟悉的概念,句柄。早在Win32编程时,海哥曾提到,我们不必去理解句柄是什么,只要记住它是一个4字节的DWORD数即可。但是到了内核层面,就有必要深入了解一下这个概念了。

一直以来,我们接触到的句柄主要分为两种:

1)窗口句柄

c
1
HWND hwnd = FindWindow(NULL,"计算器");

例如上面这个代码,就是从桌面获取到一个已经打开了的窗口的句柄,这个句柄相当于这个窗口的编号,通过这个编号,便可以快速访问到这个窗口。

2)内核句柄

c
1
HANDLE g_hMutex = CreateMutex(NULL,FALSE, "XYZ");

窗口句柄是作用于窗口对象的,内核句柄则是对应了一个内核对象,这也是通常而言我们所说的句柄。正如上面例子所示,当一个进程创建或者打开一个内核对象时,将获得一个句柄(相当于内核对象的一个编号),通过这个句柄可以访问内核对象(例如进程,线程,互斥体等)

为什么要有句柄

前面提到过,句柄相当于内核对象的编号,3环的程序可以根据这个编号,来使用这个内核对象。这样就避免3环程序直接访问到内核对象;由于内核对象位于0环,如果3环的程序可以直接访问内核对象,一旦访问到无效的内核地址,就会导致蓝屏

句柄表

在认识了句柄之后,来了解一下句柄表。前面提到了,当一个进程创建或者打开一个内核对象时,将获得一个句柄;那么当这个进程,打开或创建多个内核对象时,就会获得多个句柄,这些句柄,就会存到一个叫做句柄表的结构里。先来看看句柄表这个结构在哪。

句柄表的位置

_EPROCESS结构体+0xC4偏移处,有一个字段ObjectTable,其指向了一个HANDLE_TABLE的结构体,这就是句柄表结构体。

各个字段具体含义如下:

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
+0x000 TableCode        	:ULONG_PTR //指向句柄表的存储结构
+0x004 QuotaProcess : _EPROCESS//句柄表记录的内存资源记录在此进程中
+0x008 UniqueProcessId : Void//创建进程的ID,用于回调函数
+0x00c HandleTableLock : [4] _EX_PUSH_LOCK//句柄表锁 (仅在句柄表拓展时使用)
+0x01c HandleTableList : _LIST_ENTRY //所有句柄表形成一个链表,链表头为全局变量HandleTableListHand
+0x024 HandleContentionEvent : _EX_PUSH_LOCK//若在访问句柄表时发生竞争,则在此推锁上等待
+0x028 DebugInfo : //调试信息,当调试句柄时才有意义
+0x02c ExtraInfoPages : //审计信息所占用的页面数量
+0x030 FirstFree : //空闲链表表头的句柄索引
+0x034 LastFree : //最近被释放的句柄索引,用于FIFO类型空闲链表
+0x038 NextHandleNeedingPool : //下一次句柄表拓展的起始句柄索引
+0x03c HandleCount : //正在使用的句柄表项的数量
+0x040 Flags : //标志域
+0x040 StrictFIFO : //是否使用FIFO风格的重用,既先释放先重用

本篇主要关注TableCode这个字段,其余字段在用到时会再作分析。TableCode它指向了一个表,这个表存储了当前进程所打开的所有句柄。下面来看一个例子:

在虚拟机中,先打开应用程序计算器,然后运行如下代码:

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "stdafx.h"
#include "Windows.h"

int main(int argc, char* argv[])
{
DWORD pid;
HANDLE hProcess;

HWND hwnd = FindWindow(NULL,"计算器");
GetWindowThreadProcessId(hwnd, &pid);

for (int i = 0; i < 10; i++)
{
hProcess = OpenProcess(PROCESS_ALL_ACCESS,TRUE, pid);
printf("句柄:%x\n", hProcess);
}

getchar();
return 0;
}

在这个代码中,我们会在程序中重复10次打开计算器 根据打印的结果来看,可以发现,尽管是打开的是同一个进程,但是每次打开时的句柄都不一样。下面我们来根据句柄表的位置来查找一下这些句柄

首先,在Windbg中,根据进程结构体地址,确定句柄表结构体所在位置

然后,在句柄表结构体中,找到存储句柄表的存储结构所在地址

最后,根据句柄表存储结构的地址,找到当前进程所打开的别的进程的句柄

这里有一点需要说明一下,在应用层,句柄是一个四字节的数,但是在内核存储时,句柄表的每个成员是8字节,因此在查询的时候,3环得到句柄的值需要先除以4再乘8,才能在句柄表中找到对应的句柄(3环句柄相当于是一个查询的编号)。现在我们知道如何通过进程的句柄表找到其打开进程的句柄。现在我们来看看0环中这个句柄的结构。

句柄的结构

先来看看句柄的结构是如何定义的(取自潘爱民老师的Windows内核原理与实现)

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct _HANDLE_TABLE_ENTRY
{
union
{
PVOID Object; //指向句柄所代表的对象 (bit31- bit0)
ULONG_PTR ObAttributes; //最低3位有特别含义,参见OBJ_HANDLE_ATTRIBUTES宏定义
PHANDLE_TABLE_ENTRY_INFO InfoTable; //各个句柄表页面的第一个句柄表项,使用此成员指向一张表
ULONG_PTR Value;
};
union
{
union {
ULONG GrantedAccess; //访问掩码


struct { //当NtGlobalFlag中包含 FLE_KERNEL_STACK_TRACE_DB 标记时使用
USHORT GrantedAccessIndex;
USHORT CreatorBackTraceIndex;
};
};
LONG NextFreeTableEntry; //空闲时表示下一个句柄表索引
};
} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;

直接看结构定义的话,有太多的联合体,看着比较费劲,下面用一张图来解释一下(图片取自张嘉杰的笔记)

句柄共8字节,64位,主要分为4个部分,接下来按照标记依次介绍:

  • (bit48~bit63)这一块共计两个字节,16位;高位字节是给SetHandleInformation这个函数用的,例如当执行如下语句:
c
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)

关于反调试

句柄表和反调试,进程遍历也是息息相关的,进程遍历会在下一篇全局句柄表中再作分析。这里简单介绍反调试。一个进程加载进内存后,可以起一个线程,专门去遍历其他所有进程的句柄表,如果发现,某个进程的句柄表中有自己进程的句柄,说明自己的这个进程可能正在被调试,就算没有在被调试,也至少被打开了,这时就可以强行关闭自己的程序,不被调试,达到反调试的目的。当然反调试有很多种手段,在调试章节会再作分析。

总结

  1. 一个进程可以创建、打开很多内核对象,这些内核对象的地址存储在当前进程的句柄表中。我们在应用层得到的句柄值,实际上就是当前进程句柄表的索引。
  2. 同一个内核对象可以被不同的进程所引用,但句柄的值可能一样也可能不一样。
  3. 通过句柄表可以找到当前进程所打开或创建的其它内核对象。

参考资料

参考书籍:《Windows内核原理与实现》——潘爱民

参考教程:https://www.bilibili.com/video/BV1NJ411M7aE?p=67

参考文章:

  1. https://blog.csdn.net/weixin_42052102/article/details/83476572
  2. https://blog.csdn.net/qq_41988448/article/details/104945311
  3. https://www.cnblogs.com/joneyyana/p/12595525.html

参考笔记:张嘉杰,时间刺客.

Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/04/23/%E5%8F%A5%E6%9F%84%E8%A1%A8/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶