《windows核心编程》学习笔记——内核对象

来源:互联网 发布:python多进程 编辑:程序博客网 时间:2024/05/21 15:39

注意:这个系列的日志目的是为了促进自己对书本的理解,所以要自己提炼问题,并尽量自己的语言来回答,不要直接抄袭书本。

1        何为内核对象

问题1:什么是内核对象

内核对象包括:令牌对象、事件对象、文件对象、文件映射对象、I/O完成端口对象、作业对象、邮件槽对象、互斥量对象、管道对象、进程对象、信号量对象、线程对象、可等待的计时器对象、线程池工厂对象,等。

内核对象实际上是一个内存块,里面存放着一个数据结构,其成员是与该对象相关的信息,有些成员是所有的对象都有的(如安全描述符和使用计数器),有的则是某些对象特有的。

问题2:内核对象怎么操作?

内核对象只能由系统访问,应用程序需要通过调用系统函数才能访问和操作内核对象。

问题3:什么是句柄?

进程调用了一个创建内核对象的函数之后,函数会返回一个句柄,句柄标注了该对象,通过将句柄传递给后续操作该内核对象的函数,就能够实现对该内核对象的访问。

需要说明的是,句柄是和进程挂钩的,可以给同一进程中的所有线程使用;但是同一个句柄变量传给其他进程将无法得到正确的结果:1,可能访问失败;2,访问到了其他内核对象,造成难以察觉的错误。

1.1        使用计数

问题1:什么是使用计数?

类似一个智能指针,能够记录有多少个进程在访问当前的对象,当进程关闭了当前对象,或是进程本身被关闭,计数值都会减一,直到降为零,系统将其销毁。

1.2        内核对象的安全性

问题1:什么是安全描述符?

安全描述符描述了内核对象的拥有者,并限定了其他进程对其的访问权限。

问题2:如何使用安全描述符?

在创建内核对象的时候,几乎所有的创建函数都有一个参数PSECURITY_ATTRIBUTES,它实际上是一个只想结构体SECURITY_ATTRIBUTES的指针,而后者的结构如下:

typedef struct_SECURITY_ATTRIBUTES{

DWORD nLength;                            //初始化时,nLength=sizeof(sa);

LPVOID lpSecurityDescriptor;   //唯一和安全相关的成员

BOOL bInheritHandle;               //用来控制该内核对象的可继承性

}SECURITY_ATTRIBUTES;

在大多数的应用程序中,这个参数传入NULL,则该内核对象的安全级别就取决于当前进程的安全令牌。但是可以通过初始化一个上述的结构体并将其传入创建函数,来实现内核对象的访问限制。

问题3:安全描述符的工作机理是什么?Vista的不兼容性和安全描述符有关吗?

当一个进程需要访问某个内核对象时,调用例如open*函数,该函数需要传入一个声明访问权限的变量,调用函数后,系统将检查该进程是否具有其所声明的权限,如果权限不同,就会发生错误。

而上述的过程,正是导致了Vista操作系统兼容性较差的原因,因为此前的程序大多没有考虑安全性的问题,因此在声明访问权限的时候,往往传入了KEY_ALL_ACCESS的权限要求,这导致了其在Vista系统上难以成功访问对象。

问题4: 内核对象和普通对象的区别是什么?

这里体现了一点,就是只有内核对象才有安全描述符。有无安全描述符构成了判断是否内核对象的最简单方法。

2        进程内核对象句柄表

问题1:内核对象句柄表是什么?结构是什么样的?

每个进程在初始化的时候,系统都会为其分配一个空的句柄表,该表只供内核对象使用。表中对应于每个内核对象,有如下内容:

索引:标志着该对象在句柄表中的位置。这也是句柄值的实质含义,将句柄值右移2位,就将得到索引值。(作者说句柄的含义可能会发生变化。)

指向内核对象内存块的指针:不用解释了吧。

访问掩码:用来控制对标志位的访问。

标志:用来标明内核对象的可继承性、可关闭性等等。

问题2:句柄表的工作过程是什么样的?

一个进程,如果它不是子进程的话,在初始化的时候,句柄表是空的,当它每创建一个内核对象时,系统就会在句柄表中找到空白位置,然后对其进行初始化。

创建函数返回的句柄值,可以传递给其他要操作内核对象的函数,该函数将根据句柄值在句柄表中找到对应的内核对象,并完成对其的操作。

需要注意的是,创建函数有可能会失败,其返回值肯能是NULL(0)也有可能是INVALID_HANDLE_VALUE(-1),因此,在检查返回值的时候要注意当前函数的错误返回值是什么。

当进程关闭一个内核对象(函数CloseHandle)的时候,除了对使用计数器减一并判断是否应该销毁该对象外,一定会进行的操作就是将句柄表中的记录删除。要记住,调用CloseHandle函数后,必须将句柄变量置为NULL,以免程序误使用该句柄变量,造成错误。

问题3:忘记关闭句柄有没有内存泄漏的危险?应该如何检查?

不一定。唯一能肯定的是,当进程终止,系统一定会释放所有的资源。

检查的方法作者罗嗦了好多,需要的时候再细细研究吧。

3        跨进程边界共享内核对象

问题1:为什么要实现进程间内核对象的共享?那么又为什么要将内核对象设为进程相关?

同一台计算机的不同进程需要共享数据块;不同计算机之间也需要;进程之间需要通信;等等。

将内核对象设置为进程相关而不是设成系统相关,就是为了安全性,防止一些进程恶意篡改其他进程的数据。

问题2:有哪些方法能够实现跨进程边界共享?

内核对象句柄继承,为内核对象命名,复制内核对象句柄。

3.1        使用对象句柄继承

问题1:句柄继承的实际过程是什么样的?

首先,继承只能发生在父进程和子进程之间。

创建对象:对于一个希望被继承的内核对象,父进程在创建该对象的时候就必须初始化一个SECURITY_ATTRIBUTES结构体,并将其中的bInheritHandle标志设为True,然后传递给创建函数。

生成子进程:用CreateProcess函数来完成子进程的创建,同时将参数列表中的bInheritHandle参数设为True。此时,系统在创建了子进程之后,将不会让其直接运行,二是将为它创建一个空白的句柄表,然后搜索父进程的句柄表,将其中具有可继承性的对象对应的内容完整的复制到子进程的句柄表中同样的位置上,而相同的位置意味着父子进程之间对于同一个内核对象其句柄值是一样的

与此同时,该对象的使用计数也会加一。

问题2:子进程能够继承父进程所有的可继承对象吗?

只能继承子进程本身生成时父进程所有的可继承对象,对于子进程生成之后父进程新创建的可继承对象,子进程是不能继承的。

问题3:看起来不错,有没有什么问题?怎么解决?

有,在子进程生成的时候,它不知道自己到底继承了多少对象,也不知道哪些对象是继承而来的。

解决方法有:父进程将句柄值通过命令行参数传递给子进程;也可以使用其他进程间通信技术将继承的内核对象句柄值传递给子进程;或者让父进程向其环境块添加一个环境变量。(具体的做法以后用到再详细了解吧。)

问题4:能不能对儿子们因材施教?

可以通过改变对象句柄表中的标志位来实现让不同子进程继承不同的内核对象。

目前,每个句柄都关联了两个标志:

#typedefHANDLE_FLAG_INHERIT                                          0x00000001

#typedefHANDLE_FLAG_PROTECT_FROM_CLOSE        0x00000002

后者标志了对象是否能被关闭,如果父进程希望孙进程能够继承某个对象,又害怕在自己消亡后子进程关闭了该对象,就可以用语句:

SetHandleInformation(hobj, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE)

来实现改变对象的标志,从而增大孙进程能够继承该对象的概率。

3.2        为对象命名

问题1:命名方法是什么?有没有什么问题?

       在创建内核对象的函数中,都有一个输入对象名的参数,如果不需要命名,则传入NULL,如果需要命名,则传入一个以0为终止的字符串,最长为MAX_PATH(260)。

       存在问题,即系统没有机制保证名字不重复。因此,当创建一个和已存在对象重名的新对象时,如果对象的类型不同,创建就会失败。

问题2:为对象命名的方法如何实现跨边界共享?

方法1:对于一个已经存在的内核对象,用与对象同类型的创建函数,在对象名参数中传入想要共享的对象的名称,系统会检查是否存在同名的对象;如果存在,系统会检查类型是否匹配;如果匹配,系统会检查调用者是否用该对该对象的完全访问权限;如果答案都是肯定的,那么系统会在当前进程的句柄表中查找一个空白位置,初始化为指向现有的内核对象。如果上述过程中有一处不匹配,那么就会创建失败。

需要注意的是,如果上述过程完成,系统就会忽略Create*函数的安全属性等其余参数,如果不存在同名的对象,系统会创建一个新的内核对象。

方法2:用同类型的Open*函数,这类函数具有统一的形式,比如打开互斥量对象的函数形式如下:

HandleOpenMutex(

       DWORD dwDesiredAccess; //所要求的访问权限

       BOOL bInheritHandle;        //可继承性

       PCTSTR pszName;           //想要访问的对象名

);

用这个方法实现共享,如果对象不存在,也不会创建新的对象。

问题3:位对象命名有别的用途么?会不会带来新的问题?

       有,可以在进程中通过创建一个命名的内核对象的方法来标识是否有同样的进程在运行,如果发现有同样的进程在运行(即函数返回ERROR_AREADY_EXIST),则退出当前实例。

       会带来问题,很多攻击就是通过抢在被攻击进程之前生成一个同名的内核对象,从而使目标进程误以为已经有一个自己在运行,于是退出。这是很多“拒绝服务”攻击的基本原理。

问题4:终端服务命名空间的作用是什么?

这一部分不是特别理解,大概就是不同的终端用户的命名内核对象位于不同的命名空间中,而服务的命名内核对象位于全局命名空间内。

可以通过“Global\”“Local\”前缀来显示注明所处命名空间。

本部分有待后续理解。

问题5:专有命名空间的作用是什么?

       大意也是利用给自己命名的内核对象都加上一个自己的前缀,构成一个专有命名空间,同时用一个边界描述符对命名空间的名称自身进行保护,同时又一个相关联的特权用户组SID,只有特权用户组中的用户才能够在相同的边界中创建相同的命名空间,从而访问在这个命名空间中创建的,有专有命名前缀的内核对象。

这一部分,有待深入学习。

3.3        复制对象句柄

问题1:有哪些用法?分别是什么效果?

1, 可以涉及3个进程,让进程C调用DuplicateHandle函数(具体用法参数什么的要用了再查),从进程S中将内核对象h复制给进程T,但是存在问题,就是目标进程不知道自己拥有了访问权限,需要用窗口消息或者其他进程间通信机制来通知它。本方法实际上不常用。

2, 通常,只在涉及两个进程的时候,调用DuplicateHandle函数;

3, 还可以控制目标进程的访问权限。

 

原创粉丝点击