Win 7 下跨越Session Id 0的Windows Service 並與活動Session UI進行互動
常規的WinForm程式截圖比較簡單,只需利用Graphics的CopyFromScreen函式即可擷取當前螢幕影象,如下4行程式碼即可完成截圖並儲存檔案的功能:
Bitmap snapShot= new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height); Graphics g = Graphics.FromImage(g as Image); g.CopyFromScreen(0, 0, 0, 0, printscreen.Size); snapShot.Save(fileName, System.Drawing.Imaging.ImageFormat.Jpeg);
但是在編寫Windows Service程式時,將上面程式碼寫入到Service的loop內,想在指定時間截圖當前螢幕影象並儲存到本地硬碟內,卻發現產生的jpg影象檔案都是黑色畫面,沒有成功擷取到當前螢幕影象,即使啟動VS2010,附加到Windows Service程序進行Debug也有發現確實有執行到截圖程式碼部分。
雖然沒有必要在Windows Service裡面來進行截圖,但本著問題本身,還是經過一番搜尋。
後來Google了下,發現其他人也有類似的問題。在XP下執行正常,但是在Win Vista以及之後的Win7都會出現擷取影象為黑色畫面的情況。再後來經查證是Session 0隔離的問題。
簡單來說就是在Windows XP, Windows Server 2003或者更早期的Windows作業系統中,所有的服務和應用程式都是執行在與第一個登入到控制檯的使用者得Session中。這個Session叫做Session 0。在Session 0 中一起執行服務和使用者應用程式,由於服務是以高許可權執行的,所以會造成一些安全風險。這些因素使得一些惡意代理利用這點,來尋找提升他們自身許可權的結構。
而在Windows Vista中,服務在一個叫做Session 0 的特殊Session中承載。由於應用程式執行在使用者登入到系統後所建立的Session 0 之後的Session中,所以應用程式和服務也就隔離開來:第一個登入的使用者在Session 1中,第二個在Session 2中,以此類推。事實上執行在不同的Session中,如果沒有特別將其放入全域性名稱空間(並且設定了相應的訪問控制配置),是不能互相傳遞窗體訊息,共享UI元素或者共享kernel物件。
MSDN上介紹了通過使用WTSSendMessage 來向當前活動Session Desktop UI傳送跨Session的MessageBox訊息(從Session 0 傳送到Session 1 等等)
[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);
WTSSendMessage第二個引數即是要傳送的Session Id,可通過WTSGetActiveConsoleSessionId()來獲取當前活動的Console
Session Id。這只是方法一,其實還有其他兩種方式獲取,第二種方式是通過WTSEnumerateSessions來列舉列出當前所有Session的WTS_SESSION_INFO,並通過其state欄位是否為WTSActive來獲得活動的Console Session Id。第三種方式是,因為在Service裡面也可以枚舉出當前Active Session的各種程序Process資訊,從其中的explorer.exe程序資訊裡面可以得到Active Session Id,如下圖:由此可見,由於Window Service本身執行在Session 0 裡面,只有通過傳送Message的方式與活動UI進行互動。可在Active Session Id裡建立程序的方式讓新產生的程序能夠與活動UI進行互動(如在Service裡面建立之前講到的截圖程式的程序),需要使用到CreateProcessAsUser函式,原型如下:
[DllImport("advapi32.dll", SetLastError = true)]
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);
簡單來說,可通過得到的Active Session Id來建立相應的hToken引數,以此啟動的新程序會執行在Active Session Id內,而不會執行在Session 0隔離區內。
當然核心是CreateProcessAsUser函式,其它還會相應用到WTSQueryUserToken,DuplicateTokenEx,CreateEnvironmentBlock,DestroyEnvironmentBlock等函式。基於此,可寫出一個供C#直接呼叫的class。
{_ProcessUtil.cs} 程式碼如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Security.Principal;
using System.ComponentModel;
using System.IO;
namespace dllCoast
{
public class _ProcessUtil
{
#region ~declare struct,enum
[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;
}
[StructLayout(LayoutKind.Sequential)]
public struct WTS_SESSION_INFO
{
public int SessionId;
public IntPtr pWinStationName;
public WTS_CONNECTSTATE_CLASS State;
}
public enum SECURITY_IMPERSONATION_LEVEL
{
SecurityAnonymous,
SecurityIdentification,
SecurityImpersonation,
SecurityDelegation
}
public enum TOKEN_TYPE
{
TokenPrimary = 1,
TokenImpersonation
}
public enum WTS_CONNECTSTATE_CLASS {
WTSActive,
WTSConnected,
WTSConnectQuery,
WTSShadow,
WTSDisconnected,
WTSIdle,
WTSListen,
WTSReset,
WTSDown,
WTSInit
}
public enum _SESSION_TYPE
{
SessionFromActiveConsoleSessionId=0,
SessionFromEnumerateSessions,
SessionFromProcessExplorerSession
}
#endregion
#region ~delacre const value
public const int GENERIC_ALL_ACCESS = 0x10000000;
public const int CREATE_NO_WINDOW = 0x08000000;
public const int CREATE_UNICODE_ENVIRONMENT = 0x00000400;
public const Int32 STANDARD_RIGHTS_REQUIRED = 0x000F0000;
public const Int32 STANDARD_RIGHTS_READ = 0x00020000;
public const Int32 TOKEN_ASSIGN_PRIMARY = 0x0001;
public const Int32 TOKEN_DUPLICATE = 0x0002;
public const Int32 TOKEN_IMPERSONATE = 0x0004;
public const Int32 TOKEN_QUERY = 0x0008;
public const Int32 TOKEN_QUERY_SOURCE = 0x0010;
public const Int32 TOKEN_ADJUST_PRIVILEGES = 0x0020;
public const Int32 TOKEN_ADJUST_GROUPS = 0x0040;
public const Int32 TOKEN_ADJUST_DEFAULT = 0x0080;
public const Int32 TOKEN_ADJUST_SESSIONID = 0x0100;
public const Int32 TOKEN_READ = (STANDARD_RIGHTS_READ | TOKEN_QUERY);
public const Int32 TOKEN_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED | TOKEN_ASSIGN_PRIMARY |
TOKEN_DUPLICATE | TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_QUERY_SOURCE |
TOKEN_ADJUST_PRIVILEGES | TOKEN_ADJUST_GROUPS | TOKEN_ADJUST_DEFAULT |
TOKEN_ADJUST_SESSIONID);
public const int MB_ABORTRETRYIGNORE = 0x0002;
public const int MB_CANCELTRYCONTINUE =0x0006;
public const int MB_HELP =0x4000;
public const int MB_OK =0x0000;
public const int MB_OKCANCEL= 0x0001;
public const int MB_RETRYCANCEL =0x0005;
public const int MB_YESNO= 0x0004;
public const int MB_YESNOCANCEL =0x0003;
#endregion
#region ~import functions with Win32 API from win32 system dll
[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);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern int WTSGetActiveConsoleSessionId();
[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);
[DllImport("userenv.dll", SetLastError = true)]
static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr handle);
[DllImport("advapi32.dll", SetLastError = true)]
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 void WTSFreeMemory(IntPtr pMemory);
[DllImport("wtsapi32.dll", SetLastError = true)]
public static extern bool WTSEnumerateSessions(
IntPtr hServer,
int Reserved,
int Version,
ref IntPtr ppSessionInfo,//WTS_SESSION_INFO PWTS_SESSION_INFO *ppSessionInfo,
ref int pCount
);
#endregion
/// <summary>
/// Show a MessageBox on the active UI desktop
/// </summary>
/// <param name="title">title of the MessageBox</param>
/// <param name="message">message to show to the user</param>
/// <param name="bWait">indicates if wait for some time by parameter timeout </param>
/// <returns>success to return true</returns>
public static bool SendMessageBoxToRemoteDesktop(string title, string message, bool bWait)
{
int pResponse = 0;
IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero;
return WTSSendMessage(WTS_CURRENT_SERVER_HANDLE, GetSessionIdFromActiveConsoleSessionId(), title, title.Length, message, message.Length, MB_OK, 0, out pResponse, bWait);
}
/// <summary>
/// Show a MessageBox on the active UI desktop
/// </summary>
/// <param name="title">title of the MessageBox</param>
/// <param name="message">message to show to the user</param>
/// <param name="button_style">can be a combination of MessageBoxButtons and MessageBoxIcon, need to convert it to int</param>
/// <param name="timeout">timeout to determine when to return this function call, 0 means wait until the user response the MessageBox</param>
/// <param name="bWait">indicates if wait for some time by parameter timeout </param>
/// <returns>success to return true</returns>
public static bool SendMessageBoxToRemoteDesktop(string title, string message, int button_style, int timeout, bool bWait)
{
int pResponse = 0;
IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero;
return WTSSendMessage(WTS_CURRENT_SERVER_HANDLE, GetSessionIdFromActiveConsoleSessionId(), title, title.Length, message, message.Length, button_style, timeout, out pResponse, bWait);
}
/// <summary>
/// Show a MessageBox on the active UI desktop
/// </summary>
/// <param name="title">title of the MessageBox</param>
/// <param name="message">message to show to the user</param>
/// <param name="button_style">can be a combination of MessageBoxButtons and MessageBoxIcon, need to convert it to int</param>
/// <param name="timeout">timeout to determine when to return this function call, 0 means wait until the user response the MessageBox</param>
/// <param name="pResponse">pointer to receive the button result which clicked by user</param>
/// <param name="bWait">indicates if wait for some time by parameter timeout </param>
/// <returns>success to return true</returns>
public static bool SendMessageBoxToRemoteDesktop(string title, string message, int button_style, int timeout, out int pResponse, bool bWait)
{
IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero;
return WTSSendMessage(WTS_CURRENT_SERVER_HANDLE, GetSessionIdFromActiveConsoleSessionId(), title, title.Length, message, message.Length, button_style, timeout, out pResponse, bWait);
}
public static int GetSessionIdFromActiveConsoleSessionId()
{
int dwSessionID = WTSGetActiveConsoleSessionId();
return dwSessionID;
}
public static int GetSessionIdFromEnumerateSessions()
{
IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero;
int dwSessionId = 0;
IntPtr pSessionInfo = IntPtr.Zero;
int dwCount = 0;
WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1,
ref pSessionInfo, ref dwCount);
Int32 dataSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO));
Int32 current = (int)pSessionInfo;
for (int i = 0; i < dwCount; i++)
{
WTS_SESSION_INFO si = (WTS_SESSION_INFO)Marshal.PtrToStructure(
(System.IntPtr)current, typeof(WTS_SESSION_INFO));
if (WTS_CONNECTSTATE_CLASS.WTSActive == si.State)
{
dwSessionId = si.SessionId;
break;
}
current += dataSize;
}
WTSFreeMemory(pSessionInfo);
return dwSessionId;
}
public static int GetSessionIdFromExplorerSessionId()
{
int dwSessionId = 0;
Process[] process_array = Process.GetProcessesByName("explorer");
if (process_array.Length>0)
{
dwSessionId = process_array[0].SessionId;
}
return dwSessionId;
}
public static Process CreateProcessAsUser(string filename, string args, _SESSION_TYPE session_method)
{
IntPtr hToken = IntPtr.Zero;//WindowsIdentity.GetCurrent().Token;
int dwSessionId = 0;
IntPtr hDupedToken = IntPtr.Zero;
Int32 dwCreationFlags = 0;
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);
si.lpDesktop = "";
IntPtr lpEnvironment = IntPtr.Zero;
string full_filepath = Path.GetFullPath(filename);
string working_Dir = Path.GetDirectoryName(full_filepath);
try
{
#region ~ get sessionid
switch (session_method)
{
case _SESSION_TYPE.SessionFromActiveConsoleSessionId:
dwSessionId = GetSessionIdFromActiveConsoleSessionId();
break;
case _SESSION_TYPE.SessionFromEnumerateSessions:
dwSessionId = GetSessionIdFromEnumerateSessions();
break;
case _SESSION_TYPE.SessionFromProcessExplorerSession:
dwSessionId = GetSessionIdFromExplorerSessionId();
break;
default:
dwSessionId = GetSessionIdFromActiveConsoleSessionId();
break;
}
#endregion
#region ~ retrieve Token from a specified SessionId
bool bResult = WTSQueryUserToken(dwSessionId, out hToken);
if (!bResult)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
#endregion
#region ~ Duplicate from the specified Token
if (!DuplicateTokenEx(
hToken,
GENERIC_ALL_ACCESS,
ref sa,
(int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification,
(int)TOKEN_TYPE.TokenPrimary,
ref hDupedToken
))
throw new Win32Exception(Marshal.GetLastWin32Error());
#endregion
#region ~ Create a Environment Block from specifid Token
bool result = CreateEnvironmentBlock(out lpEnvironment, hDupedToken, false);
if (!result)
{
lpEnvironment = IntPtr.Zero; //if fail, reset it to Zero
}
else
{
dwCreationFlags = CREATE_UNICODE_ENVIRONMENT; //if success, set the CreationsFlags to CREATE_UNICODE_ENVIRONMENT, then pass it into CreateProcessAsUser
}
#endregion
#region ~ Create a Process with Specified Token
if (!CreateProcessAsUser(
hDupedToken,
full_filepath,
string.Format("\"{0}\" {1}", filename.Replace("\"", ""), args),
ref sa,
ref sa,
false,
dwCreationFlags,
lpEnvironment,
working_Dir,
ref si,
ref pi
))
throw new Win32Exception(Marshal.GetLastWin32Error());
#endregion
#region ~ Destroy the Environment Block which is Created by CreateEnvironment
DestroyEnvironmentBlock(lpEnvironment);
#endregion
return Process.GetProcessById(pi.dwProcessID);
}
catch(Win32Exception e)
{
#region ~handle win32 exception
int pResponse = 0;
string errMsg =
"NativeErrorCode:\t" + e.NativeErrorCode
+ "\n\nSource: " + e.Source
+ "\n\nMessage: " + e.Message
+ "\n\nStackTrace: " + e.StackTrace;
SendMessageBoxToRemoteDesktop("Win32 Exception!!", errMsg, MB_OK, 0, out pResponse, false); //send the error message to the remote desktop
return null;
#endregion
}
finally
{
#region ~ release hanlde
if (pi.hProcess != IntPtr.Zero)
CloseHandle(pi.hProcess);
if (pi.hThread != IntPtr.Zero)
CloseHandle(pi.hThread);
if (hDupedToken != IntPtr.Zero)
CloseHandle(hDupedToken);
#endregion
}
}
}
}