Windows编程—-内核对象竟然如此简单?

什么是内核对象

内核对象本质上就是内存中的一块内存 ,这块内存由操作系统进行管理和分配,任何应用程序都无法直接操作这块内存区域。至于内核对象的作用,我们暂且不说,这里只需要直到它是内存中的一块内存。

在内存中,内核对象的存储类似下图,进程中的每个内核对象都有自己的地址,并且内核对象有一个固定的数据结构。

Windows编程----内核对象竟然如此简单?

每个内核对象的结构体如下:

typedef struct _OBJECT_HEADER {     LONG PointerCount;//引用计数,表示有多少指针引用该对象。     union {         LONG HandleCount;//句柄计数,表示有多少句柄引用该对象。         PVOID NextToFree;     };     PVOID Lock;//用于同步访问该对象的锁。     UCHAR TypeIndex;//对象类型的索引。     UCHAR TraceFlags;//跟踪标志,用于调试和跟踪对象的使用。     UCHAR InfoMask;//信息掩码,指示哪些信息可用。     UCHAR Flags;//对象的标志,指示对象的状态和属性。     union {         PVOID ObjectCreateInfo;//对象创建信息。         PVOID QuotaBlockCharged;//配额块信息。     };     PVOID SecurityDescriptor;//安全描述符,定义对象的安全属性。     QUAD Body;//对象的主体,包含对象的实际数据。 } OBJECT_HEADER, *POBJECT_HEADER;

简单一点说,对于内核对象,大家把他理解成存储在内存中的struct结构体数组,每个结构体都有自己的地址,这个地址叫做内核对象的地址。

_OBJECT_HEADER 是一个内部结构体,用于描述内核对象的元数据。这个结构体在 Windows 内核中定义,但并不直接暴露给用户模式应用程序。

句柄表

既然应用程序无法直接操作内核对象,那么应该如何访问和操作内核对象呢,这其中有个很重要的桥梁:句柄表。句柄表本质上也是内存中的一块内存,在每个进程启动的时候,操作系统会在进程的地址空间上开辟一块内存,用来保存句柄表,每个进程都有自己的一个句柄表。句柄表的基本结构如下图:

Windows编程----内核对象竟然如此简单?

注意:句柄表中有多条句柄,每个句柄也有自己的数据结构,主要有四个字段,第一个字段索引(句柄值),这是直接暴露给我们的应用程序的,第二个字段内核对象地址,表示的是某个内核对象真正的地址,就是上面第一张图中描述的内核对象的地址,访问掩码和标识我们暂时不讨论,后面会做特殊说明。

从这张图,我们就可以看到,句柄表其实就是连接内核对象和应用程序之间的一个桥梁,因为句柄表中的第二列,内核对象地址存储了真正的内核对象地址。

遗憾的是,句柄表的内存也是由操作系统分配和管理的,应用程序依然无法直接操作句柄表的内存,那到底如何访问内核对象呢,请继续往下看。

查看进程的所有句柄

通过ProcessExplorer可以查看一个进程的所有句柄列表。打开ProcessExplorer,选中一个进程,然后选择下方Tab栏中的Handles,可以查看到当前进程的句柄列表。注意:ProcessExplorer最好用管理员权限打开,如果用普通权限用户打开的话,有些句柄列信息是看不到,比如说ShareFlags这一列。

Windows编程----内核对象竟然如此简单?

句柄表有Type、Name、Handle、Address、Access、ObjectAddress、DecodeAccess、ShareFlags、Attributes几列,每列分别代表的意义如下:

1、Type:表示句柄的类型。文件、事件、互斥体、信号量、注册表、作业、线程、管道、文件映射等类型。    通过CreateThread 创建线程内核对象、CreateFileMapping创建文件映射内核对象、CreateMutex创建互斥体内核对象、CreateProcess创建进程内核对象、CreateEvent创建事件内核对象、CreateWaitableTimer创建时间等待期内核对象,还要其他的很多类似CreateXXX的函数用于创建不同类型的内核对象,这里不在举例了

2、Name:句柄名称,一般句柄都可以设置一个名称。在调用CreateXXX创建内核对象的时候,一般最后一个参数叫做pszName就是指定内核对象名称的。

3、Handle:句柄值,这个就是传递给WindowsApi的句柄值,也就是上面句柄句柄表那张图中的第一列。

4、Access:表示句柄的访问权限,以下是不同数值的访问权限:

0x0012019F:对文件的读写访问权限。
0x00120189:对文件的只读访问权限。
0x001F01FF:对文件的完全访问权限。
0x00100000:对进程的查询信息权限。
0x001F0FFF:对进程的完全访问权限。
0x00100000:对线程的查询信息权限。
0x001F03FF:对线程的完全访问权限。

5、ObjectAddress:表示内核对象在内核地址空间中的真实地址。这个地址是内核对象的实际地址。也就是上面第一张图表示的内核对象地址。

6、Decoded Access:Access这一列解码后的访问权限说明,这一列是ProcessExplorer为了方便我们阅读,将二进制解读为字符串。实际内存并没有Decoded Access这一列。

7、ShareFlags:表示句柄的共享标志。

FILE_SHARE_READ (0x00000001):允许其他进程读取文件。
FILE_SHARE_WRITE (0x00000002):允许其他进程写入文件。
FILE_SHARE_DELETE (0x00000004):允许其他进程删除文件

8、 Attributes:表示句柄的属性,比如继承属性。

下面我们用CreateFile创建内核对象,然后在ProcessExplorer中查看我们创建的句柄信息。

#include <iostream> #include <Windows.h>  int main() { 	LPCWSTR fileName = L"example.txt";  	// 定义安全属性,允许句柄继承 	SECURITY_ATTRIBUTES sa; 	sa.nLength = sizeof(SECURITY_ATTRIBUTES); 	sa.lpSecurityDescriptor = NULL; // 使用默认安全描述符 	sa.bInheritHandle = TRUE;       // 允许句柄继承  	// 创建文件并设置访问权限和共享模式 	HANDLE hFile = CreateFile( 		fileName,					  // 文件名 		GENERIC_READ | GENERIC_WRITE, // 访问模式 		FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, //共享模式 		&sa,				      // 安全属性 		CREATE_ALWAYS,           // 创建选项 		FILE_ATTRIBUTE_NORMAL,   // 文件属性 		NULL                     // 模板文件句柄 	);  	// 关闭文件句柄 	CloseHandle(hFile); 	return 0; }

CloseHandle(hFile);这一行打上断点,不要着急关闭句柄。运行控制台程序,在ProcessExplorer中会看到一个文件句柄如下:

Windows编程----内核对象竟然如此简单?

上图可以清晰的看到,我们创建的句柄类型为File,句柄名称其实就是文件的路径,Handle表示文件句柄的句柄值,后续我们对文件的操作都要用到它,

然后是访问权限Access,我们在程序中设置的访问权限为:GENERIC_READ | GENERIC_WRITE,表示当前进程对文件有读写的权限,在Decoded Access这一列可以看到FILE_GENERIC_READ | FILE_GENERIC_WRITE标志已经成功被设置,对 于ShareFlags这一列,RWD分别代表Read、Write和Delete权限,如果没有设置对于权限,一条横线,因为我们在程序中设置了FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE权限,其他进程可以对该文件读写和删除操作,当然一般情况下,当我们打开这个文件的时候,尽量只给其他进程设置一个读权限。对于最后一个Attributes,Inherit表示句柄可以被继承,这个在主进程创建子进程的时候,如果主进程允许继承,那么这个句柄将会继承到子进程。这个属性是通过句柄的安全描述符来描述的。我们通过sa.bInheritHandle = TRUE; 设置句柄的继承性。当我们调用CloseHandle(hFile);关闭句柄之后,在ProcessExplorer界面上会发现,上图显示的句柄会立刻消失。

 

 

管理内核对象

每个内核对象结构体都有一个字段叫做引用计数PointerCount,当一个引用对象被创建的时候,这个计数被设置为1,代表创建这个内核对象的进程正在使用这个内核对象,当有其他进程通过类似OpenXXX函数打开这个内核对象的时候,该计数会递增1。当进程不在对这个内核感兴趣的时候,可以调用CloseHandle函数将内核对象的引用计数递减1,只有当内核对象中引用计数被递减到0的时候,操作系统才会清空该内核对象。所以对于 内核对象来说,当我们不再使用的时候,一定要记得释放。

如何访问内核对象

Window提供了一些列的API来帮助我们操作内核对象。

假设我们要操作内核对象1(地址0x111),句柄表中有一个记录记录了对内核对象0x111的引用,其句柄之为1,内核对象地址0x111。这个时候,操作系统可以提供一个函数,比如叫做HandleObject,然后这个函数有一个参数叫做句柄值,我们把句柄值1传递给该函数,操作系统就可以找到句柄值1对应的内核对象的真实地址,然后进行操作。事实上,操作系统也确实是这样做的。

操作系统提供了一系列API,这些API几乎都会有一个句柄值的参数,这个参数就是句柄表中的第一列,操作系统通过这个参数,可以找到对应的内核对象地址,然后对内核对象进行相应的操作。

对于几乎所有的内核对象,windows都提供一个统一的操作模式,就是先调用系统API打开(一般是OpenXXX函数)内核对象或创建内核对象(一般是CreateXXX函数),OpenXXX和CreateXXX一般都会返回一个当前进程的句柄值,也就是上面句柄表中的第一列。让当前进程与目标对象之间建立起连接,然后再通过别的系统调用进行操作(这些操作内核对象的函数一般都需要一个句柄值的参数),最后通过调用系统API(一般是CloseHandle函数)关闭对象。实际上是关闭进程与目标对象的联系。

我们来简单总结一下应用程序操作内核对象的一般流程,这个流程基本上适用于所有的内核对象。

1、通过CreateXXX函数创建一个内核对象,并且得到句柄值。

2、通过OpenXXX函数打开一个内核对象,也是得到一个句柄值,当然这一步也可以省略,一般创建的时候,就可以打开内核对象。

3、调用对应的函数操作内核对象,我们假设函数叫做HandleObject,那么这个HandleObjet函数的原型大概是这样的。bool  HandleObject(HANDLE handle)。返回值表示是否操作成功,参数handle表示需要操作的句柄。操作系统会根据这个句柄从进程句柄表中找到内核对象的真实地址,然后进行操作。

4、调用CloseHandle函数,递减内核对象的引用计数。

发表评论

您必须 [ 登录 ] 才能发表留言!

相关文章