作者
Yuri Maxiutenko,
Apriorit Inc. 软件开发人员
本文档探讨了在 Windows Vista 中与服务和应用程序交互的问题。特别是,我们将研究如何从服务启动交互式用户级别应用程序。本文档可能对那些使用托管代码和本机代码在 Windows Vista 上组织服务与应用程序之间交互任务的人员有所帮助。
Windows Vista、服务和桌面
在 Vista 出现之前,Windows 系列操作系统中的服务和用户应用程序可以共同使用会话 0。从服务直接在当前用户的桌面上打开窗口,以及通过窗口消息在服务和应用程序之间交换数据,都是很容易实现的。但是,当出现一整类利用服务打开的窗口来获取对服务本身访问的攻击时,这成为了一个严重的安全问题。这种反制机制仅在 Vista 中出现。
在 Windows Vista 中,所有用户登录和注销都在与会话 0 不同的会话中进行。服务在用户桌面上打开窗口的可能性受到严格限制,如果您尝试从服务启动应用程序,它将在会话 0 中启动。相应地,如果该应用程序是交互式的,您需要切换到会话 0 的桌面。窗口消息用于数据交换的用途已大大增加。
这种安全策略是完全可以理解的。但是,如果您仍然需要从服务在用户桌面上启动一个交互式应用程序呢?本文档描述了解决此问题的一种可能方案。此外,我们将考虑几种组织服务与应用程序之间数据交换的方法。
从服务启动交互式应用程序
由于服务和当前用户桌面存在于不同的会话中,服务必须“伪装”成该用户才能启动交互式应用程序。为此,我们应该知道相应的登录名和密码,或者拥有 LocalSystem 帐户。第二种变体更常见,因此我们将对其进行考虑。
因此,我们使用 LocalSystem 帐户创建服务。首先,我们应该获取当前用户的令牌。为了做到这一点,我们
1) 获取所有终端会话的列表;
2) 选择活动会话;
3) 获取登录到活动会话的用户的令牌;
4) 复制获得的令牌。
C++ 代码 您可以在下面的 C++ 中看到相应的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
PHANDLE GetCurrentUserToken()
{
PHANDLE currentToken = 0;
PHANDLE primaryToken = 0;
int dwSessionId = 0;
PHANDLE hUserToken = 0;
PHANDLE hTokenDup = 0;
PWTS_SESSION_INFO pSessionInfo = 0;
DWORD dwCount = 0;
// Get the list of all terminal sessions WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pSessionInfo, &dwCount);
int dataSize = sizeof(WTS_SESSION_INFO);
// look over obtained list in search of the active session
for (DWORD i = 0; i < dwCount; ++i)
{
WTS_SESSION_INFO si = pSessionInfo<i>;
if (WTSActive == si.State)
{
// If the current session is active – store its ID
dwSessionId = si.SessionId;
break;
}
}
// Get token of the logged in user by the active session ID
BOOL bRet = WTSQueryUserToken(dwSessionId, currentToken);
if (bRet == false)
{
return 0;
}
bRet = DuplicateTokenEx(currentToken, TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS, 0, SecurityImpersonation, TokenPrimary, primaryToken);
if (bRet == false)
{
return 0;
}
return primaryToken;
}
|
应该提到的是,您可以使用 WTSGetActiveConsoleSessionId() 函数,而不是遍历整个列表。此函数返回活动会话的 ID。但是,在我实际使用时发现,此函数并非总是有效,而遍历所有会话的变体始终会产生正确的结果。
如果当前会话没有登录用户,则 WTSQueryUserToken() 函数会返回 FALSE 并附带错误代码 ERROR_NO_TOKEN。在这种情况下,您自然无法使用下面的代码。
在获得令牌后,我们就可以代表当前用户启动应用程序。请注意,应用程序的权限将与当前用户帐户的权限相对应,而不是 LocalSystem 帐户的权限。
代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
BOOL Run(const std::string& processPath, const std::string& arguments)
{
// Get token of the current user
PHANDLE primaryToken = GetCurrentUserToken();
if (primaryToken == 0)
{
return FALSE;
}
STARTUPINFO StartupInfo;
PROCESS_INFORMATION processInfo;
StartupInfo.cb = sizeof(STARTUPINFO);
SECURITY_ATTRIBUTES Security1;
SECURITY_ATTRIBUTES Security2;
std::string command = "\"" + processPath + "\"";
if (arguments.length() != 0)
{
command += " " + arguments;
}
void* lpEnvironment = NULL;
// Get all necessary environment variables of logged in user
// to pass them to the process
BOOL resultEnv = CreateEnvironmentBlock(&lpEnvironment, primaryToken, FALSE);
if (resultEnv == 0)
{
long nError = GetLastError();
}
// Start the process on behalf of the current user
BOOL result = CreateProcessAsUser(primaryToken, 0, (LPSTR)(command.c_str()), &Security1, &Security2, FALSE, CREATE_NO_WINDOW | NORMAL_PRIORITY_CLASS | CREATE_UNICODE_ENVIRONMENT, lpEnvironment, 0, &StartupInfo, &processInfo);
CloseHandle(primaryToken);
return result;
}
|
如果开发的软件仅在 Windows Vista 及更高版本的操作系统中使用,则可以使用 CreateProcessWithTokenW() 函数代替 CreateProcessAsUser()。例如,可以这样调用它:
|
BOOL result = CreateProcessWithTokenW(primaryToken, LOGON_WITH_PROFILE, 0, (LPSTR)(command.c_str()), CREATE_NO_WINDOW | NORMAL_PRIORITY_CLASS | CREATE_UNICODE_ENVIRONMENT, lpEnvironment, 0, &StartupInfo, &processInfo);
|
此外,我们还必须提到,有一篇关于使用 LocalSystem 帐户权限从服务启动用户级别应用程序的优秀文章。它可以在这里找到:
https://codeproject.org.cn/KB/vista-security/VistaSessions.aspx