在 Windows 7 時代,微軟引入了一項根本性的架構變更——Session 0 隔離。這項機制徹底改變了服務與使用者應用程式的互動方式,也是許多舊版應用程式在遷移至 Windows 7 時遭遇相容性問題的核心原因。本文將深入剖析 Session 0 隔離的運作原理、常見症狀,並提供完整的解決方案與除錯實戰指南。
在 Windows XP 及更早版本中,所有服務與第一個登入主控台的使用者應用程式共同運行在 Session 0。服務擁有高權限(通常為 SYSTEM),而使用者應用程式則在同一 Session 中運行。這種設計雖然方便服務直接顯示 UI 或與應用程式通訊,但卻造成嚴重的安全隱患:惡意程式可利用服務的高權限提升自身權限。
Windows Vista 起,微軟強制將服務隔離在專屬的 Session 0,而使用者登入後建立的 Session 從 Session 1 開始遞增。不同 Session 之間預設無法傳遞視窗訊息、共用 UI 元素或 Kernel 物件,除非明確放入全域命名空間並設定適當的存取控制。

受 Session 0 隔離影響的服務通常表現出以下兩大類問題:
當服務嘗試顯示任何 UI 元素(即使已勾選「允許服務與桌面互動」),系統會彈出 Interactive Services Detection 對話框,提示使用者切換到 Session 0 的安全桌面查看。這種工作流中斷嚴重破壞使用者體驗,構成重大相容性問題。

服務建立的物件若未明確放入全域命名空間(Global\ 前綴),將僅存在於 Session 0 的私有命名空間中,標準使用者應用程式根本找不到。即便物件可見,安全性描述元(DACL/SACL)也可能阻止低權限進程存取。
WTSSendMessage若服務僅需向使用者傳遞簡單訊息(類似 MessageBox),可使用 WTSSendMessage。此 API 會在使用者的作用中桌面顯示對話框,且無法控制底層服務,安全且有效。
CreateProcessAsUser需要完整 UI 介面時,應在要求使用者的桌面建立獨立進程。透過 WTSQueryUserToken 取得使用者 Token,再用 CreateProcessAsUser 啟動 UI 進程,實現真正的桌面互動。
避免使用視窗訊息(Window Messages)跨 Session 通訊。改用 WCF、.NET Remoting、命名管道 或其他 IPC 機制,配合適當的 DACL/SACL 設定,確保安全可靠的跨 Session 資料交換。
Global\ 開頭命名,置於全域命名空間。icacls 設定完整性層級(如 Medium)以供標準應用程式存取。File → Show processes from all users。Properties → Security 標籤,查看 Session ID(通常為 0)與 Integrity Level(通常為 System/High)。

View → Handles (Ctrl+H)。Properties → Security,確認 Users 群組是否擁有存取權(如 Synchronize)。

icacls MyFile /setintegritylevel Medium
將檔案完整性層級設為 Medium,即可讓大多數使用者應用程式存取。
void ShowMessage(LPWSTR lpszMessage, LPWSTR lpszTitle)
{
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);
}
BOOL bSuccess = FALSE;
STARTUPINFO si = {0};
PROCESS_INFORMATION pi = {0};
si.cb = sizeof(si);
DWORD dwSessionID = WTSGetActiveConsoleSessionId();
HANDLE hToken = NULL;
if (WTSQueryUserToken(dwSessionID, &hToken) == FALSE) goto Cleanup;
HANDLE hDuplicatedToken = NULL;
if (DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &hDuplicatedToken) == FALSE) goto Cleanup;
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");
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"An error occurred while creating fancy client UI", L"Error");
if (hToken != NULL) CloseHandle(hToken);
if (hDuplicatedToken != NULL) CloseHandle(hDuplicatedToken);
if (lpEnvironment != NULL) DestroyEnvironmentBlock(lpEnvironment);
DWORD dwSessionID = WTSGetActiveConsoleSessionId();
HANDLE hToken = NULL;
if (WTSQueryUserToken(dwSessionID, &hToken) == FALSE) goto Cleanup;
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;
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);
PSECURITY_DESCRIPTOR sd = NULL;
if (ConvertStringSecurityDescriptorToSecurityDescriptor(sddl, SDDL_REVISION_1, &sd, NULL) == FALSE) goto Cleanup;
SECURITY_ATTRIBUTES sa;
sa.bInheritHandle = FALSE;
sa.lpSecurityDescriptor = sd;
sa.nLength = sizeof(sa);
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);
Session 0 隔離是 Windows 安全架構的里程碑。開發者必須從「服務直接秀 UI」的思維轉型為「服務專注後台、UI 獨立進程、IPC 跨界溝通」的現代模式。掌握 Global\ 命名、DACL/SACL 配置、WTSSendMessage 與 CreateProcessAsUser 兩大 API,配合 Process Explorer 除錯,即可輕鬆解決 90% 以上的遷移相容性問題。