我一直在尝试使用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
函数的代码的先前版本(除用户令牌参数之外的等效函数)完全正常。据我了解,只需要调用Logon用户函数来接收相应的令牌句柄,然后将其复制,以便将其传递给CreateProcessAsUser
。
非常欢迎任何修改代码和解释的建议。
我主要是指MSDN文档(以及C#function / strut / enum声明的PInvoke.net)。特别是以下几页似乎在备注部分有很多信息,其中一些可能很重要并且不能指望我:
我刚刚尝试了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”)并独立传递域名作为第二个参数,但都没有用(相同的错误)。
答案 0 :(得分:7)
啊......似乎是骗子我被WinAPI互操作编程中最大的问题所困扰。此外,在这种情况下,发布我的函数声明的代码也是一个明智的想法。
无论如何,我需要做的就是为指定CharSet = CharSet.Unicode
的函数的DllImport属性添加一个参数。这为CreateProcessWithLogonW
和CreateProcessWithTokenW
函数提供了技巧。我想它最终只是让我知道函数名称的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);
答案 1 :(得分:6)
来自here:
通常,调用的过程 CreateProcessAsUser函数必须具有 SE_ASSIGNPRIMARYTOKEN_NAME和 SE_INCREASE_QUOTA_NAME权限。如果 这个功能失败了 ERROR_PRIVILEGE_NOT_HELD(1314),使用 CreateProcessWithLogonW函数 代替。 CreateProcessWithLogonW 不需要特殊权限,但是 指定的用户帐户必须是 允许以交互方式登录。 一般来说,最好使用 CreateProcessWithLogonW创建一个 使用备用凭据进行处理。
请参阅此博客文章How to call CreateProcessWithLogonW & CreateProcessAsUser in .NET
答案 2 :(得分:4)
Jonathan Peppers 提供了解决我的问题的这段伟大代码