从C#调用CreateProcessAsUser

我一直在尝试使用Windows API的CreateProcessAsUser函数在特定用户的上下文中创建一个新进程,但似乎遇到了一个相当讨厌的安全问题……

在我进一步解释之前,这里是我正在使用的代码来启动新进程(一个控制台进程 – PowerShell具体,但它应该无关紧要)。

  private void StartProcess() { bool retValue; // Create startup info for new console process. var startupInfo = new STARTUPINFO(); startupInfo.cb = Marshal.SizeOf(startupInfo); startupInfo.dwFlags = StartFlags.STARTF_USESHOWWINDOW; startupInfo.wShowWindow = _consoleVisible ? WindowShowStyle.Show : WindowShowStyle.Hide; startupInfo.lpTitle = this.ConsoleTitle ?? "Console"; var procAttrs = new SECURITY_ATTRIBUTES(); var threadAttrs = new SECURITY_ATTRIBUTES(); procAttrs.nLength = Marshal.SizeOf(procAttrs); threadAttrs.nLength = Marshal.SizeOf(threadAttrs); // Log on user temporarily in order to start console process in its security context. var hUserToken = IntPtr.Zero; var hUserTokenDuplicate = IntPtr.Zero; var pEnvironmentBlock = IntPtr.Zero; var pNewEnvironmentBlock = IntPtr.Zero; if (!WinApi.LogonUser("UserName", null, "Password", LogonType.Interactive, LogonProvider.Default, out hUserToken)) throw new Win32Exception(Marshal.GetLastWin32Error(), "Error logging on user."); var duplicateTokenAttrs = new SECURITY_ATTRIBUTES(); duplicateTokenAttrs.nLength = Marshal.SizeOf(duplicateTokenAttrs); if (!WinApi.DuplicateTokenEx(hUserToken, 0, ref duplicateTokenAttrs, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, out hUserTokenDuplicate)) throw new Win32Exception(Marshal.GetLastWin32Error(), "Error duplicating user token."); try { // Get block of environment vars for logged on user. if (!WinApi.CreateEnvironmentBlock(out pEnvironmentBlock, hUserToken, false)) throw new Win32Exception(Marshal.GetLastWin32Error(), "Error getting block of environment variables for user."); // Read block as array of strings, one per variable. var envVars = ReadEnvironmentVariables(pEnvironmentBlock); // Append custom environment variables to list. foreach (var var in this.EnvironmentVariables) envVars.Add(var.Key + "=" + var.Value); // Recreate environment block from array of variables. var newEnvironmentBlock = string.Join("\0", envVars.ToArray()) + "\0"; pNewEnvironmentBlock = Marshal.StringToHGlobalUni(newEnvironmentBlock); // Start new console process. retValue = WinApi.CreateProcessAsUser(hUserTokenDuplicate, null, this.CommandLine, ref procAttrs, ref threadAttrs, false, CreationFlags.CREATE_NEW_CONSOLE | CreationFlags.CREATE_SUSPENDED | CreationFlags.CREATE_UNICODE_ENVIRONMENT, pNewEnvironmentBlock, null, ref startupInfo, out _processInfo); if (!retValue) throw new Win32Exception(Marshal.GetLastWin32Error(), "Unable to create new console process."); } catch { // Catch any exception thrown here so as to prevent any malicious program operating // within the security context of the logged in user. // Clean up. if (hUserToken != IntPtr.Zero) { WinApi.CloseHandle(hUserToken); hUserToken = IntPtr.Zero; } if (hUserTokenDuplicate != IntPtr.Zero) { WinApi.CloseHandle(hUserTokenDuplicate); hUserTokenDuplicate = IntPtr.Zero; } if (pEnvironmentBlock != IntPtr.Zero) { WinApi.DestroyEnvironmentBlock(pEnvironmentBlock); pEnvironmentBlock = IntPtr.Zero; } if (pNewEnvironmentBlock != IntPtr.Zero) { Marshal.FreeHGlobal(pNewEnvironmentBlock); pNewEnvironmentBlock = IntPtr.Zero; } throw; } finally { // Clean up. if (hUserToken != IntPtr.Zero) WinApi.CloseHandle(hUserToken); if (hUserTokenDuplicate != IntPtr.Zero) WinApi.CloseHandle(hUserTokenDuplicate); if (pEnvironmentBlock != IntPtr.Zero) WinApi.DestroyEnvironmentBlock(pEnvironmentBlock); if (pNewEnvironmentBlock != IntPtr.Zero) Marshal.FreeHGlobal(pNewEnvironmentBlock); } _process = Process.GetProcessById(_processInfo.dwProcessId); } 

为了解决这里的问题,请忽略处理环境变量的代码(我已经独立地测试了该部分,它似乎有效。)

现在,我得到的错误如下(抛出CreateProcessAsUSer调用后的行):

“客户不持有所需的特权”(错误代码1314)

(通过从Win32Exception构造函数中删除message参数来发现错误消息。不可否认,我的error handling代码可能不是最好的,但这是一个有点无关紧要的问题。如果您愿意,欢迎您发表评论。在这种情况下,我真的很困惑这个模糊错误的原因。 MSDN文档和各种论坛post只给了我很多建议,特别是考虑到这些错误的原因似乎变化很大,我不知道我需要修改哪一段代码。 也许它只是我需要改变的一个参数,但我可能会为我所知道的所有人做出错误/不够的WinAPI调用。 令我困惑的是,使用普通CreateProcess函数的代码的先前版本(除了用户令牌参数之外的等价物)工作得非常好。 据我所知,只需要调用Logon用户函数来接收适当的令牌句柄,然后将其复制,以便将其传递给CreateProcessAsUser

任何修改代码的建议以及解释都是非常受欢迎的。

笔记

我主要是指MSDN文档(以及C#function / strut / enum声明的PInvoke.net )。 以下几页似乎在备注部分中提供了大量信息,其中一些可能很重要并且让我不知所措:

  • CreateProcessAsUser函数
  • LogonUserfunction
  • DuplicateTokenEx函数

编辑

我刚刚尝试了Mitch的建议,但不幸的是旧的错误刚被新的错误取代:“系统无法找到指定的文件。” (错误代码2)

之前对CreateProcessAsUser调用已替换为以下内容:

 retValue = WinApi.CreateProcessWithTokenW(hUserToken, LogonFlags.WithProfile, null, this.CommandLine, CreationFlags.CREATE_NEW_CONSOLE | CreationFlags.CREATE_SUSPENDED | CreationFlags.CREATE_UNICODE_ENVIRONMENT, pNewEnvironmentBlock, null, ref startupInfo, out _processInfo); 

请注意,此代码不再使用重复令牌,而是使用原始代码,正如MSDN文档似乎建议的那样。

这是使用CreateProcessWithLogonW的另一次尝试。 此次错误是“登录失败:未知用户名或密码错误”(错误代码1326)

 retValue = WinApi.CreateProcessWithLogonW("Alex", null, "password", LogonFlags.WithProfile, null, this.CommandLine, CreationFlags.CREATE_NEW_CONSOLE | CreationFlags.CREATE_SUSPENDED | CreationFlags.CREATE_UNICODE_ENVIRONMENT, pNewEnvironmentBlock, null, ref startupInfo, out _processInfo); 

我也尝试用UPN格式指定用户名(“Alex @ Alex-PC”)并独立地传递域名作为第二个参数,但都没有用(相同的错误)。

啊……似乎是骗子我被WinAPI互操作编程中最大的问题所困扰。 此外,在这种情况下,发布我的函数声明的代码是一个明智的想法。

无论如何,我需要做的就是在函数的DllImport属性中添加一个参数,指定CharSet = CharSet.Unicode 。 这为CreateProcessWithLogonWCreateProcessWithTokenW函数提供了技巧。 我想最后只是告诉我,函数名称的W后缀引用了Unicode,我需要在C#中明确指定它! 如果有人感兴趣,以下是正确的函数声明:

 [DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true)] public static extern bool CreateProcessWithLogonW(string principal, string authority, string password, LogonFlags logonFlags, string appName, string cmdLine, CreationFlags creationFlags, IntPtr environmentBlock, string currentDirectory, ref STARTUPINFO startupInfo, out PROCESS_INFORMATION processInfo); [DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true)] public static extern bool CreateProcessWithTokenW(IntPtr hToken, LogonFlags dwLogonFlags, string lpApplicationName, string lpCommandLine, CreationFlags dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); 

从这里 :

通常,调用CreateProcessAsUser函数的进程必须具有SE_ASSIGNPRIMARYTOKEN_NAME和SE_INCREASE_QUOTA_NAME权限。 如果此函数因ERROR_PRIVILEGE_NOT_HELD(1314)而失败,请改用CreateProcessWithLogonW函数。 CreateProcessWithLogonW不需要特殊权限,但必须允许指定的用户帐户以交互方式登录。 通常,最好使用CreateProcessWithLogonW来创建具有备用凭据的进程。

请参阅此博客文章如何在.NET中调用CreateProcessWithLogonW和CreateProcessAsUser