Win7中如何在服务中启动一个当前用户的进程——一次CreateProcessAsUser()使用记录
这次工作中遇到要从服务中启动一个具有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.Ansi,
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