• 文章
  • 服务与应用程序的交互
作者:
2009年5月20日

Windows Vista 中服务与用户级别应用程序的交互

评分:3.8/5 (15 票)
*****
作者
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