VS與Win7共舞:系統服務的Session 0隔離
2010-02-09 09:21:18| 分類: Delphi | 標籤: |字號大中小 訂閱
隔離,是爲了更好的保護。但是,衆所周知的,隔離也會給我們的生活帶來一些不便。在Windows 7中,操作系統服務的Session 0隔離,阻斷了系統服務和用戶桌面進程之間進行交互和通信的橋樑。通過Session 0隔離,雖然可以讓操作系統更加安全,但是也給系統服務帶來了不少兼容性的問題。
系統服務在Windows 7上遇到的問題
操作系統服務是Windows操作系統中一套完整的機制。服務不同於普通用戶程序之處在於,你可以配置服務,讓它們從系統啓動時開始運行直到系統關閉, 而整個過程無需用戶參與。操作系統服務負責所有無需用戶參與的後臺活動,從遠程過程調用(PRC)到網絡位置識別服務等等。
雖然操作系 統服務在執行過程中無需用戶參與,但是,有些服務可能需要向用戶顯示一些用戶界面以反饋消息或者是跟用戶應用程序進行通信。這種服務在Windows 7中將遇到一些兼容性問題。比如,在Windows 7中,當你的服務嘗試向用戶顯示一個消息對話框時,你將只會看到在任務欄上的一個圖標,而無法正常地看到服務想要顯示的對話框。確切地講,你的服務可能會 遇到下面這些千奇百怪的問題:
? 雖然服務在運行,但是沒幹任何它應該乾的事情
? 雖然服務在運行,但是其他進程卻無法與之通信
? 當你的服務試圖通過Windows消息與用戶應用程序進行通信時,消息卻無法到達應用程序
? 當你的服務試圖與桌面進行交互時,僅僅在任務欄上顯示一個閃動的圖標,無法進行正常的用戶交互
以上這些問題,實際上都是因爲Windows 7的系統服務Session 0隔離而引起的。這些問題大致可以分爲兩類:
? 服務無法正確顯示用戶界面或者僅僅顯示一個提示界面
當一個服務想在桌面顯示任何用戶界面的時候(即使它被容許跟桌面進行交互),一個緩衝層會用“Interactive Service Detection”對話框提示用戶,詢問是否需要顯示來自服務的用戶交互界面。雖然用戶可以選擇繼續查看來自服務的用戶界面消息,但是,工作流的被中 斷,使這成爲一個嚴重的應用程序兼容性問題。例如,下面的代碼視圖在服務中顯示一個對話框:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
DWORD WINAPI TimeServiceThread(LPVOID)
{
// 進入服務循環
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函數,從系統服務顯示一個消息對話框到用戶桌 面:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
// 顯示消息對話框
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的過程:
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
DWORD WINAPI TimeServiceThread(LPVOID)
{
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權限,訪問系統服務所創建的全局名字空間的核心 對象(事件)。
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
DWORD WINAPI AlertServiceThread(LPVOID)
{
// 獲取當前的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的轉換,創建了安全描述器對象,並通過這個安全描述器對象最終創建了具有合適訪問控制的全局名字空間的對象。現在,在客戶端我們就可以順利的訪問這個全局名字空間的對象,與之進行通信了。
Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/
#include <windows.h>
#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的隔離,對各種交互和通信方式的安全性提出了更高的要求。
系列文章索引:
VS與Win7共舞:UAC惹禍 如何進行安裝程序檢測?
VS2010與Win7共舞:UAC與數據重定向
VS2010與Win7共舞:ETW自定義程序日誌
VS與Win7共舞:性能計數器進行性能分析
VS2010與Windows7共舞:對庫進行編程
VS與Windows 7共舞:庫(Library)
VS2010與Win7共舞:響應Ribbon控件消息
VS與Win7共舞:用XML文件定義Ribbon界面
VS 2010與Windows7共舞:又見Ribbon
VS2010與Win7共舞 :任務欄狀態提示
VS2010與Win7共舞 :任務欄縮略圖
VS2010與Windows 7共舞:Jumplist