作者
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