我一直在尝试使用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调用后的行):



(错误消息是通过从Win32Exception构造函数中删除message参数而发现的。诚然,我的错误处理代码可能不是最好的,但这是无关紧要的事情。不过,如果您愿意,欢迎对其发表评论。 )对于这种情况下这种模糊错误的原因,我真的很困惑。 MSDN文档和各种论坛线程只给了我很多建议,尤其是考虑到造成此类错误的原因千差万别,我不知道需要修改哪一部分代码。也许这只是我需要更改的单个参数,但是对于我所知道的所有信息,我可能进行了错误/不够的WinAPI调用。令我感到困惑的是,使用普通CreateProcess函数的代码的先前版本(除了用户标记参数,其等效方式)工作得很好。据我了解,仅需要调用Logon用户函数来接收适当的 token 句柄,然后对其进行复制,以便可以将其传递给CreateProcessAsUser

任何对代码的修改和解释的建议都将受到欢迎。

笔记

我主要指的是MSDN文档(以及C#函数/strut/enum声明的PInvoke.net)。特别是以下页面在“备注”部分中似乎有很多信息,其中一些信息可能很重要,使我难以理解:
  • CreateProcessAsUser function
  • LogonUser function
  • DuplicateTokenEx function

  • 编辑

    我刚刚尝试了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);
    

    请注意,此代码不再使用重复的 token ,而是使用原始 token ,正如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互操作编程中最大的陷阱之一迷住了。同样,在这种情况下,为我的函数声明发布代码将是一个明智的主意。

    无论如何,我要做的就是将一个参数添加到指定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);
    

    10-04 13:47