Вызов CreateProcessAsUser из C#
Я пытался создать новый процесс в контексте конкретного пользователя, используя CreateProcessAsUser
функция Windows API, но, похоже, сталкивается с довольно неприятной проблемой безопасности...
Прежде чем я объясню что-то еще, вот код, который я сейчас использую для запуска нового процесса (консольный процесс - 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. Правда, мой код обработки ошибок здесь может быть не самым лучшим, но это несколько неуместно. Однако вы можете прокомментировать его, если хотите.) Я действительно очень запутался относительно причины этой неопределенной ошибки в этой ситуации. Документация по MSDN и различные ветки форума дали мне столько советов, и особенно учитывая, что причины таких ошибок, как представляется, сильно различаются, я понятия не имею, какой раздел кода мне нужно изменить. Возможно, это просто один параметр, который мне нужно изменить, но я мог делать неправильные / недостаточные вызовы WinAPI для всего, что я знаю. Что меня сильно смущает, так это то, что предыдущая версия кода, которая использует простой CreateProcess
Функция (эквивалентная кроме параметра токена пользователя) работала отлично. Как я понимаю, необходимо только вызвать функцию пользователя Logon, чтобы получить соответствующий дескриптор токена, а затем продублировать его, чтобы его можно было передать CreateProcessAsUser
,
Любые предложения по модификации кода, а также пояснения будут очень приветствоваться.
Заметки
Я прежде всего имел в виду документы MSDN (а также http://pinvoke.net/ для объявлений функции C# /strut/enum). Следующие страницы, в частности, содержат много информации в разделах "Замечания", некоторые из которых могут быть важными и ускользать от меня:
редактировать
Я только что опробовал предложение Митча, но, к сожалению, старая ошибка была заменена новой: "Система не может найти указанный файл". (код ошибки 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") и передать домен независимо в качестве второго аргумента, но все безрезультатно (идентичная ошибка).
3 ответа
Ааа... кажется, мне понравилась одна из самых больших проблем в программировании взаимодействия WinAPI. Кроме того, публикация кода для моих объявлений функций была бы разумной идеей в этом случае.
В любом случае, все, что мне нужно было сделать, это добавить аргумент в атрибут DllImport функции, указывающей CharSet = CharSet.Unicode
, Это помогло обоим 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);
Как правило, процесс, который вызывает функцию CreateProcessAsUser, должен иметь привилегии SE_ASSIGNPRIMARYTOKEN_NAME и SE_INCREASE_QUOTA_NAME. Если эта функция завершается с ошибкой ERROR_PRIVILEGE_NOT_HELD (1314), используйте вместо этого функцию CreateProcessWithLogonW. CreateProcessWithLogonW не требует специальных привилегий, но указанная учетная запись пользователя должна иметь возможность входить в систему в интерактивном режиме. Как правило, лучше всего использовать CreateProcessWithLogonW для создания процесса с альтернативными учетными данными.
Смотрите этот пост в блоге. Как вызвать CreateProcessWithLogonW & CreateProcessAsUser в.NET
Джонатан Пепперс (Jonathan Peppers) предоставил этот замечательный кусок кода, который исправил мои проблемы