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.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 }
View Code

 

三、几个遇到的问题

  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

posted @ 2014-11-07 23:29  Alvin Huang  阅读(11581)  评论(0编辑  收藏  举报