这次工作中遇到要从服务中启动一个具有桌面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