系统创建和维护了多种类型的内核对象,如:令牌对象、事件对象、文件对象、文件映射对象、I/O完成端口对象、作业对象、邮件槽对象、互斥对象、管道对象、进程对象、信号量对象、线程对象、等待对象和线程池工作工厂对象。
内核对象只不过是一块由内核分配的内存,这个内存块的数据结构成员维护着这个内核对象,一些成员对所有内核对象来说是相同的,如安全标识符、使用计数等,但多数成员是特定于对象类型的,如进程对象有一个进程 ID,一个基本的优先级和一个退出代码,而一个文件对象有一个偏移量,一个共享模式和一个打开模式。内核对象只能由内核访问,应用程序不可能知道这些数据结构在内存中的位置,也不可能直接修改它们的内容。当用户调用一个函数创建了一个内核对象后,这个函数返回一个标识这个内核对象的句柄,以后对这个内核对象的操作都通过个句柄进行。这个句柄是一个 32 位的整数值(64 位系统是一个 64 位的整数),它可以用于你进程中的任何线程中,但不可直接用于其他进程的线程中,因为内核对象句柄是进程相关的,这个句柄值只在本进程有意义。如果将一个进程句柄传递给另一个进程,另一个进程对这个句柄的操作可能会失败,更糟的是,第二个进程会在进程句柄表中以相同的索引创建一个完全不同的内核对象。
使用计数
内核对象属于内核而不是创建它的进程所有。内核利用使用计数来管理一个内核对象的生命周期,当一个进程创建一个内核对象时,它的使用计数为 1,当另一个进程使用它时,它的使用计数增加 1,一个进程关闭一个内核对象或这个进程运行终止内核对象的使用计数减 1,如果内核检测到一个内核对象的使用计数为 0 时,它会将这个内核对象销毁并释放它占用的内存。
安全
内核对象使用安全描述符进行保护。安全描述符表示谁拥有这个对象,哪些组或用户可以访问或使用这个对象,而哪些组或用户不可允许访问。大部分创建内核对象的函数都有一个指向 SECURITY_ATTRIBUTES 结构指针的参数,这也是区分内核对象和用户对象的一个标志。如果将这个参数设置为 NULL,则表示创建一个基于当前进程安全令牌的内核对象。当打开一个内核对象时,系统会首先根据用户需要的权限进行安全测试,然后返回给用户一个有效的句柄或 NULL 值,如果用户在后面的函数中使用返回的句柄时使用了大于请求的权限的操作时,也会发生“access denied ”错误。
进程内核对象句柄表
当一个进程被初始化,系统会为这个进程分配一个句柄表,这个句柄表只用于内核对象,不会用于用户对象或 GDI 对象。句柄表类似一个简单的数据结构数组,每个数据结包含了一个指向内核对象内存块的指针,一个访问掩码和一些标志信息。
当一个进程成功创建了一个内核对象,内核会在这个进程的句柄表中查找一个空的条目,然后将有关这个内核对象的信息填写到这个条目中,返回给用户这个条目所在句柄表的索引,这就是内核对象的句柄,因此这个句柄是进程相关的,这个进程的任何线程都可以使用这个句柄值,但是这个句柄值不可以直接在其它进程中使用,其它进程的与这个句柄值相等句柄可能指向一个另外的内核对象,或者根本就不存在这样的一个句柄。每一个句柄值的最后两位都是 0。注意,并不是所有创建内核对象的函数发生错误时都返回 NULL,如 CreateFile 返回 INVALID_HANDLE_VALUE。
所有的内核句柄都可以使用 CloseHandle 来关闭。这个函数首先检查调用这个函数的进程的句柄表,确保句柄标识的对象进程可以使用,如果这个句柄有效,系统会得到内核对象数据结构的地址来减少使用计数,如果计数为 0,则这个内核对象会被销毁,在 CloseHandle 返回之前,它会清除这个句柄在句柄的条目,这个句柄就不可再次使用,最好将这个句柄变量立即设置为 NULL,防止以后无意地再次使用这个句柄值。进程终止运行后,系统会检查这个进程的句柄表,并将所有的句柄关闭,并减少与之相关的内核对象的使用计数,如果内核对象的计数变为 0,则会将这个内核对象销毁。
跨进程的内核对象共享
有三种方法允许进程间共享内核对象。
一、使用对象句柄继承
这种方法只能用于父子关系的进程中。父进程创建一个内核对象时,必须将 SECURITY_ATTRIBUTE 结构的 bInheritHandle 成员设置为 TRUE,这样在进程句柄表中的相应条目就会有一个表示可继承的标志,然后,父进程调用 CreateProcess 函数并传递一个 TRUE 给 bInheritHandle 参数,表示新创建的子进程将继承父进程的所有可继承句柄,这个函数调用成功后并不立即开始运行,系统会首先在父进程句柄表中查找标记为可继承的所有条目,依次复制到子进程中,这些条目在父子进程中是完全相同的,包括句柄值和可继承标志,同时,系统会将这些条目对应的内核对象的使用计数加 1。因为内核对象的内容保存在内核地址空间中,这个地址空间对所有进程共享,所有父子进程句柄表中指向的内核对象内存块是同一个内核对象。子进程创建之后,父进程新的可继承句柄不会再复制到子进程句柄表中。
通常情况下,子进程并不知道它的句柄表中存在从父进程中继承来的句柄,可以将句柄值通过命令行参数传递给子进程,子进程的初始化代码通过解析命令行(通常使用 _stscanf_s 函数)来获得这个值;还可以通过进程间通信父进程将句柄值传送给子对象,一个方法是父进程等待子进程初始化完成(使用 WaitForInputIdle),然后父进程发送一个消息给子进程线程创建的窗口;另一种方法是父进程创建一个环境变量并将它放到的环境变量块中,子进程继承父进程的环境变量,使用 GetEnvironmentVariable 取得内核对象句柄值,这个方法比较好,因为子进程还可以创建一个它的子进程,同样的环境变量还会继承给它的子进程。
用户可以使用 SetHandleInformation 函数修改内核对象句柄的标志,与句柄继承有关的标志有两个,分别是 HANDLE_FLAG_INHERIT 和 HANDLE_FLAG_PROTECT_FROM_CLOSE,具有 HANDLE_FLAG_PROTECT_FROM_CLOSE 标志的句柄不能被关闭,这样就可保证子进程的子进程能够正确地获得这个句柄。使用 GetHandleInformation 可以获取所有句柄标志。
二、使用命名对象
可以给内核对象创建函数 Create* 传递一个 0 结尾的字符串来创建一个命名对象,对象名称长度最大为 MAX_PATH (260个字符)。系统在调用这个创建函数时,首先会查找是否存在一个相同名称的对象,如果不存在,则创建一个指定类型的内核对象,否则,系统检查要创建的内核对象是否与已存在的对象的类型相同,如果不同,则返回 NULL,GetLastError 会返回 ERROR_INVALID_HANDLE,如果类型相同,系统还要检查调用者是否有这个对象的完全的访问权限。
如果已存在一个相同的命名对象,Create* 函数不会创建一个新内核对象,只是在调用进程的句柄表中增加一个指向已存在内核对象的条目,同时这个内核对象的使用计数增 1,如果调用 GetLastError,可以发现返回一个 ERROR_ALREADY_EXISTS 值。如果 Create* 函数参数中使用了 SECURITY_ATTRIBUTES,这个数据结构的 lpSecurityDescriptor 属性会被忽略。
用户还以使用 Open* 函数打开一个命名内核对象。这个函数会查找一个相同名称的内核对象,如果没有找到,函数返回 NULL,GetLastError 会返回 ERROR_FILE_NOT_FOUNT,如果找到的内核对象与期望的对象类型不同,函数返回 NULL,GetLastError 返回 ERROR_INVALID_HANDLE,系统还会检查是否有访问权限。Create* 和 Open* 函数的不同之处在于,如果不存在一个名称相同的对象,前者会创建一个,后者直接返回 NULL。
运行终端服务的主机中对内核对象有多个名称空间,一个是全局名称空间,在这个名称空间中出现的对象可以由所有的客户会话访问到,这个名称空间主要用于系统服务,另外,每一个客户会话有它自己的私有名称空间,运行相同应用程序的多个会话不会相互影响,一个会话不能访问另一个会话的内核对象,即使它们有相同的类型和名称。默认情况下,在一个终端服务中一个应用程序的命名内核对象放到本会话的名称空间中,在对象名称加一个 Global\ 前缀可以将对象放置到全局命名空间,使用 Local\ 前缀显式地将对象放置到当前会话的命名空间中。Global 和 Local 是微软保留字。
如果你想将你创建的命名内核对象不会同其它应用程序创建的内核对象名称冲突或为了防止恶意用户使用它,你可以定义你自己的名称前缀,并将它放到一个私有名称空间中,负责创建内核对象的服务进程会定义一个边界描述符来保护私有名称空间。首先创建一个边界描述符(CreateBoundaryDescriptor)和一个与某个用户或组相关的安全标识符(SID),将这个安全标识符加入到边界描述符中,这样,Windows 能够确保只有运行在指定安全上下文的用户才能够创建或打开相同边界的相同的名称空间,如果低安全级别的一个恶意的用户窃取了这个 SID 并使用这个 SID 创建了一个相同边界的相同名称空间,那么它相应的调用也会失败,可是具有高安全级别的恶意用户这样做就难说了。
三、复制对象句柄
这个方法非常简单,调用 DuplicateHandle 函数来完成,需要注意的有几点:1. DuplicateHandle 的参数 hSourceProcessHandle 和 hTargetProcessHandle,分别表示源和目标进程的句柄,这两个句柄都与调用这个函数的进程有关;2. DuplicateHandle 的参数 hSourceHandle 和 phTargetHandle,分别表示要复制的源句柄和目标句柄,这两个句柄分别与 hSourceProcessHandle 和 hTargetProcessHandle 标识的进程有关;3. 如果调用 DuplicateHandel 的进程不是 hTargetProcessHandle 标识的进程是,调用 DuplicateHandel 的进程不可关闭 hTargetHandle,因为这个句柄值与调用进程无关。