当一个服务想在桌面显示任何用户界面的时候(即使它被容许跟桌面进行交互),一个缓冲层会用“Interactive Service Detection”对话框提示用户,询问是否需要显示来自服务的用户交互界面。虽然用户可以选择继续查看来自服务的用户界面消息,但是,工作流的被中断,使这成为一个严重的应用程序兼容性问题。例如,下面的代码视图在服务中显示一个对话框:
DWORD WINAPI TimeServiceThread(LPVOID) { // 进入服务循环 while (!g_Stop) { DWORD dwResponse = 0; Sleep(5000); // 显示对话框 dwResponse = MessageBox(NULL,L"这是一个从Session 0显示的对话框",L"Session 0隔离", MB_YESNO); if (dwResponse == IDNO) continue; } return 0; }
如果我们在服务属性中,配置服务不可以与桌面进行交互,我们将看不到任何对话框,即使我们在服务中配置它可以与琢磨进行交互,它也会被“Interactive Service Detection”对话框打断,使得工作流被中断。
图1 设置系统服务属性
从Windows Vista开始,系统服务开始运行在一个被称为Session 0的特殊session中。而应用程序则被跟系统服务隔离开来,这是因为应用程序运行在由用户登录系统后创建的一系列session中。比如,Session 1对应于第一个登陆的用户,Session 2对应于第二个登录系统的用户,以此类推。
图3 Windows操作系统的Session
各个Session之间是相互独立的。在不同Session中运行的实体,相互之间不能发送Windows消息、共享UI元素或者是在没有指定他们有权限访问全局名字空间(并且提供正确的访问控制设置)的情况下,共享核心对象。
如果一个系统服务想要发送消息对话框给用户,我们可以使用WTSSendMessage函数。这个函数提供了跟MessageBox大致相同的功能。这将为那些无需复杂UI的服务提供了一个简单的,易于实现的,但是功能足够的解决方案。并且,这也是安全的,因为被显示的消息框不能被用来控制底层服务。还是上文的MessageBox的例子,我们用ShowMessage函数封装WTSSendMessage函数,从系统服务显示一个消息对话框到用户桌面:
void ShowMessage(LPWSTR lpszMessage, LPWSTR lpszTitle) { // 获得当前Session ID DWORD dwSession = WTSGetActiveConsoleSessionId(); DWORD dwResponse = 0; // 显示消息对话框 WTSSendMessage(WTS_CURRENT_SERVER_HANDLE, dwSession, pszTitle, static_cast<DWORD>((wcslen(lpszTitle) + 1) * sizeof(wchar_t)), lpszMessage, static_cast<DWORD>((wcslen(lpszMessage) + 1) * sizeof(wchar_t)),0, 0, &dwResponse, FALSE); } DWORD WINAPI TimeServiceThread(LPVOID) {// 进入服务循环 while (!g_Stop) { DWORD dwResponse = 0; Sleep(5000); // 显示对话框 dwResponse = MessageBox(NULL,L"这是一个从Session 0显示的对话框",L"Session 0隔离", MB_YESNO); if (dwResponse == IDNO) continue; } return 0;
这样,我们就可以直接看到来自服务的消息对话框而不会被“Interactive Service Detection”所打断工作流。 显示更复杂的UI 如果我们不满足于仅仅显示一个消息对话框,而需要从系统服务显示一个更加复杂的用户界面,这时我们可以使用CreateProcessAsUser函数在用户的桌面上创建一个新的进程来显示更加复杂的用户界面,而这个进程虽然是由系统服务创建,但是却是运行在用户环境下。以下的代码演示了创建进程显示复杂UI的过程:DWORD WINAPI TimeServiceThread(LPVOID) { while (!g_Stop) { Sleep(5000); // 为了显示更加复杂的用户界面,我们需要从Session 0创建 // 一个进程,但是这个进程是运行在用户环境下。 // 我们可以使用CreateProcessAsUser实现这一功能。 BOOL bSuccess = FALSE; STARTUPINFO si = {0}; // 进程信息 PROCESS_INFORMATION pi = {0}; si.cb = sizeof(si); // 获得当前Session ID DWORD dwSessionID = WTSGetActiveConsoleSessionId(); HANDLE hToken = NULL; // 获得当前Session的用户令牌 if (WTSQueryUserToken(dwSessionID, &hToken) == FALSE) { goto Cleanup; } // 复制令牌 HANDLE hDuplicatedToken = NULL; if (DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &hDuplicatedToken) == FALSE) { goto Cleanup; } // 创建用户Session环境 LPVOID lpEnvironment = NULL; if (CreateEnvironmentBlock(&lpEnvironment, hDuplicatedToken, FALSE) == FALSE) { goto Cleanup; } // 获得复杂界面的名字,也就是获得可执行文件的路径 WCHAR lpszClientPath[MAX_PATH]; if (GetModuleFileName(NULL, lpszClientPath, MAX_PATH) == 0) { goto Cleanup; } PathRemoveFileSpec(lpszClientPath); wcscat_s(lpszClientPath, sizeof(lpszClientPath)/sizeof(WCHAR), L"\\TimeServiceClient.exe"); // 在复制的用户Session下执行应用程序,创建进程。 // 通过这个进程,就可以显示各种复杂的用户界面了 if (CreateProcessAsUser(hDuplicatedToken, lpszClientPath, NULL, NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS CREATE_NEW_CONSOLE CREATE_UNICODE_ENVIRONMENT, lpEnvironment, NULL, &si, &pi) == FALSE) { goto Cleanup; } CloseHandle(pi.hProcess); CloseHandle(pi.hThread); bSuccess = TRUE; // 清理工作 Cleanup: if (!bSuccess) { ShowMessage(L"无法创建复杂UI", L"错误"); } if (hToken != NULL) CloseHandle(hToken); if (hDuplicatedToken != NULL) CloseHandle(hDuplicatedToken); if (lpEnvironment != NULL) DestroyEnvironmentBlock(lpEnvironment); } return 0; }
在这段代码中,我们首先获得了当前的Session ID,然后通过Session ID,我们获得用户令牌。有了用户令牌后,我们就可以创建一个相同的用户环境了,而最终我们所创建的复杂界面进程将在这个环境下运行和显示。完成这些准备工作后,我们利用CreateProcessAsUser函数在复制的用户环境下创建新的进程,显示复杂的用户界面。用这种方式创建的进程,不会受到“Interactive Service Detection”对话框的打扰而直接显示到用户桌面上,这跟从当前用户Session执行应用程序并无太大的差别。
图5 从系统服务显示的复杂界面操作系统服务和用户进程进行通信
以上的例子,展示了如何在服务中显示用户界面到用户桌面。这只是系统服务因为Session 0隔离而遇到的第一类问题。如果系统服务想与用户进程进行通信,又该如何处理呢?在这种情况下,我们可以使用Windows Communication Foundation (WCF), .NET remoting, 命名管道(named pipes)或者是其他的进程通信(interprocess communication ,IPC))机制(除了Windows消息之外)在Session之间进行通信。有人可能要问,Session 0隔离本身是为了系统安全而采取的保护措施,如果在隔离的同时又允许系统服务和用户进程进行通信,那岂不是Session 0隔离毫无意义?实际上,隔离并不是完全意义上的隔断。Session 0隔离后,我们需要以更加安全的方式进行操作系统服务和用户进程之间的交互和通信。
安全通讯和其他共享对象(例如,命名管道,文件映射),通过使用自由访问控制列表(DACL)来加强用户组访问权限的控制。同时我们可以使用一个系统访问控制列表(SACL),以确保中低权限的进程可以访问共享对象,即使这个对象是一个系统或更高权限的服务所创建的。下面这段代码,就演示了如何通过DACL权限,访问系统服务所创建的全局名字空间的核心对象(事件)。DWORD WINAPI AlertServiceThread(LPVOID) { // 获取当前的Session ID和用户令牌 DWORD dwSessionID = WTSGetActiveConsoleSessionId(); HANDLE hToken = NULL; if (WTSQueryUserToken(dwSessionID, &hToken) == FALSE) { goto Cleanup; } // 获取用户的SID(security identifier) // 注意这里我们两次调用了GetTokenInformation函数 // 第一次是为了获取TKOEN_USER结构体的大小 // 第二次才是真正地获取信息,填充这个结构体 DWORD dwLength; TOKEN_USER* account = NULL; if (GetTokenInformation(hToken, TokenUser, NULL, 0, &dwLength) == FALSE &&GetLastError() != ERROR_INSUFFICIENT_BUFFER) { goto Cleanup; } account = (TOKEN_USER*)new BYTE[dwLength]; if (GetTokenInformation(hToken, TokenUser,(LPVOID)account, dwLength, &dwLength) == FALSE) { goto Cleanup; } // 在这里,我们调用ConvertSidToStringSid函数将 // 用户的SID转换成SID字符串,然后通过SID字符串我们创建一个SDDL字符串, // 有了SDDL字符串之后,我们可以创建一个安全描述器(Security Descriptor)。 // 而这个安全描述器,是我们在后面创建全局对象所需要的. LPWSTR lpszSid = NULL; if (ConvertSidToStringSid(account->User.Sid, &lpszSid) == FALSE) { goto Cleanup; } WCHAR sddl[1000]; wsprintf(sddl, L"O:SYG:BAD:(A;;GA;;;SY)(A;;GA;;;%s)S:(ML;;NW;;;ME)", lpszSid); // 转换SDDL字符串到一个安全描述器对象 PSECURITY_DESCRIPTOR sd = NULL; if (ConvertStringSecurityDescriptorToSecurityDescriptor(sddl, SDDL_REVISION_1, &sd, NULL) == FALSE) { goto Cleanup; } // 用上面创建的安全描述器对象初始化SECURITY_ATTRIBUTES结构体 SECURITY_ATTRIBUTES sa; sa.bInheritHandle = FALSE; sa.lpSecurityDescriptor = sd; sa.nLength = sizeof(sa); // 创建全局名字空间的事件 // 这里需要注意的是,全局名字空间的对象都需要有Global的前缀 g_hAlertEvent = CreateEvent(&sa, FALSE, FALSE, L"Global\\AlertServiceEvent"); if (g_hAlertEvent == NULL) { goto Cleanup; } while (!g_Stop) { Sleep(5000); // 发送一个事件 SetEvent(g_hAlertEvent); } // 清理工作 Cleanup: if (hToken != NULL) CloseHandle(hToken); if (account != NULL) delete[] account; if (lpszSid != NULL) LocalFree(lpszSid); if (sd != NULL) LocalFree(sd); if (g_hAlertEvent == NULL) CloseHandle(g_hAlertEvent); return 0; }
在这段代码中,我们通过用户令牌,获取用户的SID,然后通过SID和SDDl的转换,创建了安全描述器对象,并通过这个安全描述器对象最终创建了具有合适访问控制的全局名字空间的对象。现在,在客户端我们就可以顺利的访问这个全局名字空间的对象,与之进行通信了。
#include <windows.h> #include <stdio.h> int main() { // 打开全局名字空间的共享事件对象 // 注意,这里我们同样适用了Global前缀 HANDLE hEvent = OpenEvent(SYNCHRONIZE, FALSE, L"Global\\AlertServiceEvent"); if (hEvent == NULL) { printf("无法打开服务事件: %d\n", GetLastError()); return -1; } while (TRUE) { printf("等待服务事件...\n"); WaitForSingleObject(hEvent, INFINITE); printf("获得服务事件!\n"); } return 0; }
牛郎织女隔着银河还有鹊桥来沟通,所以系统服务和用户桌面之间的Session 0隔离,也有相应的方式来完成它们之间的交互和通信。只是Session 0的隔离,对各种交互和通信方式的安全性提出了更高的要求.