【第三章】内核对象

前言

本文章以及后面的所有文章的出处都是来自《windows核心编程》这本书,且只是对一些重点且基本的概念做一些笔记。想要更为深入的了解这windows可以自己去看这本书。

pdf版链接在此,提取码为:kkkk

内核对象

像互斥量(mutex)对象/管道对象/进程对象/线程对象等都是常见的内核对象。不同的对象都是通过不同名称的函数来创建的。例如,调用CreateFileMapping函数来创建一个文件内核对象。每个内核对象都只是一个内存块,由操作系统分配,也只能由操作系统来访问

对与每一个内核对象,其本质就是一个结构体,有着自己的成员属性和方法。此外安全描述符和计数是所有内核对象都有的

  • 安全描述符用来表示谁拥有对象,哪些组和用户被允许访问或使用此对象,哪些被拒绝使用此对象。说白了,就是通过这个安全描述符来约定哪些用户可以访问这个对象。

  • 计数是用来表示当前有哪些进程正在使用这个内核对象。这里面涉及到内核对象共享的概念,后面会讲到。这个计数实际上也可以用来表示一个内核对象是否能够被释放。为0则表示可以真正销毁这个内核对象。即使某个进程调用了销毁内核对象的接口,系统也不一定真正释放掉这个内核对象,有可能只是让其计数减一

进程的内核句柄表

在书中提到,每个进程在初始化的过程中,系统都会为其分配一个句柄表(handle tanble)。这个句柄表仅供内核对象使用。句柄表的结构如下:
在这里插入图片描述
可以看出,句柄表其实就是一个由数据结构组成的数组。每一项都记录了当前进程拥有的内核对象资源。对于每一项记录,都拥有一个内核对象的指针(用来找到对象),一个访问掩码 access mask表示该进程对该内核对象的访问权限)以及一些标志。

当创建内核对象时,比如CreateFileMapping。内核会为该对象分配一个内存块,该内存块存放在该对象的一些属性。然后,内核会扫描调用这个函数进程的句柄表,查找一个空白的记录项,将创建好的内核对象的指针填进去。具体的说,句柄表的记录项的指针实际上是内核对象的数据结构的内存地址。 此外,由于当前进程是创造者,访问掩码初始化被设置成拥有完全访问权限。

值得注意的是,如果句柄保存到一个变量中,调用CloseHandle函数之后,应该把这个变量重置为NULL。否则,如果错误的将这个变量来调用一个Win32函数,可能会发生两种意外情况:

  • 由于该变量引用的句柄已经被清除,所以Windows函数可能会因为参数无效而直接报错
  • 后续使用该变量之前,进程又创建了一些新的内核对象,很有可能,巧合地,该变量又指向了一个没有被清除的句柄项。此时,程序会发生意料不到的错误,很难排查

跨进程边界共享内核对象

为什么不同进程之间需要共享内核对象?书中给出如下理由:

  • 利用文件映射对象,可以在同一台机器上运行的两个不同进程之间共享数据块
  • 借助邮槽和命名管道,在网络中不同计算机上运行的两个进程可以相互发送数据块。
  • 互斥量/信号量可能需要一个进程向另一个进程发送通知

三种共享方式:

  • 使用对象句柄继承
  • 为对象命名
  • 复制对象句柄

使用对象句柄继承

顾名思义,借助继承,一个父进程在创建子进程的时候,父进程可以控制自己的句柄表中的那些句柄能够被子进程访问。

句柄表中每个记录项都有一个指明该句柄是否可以被继承的标志位。在创建内核对象的时候可以设置该标记位。
比如下面代码:

SECURITY_ATTRIBUTES sa; 
sa.nLength = sizeof(sa); 
sa.lpSecurityDescriptor = NULL; 
sa.bInheritHandle = TRUE; // Make the returned handle inheritable. 
HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);

上面代码是创建互斥量内核对象的过程,其中,sa.bInheritHandle = TRUE;表明该内核对象是可以被继承的。
在这里插入图片描述
其中句柄3是可以被继承的。
对于创建子进程的函数CreateProcess:

BOOL CreateProcess( 
PCTSTR pszApplicationName, 
PTSTR pszCommandLine, 
PSECURITY_ATTRIBUTES psaProcess, 
PSECURITY_ATTRIBUTES psaThread, 
BOOL bInheritHandles, 
DWORD dwCreationFlags, 
PVOID pvEnvironment, 
PCTSTR pszCurrentDirectory, 
LPSTARTUPINFO pStartupInfo, 
PPROCESS_INFORMATION pProcessInformation); 

其中第五个参数bInheritHandles,表示创建的子进程是否需要继承父进程的句柄表。
当某个进程通过CreateProcess接口创建一个子进程,如果第五个参数为true,系统首先会为子进程创建一个新的空白的句柄表,然后遍历父进程的句柄表,对该句柄表的每一个记录项进行检查,看其标志位是否可继承。如果可继承,这个句柄就会被完整的复制到子进程的句柄表

当然,在多个子进程时,父进程也可以选择哪些句柄可以被哪些子进程继承,并不一定让所有的子进程都继承句柄表。
可以调用SetHandleInformation函数来更改内核对象句柄的继承标志。函数原型如下:

BOOL SetHandleInformation( 
HANDLE hObject, 
DWORD dwMask, 
DWORD dwFlags); 
  • hObject:表示一个有效的句柄
  • dwMask:告诉函数想更改哪个或者哪些标志。目前每个句柄都广联了两个标志:
#define HANDLE_FLAG_INHERIT   0x00000001   //指示句柄是否可以被子进程继承
#define HANDLE_FLAG_PROTECT_FROM_CLOSE   0x00000002 //指示句柄是否应该在关闭的时候受到保护
  • dwValue:要设置的新值。这个值依赖于 dwMask 中指定的信息类型。

示例如下

SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, 0); 

上行代码表示关闭hObj指向句柄的可继承标记位。

为对象命名

书中指出,大部分内核对象其实都可以命名,但不是全部。例如,下面所有函数在创建内核对象的时候都可以命名。

//创建互斥量对象
HANDLE CreateMutex( 
PSECURITY_ATTRIBUTES psa, 
BOOL bInitialOwner, 
PCTSTR pszName); 
//创建事件对象
HANDLE CreateEvent( 
PSECURITY_ATTRIBUTES psa, 
BOOL bManualReset, 
BOOL bInitialState, 
PCTSTR pszName); 
//创建定时器对象
HANDLE CreateWaitableTimer( 
PSECURITY_ATTRIBUTES psa, 
BOOL bManualReset, 
PCTSTR pszName); 
//创建文件映射对象
HANDLE CreateFileMapping( 
HANDLE hFile, 
PSECURITY_ATTRIBUTES psa, 
DWORD flProtect, 
DWORD dwMaximumSizeHigh, 
DWORD dwMaximumSizeLow, 
PCTSTR pszName); 
//作业对象
HANDLE CreateJobObject( 
PSECURITY_ATTRIBUTES psa, 
PCTSTR pszName); 

其中最后一个参数pszName,如果为NULL,表明创建的是一个匿名的内核对象。值得注意的是,Microsoft没有提供任何的专门的机制来保证内核对象指定的名称是唯一的。也就是说,如果我们要创建一个有名字的内核对象,那么我们并不能保证在此之前没有一个内核对象与此同名。例如:

HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("JeffObj")); 
HANDLE hSem = CreateSemaphore(NULL, 1, 1, TEXT("JeffObj")); 
DWORD dwErrorCode = GetLastError(); //获得上一个报错函数的错误码

例如上面代码创建了两个内核对象,第二个创建失败了,函数调用返回了NULL.
对于继承句柄表来共享内核对象,命名内核对象可以实现两个没有关系的进程共享同一个内核对象。
如何使用共享内核对象呢?或者说一个进程怎么知道A对象可不可以用呢?

方法一:

观察下面例子:

HANDLE hMutex = CreateMutex(&sa, FALSE, TEXT("JeffObj")); 
if (GetLastError() == ERROR_ALREADY_EXISTS) { 
// Opened a handle to an existing object. 
// sa.lpSecurityDescriptor and the second parameter 
// (FALSE) are ignored. 
} else { 
// Created a brand new object. 
// sa.lpSecurityDescriptor and the second parameter 
// (FALSE) are used to construct the object. 
} 

在调用Create系列函数创建A内核对象的时候,有两种可能:

  1. 内核本来就存在A内核对象
  2. 内核不存在A内核对象

如果内核本来就存在A内核对象,Create*函数会打开这个现有的内核对象,也就是在句柄表中吧该对象的信息填在空白的句柄项中。这种情况错误码会被设置成ERROR_ALREADY_EXISTS。
所以我们可以通过GetLastError()来知道获得的这个内核对象A是否是新的。如果不是那不就和其他进程共享同一个A了吗。

方法二:
可以调用Open系列函数来基于名称共享对象:

HANDLE OpenMutex( 
DWORD dwDesiredAccess, 
BOOL bInheritHandle, 
PCTSTR pszName); 

HANDLE OpenEvent( 
DWORD dwDesiredAccess, 
BOOL bInheritHandle, 
PCTSTR pszName); 

HANDLE OpenSemaphore( 
DWORD dwDesiredAccess, 
BOOL bInheritHandle, 
PCTSTR pszName); 

HANDLE OpenWaitableTimer( 
DWORD dwDesiredAccess, 
BOOL bInheritHandle, 
PCTSTR pszName); 

HANDLE OpenFileMapping( 
DWORD dwDesiredAccess, 
BOOL bInheritHandle, 
PCTSTR pszName); 

HANDLE OpenJobObject( 
DWORD dwDesiredAccess, 
BOOL bInheritHandle, 
PCTSTR pszName); 

同样,pszName表示打开内核对象的名称。和Create函数不一样的是,如果对象不存在,Open函数只是会简单的调用失败,而不会创建一个。

也就是说,Open*只要调用成功,获得的内核对象句柄就一定是之前已经存在的。
虽然,命名共享内核对象在Windows 操作系统中提供了强大的进程间通信方式,但是会带来一些安全问题。

多个进程可能会试图使用相同的名称创建某个内核对象,就可能会导致后面创建的对象会覆盖先创建的对象。

书中指出,任何进程–即使是最低权限的进程都可以指定任何名称来创建一个对象,很容易另外写一个程序来创建一个同名的内核对象,如果它先于单例应用程序启动,单例应用程序可能会错误的认为它的另一个实例已经运行,所以就会一启动就退出,这是拒绝服务(DoS)攻击的基本机制。

为了解决命名冲突的问题,可以创建一个GUID,并将这个字符串形式作为对象名称去使用。

我们可以自定义一个前缀,作为自己的专有命名空间来使用。通过通过定义专有命名空间,可以防止与其他库或模块中的相同名称的标识符冲突。

复制对象句柄

可以使用DuplicateHndle函数来实现跨进程边界共享内核对象。

BOOL DuplicateHandle( 
 HANDLE hSourceProcessHandle,  // 源进程的句柄
    HANDLE hSourceHandle,          // 源句柄
    HANDLE hTargetProcessHandle,   // 目标进程的句柄
    LPHANDLE lpTargetHandle,        // 输出参数,目标句柄
    DWORD dwDesiredAccess,          // 目标句柄的访问权限
    BOOL bInheritHandle,            // 是否可继承
    DWORD dwOptions                 // 句柄复制选项
    }

这个函数可以创建一个现有句柄的副本,这个副本可以在不同的进程中使用。书中给出了一种使用该API的方式:

如果一个进程拥有对一个文件映射对象的读写权限。在程序的某个位置需要调用一个函数,但是我们希望这个函数只能对该文件映射对象具有读权限。那我们就可以使用这个函数,复制这个文件映射内核对象的同时设置这个内核对象的权限为可读。然后将这个只具有可读权限的句柄传给这个函数。

这个函数并不会创建新的内核对象,只是创建了一个新的指向这个内核对象的句柄

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值