3.3 跨进程边界共享内核对象
3.3.1 使用对象句柄继承
(1)对象句柄继承,只发生在进程之间有父子关系的时候(即一个进程由另一个进程CreateProcess出来)
(2)内核对象句柄继承的实现
①父进程必须先指出哪些内核对象句柄是可继承(注意不是内核对象本身的继承,而是内核对象的句柄继承),父进程在创建内核对象时要将SECURITY_ATTRIBUTES的bInheritHandle字段设为TRUE,表示可继承。这时句柄表中相应的记录项的标志位被设为1,否则为0。
②父进程调用CreateProcess创建子进程,其中的bInheritHandles设为TRUE,表示希望子进程继承父进程句柄表的中“可继承句柄”。这时新创建的子进程不会立即执行,而是先遍历父进程的句柄表,并将每一个有效的“可继承句柄”完整地复制到子进程的句柄表,并且复制项的位置与父进程的位置完全一致。(即两句柄的索引值一样,如索引值为3,则子进程句柄表中也有3个记录项,其中第3个就是父进程可继承句柄,其余两个被标记为不可用,见课本44表3-3),并递增内核对象的使用计数。
③如果子进程再调用CreateProcess生成自己的子进程(父进程的孙进程),并且bInheritHandles也设为TRUE,则孙进程也会继承这个内核对象,并在孙进程的句柄表中,继承的对象句柄具有相同的句柄值、访问掩码及标志。
④为了让子进程得到这个内核对象的句柄值,可通过命令行参数或通过向父进程环境块添加一个环境变量(环境变量也是会被继承的)或其他进程间通信技述,将句柄值从父进程传递给子进程。
⑤为了销毁内核对象,父子乃至孙进程都要调用CloseHandle(因为他们使用的是相同的内核对象),只有都CloseHandle,使用计数值才会减为0。
(3)对象句柄的继承只发生在生成子进程的时候,如果父进程后来又创建了一个可继承的内核对象,正在运行的子进程是不会继承这个新的内核对象句柄的。
(4)改变句柄的标志
①使用场景,如父进程的可继承句柄只希望被多个子进程中的其中一个继承。即改变了这句柄标志,只影响之后创建的子进程。
②SetHandleInformation函数——改变句柄的标志
参数 |
描述 |
HANDLE hObject |
在本进程中要改变标志的内核对象句柄,会影响该进程的子进程 |
DWORD dwMask |
想更改哪个或者哪些标志位 HANDLE_FLAG_INHERIT(1)——是否被继承标志 HANDLE_FLAG_PROTECT_FROM_CLOSE(2)——允许或禁止关闭句柄 |
DWORD dwFlags |
将上述的标志位设成什么样的值 |
★改变内核对象句柄可继承标志
打开继承:SetHandleInformation(hObj,HANDLE_FLAG_INHERIT,HANDLE_FLAG_INHERIT)
关闭继承:SetHandleInformation(hObj,HANDLE_FLAG_INHERIT,0)
★改变可关闭标志
禁止关闭:SetHandleInformation(hObj, HANDLE_FLAG_PROTECT_FROM_CLOSE,
HANDLE_FLAG_PROTECT_FROM_CLOS)
允许关闭:SetHandleInformation(hObj, HANDLE_FLAG_PROTECT_FROM_CLOSE,0)
这时如果任何一个进程(含父子进程)调用CloseHandle都会引发异常。但要注意在子进程里面如果又将该内核对象的该标志更改为允许关闭的,那该对象照样会被关闭。
③获取内核对象有关的标志信息GetHandleInformation,如获取是否可以继承
DWORD dwFlags;
GetHandleInformation(hObj,&dwFlags);
BOOL fHandleIsInheritable =(0!=(dwFlags & HANDLE_FLAG_INHERIT));
3.3.2 为对象命名
当以不同的用户名同时登录计算机,会产生不用的Session来服务这些用户。其中Session0是随系统启动自动创建的会话,用于提供一些系统服务(如打印机等服务进程),以后登录的用户,会分别产生Session1、Session2……等等。
命名空间 |
描述 |
备注 |
全局空间 |
创建的内核对象,能同时被Session1、Session2、……、SessionN等所有会话中的程序同时访问到的对象。 对象的命名形式:GlobalMyObjectName |
①对象名称前面的Global、Local或MyNameSpace表示出对象的命名空间,也即指定了可见范围。 ②私有空间只有指定的用户能访问! |
本地空间 |
本地空间:只能被当前Session里的应用程序访问到的内核对象 命名形式:LocalMyObjectName或MyObjectName |
|
私有空间 |
自定义的命名空间如:最终MyNameSpaceMyObjectName 其中的“MyNameSpace”是我们定义的命名空间的别名,其他应用程序根本不知道实际的空间名字,因此可以起到保护内核对象的效果。 |
★私有空间默认是全局可见的(这有点奇怪),但可在名称前面再加上Global和Local来指定可见范围。如
CreatePrivateNameSpace(&sa,&h_gBoundary,L"aliasName");//创建私有空间,别名为aliasName
//在私有空间aliasName中,创建名为MyMutex的互斥对象,可以在"Local\aliasName",改为本地空间里的一个私有空间,这个空间下创建的对象将是本地空间里且指定用户才能访问。
StringCchPrint(szMutex,_countof(szMutex),TEXT("%s\%s"),L"aliasName","MyMutex");
HANDLE hMutex = CreateMetux(NULL,FALSE,szMutex);
3.3.2.1 创建一般的命名对象
(1)创建内核对象的函数一般最后一个参数是pszName,当为NULL表示创建匿名内核对象。否则创建一个以pszName参数命名的对象(但Windows内部并没有办法保证该名称是唯一的,即使内核对象的类型不同也不行),如果创建同名对象,会返回NULL,用GetLastError得到ERROR_INVALID_HANDLE,但这错误代码说明不了什么问题。
(2)利用对象命名来共享内核对象时,两个进程不一定是父子关系、内核对象句柄也可以不是可继承的。
①以创建互斥量内核对象为例来分析
进程A:HANDLE hMutexProcessA = CreateMutex(NULL,FALSE,TEXT("JeffMutex"));
进程B:HANDLE hMutexProcessB = CreateMutex(NULL,FALSE,TEXT("JeffMutex"));
②系统首先检查"JeffMutex"内核对象是否存在,然后检查对象类型是否相同,接着进行访问权限的检查。如果都通过,就在进程B的句柄表中找到一个空白记录项,让其指向现有的内核对象,这时进程B得到的对象与进程A实际上是同一个对象,实际上的真正的创建,只是将使用计数增加1。(注意两个对象的句柄值可能不一样,这与句柄继承机制是不同的!)
③进程B创建内核对象时,如果指定名称的对象确实存在,则其CreateMutex函数中的安全属性信息和第2个参数将被忽略。
④要判断是新创建一个内核对象还是打开一个现有的,可在CreateMutex之后,调用GetLastError函数,当错误代码为ERROR_ALREADY_EXISTS时,表示打开一个现有的。
⑤为了实现内核对象的共享,也可以考虑用Open*函数,找到指定名称的对象时,会在自己所在的进程句柄表中增加一个相应的记录项,并使该对象的使用计数递增。该函数与Create*主要区别在于,如果对象不存在,Create*会创建它,而Open*不会,只是简单地以调用失败而告终。还有一个不同,Open*还可以指定打开的内核对象是否可继承。
⑥因Windows内部没办法保证名称的唯一性,建议用GUID来作对象的名称,以达到对象名称不重复。(方法是VS→工具→创建GUID)
【Singleton1程序】利用互斥量对象实现
#include <stdio.h> #include <windows.h> int main() { //利用GUID生成唯一的锁名,防止内核对象重名 HANDLE h = CreateMutex(NULL, FALSE, TEXT("{349210D3-EF54-4EC9-8313-9F47435D785D}")); if (GetLastError()== ERROR_ALREADY_EXISTS) { CloseHandle(h); return 0; } printf("单例程序正在运行中... "); //为了演示,这里暂停一下cmd输出窗口 system("pause"); CloseHandle(h); return 0; }
3.3.2.2 终端服务命名空间
(1)终端服务(Terminal Services),其工作原理是客户机和服务器通过TCP/IP协议和标准的局域网构架联系。通过客户端终端,客户机的鼠标、键盘的输入传递到终端服务器上,再把服务器上的显示传递回客户端。客户端不需要具有计算能力,至多只需提供一定的缓存能力。众多的客户端可以同时登录到服务器上,仿佛同时在服务器上工作一样,它们之间作为不同的会话连接是互相独立的。此外,远程桌面(Remote Desktop)和快速用户切换也是利用终端服务会话来实现的。(快速用户切换的方法:按Win+L或当登录两用户后,在任务管理器→用户→选择相应的用户,然后右键,连接)
(2)全局命名空间和会话私有的命名空间
①全局命名空间的内核对象供所有客户端会话共享,在全局空间中创建内核对象要在名称前加“Global”为前缀,如
HANDLE h = CreateEvent(NULL,FALSE,FALSE,TEXT("Global\JeffEvent"))
②会话私有的命名空间(默认),名称前加“Local”前缀或省略前缀
HANDLE h = CreateEvent(NULL,FALSE,FALSE,TEXT("Local\JeffEvent"))
HANDLE h = CreateEvent(NULL,FALSE,FALSE,TEXT("JeffEvent"))//同上
【TerminalService】终端服务命名空间中内核对象的测试程序
保持帐号A的程序仍在运行中,再登录Windows帐号B,运行本程序两个实例,测试结果
/*----------------------------------------------------------------------------------- TerminalService程序需要用登录不同的Windows帐号同时运行,才能看出效果! 建议程序测试流程: 1、先登录Windows帐号A,运行两个本程序实例 2、保持上述两个实例仍在运行中,再登录Windows帐号B,再运行两个实例 -----------------------------------------------------------------------------------*/ #include <stdio.h> #include <windows.h> int main() { //先显示进程ID号和所在的会话ID DWORD processID = GetCurrentProcessId(); DWORD sessionID; if (ProcessIdToSessionId(processID, &sessionID)) { wprintf(TEXT("Process '%u' runs in Terminal Services session '%u' "), processID, sessionID); //测试,尝试在全局命名空间中创建内核对象,实验在不同帐户下同时运行该程序时, //第2个启动的程序会提示内核对象己存在的错误 HANDLE hGlobalMutex = CreateMutex(NULL, FALSE, TEXT("Global\MyMutex")); if (hGlobalMutex == NULL || ERROR_ALREADY_EXISTS == GetLastError()) printf("错误提示:全局命名空间己经存在名称为“MyMutex”的内核对象! "); else printf("在全局命名空间中成功创建名称为“MyMutex”内核对象! "); //在局部命名空间中创建内核对象,在不同帐户下同时运行该程序,可以创建同名的内核对象 HANDLE hLocalMutex = CreateMutex(NULL, FALSE, TEXT("Local\MyMutex")); //HANDLE hLocalMutex = CreateMutex(NULL, FALSE, TEXT("MyLocalMutex")); //同上 if (hLocalMutex == NULL || ERROR_ALREADY_EXISTS == GetLastError()) printf("错误提示:会话(SessionID=%u)命名空间中己存在名称为“MyMutex”同名内核对象! ",sessionID); else printf("在会话(sessionID=%u)的命名空间中成功创建名称为“MyMutex”的内核对象! ",sessionID); system("pause"); //这里必须暂时,否则当CloseHandle后内核对象会被释放 CloseHandle(hGlobalMutex); CloseHandle(hLocalMutex); } else { wprintf(TEXT("Unable to get Terminal Service session ID for process:'%u' "), processID); } system("pause"); return 0; }
3.3.2.3 专有命名空间
(1)DoS攻击:如果恶意程序先于Singleton1程序建立同名的互斥量对象,该程序无法启动,很容易被劫持,会错误地以为它自己的另一个实例正在运行,这就是DoS攻击机制。显然未命名的内核对象不会遭受DoS攻击。
(2)利用边界描述符创建私有命名空间自身名称加以保护,从而达到保护内核对象的目的。
专有命名空间类似于在内核对象的名称之前加一个目录名称,但这个命名空间没有父目录,也没有名称,所以显示出来的前缀为"..锁名 ",从而不会暴露专有命名空间的名称,即减少名称冲突,又可免遭劫持能更好的防范名称被劫持。
(3)创建专有命名空间
①创建边界描述符CreateBoundaryDescriptor:用于定义那些在命名空间中要被隔离的对象的边界。
参数 |
描述 |
LPCTSTR Name |
边界描述符的名称 |
ULONG Flags |
这个参数是为以后保留的。目前没什么用,可以为之传入0 |
返回值 |
注意这里的返回值虽然是HANDLE类型的,但并不是一个内核对象句柄,而是指针,指向一个用户模式的结构,该结构包含了边界的定义。删除里用DeleteBoundaryDescriptor。 |
②将边界描述符与本地管理员组的安全描述符关联起来
A、创建一个SID(安全描述符):CreateWellKnownSid
参数 |
描述 |
WELL_KNOWN_SID_TYPE WellKnownSidType |
SID类型 WinBuiltinAdministratorsSid:表示管理员账户组 WinWorldSid:表示所有账户 |
PSID DomainSid |
指向创建了SID的域的指针,为NULL时表示使用本地计算机 |
PSID pSid, |
指向要返回的SID的存储地址,为传出的参数 |
DWORD *cbSid |
指向存储pSid的大小的地址 |
B、将SID与边界描述符关联起来AddSIDToBoundaryDescriptor,用来决定谁能进入边界并创建命名空间。
参数 |
描述 |
HANDLE *BoundaryDescriptor |
创建边界描述符返回的句柄 |
PSID RequiredSid |
指向SID结构体的指针 |
③创建或打开专有命名空间的名称
A、创建专有命名空间CreatePrivateNamespace
参数 |
描述 |
SECURITY_ATTRIBUTES* sa |
传给Windows使用,用于允许或禁止应用程序通过OpenPrivateNamespace访问专有命名空间来打开或创建内核对象。为谁能“打开命名空间”设置一个筛选层。 |
LPVOID lpBoundaryDescriptor |
指向边界描述符的指针 |
LPCTSTR lpAliasPrefix |
用于创建内核对象的字符串前缀的别名 |
★将一个字符串格式 安全描述符 转换为一个有效的、 功能的安全描述符
ConvertStringSecurityDescriptorToSecurityDescriptor
参数 |
描述 |
LPCTSTR StringSecurityDescriptor |
指向一个空结尾的字符串包含要转换的字符串格式安全描述符的指针 |
DWORD StringSDRevision |
指定 StringSecurityDescriptor 字符串的修订级别。 当前,此值必须 SDDL_REVISION_1 |
PSECURITY_DESCRIPTOR SecurityDescriptor |
指向一个变量,接收转换后的安全描述符的指针的指针。要释放返回的缓冲区,调用 LocalFree 函数 |
PULONG SecurityDescriptorSize |
指向一个变量,接收以字节为单位的转换后的安全描述符的指针的大小。此参数可以是NULL |
返回值 |
如果该函数成功,返回值是,则返回非零值。 如果函数失败,返回值是零。 若要获取扩展的错误的信息,请调用 GetLastError 。 GetLastError 可能会返回以下错误代码之一。 ERROR_INVALID_PARAMETER:参数不是有效的 ERROR_UNKNOWN_REVISION:SDDL 修订级别无效 ERROR_NONE_MAPPED:一个 安全标识符 (SID 输入的安全描述符字符串中) 找不到一个帐户查找操作 |
★打开己有的专有命名空间:OpenPrivateNamespace
★关闭专有命名空间:ClosePrivateNamespace
④利用专有命名空间名称创建内核对象
【Singleton2程序】
/***************************************************************************************** Module:Singleton.cpp Notices:Copyright(c) 2008 Jeffery Richter & Christophe Nasarre *****************************************************************************************/ #include "....CommonFilesCmnHdr.h" #include <tchar.h> #include <strsafe.h> #include <sddl.h> #include "resource.h" ///////////////////////////////////////////////////////////////////////////////////////// //主对话框 HWND g_hDlg; //互斥对象、边界描述符和命名空间——用来检测前一个实例是否正在运行 HANDLE g_hSingleton = NULL; HANDLE g_hBoundary = NULL; HANDLE g_hNameSpace = NULL; //跟踪命令空间是否被创建或被打开 BOOL g_bNamesapceOpened = FALSE; //专有命名空间和边界描述符的名称 PCTSTR g_szBoundary = TEXT("3-Boundary"); PCTSTR g_szNameSpace = TEXT("3-Namespace"); #define DETAILS_CTRL GetDlgItem(g_hDlg,IDC_EDIT_DETAILS) ///////////////////////////////////////////////////////////////////////////////////////// //增加字符串到编辑框控件中 /* VA_LIST的用法: *(1)首先在函数里定义一个VA_LIST型的变量,这个变量是指向参数的指针; *(2)然后用VA_START宏初始化变量刚定义的VA_LIST变量; *(3)然后用VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的 * 类型(如果函数有多个可变参数的,依次调用VA_ARG获取各个参数); *(4)最后用VA_END宏结束可变参数的获取。 */ void AddText(PCTSTR pszFormat, ...) { va_list argList; va_start(argList, pszFormat); TCHAR sz[20 * 1024]; Edit_GetText(DETAILS_CTRL, sz, _countof(sz)); //strchr函数原型:extern char *strchr(const char *s,char c);查找字符串s中首次出现字符c的位置 //strchr在字符串str中查找字符ch第一次出现的位置,找到后返回一个指向该位置的指针。如果该字符不 //存在于字符串中,则返回一个NULL指针 //本例先找到编辑框字符串最后的位置,用strchr函数查找,然后用_vstprintfs_s格式化缓冲区 _vstprintf_s(_tcschr(sz, TEXT('