• 文章
  • 服務與應用程式的互動
作者:
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 帳戶許可權從服務啟動使用者級別應用程式的優秀文章。它可以在這裡找到:http://www.codeproject.com/KB/vista-security/VistaSessions.aspx