C# window Service实现调用有UI的应用程序(关于win xp以后的window系统)
时间:2022-03-18 10:35
用服务去打开一个UI程序,是不可能的,服务后台的进程所使用的用户是system,这个用户是不需要UI的,因此也就限制了打开有UI线程的应用。因此,你要打开一个UI线程,必须使用一个管理员权限的账号去打开程序,默认一个程序去打开另一个程序,后一个程序其使用的win账号是前一个程序的win账号。
我开发的系统中有一接口程序(这里就称Task,是一个C#的Console Application)经常无故的死掉,导致第二天的数据不能正常解析,所以,我写了一个window service去监视Task,如果发现Task在进程列表中不存在或线程数少于两个(Task为多线程程序),就重新调起Task。
开始没接触过window service调用application的例子,在网上查了下,百度的实现方法大致都是直接初始一个新的进程实例,然后将要调用的程序路径赋给这个新的进程实例,最后启动进程。这样写了后,我在window server 2008(R2)系统或window 7的任务管理器中能看到被调用程序的进程,但是没有被调用程序的UI。这问题一直耽搁我好长时间,最后在google中看到 写的一篇文章 Subverting Vista UAC in Both 32 and 64 bit Architectures
原文章地址为:
这里也很感谢作者,我在google和bing中搜了好多都没能解决问题。
原来问题在于,xp系统的用户和window service运行在一个session下,在xp以后,windows系统改变了用户会话管理的策略,window service独立运行在session0下,依次给后续的登录用户分配sessionX(X =1,2,3...),session0没有权限运行UI。所以在window xp以后的系统下,window service调用有UI的application时只能看到程序进程但不能运行程序的UI。
原文章给出了详细的解释和代码,请大家去仔细阅读,这里我只想谢我自己的解决过程中的问题
作者的解决思路是:window service创建一个和与当前登陆用户可以交互的进程,这个进程运行在admin权限下,能够调起应用程序的UI
具体的做法是:widow service复制winlogon.exe进程句柄,然后通过调用api函数CreateProcessAsUser
()以winlogon.exe权限创建新进程,新创建的进程有winlogon.exe的权限(winlogon.exe运行在system权限下),负责调用程序。
这里CreateProcessAsUser
()是以用户方式创建新的进程,winlogon.exe进程是负责管理用户登录的进程,每个用用户登录系统后都会分配一个winlogon.exe进程,winlogon.exe与用户的session ID关联。以下是作者原文,作者的描述很详细很到位,这也是我贴出来作者原文的目的。^_^ ^_^
作者原文:
First, we are going to create a Windows Service that runs under the System account. This service will be responsible for spawning an interactive process within the currently active User’s Session. This newly created process will display a UI and run with full admin rights. When the first User logs on to the computer, this service will be started and will be running in Session0; however the process that this service spawns will be running on the desktop of the currently logged on User. We will refer to this service as the LoaderService.
Next, the winlogon.exe process is responsible for managing User login and logout procedures. We know that every User who logs on to the computer will have a unique Session ID and a corresponding winlogon.exe process associated with their Session. Now, we mentioned above, the LoaderService runs under the System account. We also confirmed that each winlogon.exe process on the computer runs under the System account. Because the System account is the owner of both the LoaderService and the winlogon.exe processes, our LoaderService can copy the access token (and Session ID) of the winlogon.exe process and then call the Win32 API function CreateProcessAsUser
to launch a process into the currently active Session of the logged on User. Since the Session ID located within the access token of the copied winlogon.exe process is greater than 0, we can launch an interactive process using that token.
我以作者的源代码实现后在win7下完美的调起了Task的UI。但当我部署到服务器(服务器系统是window server2008r2)时出问题了,怎么都调不起Task程序的UI,甚至连Task的进程都看不到了。之后我通过测试程序,看到在服务器上登陆了三个用户,也分配了三个session,我所登陆账户的session id为3,但却发现存在4个winlogon.exe进程,通过我所登陆的session id关联到的winlogon.exe进程的id却是4,???这问题让我彻底的乱了...(现在依然寻找这问题的答案)
winlogon.exe不靠谱,我只能通过拷贝其他进程的句柄创建用于调用程序UI的进程,找了半天发现explorer.exe桌面进程肯定运行在用户下,所以尝试了用explorer.exe进程替代winlogon.exe,测试后,在win7 和window server 2008r2下都能完美调用Task的UI。
启动程序的代码:
//启动Task程序 ApplicationLoader.PROCESS_INFORMATION procInfo; ApplicationLoader.StartProcessAndBypassUAC(applicationName, out procInfo);
ApplicationLoader类
using System; using System.Collections.Generic; using System.Text; using System.Security; using System.Diagnostics; using System.Runtime.InteropServices; namespace WS_Monitor_Task_CSharp { /// <summary> /// Class that allows running applications with full admin rights. In /// addition the application launched will bypass the Vista UAC prompt. /// </summary> public class ApplicationLoader { #region Structrures [StructLayout(LayoutKind.Sequential)] public struct SECURITY_ATTRIBUTES { public int Length; public IntPtr lpSecurityDescriptor; public bool bInheritHandle; } [StructLayout(LayoutKind.Sequential)] public struct STARTUPINFO { public int cb; public String lpReserved; public String lpDesktop; public String lpTitle; public uint dwX; public uint dwY; public uint dwXSize; public uint dwYSize ; public uint dwXCountChars; public uint dwYCountChars; public uint dwFillAttribute; public uint dwFlags; public short wShowWindow; public short 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 uint dwProcessId; public uint dwThreadId; } #endregion #region Enumberation enum TOKEN_TYPE : int { TokenPrimary = 1, TokenImpersonation = 2 } enum SECURITY_IMPERSONATION_LEVEL : int { SecurityAnonymous = 0, SecurityIdentification = 1, SecurityImpersonation = 2, SecurityDelegation = 3, } #endregion #region Constants public const int TOKEN_DUPLICATE = 0x0002; public const uint MAXIMUM_ALLOWED = 0x2000000; public const int CREATE_NEW_CONSOLE = 0x00000010; public const int IDLE_PRIORITY_CLASS = 0x40; public const int NORMAL_PRIORITY_CLASS = 0x20; public const int HIGH_PRIORITY_CLASS = 0x80; public const int REALTIME_PRIORITY_CLASS = 0x100; #endregion #region Win32 API Imports [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CloseHandle(IntPtr hSnapshot); [DllImport("kernel32.dll")] static extern uint WTSGetActiveConsoleSessionId(); [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] public extern static bool CreateProcessAsUser(IntPtr hToken, String lpApplicationName, String lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes, ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandle, int dwCreationFlags, IntPtr lpEnvironment, String lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [DllImport("kernel32.dll")] static extern bool ProcessIdToSessionId(uint dwProcessId, ref uint pSessionId); [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")] public extern static bool DuplicateTokenEx(IntPtr ExistingTokenHandle, uint dwDesiredAccess, ref SECURITY_ATTRIBUTES lpThreadAttributes, int TokenType, int ImpersonationLevel, ref IntPtr DuplicateTokenHandle); [DllImport("kernel32.dll")] static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId); [DllImport("advapi32", SetLastError = true), SuppressUnmanagedCodeSecurityAttribute] static extern bool OpenProcessToken(IntPtr ProcessHandle, int DesiredAccess, ref IntPtr TokenHandle); #endregion /// <summary> /// Launches the given application with full admin rights, and in addition bypasses the Vista UAC prompt /// </summary> /// <param name="applicationName">The name of the application to launch</param> /// <param name="procInfo">Process information regarding the launched application that gets returned to the caller</param> /// <returns></returns> public static bool StartProcessAndBypassUAC(String applicationName, out PROCESS_INFORMATION procInfo) { uint winlogonPid = 0; IntPtr hUserTokenDup = IntPtr.Zero, hPToken = IntPtr.Zero, hProcess = IntPtr.Zero; procInfo = new PROCESS_INFORMATION(); // obtain the currently active session id; every logged on user in the system has a unique session id TSControl.WTS_SESSION_INFO[] pSessionInfo = TSControl.SessionEnumeration(); uint dwSessionId = 100; for (int i = 0; i < pSessionInfo.Length; i++) { if (pSessionInfo[i].SessionID != 0) { try { int count = 0; IntPtr buffer = IntPtr.Zero; StringBuilder sb = new StringBuilder(); bool bsuccess = TSControl.WTSQuerySessionInformation( IntPtr.Zero, pSessionInfo[i].SessionID, TSControl.WTSInfoClass.WTSUserName, out sb, out count); if (bsuccess) { if (sb.ToString().Trim() == "dmpadmin") { dwSessionId = (uint)pSessionInfo[i].SessionID; } } } catch (Exception ex) { LoaderService.WriteLog(ex.Message.ToString(),"Monitor"); } } } // obtain the process id of the winlogon process that is running within the currently active session Process[] processes = Process.GetProcessesByName("explorer"); foreach (Process p in processes) { if ((uint)p.SessionId == dwSessionId) { winlogonPid = (uint)p.Id; } } LoaderService.WriteLog(winlogonPid.ToString(), "Monitor"); // obtain a handle to the winlogon process hProcess = OpenProcess(MAXIMUM_ALLOWED, false, winlogonPid); // obtain a handle to the access token of the winlogon process if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE, ref hPToken)) { CloseHandle(hProcess); return false; } // Security attibute structure used in DuplicateTokenEx and CreateProcessAsUser // I would prefer to not have to use a security attribute variable and to just // simply pass null and inherit (by default) the security attributes // of the existing token. However, in C# structures are value types and therefore // cannot be assigned the null value. SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES(); sa.Length = Marshal.SizeOf(sa); // copy the access token of the winlogon process; the newly created token will be a primary token if (!DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, ref sa, (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, (int)TOKEN_TYPE.TokenPrimary, ref hUserTokenDup)) { CloseHandle(hProcess); CloseHandle(hPToken); return false; } // By default CreateProcessAsUser creates a process on a non-interactive window station, meaning // the window station has a desktop that is invisible and the process is incapable of receiving // user input. To remedy this we set the lpDesktop parameter to indicate we want to enable user // interaction with the new process. STARTUPINFO si = new STARTUPINFO(); si.cb = (int)Marshal.SizeOf(si); si.lpDesktop = @"winsta0\default"; // interactive window station parameter; basically this indicates that the process created can display a GUI on the desktop // flags that specify the priority and creation method of the process int dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE; // create a new process in the current user‘s logon session bool result = CreateProcessAsUser(hUserTokenDup, // client‘s access token null, // file to execute applicationName, // command line ref sa, // pointer to process SECURITY_ATTRIBUTES ref sa, // pointer to thread SECURITY_ATTRIBUTES false, // handles are not inheritable dwCreationFlags, // creation flags IntPtr.Zero, // pointer to new environment block null, // name of current directory ref si, // pointer to STARTUPINFO structure out procInfo // receives information about new process ); // invalidate the handles CloseHandle(hProcess); CloseHandle(hPToken); CloseHandle(hUserTokenDup); LoaderService.WriteLog("launch Task","Monitor"); return result; // return the result } } }
TSControl类
using System; using System.Collections.Generic; using System.Text; using System.Runtime.InteropServices; namespace WS_Monitor_Task_CSharp { public class TSControl { /**/ /// <summary> /// Terminal Services API Functions,The WTSEnumerateSessions function retrieves a list of sessions on a specified terminal server, /// </summary> /// <param name="hServer">[in] Handle to a terminal server. Specify a handle opened by the WTSOpenServer function, or specify WTS_CURRENT_SERVER_HANDLE to indicate the terminal server on which your application is running</param> /// <param name="Reserved">Reserved; must be zero</param> /// <param name="Version">[in] Specifies the version of the enumeration request. Must be 1. </param> /// <param name="ppSessionInfo">[out] Pointer to a variable that receives a pointer to an array of WTS_SESSION_INFO structures. Each structure in the array contains information about a session on the specified terminal server. To free the returned buffer, call the WTSFreeMemory function. /// To be able to enumerate a session, you need to have the Query Information permission.</param> /// <param name="pCount">[out] Pointer to the variable that receives the number of WTS_SESSION_INFO structures returned in the ppSessionInfo buffer. </param> /// <returns>If the function succeeds, the return value is a nonzero value. If the function fails, the return value is zero</returns> [DllImport("wtsapi32", CharSet = CharSet.Auto, SetLastError = true)] private static extern bool WTSEnumerateSessions(int hServer, int Reserved, int Version, ref long ppSessionInfo, ref int pCount); /**/ /// <summary> /// Terminal Services API Functions,The WTSFreeMemory function frees memory allocated by a Terminal Services function. /// </summary> /// <param name="pMemory">[in] Pointer to the memory to free</param> [DllImport("wtsapi32.dll")] public static extern void WTSFreeMemory(System.IntPtr pMemory); /**/ /// <summary> /// Terminal Services API Functions,The WTSLogoffSession function logs off a specified Terminal Services session. /// </summary> /// <param name="hServer">[in] Handle to a terminal server. Specify a handle opened by the WTSOpenServer function, or specify WTS_CURRENT_SERVER_HANDLE to indicate the terminal server on which your application is running. </param> /// <param name="SessionId">[in] A Terminal Services session identifier. To indicate the current session, specify WTS_CURRENT_SESSION. You can use the WTSEnumerateSessions function to retrieve the identifiers of all sessions on a specified terminal server. /// To be able to log off another user‘s session, you need to have the Reset permission </param> /// <param name="bWait">[in] Indicates whether the operation is synchronous. /// If bWait is TRUE, the function returns when the session is logged off. /// If bWait is FALSE, the function returns immediately.</param> /// <returns>If the function succeeds, the return value is a nonzero value. /// If the function fails, the return value is zero.</returns> [DllImport("wtsapi32.dll")] public static extern bool WTSLogoffSession(int hServer, long SessionId, bool bWait); [DllImport("Wtsapi32.dll")] public static extern bool WTSQuerySessionInformation( System.IntPtr hServer, int sessionId, WTSInfoClass wtsInfoClass, out StringBuilder ppBuffer, out int pBytesReturned ); public enum WTSInfoClass { WTSInitialProgram, WTSApplicationName, WTSWorkingDirectory, WTSOEMId, WTSSessionId, WTSUserName, WTSWinStationName, WTSDomainName, WTSConnectState, WTSClientBuildNumber, WTSClientName, WTSClientDirectory, WTSClientProductId, WTSClientHardwareId, WTSClientAddress, WTSClientDisplay, WTSClientProtocolType } /**/ /// <summary> /// The WTS_CONNECTSTATE_CLASS enumeration type contains INT values that indicate the connection state of a Terminal Services session. /// </summary> public enum WTS_CONNECTSTATE_CLASS { WTSActive, WTSConnected, WTSConnectQuery, WTSShadow, WTSDisconnected, WTSIdle, WTSListen, WTSReset, WTSDown, WTSInit, } /**/ /// <summary> /// The WTS_SESSION_INFO structure contains information about a client session on a terminal server. /// if the WTS_SESSION_INFO.SessionID==0, it means that the SESSION is the local logon user‘s session. /// </summary> public struct WTS_SESSION_INFO { public int SessionID; [MarshalAs(UnmanagedType.LPTStr)] public string pWinStationName; public WTS_CONNECTSTATE_CLASS state; } /**/ /// <summary> /// The SessionEnumeration function retrieves a list of ///WTS_SESSION_INFO on a current terminal server. /// </summary> /// <returns>a list of WTS_SESSION_INFO on a current terminal server</returns> public static WTS_SESSION_INFO[] SessionEnumeration() { //Set handle of terminal server as the current terminal server int hServer = 0; bool RetVal; long lpBuffer = 0; int Count = 0; long p; WTS_SESSION_INFO Session_Info = new WTS_SESSION_INFO(); WTS_SESSION_INFO[] arrSessionInfo; RetVal = WTSEnumerateSessions(hServer, 0, 1, ref lpBuffer, ref Count); arrSessionInfo = new WTS_SESSION_INFO[0]; if (RetVal) { arrSessionInfo = new WTS_SESSION_INFO[Count]; int i; p = lpBuffer; for (i = 0; i < Count; i++) { arrSessionInfo[i] = (WTS_SESSION_INFO)Marshal.PtrToStructure(new IntPtr(p), Session_Info.GetType()); p += Marshal.SizeOf(Session_Info.GetType()); } WTSFreeMemory(new IntPtr(lpBuffer)); } else { //Insert Error Reaction Here } return arrSessionInfo; } public TSControl() { // // TODO: 在此处添加构造函数逻辑 // } } }
以上TSControl类为通过用户登录名获取用户session Id的类,ApplicationLoader类中大多是原作者的代码。
第一次发博客,语言组织的很乱,不正确或者大家不是很懂的地方请指出,我会重新修改或者直接回复的,希望这些东西对大家有用