转自:http://technet.microsoft.com/zh-cn/ee791007.aspx
面试问题:Vista与XP的Session 0与Session X的区别
1、在Windows Xp/2003中第一个用户登陆后 Session 0中会启动应用程序进程 和服务应用程序 进程,第 二个用户登陆后会产生Sessino 1会话 Session1中包含的 是仅是应用程序,第三个用户登 陆后的Session 2和Session 1相同也是仅包括应用程序 进程。
2、在Windows Vista中第一个用户登陆后的Session是1仅产生应用程序,而Session0仅用于启 动服务应用程序,第二个用户会产生 Session 2与Session1相似。
Session0会被其它用户拿来共享
在Vista中的Session的功能使用其比在XP和Win2003中更加安全。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Session 0隔离
Windows 7的GA发布日期依然是10月22日,不到一个月的时间了,这意味着您的应用程序应该已经针对Windows 7做好了准备。在 针对Windows 7准备您的应用程序的同时,请一定要验证您的程序在 版本检查和 UAC数据重定向方面不存在问题。本文Session 0隔离介绍的则是另一个值得您注意的,可能导致应用程序兼容性问题的因素,尤其是在您的应用程序包含服务的情况下。如果您的服务在Windows Vista下工作正常,那么通常在Windows 7中也将可以正常使用(但您依然需要在Windows 7下对您的应用程序进行彻底的测试)。然而,如果在Windows Vista下就没有进行妥善的兼容性测试,那么建议您花些时间阅读本文。
首先简单介绍一下服务是什么吧。
服务是什么?
服务是Microsoft Windows操作系统的一个组成机制,您可以将服务想像成一种忽略当前用户上下文环境的“特殊的应用程序”。服务与“普通”的用户应用程序有很大不同,主要是因为您可以配置让服务在系统启动(引导)后就自动运行,直到系统关闭才结束,而并不需要用户登录。也就是说,就算没有用户登录,服务业可以正常运行。
我们也可以把服务看作是不需要用户的干预,直接在后台运行的“任务”。Windows上的服务需要负责所有不需要用户参与的后台活动,例如远程过程调用(RPC)服务,以及打印池服务,甚至还有网络位置感知服务等。
问题在哪里?
有些服务可能需要在用户界面上显示对话框,或需要与用户的应用程序通讯,这种类型的功能“通常”属于Windows XP服务,因为在Windows XP中,这样做很容易。如果您的服务恰好需要显示某些用户界面对象,例如对话框,或者需要与应用程序通讯,则在Windows 7下运行可能会遇到问题。
如果在Windows 7上运行需要显示对话框的服务,此时并不能看到所需的对话框,您将在任务栏上看到一个闪烁的图标。而且,如果单击这个闪烁图标,还会看到一个对话框。更具体来说,在Windows 7中运行时,您的服务可能会遇到下列一个或多个现象,这个服务可能会:
- 正常运行,但无法完成既定工作,同时耗费大量CPU时钟和内存。
- 正常运行,但其他进程无法与该服务通讯,该服务业无法与用户或其他应用程序/服务通讯。
- 尝试通过Windows消息机制与用户应用程序通讯,但Windows消息机制无法实现目标。
- 在任务栏上显示一个闪烁的图标,代表该服务希望与桌面交互。
上述所有现象都意味着,您的服务遇到了Windows 7服务的Session 0隔离问题,也就是说,服务和用户应用程序之间存在“物理上的”分离,但这只是冰山一角。首先,让我们先来定义您的服务在Windows 7下可能遇到的两种问题类型:
- 服务无法显示UI,或显示替代UI(例如烦人的闪烁对话框):当服务尝试显示用户界面元素(哪怕该服务允许与桌面交互)时,替代层会使用下图所示的检测到交互式服务对话(Interactive Service Detection)这样的对话框提示用户,这个通知对话框会显示在用户的桌面上。如果用户单击以查看该“信息”,显示内容会切换到安全模式下,虽然用户可以选择是否查看服务的UI对话框,但这种做法会打断工作流,导致严重的应用程序兼容性问题。
例如,下面的代码视图在服务中显示一个对话框:
如果我们在服务属性中,配置服务不可以与桌面进行交互,我们将看不到任何对话框,即使我们在服务中配置它可以与琢磨进行交互,它也会被“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; }
图1 设置系统服务属性
图2 系统服务显示消息对话框
- 服务和应用程序所共享的对象变得不可见或无法访问:当服务所创建的对象由标准应用程序(使用标准用户特权运行)访问时,在全局命名空间中无法找到该对象(也就是说,该对象是Session 0专用的)。这意味着其他应用程序将无法从全局命名空间访问所谓的“共享对象”,而更严格来说,无法直接从Session 0访问。另外,安全性上的某些变动可能会导致另一种情况,就算该对象是可见的,但也无法访问。这可能会影响其他进程(例如标准用户应用程序)与您的服务交互,自然也就妨碍了应用程序的执行。
-
很明显,Session 0隔离可能会导致一些非常严重的兼容性问题,因此本文将向您介绍用于判断您的服务是否存在该问题的相关方法,以及问题的解决方法。然而这里需要提醒您,将用户应用程序与服务隔离的主要原因是为了让恶意软件更加难以使用提升后的特权运行,因为提权后运行的恶意软件将比标准用户权限下运行时造成更大的破坏,这些内容会在下一节介绍。而正是因为这样才产生了越来越安全的Windows操作系统。
原因:Windows 7服务的Session 0隔离
在Windows XP、Windows Server 2003,以及更老版本的Windows操作系统中,服务和应用程序使用相同的会话(Session)运行,而这个会话是由第一个登录到控制台的用户启动的。该会话就叫做Session 0,如下图所示,在Windows Vista之前,Session 0不仅包含服务,也包含标准用户应用程序。
将服务和用户应用程序一起在Session 0中运行会导致安全风险,因为服务会使用提升后的权限运行,而用户应用程序使用用户特权(大部分都是非管理员用户)运行,这会使得恶意软件以某个服务为攻击目标,通过“劫持”该服务,达到提升自己权限级别的目的。
从Windows Vista开始,只有服务可以托管到Session 0中,用户应用程序和服务之间会被隔离,并需要运行在用户登录到系统时创建的后续会话中。
例如第一个登录的用户创建 Session 1,第二个登录的用户创建Session 2,以此类推,如下图所示。
使用不同会话运行的实体(应用程序或服务)如果不将自己明确标注为全局命名空间,并提供相应的访问控制设置,将无法互相发送消息,共享UI元素,或共享内核对象。这一过程如下图所示:
图3 Windows操作系统的Session
各个Session之间是相互独立的。在不同Session中运行的实体,相互之间不能发送Windows消息、共享UI元素或者是在没有指定他们有权限访问全局名字空间(并且提供正确的访问控制设置)的情况下,共享核心对象。
有关Session 0隔离对Windows Vista中服务和驱动的影响的详细信息,请参考 http://www.microsoft.com/whdc/system/vista/services.mspx ,该文也适用于Windows 7。
如何确定自己的服务是否会遇到上述某些问题?
至此,我们已经介绍了Windows服务的Session 0隔离相关的现象,并且解释了服务隔离的概念,同时介绍了这种机制会对您的服务和应用程序有何影响。下面是为了了解您的实际问题,以及着手解决之前,您应该进行的测试和其他操作。
测试1 – 验证服务(或任何其他进程)的会话分配
- 启动Process Explorer。
1. 要下载Process Explorer或了解详细信息,请访问Microsoft TechNet上的Process Explorer网站。 - 确保Process Explorer显示了所有进程:
1. 单击File 。
2. 选择Show Details from All Processes。 - 找到第一个csrss.exe进程,这是一项服务,位于System Idle Process下方(见下图),查看其属性:
1. 用鼠标右键单击该进程。
2. 选择Properties。
3. 打开Security选项卡。
4. 记下该服务所运行的会话(通常是Session 0),及其完整性级别。 -
找到 第二个csrss.exe进程,该进程位于Wininit.exe下方(见下图),按照上文介绍的步骤查看其属性:
下面左图所示的是使用System级别Session 0运行的csrss.exe实例的进程属性,而下面右图所示的是以相同System级别运行,但位于不同会话,Session 2中的csrss.exe实例的进程属性:
如果您的服务运行于Session 0之下,并且使用了System完整性级别,那么就无法直接显示UI。同时在与该服务共享内核对象或文件时也会遇到一定的问题。
测试2 – 确保对象的可访问性
- 启动Process Explorer。
- 确保Process Explorer显示了所有进程:
1. 单击File。
2. 选择Show Details from All Processes。 - 找到目标服务。
- 如果该服务包含了您已知需要与用户应用程序共享的对象,请在下方Handles窗格(按下Ctrl-H可打开,或者可从View菜单下打开)中打开对应的句柄。
1. 用鼠标右键单击每个怀疑的句柄,然后选择Properties。
2. 打开Security选项卡即可看到允许访问被该句柄所引用的对象的用户和组。
下图所示的是一个每个人都能访问(“Synchronize”权限)的共享对象的例子,哪怕这是一个运行于Session 0的系统服务,也不会受到限制。
下图所示的则是只能被管理员以及SYSTEM组访问的共享对象的例子:
现在您知道了问题的起因,那么应该如何解决?
最难理解的部分已经说完了,知道并了解了这些内容后,您可能确定自己遇到了Session 0问题,其实解决起来也很简单。
这里就列出了几个用于解决此类问题的思路:
- 如果服务需要通过发送消息的方式与用户交互,请使用WTSSendMessage函数。在功能上,该函数几乎与MessageBox相同,对于不需要过于完整的UI的服务,这是一种简单易行的方法,而且足够安全,因为所显示的信息框无法用于夺取对底层服务的控制。
eg.
// 显示消息对话框 void ShowMessage(LPWSTR lpszMessage, LPWSTR lpszTitle) { // 获得当前Session ID DWORD dwSession = WTSGetActiveConsoleSessionId(); DWORD dwResponse = 0; // 显示消息对话框 WTSSendMessage(WTS_CURRENT_SERVER_HANDLE, dwSession, lpszTitle, 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); // 显示对话框 ShowMessage(L"这是一个从Session 0显示的对话框", L"Session 0隔离"); if (dwResponse == IDNO) continue; } return 0; }
这样,我们就可以直接看到来自服务的消息对话框而不会被“Interactive Service Detection”所打断工作流。
- 如果您的服务需要更完整的UI,请使用CreateProcessAsUser函数在发起请求的用户的桌面上创建进程。但是这里需要注意,您依然需要在新创建的进程和原始服务之间进行通讯,因此这里就需要用到下一点内容。
eg.
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权限,访问系统服务所创建的全局名字空间的核心对象(事件)。
eg.
在这段代码中,我们通过用户令牌,获取用户的SID,然后通过SID和SDDl的转换,创建了安全描述器对象,并通过这个安全描述器对象最终创建了具有合适访问控制的全局名字空间的对象。现在,在客户端我们就可以顺利的访问这个全局名字空间的对象,与之进行通信了。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; }
#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; }
请确保需要跨会话共享的内核对象使用了带有Global\字符串的名称前缀,该字符串意味着这个对象属于会话全局(session-global)命名空间。参考资源
有关该话题的详细信息,请参考 Windows 7 Training Kit for Developers,其中包含了详细的白皮书和上手实验室。如果需要,您可以只下载Session 0隔离上手实验室。
下面列出了有关本文用到的工具的简单介绍:
Process Explorer– 用于监控Windows进程的工具,可显示进程完整性级别和对象安全信息。
- 详细信息: http://technet.microsoft.com/en-us/sysinternals/bb896645.aspx
- 下载“ http://download.sysinternals.com/Files/ProcessExplorer.zip
有关该问题和其他问题的更多信息,还可以参考 Channel 9的Windows 7话题。
有关Windows 7的技术信息和上手体验,请下载 Windows 7 Training Kit for Developers,在这里还可了解很多其他问题。
- 启动Process Explorer。