内核对象
- 系统会创建和处理的几种内核对象
- 访问令牌对象(access token)
- 事件对象
- 文件对象
- 文件映射对象
- I/O完成端口对象
- 作业对象
- 邮件槽对象(mailslot)
- 互斥量对象
- 管道对象
- 进程对象
- 信号量对象
- 线程对象
- 可等待的计时器对象
- 线程池工厂对象
- 每个内核对象都只是一个内存块,它由操作系统内核分配,并只能由操作系统内核访问。
- 这个内存块是一个数据结构,其成员维护着与对象相关的信息。
- 少数成员(安全描述符和使用计数等)是所有对象都有的,但其他大多数成员都是不同类型的对象特有的。
- 由于内核对象的数据结构只能由操作系统内核访问,所有应用程序不能在内存中定位这些数据结构并直接更改其内容。
- 虽然不能更改这些结构,但我们可以通过Windows提供的一组函数,来操纵这些结构。
- 比如调用了一个创建内核对象的函数后,函数会返回一个句柄,它标识了所创建的对象。而这个句柄值,可由进程中的任何线程使用。
内核对象的句柄
- 在32位Windows进程中,句柄是一个32位值。在64位Windows进程中,则是一个64位值。
使用计数
- 内核对象的所有者是操作系统内核,而不是进程。
- 内核对象的生命周期可能长于创建它的那个进程的生命周期。
- 操作系统是知道有多少个进程正在使用一个特定的内核对象的。因为每个内核对象都包含一个使用计数。
- 使用计数是所有内核对象都有的一个数据成员,初次创建内核对象的时候,该内核对象的使用计数被设定为1。每当其他进程获得对现有内核对象的访问后,该内核对象的使用计数就会加1。
- 进程终止运行后,操作系统内核将自动递减此进程仍然打开的所有内核对象的使用计数。
- 如果一旦内核对象的使用计数变成0了,操作系统内核就会销毁该对象。
安全性
- 内核对象可以用一个安全描述符来保护。
- 安全描述符描述了谁拥有对象,哪些组和用户被允许访问或使用此对象,哪些组和用户被拒绝访问此对象。
- 安全描述符通常在编写服务器应用程序时使用。
- 用于创建内核对象的所有函数几乎都有指向一个SECURITY_ATTRIBUTES结构的指针作为参数。大多数应用程序为这个参数传入了NULL,这样创建的内核对象具有默认的安全性。
1 2 3 4 5 6 7 |
HANDLE CreateFileMapping( HANDLE hFile, PSECURITY_ATTRIBUTES psa, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, PCTSTR pszName); |
- 也可以分配一个SECURITY_ATTRIBUTES结构,对它进行初始化,然后把它地址作为参数传过去。
进程内核对象句柄表
- 一个进程在初始化时,系统将为它分配一个句柄表。
- 这个句柄表仅提供内核对象使用,不适用于用户对象和GDI对象。
- 句柄表只是一个由数据结构组成的数组
- 一个指向内核对象的指针
- 一个访问掩码
- 一些标志
创建一个内核对象
- 一个进程首次初始化的时候,其句柄表为空。
- 当进程内的一个线程调用一个会创建内核对象的函数时,内核将为这个对象分配并初始化一个内存块。然后,内核扫描进程的句柄表,查找一个空白的记录项,并对其进行初始化。
- 具体初始化是:指针成员会被设置成内核对象的数据结构的内部内存地址,访问掩码会被设置成拥有完全访问权限,标志也会被设置。
- 用于创建内核对象的任何函数都会返回一个与进程相关的句柄,这个句柄可由同一个进程中运行的所有线程使用。
- 系统用索引来表示内核对象的信息保存在进程句柄表中的具体位置,要拿到这个索引值,可以用句柄值除以4。
关闭内核对象
- 无论以什么方式创建内核对象,我们都要调用CloseHandle向系统表明我们已经结束使用对象。
- 该函数内部,它会首先检查主调进程的句柄表,验证“传给函数的句柄值”标识的是“进程确实有权访问的一个对象”。
- 如果句柄是有效的,系统就将获得内核对象的数据结构的地址,并将结构中的“使用计数”成员递减。
- 如果使用计数变成0了,内核对象将被销毁,并从内存中去除。
- 如果句柄是无效的,那么可能会有两种情况:
- 如果进程是正常运行的,CloseHandle将返回FALSE,GetLastError返回ERROR_INVALID_HANDLE。
- 如果进程正在被调试,那么系统将抛出0xC0000008异常。
- 在CloseHandle函数返回之前,它会清除进程句柄表中对象的记录项。无论内核对象当前是否销毁,这个清除过程一定会发生。调用之后,我们的进程就不能再访问那个内核对象了。
一 跨进程边界共享内核对象
使用对象句柄继承
- 只有在进程之间有一个父-子关系的时候,才可以使用对象句柄继承。
- 在这种情况下,父进程有一个或多个内核对象句柄可以使用,而且父进程决定生成一个子进程,并允许子进程访问父进程的内核对象。
- 父进程可以在创建一个内核对象时,向系统指出它希望这个对象的句柄是可以继承的。
1 2 3 4 5 6 7 |
SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = NULL; // 使得返回的句柄可继承 sa.bInheritHadnle = TRUE; HANDLE hMutex = CreateMutex(&sa, FALSE, NULL); |
- 创建子进程的时候,可以决定子进程是否可以继承父进程句柄表中的“可继承的句柄”。
1 2 3 4 5 6 7 8 9 10 11 12 |
BOOL CreateProcess( PCTSTR pszApplicationName, PTSTR pszCommandLine, PSECURITY_ATTRIBUTES psaProcess, PSECURITY_ATTRIBUTES psaThread, // 这个参数为TRUE,子进程就会继承父进程可继承的句柄 BOOL bInheritHandles, DWORD dwCreationFlags, PVOID pvEnvironment, LCTSTR pszCurrentDirectory, LPSTARTUPINFO pStartupInfo, PPROCESS_INFOORMATION pProcessInformation); |
- 关于子进程,当传入的是TRUE时,操作系统会创建新的子进程,但不允许子进程立即执行它的代码。当系统为子进程创建一个新的、空白的进程句柄表之后,它会遍历父进程的句柄表,对它的每一个记录项进行检查。对于凡是包含一个有效的”可继承的句柄“的项,它都会被完整的复制到子进程的句柄表中。
而且,在子进程的句柄表中,复制项的位置和在父进程的句柄表中的复制项的位置是一样的,
这意味着,对一个内核对象进行标识的句柄值是一样的。
当然,系统会在复制了句柄表的记录项之后,递增内核对象的使用计数。 - 掩码与父进程中的是一样的,标志页一样。
- 需要注意的是,对象句柄的继承只会发生在创建子进程的时候。假如父进程后来由创建了一个内核对象,并且将这个内核对象的句柄设置为可继承的句柄,子进程也是不会继承这个新的句柄的。
- 还有个特点,就是子进程并不知道自己继承了任何句柄。因而需要在子进程的文档中,指出当它从另一个进程生成时,希望获得对一个内核对象的访问权-只有这种情况内核对象的句柄继承是有用的。
句柄继承中获得句柄的方式
- 使子进程得到一个它想要的内核对象的句柄值。最常见的方法是将句柄值作为命令行参数传给子进程。子进程的初始化代码解析命令行并提取句柄值。(_stscanf_s)
- 使用进程间通信技术将继承的内核对象句柄值从父进程传到子进程。
- 或让父进程等待子进程完成初始化(WaitForInputIdle),然后父进程将一条消息发送或发布到由子进程中的一个线程创建的一个窗口
- 让父进程向其环境块添加一个环境变量。变量的名字应该是子进程知道的一个名字,变量的值是准备被子进程继承的那个内核对象的句柄值。这样,当父进程生成子进程的时候,这个子进程会继承父进程的环境变量,可以通过调用
GetEnvironmentVariable
来获得这个继承到的内核对象的值。由于环境变量是可以反复继承的,所以,当子进程再生成自己的子进程的时候,可以通过这种方式取得环境变量里面的内容。
相关内核空间
- 内核对象的内容是被保存在内核地址空间中。
- 系统上允许的所有进程都共享这个内核地址空间。
- 对于32位系统,指的是
0x80000000
到0xFFFFFFFF
之间的空间。而对于64位系统而言,指的则是0x00000400'00000000
到0xFFFFFFFF'FFFFFFFF
之间的地址空间。
改变句柄的标志
- 可以调用
SetHandleInformation
函数来改变内核对象句柄的继承标志。
1 2 3 4 |
BOOL SethandleInformation( HANDLE hObject, DWORD dwMask, DWORD dwFlags); |
- hObject标识了一个有效的句柄
- dwMask表示我们想改哪些标志
- dwFlags表示想把标志设置为什么
1 2 3 4 5 |
// 打开内核句柄的继承标识 SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); // 关闭内核句柄的继承标识 SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, 0); |
- 如果在调试器下,线程如果试图关闭一个受保护的句柄,就会引发异常。如果在调试器之外,CloseHandle就只是返回FALSE。
1 2 3 4 5 6 |
// 告诉系统不允许关闭句柄 SethandleInformation(hObj, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE); // 关闭句柄 引发异常 CloseHandle(hObj); |
二 跨进程边界共享内核对象
为对象命名
- 许多内核对象都可以进行命名(但不是全部)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
HANDLE CreateMutex( PSECURITY_ATTRIBUTES psa, BOOL bInitialOwner, PCTSTR pszName); HANDLE CreateEvent( PSECURITY_ATTRIBUTES psa, BOOL bManualResult, BOOL bInitialOwner, PCTSTR pszName); HANDLE CreateSemaphore( PSECURITY_ATTRIBUTES psa, BOOL bInitialOwner, LONG lMaximumCount, PCTSTR pszName); HANDLE CreateWaitableTimer( PSECURITY_ATTRIBUTES psa, BOOL bManualResult, 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,则表明我们要创建的是一个未命名的匿名的内核对象。要创建有名称的内核对象,这个参数要传入的是一个”以0为终止符的名字字符串“。这个字符串的长度可以是MAX_PATH。
- 需要注意的是,没有任何一种机制可以保证我们当前要创建的具名内核对象,并不存在一个同名的内核对象。
- 另,通过为内核对象命名这种方式,实现共享内核对象时,那么该内核对象是否可继承就变成一个非必要条件。
1 2 3 4 5 |
// a进程创建了一个互斥量 HANDLE hMutexProcessA = CreateMutex(NULL, FALSE, TEXT("testmutex")); // b进程创建互斥量,b不需要一定是a的子进程 HANDLE hMutexProcessB = CreateMutex(NULL, FALSE, TEXT("testmutex")); |
- 当进程b调用CreateMutex时,系统首先会查看是否已经存在一个名为"testmutex"的内核对象。如果确实已经存在,内核会接着检查该对象的类型。由于系统试图创建一个互斥量对象,而名为"testmutex"的对象也是个互斥量对象,所以系统会接着执行一次安全检查,验证调用者是否拥有对该对象的完全访问权限。如果调用者拥有权限,系统就会在进程b的句柄表中找一个空白记录项,并将其初始化为指向现有的内核对象。如果是对象的类型不匹配,或调用者被拒绝访问,CreateMutex就会返回NULL。
- 进程b调用CreateMutex成功之后,不会实际的创建一个互斥量对象。会为进程b分配一个新的句柄值用来标识一个内核中现有的对象。另,由于在进程b的句柄表中,用一个新的记录项引用了这个对象,所以该内核对象对应的使用技术会增加。两个进程中的句柄值极有可能是不同的值。
- 可以在使用了Create*函数之后马上通过GetLastError分辨是创建了一个新的内核对象,还是打开了一个现有对象。GetLastError返回的是ERROR_ALREADY_EXISTS意味着已经存在了一个对象,否则就是创建了一个新的对象。
三 跨进程边界共享内核对象
Open*函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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指出内核对象的名字,这个不能为NULL。
- 如果没找到,返回NULL。如果找到了,但类型不同,也会返回NULL。如果找到了,类型也相同,系统会检查访问是否被允许。如果允许,就会更新主调进程的句柄表,并使对象的使用计数增加。
- 如果bInheritHandle传入了TRUE,那么返回的句柄就是”可继承的“。
- Open*和Create*的区别,Create*当对象不存在的时候会创建,Open*不会。
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ x86_64汇编学习记述二08/07
- ♥ Windows 核心编程 _ 线程优先级与关联性07/09
- ♥ WindowsHOOK相关03/17
- ♥ Windows 核心编程 _ 内核对象:同步异步设备IO08/24
- ♥ Windbg关于死锁的简单调试分析总结09/13
- ♥ Windows Dll自卸载相关10/19