這次工作中遇到要從服務中啟動一個具有桌面UI交互的應用,這在winXP/2003中只是一個簡單創建進程的問題。但在Vista 和 win7中增加了session隔離,這一操作系統的安全舉措使得該任務變得復雜了一些。
一、Vista和win7的session隔離
一個用戶會有一個獨立的session。在Vista 和 win7中session 0被單獨出來專門給服務程序用,用戶則使用session 1、session 2...
這樣在服務中通過CreateProcess()創建的進程啟動UI應用用戶是無法看到的。它的用戶是SYSTEM。所以用戶無法與之交互,達不到需要的目的。
關於更多session 0的信息請點擊這里查看微軟介紹。
二、實現代碼
首先貼出自己的實現代碼,使用的是C#:

1 using System; 2 using System.Security; 3 using System.Diagnostics; 4 using System.Runtime.InteropServices; 5 6 namespace Handler 7 { 8 /// <summary> 9 /// Class that allows running applications with full admin rights. In 10 /// addition the application launched will bypass the Vista UAC prompt. 11 /// </summary> 12 public class ApplicationLoader 13 { 14 #region Structures 15 16 [StructLayout(LayoutKind.Sequential)] 17 public struct SECURITY_ATTRIBUTES 18 { 19 public int Length; 20 public IntPtr lpSecurityDescriptor; 21 public bool bInheritHandle; 22 } 23 24 [StructLayout(LayoutKind.Sequential)] 25 public struct STARTUPINFO 26 { 27 public int cb; 28 public String lpReserved; 29 public String lpDesktop; 30 public String lpTitle; 31 public uint dwX; 32 public uint dwY; 33 public uint dwXSize; 34 public uint dwYSize; 35 public uint dwXCountChars; 36 public uint dwYCountChars; 37 public uint dwFillAttribute; 38 public uint dwFlags; 39 public short wShowWindow; 40 public short cbReserved2; 41 public IntPtr lpReserved2; 42 public IntPtr hStdInput; 43 public IntPtr hStdOutput; 44 public IntPtr hStdError; 45 } 46 47 [StructLayout(LayoutKind.Sequential)] 48 public struct PROCESS_INFORMATION 49 { 50 public IntPtr hProcess; 51 public IntPtr hThread; 52 public uint dwProcessId; 53 public uint dwThreadId; 54 } 55 56 #endregion 57 58 #region Enumerations 59 60 enum TOKEN_TYPE : int 61 { 62 TokenPrimary = 1, 63 TokenImpersonation = 2 64 } 65 66 enum SECURITY_IMPERSONATION_LEVEL : int 67 { 68 SecurityAnonymous = 0, 69 SecurityIdentification = 1, 70 SecurityImpersonation = 2, 71 SecurityDelegation = 3, 72 } 73 74 #endregion 75 76 #region Constants 77 78 //public const int TOKEN_DUPLICATE = 0x0002; 79 public const uint MAXIMUM_ALLOWED = 0x2000000; 80 public const int CREATE_NEW_CONSOLE = 0x00000010; 81 public const int CREATE_UNICODE_ENVIRONMENT = 0x00000400; 82 83 public const int NORMAL_PRIORITY_CLASS = 0x20; 84 //public const int IDLE_PRIORITY_CLASS = 0x40; 85 //public const int HIGH_PRIORITY_CLASS = 0x80; 86 //public const int REALTIME_PRIORITY_CLASS = 0x100; 87 88 #endregion 89 90 #region Win32 API Imports 91 92 [DllImport("Userenv.dll", EntryPoint = "DestroyEnvironmentBlock", 93 SetLastError = true)] 94 private static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment); 95 96 [DllImport("Userenv.dll", EntryPoint = "CreateEnvironmentBlock", 97 SetLastError = true)] 98 private static extern bool CreateEnvironmentBlock(ref IntPtr lpEnvironment, 99 IntPtr hToken, bool bInherit); 100 101 [DllImport("kernel32.dll", EntryPoint = "CloseHandle", SetLastError = true)] 102 private static extern bool CloseHandle(IntPtr hSnapshot); 103 104 [DllImport("kernel32.dll", EntryPoint = "WTSGetActiveConsoleSessionId")] 105 static extern uint WTSGetActiveConsoleSessionId(); 106 107 [DllImport("Kernel32.dll", EntryPoint = "GetLastError")] 108 private static extern uint GetLastError(); 109 110 [DllImport("Wtsapi32.dll", EntryPoint = "WTSQueryUserToken", SetLastError = true)] 111 private static extern bool WTSQueryUserToken(uint SessionId, ref IntPtr hToken); 112 113 [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, 114 CharSet = CharSet.Unicode, 115 CallingConvention = CallingConvention.StdCall)] 116 public extern static bool CreateProcessAsUser(IntPtr hToken, 117 String lpApplicationName, 118 String lpCommandLine, 119 ref SECURITY_ATTRIBUTES lpProcessAttributes, 120 ref SECURITY_ATTRIBUTES lpThreadAttributes, 121 bool bInheritHandle, 122 int dwCreationFlags, 123 IntPtr lpEnvironment, 124 String lpCurrentDirectory, 125 ref STARTUPINFO lpStartupInfo, 126 out PROCESS_INFORMATION lpProcessInformation); 127 128 [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")] 129 public extern static bool DuplicateTokenEx(IntPtr ExistingTokenHandle, uint dwDesiredAccess, 130 ref SECURITY_ATTRIBUTES lpThreadAttributes, int TokenType, 131 int ImpersonationLevel, ref IntPtr DuplicateTokenHandle); 132 133 #endregion 134 135 /// <summary> 136 /// Launches the given application with full admin rights, and in addition bypasses the Vista UAC prompt 137 /// </summary> 138 /// <param name="commandLine">A command Line to launch the application</param> 139 /// <param name="procInfo">Process information regarding the launched application that gets returned to the caller</param> 140 /// <returns></returns> 141 public static bool StartProcessAndBypassUAC(String commandLine, out PROCESS_INFORMATION procInfo) 142 { 143 IntPtr hUserTokenDup = IntPtr.Zero, hPToken = IntPtr.Zero, hProcess = IntPtr.Zero; 144 procInfo = new PROCESS_INFORMATION(); 145 146 // obtain the currently active session id; every logged on user in the system has a unique session id 147 uint dwSessionId = WTSGetActiveConsoleSessionId(); 148 149 if (!WTSQueryUserToken(dwSessionId, ref hPToken)) 150 { 151 return false; 152 } 153 154 SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES(); 155 sa.Length = Marshal.SizeOf(sa); 156 157 // copy the access token of the dwSessionId's User; the newly created token will be a primary token 158 if (!DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, ref sa, (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, 159 (int)TOKEN_TYPE.TokenPrimary, ref hUserTokenDup)) 160 { 161 CloseHandle(hPToken); 162 return false; 163 } 164 165 IntPtr EnvironmentFromUser = IntPtr.Zero; 166 if (!CreateEnvironmentBlock(ref EnvironmentFromUser, hUserTokenDup, false)) 167 { 168 CloseHandle(hPToken); 169 CloseHandle(hUserTokenDup); 170 return false; 171 } 172 173 // By default CreateProcessAsUser creates a process on a non-interactive window station, meaning 174 // the window station has a desktop that is invisible and the process is incapable of receiving 175 // user input. To remedy this we set the lpDesktop parameter to indicate we want to enable user 176 // interaction with the new process. 177 STARTUPINFO si = new STARTUPINFO(); 178 si.cb = (int)Marshal.SizeOf(si); 179 si.lpDesktop = @"winsta0\default"; 180 181 // flags that specify the priority and creation method of the process 182 int dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT; 183 184 // create a new process in the current user's logon session 185 bool result = CreateProcessAsUser(hUserTokenDup, // client's access token 186 null, // file to execute 187 commandLine, // command line 188 ref sa, // pointer to process SECURITY_ATTRIBUTES 189 ref sa, // pointer to thread SECURITY_ATTRIBUTES 190 false, // handles are not inheritable 191 dwCreationFlags, // creation flags 192 EnvironmentFromUser, // pointer to new environment block 193 null, // name of current directory 194 ref si, // pointer to STARTUPINFO structure 195 out procInfo // receives information about new process 196 ); 197 198 // invalidate the handles 199 CloseHandle(hPToken); 200 CloseHandle(hUserTokenDup); 201 DestroyEnvironmentBlock(EnvironmentFromUser); 202 203 return result; // return the result 204 } 205 } 206 }
三、幾個遇到的問題
1.環境變量
起初用CreateProcessAsUser()時並沒有考慮環境變量,雖然要的引用在桌面起來了,任務管理器中也看到它是以當前用戶的身份運行的。進行一些簡單的操作也沒有什么問題。但其中有一項操作發生了問題,打開一個該程序要的特定文件,彈出如下一些錯誤:
Failed to write: %HOMEDRIVE%%HOMEPATH%\...
Location is not avaliable: ...
通過Browser打開文件夾命名看看到文件去打不開!由於該應用是第三方的所以不知道它要做些什么。但是通過Failed to write: %HOMEDRIVE%%HOMEPATH%\...這個錯誤信息顯示它要訪問一個user目錄下的文件。在桌面用cmd查看該環境變量的值為:
HOMEDRIVE=C:
HOMEPATH=\users\Alvin
的確是要訪問user目錄下的文件。然后我編寫了一個小程序讓CreateProcessAsUser()來以當前用戶啟動打印環境變量,結果其中沒有這兩個環境變量,及值為空。那么必然訪問不到了,出這些錯誤也是能理解的了。其實CreateProcessAsUser()的環境變量參數為null的時候,會繼承父進程的環境變量,即為SYSTEM的環境變量。在MSDN中有說:
使用CreateEnvironmentBlock()函數可以得到指定用戶的環境變量,不過還是略有差別——沒有一下兩項:
PROMPT=$P$G
SESSIONNAME=Console
這個原因我就不清楚了,求告知。
值得注意的是,產生的環境變量是Unicode的字符時dwCreationFlags 要有CREATE_UNICODE_ENVIRONMENT標識才行,在MSDN中有解釋到:
An environment block can contain either Unicode or ANSI characters. If the environment block pointed to by lpEnvironment contains Unicode characters, be sure thatdwCreationFlags includes CREATE_UNICODE_ENVIRONMENT. If this parameter is NULL and the environment block of the parent process contains Unicode characters, you must also ensure that dwCreationFlags includes CREATE_UNICODE_ENVIRONMENT.
C#中字符char、string都是Unicode字符。而且這里的CreateEnvironmentBlock()函數在MSDN中有說到,是Unicode的:
lpEnvironment [in, optional]
A pointer to an environment block for the new process. If this parameter is NULL, the new process uses the environment of the calling process.
2.一個比較奇怪的問題
按以上分析后我加入了環境變量,並添加了CREATE_UNICODE_ENVIRONMENT標識。但是我覺得這是不是也應該把CreateProcessAsUser()的DllImport中的CharSet = CharSet.Ansi改為CharSet = CharSet.Unicode。這似乎合情合理,但是改完之后進程就起不來,且沒有錯誤。一旦改回去就完美運行,並且沒有環境變量的問題。想了半天也沒有搞明白是為什么,最后問了一個前輩,他要我注意下CreateProcessAsUser()的第三個參數的聲明,然后我一琢磨才知道問題的真正原因,大家先看CreateProcessAsUser()的函數聲明:
注意第二、三個參數的區別,並查看我寫的代碼。我用的是第三個參數,第二個我賦null。LPCTSTR是一個支持自動選擇字符編碼[Ansi或Unicode] 的常量字符串指針;LPTSTR與之的差別是不是常量。它為什么有這樣的差別呢,看MSDN的解釋:
The system adds a null character to the command line string to separate the file name from the arguments. This divides the original string into two strings for internal processing.
在第二個參數為空時,可以用第三個參數完成AppName和CmdLineArg的功能,方法是添加一個null進行分割。那么這為什么能導致函數不起作用呢?原因是C#中string類型是只讀的,在我這里給它的賦值是string類型。它不能完成分割的動作,所以會造成訪問違例。這其實在MSDN中都有相關的描述:
The Unicode version of this function, CreateProcessAsUserW, can modify the contents of this string. Therefore, this parameter cannot be a pointer to read-only memory (such as a const variable or a literal string). If this parameter is a constant string, the function may cause an access violation.
那么為什么用CharSet = CharSet.Ansi可以呢?LPTSTR支持兩種字符格式,它會自動將Unicode的字符串轉變為Ansi的字符串,即產生另外一個Ansi的字符串,該字符串是復制來的當然可以修改了!!哈哈!
這里可以看出認認真真看好MSDN的解釋是很有幫助的。順便說下這第三個參數分割辦法,以及我們要注意自己的路徑。來看MSDN的說明:
The lpApplicationName parameter can be NULL. In that case, the module name must be the first white space–delimited token in the lpCommandLine string. If you are using a long file name that contains a space, use quoted strings to indicate where the file name ends and the arguments begin; otherwise, the file name is ambiguous. For example, consider the string "c:\program files\sub dir\program name". This string can be interpreted in a number of ways. The system tries to interpret the possibilities in the following order:
- c:\program.exe files\sub dir\program name
- c:\program files\sub.exe dir\program name
- c:\program files\sub dir\program.exe name
- c:\program files\sub dir\program name.exe
關於CreateProcessAsUser()詳細信息請查看http://msdn.microsoft.com/en-us/library/ms682429.aspx
關於CreateEnvironmentBlock()請查看http://msdn.microsoft.com/en-us/library/bb762270(VS.85).aspx