本文主要介绍了不同session之间的进程通信。应用场景如下:
SessionID为0的windows服务S需要检测和控制SessionID为1或2(取决于该进程的用户)的UI程序的多个实例(P1 ,P2,P3……)的运行状态。
1.遇到的第一个问题是,需要在服务S中检测指定进程P_i的运行状态。在该进程为多实例运行的状态下,区分不同的实例可以根据进程ID和进程的窗口名称来进行区分。服务S接收到检测窗口名称为“WindowText_i”的进程的运行状态,而服务不能调用窗口相关的API来获得进程的窗口,只能遍历到每个进程的进程ID,因此无法将窗口名称和进程ID对应起来。
解决方案:进程P_i启动时创建一个共享内存,该共享内存的名称的唯一性标志为该进程P_i的进程ID,然后进程P_i向这块共享内存中写入自己的窗口名称;服务S需要检测进程P_i的状态时,先枚举所有的进程ID,然后根据进程ID查找共享内存,如果找到,说明该进程是自己需要的检测的进程,然后读取共享内存里的数据,可以获得该进程的窗口名称“WindowText_i”,达到将窗口名称和进程ID对应起来的目的。
共享内存名称示例:
'''
strMemName.Format(_T("Global\\ShareMemory_%d"),GetCurrentProcessId());
'''
前面加入的“global\\”是为了创建全局的共享内存,不同session区域的进程只能够找到自己所属session区域的共享内存和全局共享内存。服务和进程P_i属于不同的session区域,因此必须要加上全局标识。
创建共享内存的语句:
// 创建共享文件句柄
g_hMapFile = CreateFileMapping(
NULL, // 物理文件句柄
&sa, // 默认安全级别
PAGE_EXECUTE_READWRITE, // 可读可写
0, // 高位文件大小
SHRE_MEMORY_BUF_SIZE, // 低位文件大小
strMemName // 共享内存名称
);
创建共享文件句柄的第二个参数为安全级别,创建一般的共享内存时,可以将这个参数设置为NULL,表示使用默认的安全级别。但是在创建一个可以被服务获取到的全局共享内存时,需要指定一个更加广泛的的安全描述符,增加一个新的访问控制项目(ACE)给你的ASP进程的拥有者,保证服务能够获取到该共享内存。默认的访问控制列表(ACL)只包含创建者和管理员组。
SECURITY_ATTRIBUTES sa = { sizeof(sa) };
SECURITY_DESCRIPTOR sd;
InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
SetSecurityDescriptorDacl(&sd, TRUE, NULL, FALSE);
sa.lpSecurityDescriptor = &sd;
创建全局的共享内存之前,最好再创建一个全局的互斥量,保证共享内存不会被多个进程同时访问,增加安全性。互斥量的命名模式与共享内存一致,但是名字不能相同(相同的名字,创建时会失败),创建互斥量时的安全描述符,可以与共享内存使用同一个。
共享内存和互斥量的资源释放都要在进程P_i中实现。
2.遇到的第二个问题是,当由服务S启动某个进程P_i时,如果服务S使用的API为CreateProcesss,那么启动后的进程P_i是看不到界面的,因为CreateProcess创建的子进程与父进程的sessionID相同。如果想要创建有界面的进程P_i,需要调用另一个API函数:CreateProcessAsUser。调用过程如下:
BOOL WINAPI CreateProcessAsUser(
_In_opt_ HANDLE hToken,
_In_opt_ LPCTSTR lpApplicationName,
_Inout_opt_ LPTSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ BOOL bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCTSTR lpCurrentDirectory,
_In_ LPSTARTUPINFO lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation
);
第一个参数 hToken是用户认证令牌的句柄,一般而言可以通过遍历进程,然后获得资源管理器(explorer.exe)的用户令牌来得到当前用户的用户认证令牌:
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION,FALSE,pe32.th32ProcessID);
bRet = OpenProcessToken(hProcess,TOKEN_ALL_ACCESS,&hToken);
其中,hProcess 为资源管理器(explorer.exe)的进程句柄。
但是在本例中,使用当前登录用户的认证令牌创建的进程P_i是没有权限创建全局共享内存的,因为只有具有管理员权限的用户才可以创建全局共享内存。普通进程可以选择右键管理员权限并单击确认按钮启动程序来获得管理员权限,但是本例中的进程通过服务启动,而服务通过网络接收远程计算机的控制,不可能在启动进程P_i时再点击确认按钮。因此需要使用另一种方式为进程P_i获得管理员权限。以下代码添加到服务中。
第一,需要获得服务所属用户(system)的令牌hPToken:
OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS,&hPToken);
第二,复制该令牌,得到新的令牌hUserTokenDup
DuplicateTokenEx(hPToken,TOKEN_ALL_ACCESS,NULL,SecurityAnonymous, TokenPrimary,&hUserTokenDup);
第三,设置新的令牌hUserTokenDup的令牌信息
dwConsoleSessionId = WTSGetActiveConsoleSessionId();//获取当前用户的会话ID,如果不是远程桌面登录的情况下,只执行这一步就可以了
SetTokenInformation(hUserTokenDup,TokenSessionId,(void*)&dwConsoleSessionId,sizeof(DWOR));
当操作系统上登录了多个用户的时候,WTSGetActiveConsoleSessionId()获得的sessionid可能与当前远程桌面用户的不一致,如果使用这个会话ID来设置令牌信息,远程用户无法看到启动后的进程P_i的界面,因此需要获得当前资源管理器(explorer.exe)用户的会话ID:ProcessIdToSessionId(pe32.th32ProcessID, &dwSessionID);然后比较两个会话ID。
第四,为了保证进程P_I具有创建全局共享内存的权限,需要赋予令牌这个权限:
LookupPrivilegeValue(NULL,SE_CREATE_GLOBAL_NAME/*SE_DEBUG_NAME*/,&luid);//查找创建全局变量的权限
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; //权限使能为启用
AdjustTokenPrivileges(hUserTokenDup,FALSE,&tp,sizeof(TOKEN_PRIVILEGES),(PTOKEN_PRIVILEGES)NULL,NULL);
第五,创建进程环境块,保证环境块是在用户桌面的环境下
HANDLE hToken;
WTSQueryUserToken(dwConsoleSessionId, &hToken);
if(!CreateEnvironmentBlock(&pEnv,hToken,FALSE))
{
dwCreationFlags |= CREATE_UNICODE_ENVIRONMENT;
}
第六,创建进程
CreateProcessAsUser(hUserTokenDup,NULL,lpCmdLine,NULL,NULL,FALSE, dwCreationFlags,pEnv,NULL,&si,&pi);
第七,释放资源
关闭前面所有的句柄,并释放环境:DestroyEnvironmentBlock(pEnv)。
3.遇到的第三个问题,由服务创建的进程P_i可以创建全局共享内存,但是进程P_i有很多中启动方式,其他方式启动的进程P_i不具有管理员权限,且用户一般为当前登录用户。这时候可以为该用户添加一个创建全局变量的权限。创建过程如图所示。
按照序号1 - 6的顺序,依次点击,然后在弹出的对话框中输入需要添加的用户名(以user为例),即可为该用户添加创建全局对象的权限,完成后如序号7所示。
添加完成后,使用普通用户也可以双击启动进程P_i,并由进程P_i创建全局共享内存。
