【IT168 专稿】甲型H1N1流行,人人谈隔离而色变。好在我们今天谈的不是H1N1的隔离,而是系统服务的Session 0的隔离。
隔离,是为了更好的保护。但是,众所周知的,隔离也会给我们的生活带来一些不便。在Windows 7中,操作系统服务的Session 0隔离,阻断了系统服务和用户桌面进程之间进行交互和通信的桥梁。通过Session 0隔离,虽然可以让操作系统更加安全,但是也给系统服务带来了不少兼容性的问题。
系统服务在Windows 7上遇到的问题
操作系统服务是Windows操作系统中一套完整的机制。服务不同于普通用户程序之处在于,你可以配置服务,让它们从系统启动时开始运行直到系统关闭,而整个过程无需用户参与。操作系统服务负责所有无需用户参与的后台活动,从远程过程调用(PRC)到网络位置识别服务等等。
虽然操作系统服务在执行过程中无需用户参与,但是,有些服务可能需要向用户显示一些用户界面以反馈消息或者是跟用户应用程序进行通信。这种服务在Windows 7中将遇到一些兼容性问题。比如,在Windows 7中,当你的服务尝试向用户显示一个消息对话框时,你将只会看到在任务栏上的一个图标,而无法正常地看到服务想要显示的对话框。确切地讲,你的服务可能会遇到下面这些千奇百怪的问题:
• 虽然服务在运行,但是没干任何它应该干的事情
• 虽然服务在运行,但是其他进程却无法与之通信
• 当你的服务试图通过Windows消息与用户应用程序进行通信时,消息却无法到达应用程序
• 当你的服务试图与桌面进行交互时,仅仅在任务栏上显示一个闪动的图标,无法进行正常的用户交互
以上这些问题,实际上都是因为Windows 7的系统服务Session 0隔离而引起的。这些问题大致可以分为两类:
• 服务无法正确显示用户界面或者仅仅显示一个提示界面
当一个服务想在桌面显示任何用户界面的时候(即使它被容许跟桌面进行交互),一个缓冲层会用“Interactive Service Detection”对话框提示用户,询问是否需要显示来自服务的用户交互界面。虽然用户可以选择继续查看来自服务的用户界面消息,但是,工作流的被中断,使这成为一个严重的应用程序兼容性问题。例如,下面的代码视图在服务中显示一个对话框:
{
// 进入服务循环
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 设置系统服务属性
图2 系统服务显示消息对话框
• 服务和应用程序所共享的对象变得不可见或者是不可访问
当服务创建的对象被一个普通应用程序(以普通用户权限运行)访问的时候,在全局名字空间中将无法找到这个对象(这是因为它是Session 0所私有的)。另外,即使对象是可见的,为了与之通信,其他进程的安全性也要做相应的改变。这将会影响到其他进程,比如普通用户权限运行的应用程序与服务之间的交互。
Windows 7系统服务的Session 0隔离
在Windows XP,Windows Server 2003以及其他更早期的Windows操作系统中,所有操作系统服务和应用程序都在相同的session中运行,这个session由第一个登陆系统的用户所启动。这个session被称为Session 0。在Session 0中同时运行系统服务和应用程序会给操作系统带来一些安全风险,因为服务运行在一个更高的用户权限下,这就使得系统服务成为那些想要提升自己权限的病毒或者恶意软件的攻击目标。
从Windows Vista开始,系统服务开始运行在一个被称为Session 0的特殊session中。而应用程序则被跟系统服务隔离开来,这是因为应用程序运行在由用户登录系统后创建的一系列session中。比如,Session 1对应于第一个登陆的用户,Session 2对应于第二个登录系统的用户,以此类推。
图3 Windows操作系统的Session
各个Session之间是相互独立的。在不同Session中运行的实体,相互之间不能发送Windows消息、共享UI元素或者是在没有指定他们有权限访问全局名字空间(并且提供正确的访问控制设置)的情况下,共享核心对象。
图4 Session之间是相互独立的
跨越鸿沟,如何突破Session 0隔离
虽然Session 0隔离可以使得操作系统更加安全,但是,有时候运行于Session 0的系统服务和运行于其他Session的进程之间进行交互和通信时必须的。就像大禹治水,我们不能仅仅把Session 0隔离起来就万事大吉了,我们还需要采用疏导的方式,用更加安全的方式完成Session 0和其他Session之间的交互和通信。针对Session 0隔离所带来的两类问题,我们提供相应的解决方案。
从系统服务显示消息对话框
如果一个系统服务想要发送消息对话框给用户,我们可以使用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,
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函数在用户的桌面上创建一个新的进程来显示更加复杂的用户界面,而这个进程虽然是由系统服务创建,但是却是运行在用户环境下。以下的代码演示了创建进程显示复杂UI的过程:
{
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权限,访问系统服务所创建的全局名字空间的核心对象(事件)。
{
// 获取当前的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 <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的隔离,对各种交互和通信方式的安全性提出了更高的要求。
系列文章索引: