【问题标题】:Calling CreateProcessAsUser from C#从 C# 调用 CreateProcessAsUser
【发布时间】:2010-10-14 16:19:52
【问题描述】:

我一直在尝试使用 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参数发现的。诚然,我这里的错误处理代码可能不是最好的,但这有点无关紧要。如果你愿意,欢迎评论,但是。)对于这种情况下这种模糊错误的原因,我真的很困惑。 MSDN 文档和各种论坛帖子只给了我这么多建议,特别是考虑到此类错误的原因似乎多种多样,我不知道我需要修改哪段代码。也许它只是我需要更改的一个参数,但我可能会做出错误/不足的 WinAPI 调用,就我所知。令我非常困惑的是,使用普通 CreateProcess 函数(除用户令牌参数外等效)的代码的先前版本工作得非常好。据我了解,只需要调用登录用户函数来接收适当的令牌句柄,然后复制它,以便可以将其传递给CreateProcessAsUser

非常欢迎任何关于修改代码的建议和解释。

注意事项

我主要参考 MSDN 文档(以及 C# 函数/strut/enum 声明的 PInvoke.net)。尤其是以下页面,备注部分似乎有很多信息,其中一些可能很重要,但我无法理解:

编辑

我刚刚尝试了米奇的建议,但不幸的是,旧的错误刚刚被一个新的错误所取代:“系统找不到指定的文件。” (错误代码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”)指定用户名并将域作为第二个参数独立传递,但均无济于事(相同错误)。

【问题讨论】:

  • 永远不要将您自己的描述传递给 Win32Exception。相反,将默认构造的 Win32Exception 作为更适合您的函数的某种异常类型的 InnerException 传递,您的描述进入外部异常。这样,调用者既可以获取您的信息(失败的原因),也可以获取 Win32 错误代码(如何失败)和完全匹配的错误消息。
  • 有没有办法从这个过程中得到一个标准输入/标准输出?

标签: c# winapi process privileges createprocessasuser


【解决方案1】:

乔纳森·佩珀斯 提供了这段很棒的代码来解决我的问题

http://social.msdn.microsoft.com/Forums/en-US/csharpgeneral/thread/0c0ca087-5e7b-4046-93cb-c7b3e48d0dfb?ppud=4

【讨论】:

  • 应始终避免仅链接的答案。 SO有一个反对的政策。如果该链接损坏(存档等),如果没有总结那里的内容和代码示例,这个答案将变得毫无用处。
  • 这段代码太神奇了!没有人发布这段代码来实现以用户身份启动进程的无密码。非常感谢 ermagana 和他本人 - Jonathan Peppers! :)
【解决方案2】:

啊……似乎我被 WinAPI 互操作编程中最大的陷阱之一抓住了。此外,在这种情况下,发布我的函数声明的代码将是一个明智的想法。

无论如何,我需要做的就是向指定CharSet = CharSet.Unicode 的函数的DllImport 属性添加一个参数。这对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);

【讨论】:

    【解决方案3】:

    来自here

    通常,调用 CreateProcessAsUser 函数必须有 SE_ASSIGNPRIMARYTOKEN_NAME 和 SE_INCREASE_QUOTA_NAME 权限。如果 此功能失败 ERROR_PRIVILEGE_NOT_HELD (1314),使用 CreateProcessWithLogonW 函数 反而。 CreateProcessWithLogonW 不需要特权,但 指定的用户帐户必须是 允许交互式登录。 一般来说,最好使用 CreateProcessWithLogonW 创建一个 使用备用凭据进行处理。

    查看这篇博文How to call CreateProcessWithLogonW & CreateProcessAsUser in .NET

    【讨论】:

    • 感谢您指出这一点。不幸的是,CreateProcessWithLogonW 不是我的解决方案,因为我也需要从 Windows 服务中执行此代码,并且正如该博客文章中的一位评论者所指出的那样,这是不可能的。
    • (续)从您帖子中引用的段落来看,尝试一些本地安全策略规则可能会有所帮助。
    • 注意:我已经更新了原始帖子并尝试了您提出的解决方案。
    • 刚刚注意到我尝试了 CreateProcessWithTokenW。 CreateProcessAsLogonW 不走运:错误 1326(无效的登录凭据)。这不是很好吗?...这三个函数中的每一个都是一个完全独特的错误。
    • @Noldorin:“无效登录凭据”的错误似乎相当确定。您确定您的域\用户名和密码正确吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多