概念:

内核对象是操作系统维护的一个数据结构(结构体)。

内核对象是操作系统级别的,不同的进程可以访问同一个内核对象。
内核对象是操作系统级别的,进程终止不一定触发其创建的内核对象的销毁,大部分情况下会跟着进程一起销毁,有些则会残留。
内核对象是操作系统级别的,如果多个进程同时使用某个内核对象,那么任何一个进程终止都不会触发内核对象的销毁,因为系统会维持引用计数。

所有内核对象的数据结构中都有引用计数这个字段。

所有内核对象都有一个安全属性,这个属性会在内核对象创建函数中指定,其描述了那些进程可以更改此对象,以及哪些组和用户不能访问此对象。
因此,内核对象操作函数的内部实现一般都是先进行权限校验,检查当前进程是否有权限对对象进行期待的操作。
在编写运行在不同windows版本上的程序时,权限显得尤为重要,因为某些内核对象的默认安全权限在不同的windows版本上不一致,这会导致程序运行失败和安全风险。

句柄是进程级别的,不同进程对于同一个内核对象的句柄是不一样的,因此不能跨进程使用句柄,相同的句柄值对于不同的进程来说指向的是不同的内核对象。
每个进程都有自己的句柄表,句柄表中 “只会存放” 内核对象的句柄。
句柄在句柄表中是以索引的形式存在的,如果某个句柄被释放,那么它的位置可能会被复用,这就是为什么CloseHandle之后要把存放句柄的变量设置为NULL的原因。

使用CloseHandle关闭句柄时,会通知操作系统对相应内核对象数据结构中的计数减一,当计数为零时,内核对象被销毁。
在使用CloseHnandle之后,应当将用于存放句柄的变量设置为NULL,以防止后续相应的句柄值被复用时,在操作此变量而导致误操作。

句柄泄露仅会发生在进程运行时,windows确保 “进程退出时,其持有的‘所有’资源都被清理”。如果某个内核对象被其他进程引用,那么当前进程会对内核对象计数减一,如果减一后计数为零,则也会触发内核对象销毁。
在任务管理器中,进程选项卡选中的情况下,点击 查看->选择列 可以显示进程持有句柄的数量和一些其他信息。
使用 https://docs.microsoft.com/zh-cn/sysinternals/downloads/process-explorer 可以更方便地查看句柄信息,甚至可以看到命名内核对象的名称。

 

进程间共享内核对象的三种方法:

1)对象句柄继承(不是对象继承,是句柄继承),仅限父子进程间
所谓的对象句柄继承,可以理解为在子进程的句柄表中分配同样的索引。
在创建内核对象时,在安全属性设置可继承性为true则表示本次创建的句柄可被子进程继承,与此同时,父进程的句柄表中相应索引对应条目的属性一列也会有体现。
在使用CreateProcess创建子进程时,如果将是否继承句柄的入参设置为true,则内核会扫描父进程的句柄表,然后依次在子进程的句柄表中创建那些在父进程中被设置为可继承的句柄。
鉴于内核的上述动作,除了那些不可继承的句柄,子进程的句柄表的布局和父进程完全一致。因此子进程 “看起来” 可以使用同样的句柄 访问 内核对象,其实是因为相应的句柄索引在父子进程中是一样的。但是要注意父子进程分别占用内核对象的一个计数。
如果父进程在调用CreateProcess之后又创建了新的可继承句柄,那么这个句柄不会被同步到子进程中,因为CreateProcess创建句柄表的时候使用的是 “快照机制” 。

子进程继承了句柄之后,其实是没办法直接使用的,因为每一次程序运行句柄的值都是变化的,因此父进程需要通过某种方式告知子进程哪些句柄被继承了,以及这些被继承的句柄分别对应哪些内核对象。一般情况下可以将句柄值及其对应的用途组织成字符串,然后作为CreateProcess创建进程时指定进程参数的入参传递进去。另外,也可以写入环境变量,本地文件等等。

2)使用命名对象
Windows下所有能有名称的对象共享同一个名称空间,而且windows也不提供任何机制来防止冲突,因此在创建命名对象的时候需要格外注意。
当进程使用Create*创建命名对象的时候,如果getlasterror返回的是 ERROR_ALREADY_EXISTS 说明本次Create*是在打开对象,而不是创建了对象,但是本次打开是成功的,因此我们在使用的时候不要认为是真的 ERROR 。此外,如果确确实实是只想打开,而不是通过创建来随机创建或打开,那么可以使用Open*相关的函数实现。

命名对象可以用来保证应用程序的单例运行,但是单例运行的程序可能会被恶意攻击(劫持内核对象)而导致永远无法启动。具体的防治策略见 p51-p60

3)复制对象句柄
使用DuplicateHandle实现,一般在两个进程间使用,多与两个进程会比较复杂,此外,此动作是在分发句柄的进程中执行,希望获得句柄的那个进程无从得知自己什么时候开始可以访问什么句柄。完全由分发进程通过某种IPC的方式(因为两个进程都已经在运行,所以没法通过命令行参数的形式告知接收进程)通知希望接收句柄的进程。