3.1 什么是内核对象
内核对象: 通过调用函数来创建的对象;如,CreateFileMapping函数 创建一个文件映射对象;
每个内核对象 都只是内核分配的一个内存块,并且只能由该内核访问。该内存块是一种数据结构,它的成员负责维护该对象的各种信息;
3.1.1 内核对象的使用计数
内核对象由内核所拥有,而不是由进程所拥有;
即,如果进程调用一个创建内核对象的函数,然后该进程中止了,那么内核对象不一定被撤销;
每个内核对象都包含一个使用计数,当一个内核对象刚创建时,使用计数为1;
当另一个进程访问一个现有的内核对象时,使用计数就递增 1;
3.1.2 安全性
内核对象能够得到安全描述符的保护。
用于创建内核对象的函数,都有一个指向SECURITY_ATTRIBUTES结构的指针作为其参数,如:
HANDLE
WINAPI
CreateFileMapping(
_In_ HANDLE hFile,
_In_opt_ LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
_In_ DWORD flProtect,
_In_ DWORD dwMaximumSizeHigh,
_In_ DWORD dwMaximumSizeLow,
_In_opt_ LPCWSTR lpName
);
如果该安全参数lpFileMappingAttributes指定为NULL,则可以创建带有默认安全性的内核对象; 也可以指定一个SECURITY_ATTRIBUTES结构,初始化,并传递该结构的地址给参数;SECIRITY_ATTRIBUTES结构如下:
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
只是获得一个内核对象的访问权(而不是创建一个新对象),可以使用对应的Open*开头的函数,如OpenFileMapping;
除内核对象外,还有其他类型的对象,如菜单,窗口,鼠标光标,字体等;这些对象是用户对象或图形设备接口(GDI)对象;
判断一个对象是否是内核对象,可通过其创建函数中是否有用来设定安全属性 的参数来判断,也就是上面的SECIRITY_ATTRIBUTES;
3.2 进程的内核对象 句柄表
当一个进程初始化时,系统会为它分配一个句柄表;该句柄表只用于内核对象,不用于用户对象或GDI对象;进程的句柄表只是个数据结构的 数组,该结构包含一个指向内核对象的指针,一个访问屏蔽和一些标志:
3.2.1 创建内核对象
1. 当进程被初次初始化时,它的句柄表是空的;
2.当进程中的线程调用创建内核对象的函数时,如CreateThread,内核为该对象分配一个内存块,并初始化;
3. 这时,内核对进程的句柄表进行扫描,找出一个 空项;
4.由于表3-1的句柄表是空的,内核便找到索引 1位置上的结构并对它进行初始化; 该指针成员被设置为内核对象的数据结构的地址,访问屏蔽设为全部访问权,标志也相应设置;
用于创建内核对象的函数,均返回与进程相关的句柄,这些句柄可以被相同进程中的任何线程加以使用;
该句柄值实际上是放入进程句柄表中的索引,它用于标识内核对象的信息存放的位置;
3.2.2 关闭内核对象
无论怎样创建内核对象,都要通过 CloseHandle来结束对该对象的操作;
1. CloseHandle首先扫描进程的句柄表,检测传递给它的索引(句柄);
2. 如果索引有效,那么系统就可以获得内核对象的数据结构的地址,并确定该结构中的使用计数的数据成员;
3. 如果使用计数是0, 内核便从内存中撤销该内核对象;
3.3 跨越进程边界共享内核对象
许多情况下,不同进程中运行的线程需要共享内核对象;
进程共享内核对象的 3个不同的机制: 1. 继承对象句柄; 2.命名对象; 3.用DuplicateHandle函数复制对象句柄;
3.3.1 对象句柄的继承性
只有当进程具有父子 关系时,才能使用对象句柄的继承性;实现这种继承性,父进程需要进行的操作:
1.当父进程创建内核对象时,需指明对象的句柄是个可继承的句柄;内核对象句柄有继承性,但内核对象本身没有继承性;
2. 要创建能继承的句柄,父进程必须指定一个SECURITY_ATTRIBUTES结构并对它进行初始化,然后将该结构的地址传递给create函数:
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;//使用默认安全性
sa.bInheritHandle = TRUE; //句柄可继承
HANDLE hmutex = CreateMutex(&sa, FALSE, NULL);
对于进程句柄表项目中的标志,如果将 成员bInheritHandle置为TRUE,那么该标志被置为1;
3.父进程使用CreateProcess函数来生成子进程。
3.3.2 改变句柄的标志
父进程创建一个内核对象,生成两个子进程, 如果只想要一个子进程来继承内核对象的句柄,可以调用SetHandleInformation函数来改变内核对象的 标志:
WINBASEAPI
BOOL
WINAPI
SetHandleInformation(
_In_ HANDLE hObject,
_In_ DWORD dwMask,
_In_ DWORD dwFlags
);
第一个参数用于表示有效的句柄;
第二个参数dwMask告诉函数想要改变哪个或哪几个标志;目前有两个标志与句柄相关联:
#define HANDLE_FLAG_INHERIT 0x00000001
#define HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002
第三个参数 dwFlags表明将该标志设置成什么值;如,打开一个内核对象的继承标志:
SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
关闭该标志: SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, 0);
对应的,GetHandleInformation函数的lpdwFlags参数返回 特定句柄的当前标志的设置值:
WINBASEAPI
BOOL
WINAPI
GetHandleInformation(
_In_ HANDLE hObject,
_Out_ LPDWORD lpdwFlags
);
例如:了解句柄是否可继承:
DWORD dwFlags;
GetHandleInformation(hObj, &dwFlags);
BOOL fHandleIsInheritable = (0 != (dwFlags & HANDLE_FLAG_INHERIT));
3.3.3 命名对象
共享跨越进程边界的内核对象的第二种方法 -- 命名对象; 大部分内核对象都是可命名的;在创建函数,如
WINBASEAPI
HANDLE
WINAPI
CreateSemaphore(
_In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
_In_ LONG lInitialCount,
_In_ LONG lMaximumCount,
_In_opt_ LPCWSTR lpName
);
最后一个参数 lpName为命名;当该参数为 NULL时,创建一个未命名(匿名)的内核对象;
当创建一个未命名的内核对象时,可以通过使用继承性(如上)或者DuplicateHandle函数来共享跨越进程的对象;如果要按名字共享 对象,必须为对象赋予名字;
按名字共享对象的另一种方法: 使用Open*函数,如:
HANDLE
WINAPI
OpenSemaphore(
_In_ DWORD dwDesiredAccess,
_In_ BOOL bInheritHandle,
_In_ LPCWSTR lpName
);
Create*函数和Open*函数的主要差别: 如果对象不存在,Create*函数将创建该对象, 而Open*函数则运行失败;
注意使用 GetLastError函数来获取函数运行的结果;
3.3.4 终端服务器的名字空间
终端服务器拥有内核对象的多个名字空间;
3.3.5 复制对象句柄
共享跨越进程边界的内核对象的第三种方法: 使用DuplicateHandle函数:
WINBASEAPI
BOOL
WINAPI
DuplicateHandle(
_In_ HANDLE hSourceProcessHandle, //源进程句柄
_In_ HANDLE hSourceHandle, //内核对象句柄,与源进程相关
_In_ HANDLE hTargetProcessHandle, //目标进程句柄
_Outptr_ LPHANDLE lpTargetHandle,//接收 输出的目标内核对象句柄
_In_ DWORD dwDesiredAccess,//使用的访问屏蔽值
_In_ BOOL bInheritHandle, //继承性标志
_In_ DWORD dwOptions //组合值
);
该函数取出一个进程句柄表中的项目,并将该项目拷贝到另一个进程的句柄表中;
dwOptions参数 可以是0 ,也可以是DUPLICATE_SAME_ACCESS和DUPLICATE_CLOSE_SOURCE的组合;
设定 DUPLICATE_SAME_ACCESS : 目标进程句柄 与 源进程句柄有 相同的访问屏蔽,将忽略dwDesiredAccess参数;
设定 DUPLICATE_CLOSE_SOURCE: 关闭源进程中的句柄;内核对象的使用计数 不变;
使用示例:
通常在涉及两个进程时才会用到DuplicateHandle函数:
对于用duplicateHandle函数获取到的目标 内核对象句柄 hObjInProcessT, 不能在Process S的上面这些代码里面调用CloseHandle(hObjInProcessT), 因为使用了非继承性,内核对象计数没有增加, 这个句柄要由进程T来关闭,进程S来关闭会导致各种不可预知的行为;