Windows 服務在後台執行著各種各樣任務,支援著我們日常的案頭操作。有時候可能需要服務與使用者進行資訊或介面互動操作,這種方式在XP 時代是沒有問題的,但自從Vista 開始你會發現這種方式似乎已不起作用。
Session 0 隔離實驗
下面來做一個名叫AlertService 的服務,它的作用就是向使用者發出一個提示對話方塊,我們看看這個服務在Windows 7 中會發生什麼情況。
using System.ServiceProcess;using System.Windows.Forms;namespace AlertService{ public partial class Service1 : ServiceBase { public Service1() { InitializeComponent(); } protected override void OnStart(string[] args) { MessageBox.Show("A message from AlertService."); } protected override void OnStop() { } }}
程式編譯後通過Installutil 將其載入到系統服務中:
在服務屬性中勾選“Allow service to interact with desktop” ,這樣可以使AlertService 與案頭使用者進行互動。
在服務管理員中將AlertService 服務“啟動”,這時工作列中會閃動一個表徵圖:
點擊該表徵圖會顯示下面視窗,提示有個程式(AlertService)正在試圖顯示資訊,是否需要瀏覽該資訊:
嘗試驗擊“View the message”,便會顯示下圖介面(其實這個介面我已經不能從當前案頭操作截圖了,是通過Virtual PC 截屏的,其原因請繼續閱讀)。注意觀察可以發現下圖的案頭背景已經不是Windows 7 預設的案頭背景了,說明AlertService 與案頭系統的Session 並不相同,這就是Session 0 隔離作用的結果。
Session 0 隔離原理
在Windows XP、Windows Server 2003 或早期Windows 系統時代,當第一個使用者登入系統後服務和應用程式是在同一個Session 中啟動並執行。這就是Session 0 如下圖所示:
但是這種運行方式提高了系統安全風險,因為服務是通過提升了使用者權限啟動並執行,而應用程式往往是那些不具備管理員身份的普通使用者啟動並執行,其中的危險顯而易見。
從Vista 開始Session 0 中只包含系統服務,其他應用程式則通過分離的Session 運行,將服務與應用程式隔離提高系統的安全性。如下圖所示:
這樣使得Session 0 與其他Session 之間無法進行互動,不能通過服務向案頭使用者彈出資訊視窗、UI 視窗等資訊。這也就是為什麼剛才我說那個圖已經不能通過當前案頭進行截圖了。
Session 檢查
在實際開發過程中,可以通過Process Explorer 檢查服務或程式處於哪個Session,會不會遇到Session 0 隔離問題。我們在Services 中找到之前載入的AlertService 服務,右鍵屬性查看其Session 狀態。
可看到AlertService 處於Session 0 中:
再來看看Outlook 應用程式:
很明顯在Windows 7 中服務和應用程式是處於不同的Session,它們之間加隔了一個保護牆,在下篇文章中將介紹如何穿過這堵保護牆使服務與案頭使用者進行互動操作。
如果在開發過程中確實需要服務與案頭使用者進行互動,可以通過遠端桌面服務的API 繞過Session 0 的隔離完成互動操作。
對於簡單的互動,服務可以通過WTSSendMessage 函數,在使用者Session 上顯示訊息視窗。對於一些複雜的UI 互動,必須調用CreateProcessAsUser或其他方法(WCF、.NET遠端等)進行跨Session 通訊,在案頭使用者上建立一個應用程式介面。
WTSSendMessage 函數
如果服務只是簡單的向案頭使用者Session 發送訊息視窗,則可以使用WTSSendMessage 函數實現。首先,在上一篇下載的代碼中加入一個Interop.cs 類,並在類中加入如下代碼:
public static void ShowMessageBox(string message, string title){ int resp = 0; WTSSendMessage( WTS_CURRENT_SERVER_HANDLE, WTSGetActiveConsoleSessionId(), title, title.Length, message, message.Length, 0, 0, out resp, false);}[DllImport("kernel32.dll", SetLastError = true)]public static extern int WTSGetActiveConsoleSessionId();[DllImport("wtsapi32.dll", SetLastError = true)]public static extern bool WTSSendMessage( IntPtr hServer, int SessionId, String pTitle, int TitleLength, String pMessage, int MessageLength, int Style, int Timeout, out int pResponse, bool bWait);
在ShowMessageBox 函數中調用了WTSSendMessage 來發送資訊視窗,這樣我們就可以在Service 的OnStart 函數中使用,開啟Service1.cs 加入下面代碼:
protected override void OnStart(string[] args){ Interop.ShowMessageBox("This a message from AlertService.", "AlertService Message");}
編譯器後在服務管理員中重新啟動AlertService 服務,從下圖中可以看到訊息視窗是在目前使用者案頭顯示的,而不是Session 0 中。
CreateProcessAsUser 函數
如果想通過服務向案頭使用者Session 建立一個複雜UI 程式介面,則需要使用CreateProcessAsUser 函數為使用者建立一個新進程用來運行相應的程式。開啟Interop 類繼續添加下面代碼:
public static void CreateProcess(string app, string path){ bool result; IntPtr hToken = WindowsIdentity.GetCurrent().Token; IntPtr hDupedToken = IntPtr.Zero; PROCESS_INFORMATION pi = new PROCESS_INFORMATION(); SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES(); sa.Length = Marshal.SizeOf(sa); STARTUPINFO si = new STARTUPINFO(); si.cb = Marshal.SizeOf(si); int dwSessionID = WTSGetActiveConsoleSessionId(); result = WTSQueryUserToken(dwSessionID, out hToken); if (!result) { ShowMessageBox("WTSQueryUserToken failed", "AlertService Message"); } result = DuplicateTokenEx( hToken, GENERIC_ALL_ACCESS, ref sa, (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, (int)TOKEN_TYPE.TokenPrimary, ref hDupedToken ); if (!result) { ShowMessageBox("DuplicateTokenEx failed" ,"AlertService Message"); } IntPtr lpEnvironment = IntPtr.Zero; result = CreateEnvironmentBlock(out lpEnvironment, hDupedToken, false); if (!result) { ShowMessageBox("CreateEnvironmentBlock failed", "AlertService Message"); } result = CreateProcessAsUser( hDupedToken, app, String.Empty, ref sa, ref sa, false, 0, IntPtr.Zero, path, ref si, ref pi); if (!result) { int error = Marshal.GetLastWin32Error(); string message = String.Format("CreateProcessAsUser Error: {0}", error); ShowMessageBox(message, "AlertService Message"); } if (pi.hProcess != IntPtr.Zero) CloseHandle(pi.hProcess); if (pi.hThread != IntPtr.Zero) CloseHandle(pi.hThread); if (hDupedToken != IntPtr.Zero) CloseHandle(hDupedToken);}[StructLayout(LayoutKind.Sequential)]public struct STARTUPINFO{ public Int32 cb; public string lpReserved; public string lpDesktop; public string lpTitle; public Int32 dwX; public Int32 dwY; public Int32 dwXSize; public Int32 dwXCountChars; public Int32 dwYCountChars; public Int32 dwFillAttribute; public Int32 dwFlags; public Int16 wShowWindow; public Int16 cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError;}[StructLayout(LayoutKind.Sequential)]public struct PROCESS_INFORMATION{ public IntPtr hProcess; public IntPtr hThread; public Int32 dwProcessID; public Int32 dwThreadID;}[StructLayout(LayoutKind.Sequential)]public struct SECURITY_ATTRIBUTES{ public Int32 Length; public IntPtr lpSecurityDescriptor; public bool bInheritHandle;}public enum SECURITY_IMPERSONATION_LEVEL{ SecurityAnonymous, SecurityIdentification, SecurityImpersonation, SecurityDelegation}public enum TOKEN_TYPE{ TokenPrimary = 1, TokenImpersonation}public const int GENERIC_ALL_ACCESS = 0x10000000;[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]public static extern bool CloseHandle(IntPtr handle);[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]public static extern bool CreateProcessAsUser( IntPtr hToken, string lpApplicationName, string lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes, ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandle, Int32 dwCreationFlags, IntPtr lpEnvrionment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, ref PROCESS_INFORMATION lpProcessInformation);[DllImport("advapi32.dll", SetLastError = true)]public static extern bool DuplicateTokenEx( IntPtr hExistingToken, Int32 dwDesiredAccess, ref SECURITY_ATTRIBUTES lpThreadAttributes, Int32 ImpersonationLevel, Int32 dwTokenType, ref IntPtr phNewToken);[DllImport("wtsapi32.dll", SetLastError=true)]public static extern bool WTSQueryUserToken( Int32 sessionId, out IntPtr Token);[DllImport("userenv.dll", SetLastError = true)]static extern bool CreateEnvironmentBlock( out IntPtr lpEnvironment, IntPtr hToken, bool bInherit);
在CreateProcess 函數中同時也涉及到DuplicateTokenEx、WTSQueryUserToken、CreateEnvironmentBlock 函數的使用,有興趣的朋友可通過MSDN 進行學習。完成CreateProcess 函數建立後,就可以真正的通過它來調用應用程式了,回到Service1.cs 修改一下OnStart 我們來開啟一個CMD 視窗。如下代碼:
複製代碼 代碼如下:
protected override void OnStart(string[] args)
{
Interop.CreateProcess("cmd.exe",@"C:\Windows\System32\");
}
重新編譯器,啟動AlertService 服務便可看到下圖介面。至此,我們已經可以通過一些簡單的方法對Session 0 隔離問題進行解決。大家也可以通過WCF 等技術完成一些更複雜的跨Session 通訊方式,實現在Windows 7 及Vista 系統中服務與案頭使用者的互動操作。