Пытаюсь сделать виндовый сервис (C#, net core 3.0), который запускает приложения из под другого пользователя. Сервис работает из под доменного пользователя, который является администратором на этом компьютере, в Local Security Policy ему даны права Adjust Memory Quota, Act as Part of OS, Run As Service, Replace a process level token (всё это выдал когда так и не удалось запустить приложение).
При запуске консольного приложения из сервиса, Process.Start не возвращает ошибки, но запущенно приложение сразу после запуска падает вместе с conhost, не успевая дойти до кода самого приложения. Если проделывать всё это из интерактивного приложения — всё работает. Можете подсказать куда копать?
ЗЫ. Procmon ничего подозрительного не показывает.
ЗЫ2. Сейчас пробую стандартными средствами net framework запускать, но попробую и через interop.
ЗЫ3. Из под системы тоже не запускается.
Здравствуйте, Somescout, Вы писали:
S>Разобрался в чём проблема — нужно указать lpDesktop для прикрепления окна (в данном случае консоли). Выставил в "winsta0\default" — всё заработало.
А нет, не заработало — cmd стартует, но большинство других программ вылетают с ошибкой "The application was unable to start correctly (0xc0000142)" (причём во всплывающем окне). Нет ли у кого рабочего примера, как правильно запускать процесс с использованием CreateProcessAsUser?
Здравствуйте, zou, Вы писали:
zou>А в какой сессии должны приложения запускаться? И может ли быть у них UI?
Я сейчас в отладчике (и вне его) запускаю из своей сессии (администратор, права Set Pimary Token и Adjust memory quota даны). Запускается из под имени доменного пользователя (не администратора, выдано право Logon as a batch job).
CMD.EXE запускается, но при попытке запустить, например whoami.exe или Powershell.exe — ошибка.
Я сейчас не гружу профиль, может в этом дело, но вроде это не мешало запускаться процессам, когда я использовал стандартный Process.Start() (работает через CreateProcessWithLogonW).
Здравствуйте, okman, Вы писали:
O>Здравствуйте, Somescout, Вы писали:
S>>как правильно запускать процесс с использованием CreateProcessAsUser?
O>А токен для CreateProcessAsUser ты как получаешь?
private SafeTokenHandle LoginUser(string Domain, string UserName, SecureString Password)
{
IntPtr passwordPtr = (Password != null) ?
Marshal.SecureStringToGlobalAllocUnicode(Password) : IntPtr.Zero;
try
{
string password = SecureStringToString(Password);
if (Interop.Advapi32.LogonUser(UserName, Domain, passwordPtr,
Interop.Advapi32.LOGON32.LOGON32_LOGON_BATCH,
Interop.Advapi32.LOGON32.LOGON32_PROVIDER_DEFAULT,
out var tempUserToken))
{
return tempUserToken;
}
else
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
finally
{
if (passwordPtr != IntPtr.Zero)
Marshal.ZeroFreeGlobalAllocUnicode(passwordPtr);
}
}
public int StartProcess(ProcessStartInfo processStartInfo)
{
using var userToken = LoginUser(processStartInfo.Domain, processStartInfo.UserName, processStartInfo.Password);
Interop.Advapi32.STARTUPINFO startUpInfo = new Interop.Advapi32.STARTUPINFO();
startUpInfo.cb = Marshal.SizeOf(startUpInfo);
startUpInfo.lpDesktop = "winsta0\\default";
Interop.Kernel32.PROCESS_INFORMATION processInfo = new Interop.Kernel32.PROCESS_INFORMATION();
bool processStarted = Interop.Advapi32.CreateProcessAsUser(userToken, processStartInfo.FileName, processStartInfo.Arguments,
IntPtr.Zero, IntPtr.Zero, true, 0, IntPtr.Zero, null,
ref startUpInfo, out processInfo);
if (!processStarted)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
Interop.Kernel32.CloseHandle(processInfo.hProcess);
Interop.Kernel32.CloseHandle(processInfo.hThread);
return processInfo.dwProcessId;
}
Здравствуйте, Somescout, Вы писали:
O>>А токен для CreateProcessAsUser ты как получаешь?
S>... S>if (Interop.Advapi32.LogonUser S>...
Когда-то решал аналогичную задачу, правда, у меня в LogonUser использовался флаг LOGON32_LOGON_INTERACTIVE.
Иногда приложение запускалось, но у главного окна почему-то отсутствовали визуальные стили (все контролы
были в стиле Windows 2000). Иногда я при запуске получал какие-то странные ошибки, например "недостаточно
памяти для завершения операции".
Пришлось копать глубже, в итоге пришел к следующим наблюдениям:
1. Если запустить процесс вручную, через "Run as different user" от имени другого пользователя, а затем
посмотреть набор SID-ов в его токене (с помощью Process Explorer или Process Hacker) и сравнить с аналогичным
набором из, например, explorer.exe (Проводник), то там видно, что у обоих процессов Logon SID совпадают,
хотя пользователи разные. То же самое наблюдается при использовании утилиты psexec (Sysinternals).
2. Если запустить процесс программно, через LogonUser -> CreateProcessAsUser, а затем снова сравнить
список SID-ов, то легко увидеть, что токен созданного процесса не содержит SID группы "Локальный вход"
(S-1-2-0), а также у него какой-то "левый" Logon SID, не соответствующий Logon SID пользователя, который
залогинен в сессии.
3. При создании сессии система назначает определенные права доступа для оконной станции, рабочего стола,
пространства имен диспетчера объектов и некоторых других объектов, принадлежащих пользователю.
При этом она указывает пользователя неявно, используя его Logon SID. Это значит, что для нормальной
работы процессу другого пользователя каким-то образом следует дать доступ к этим объектам.
Лучше всего "позаимствовать" у исходного пользователя его Logon SID — сама Windows именно так и
поступает (хотя MSDN в известном примере "Starting an Interactive Client Process in C++" делает иначе —
там меняются DACL оконной станции и рабочего стола, в них добавляются разрешения для нужного пользователя).
4. LogonUser не позволяет добавить в токен дополнительные SID-ы. Зато это могут делать функции
LsaLogonUser и LogonUserExExW.
В результате получил следующее решение:
Получаем Logon SID пользователя, залогиненного в сессии: WTSQueryUserToken, затем GetTokenInformation с
кодом TokenGroups и ищем SID с атрибутом SE_GROUP_LOGON_ID.
Вызываем LsaLogonUser или LogonUserExExW (я использовал флаг LOGON32_LOGON_INTERACTIVE),
при этом в созданный токен добавляем два SID-а: S-1-2-0 и Logon SID.
После создания токена вызываем SetTokenInformation с кодом TokenSessionId, указывая сессию, в
которой будет запущен процесс.
Загружаем профиль пользователя — LoadUserProfile.
Создаем блок переменных окружения — CreateEnvironmentBlock.
Далее уже вызывается CreateProcessAsUser...
Когда писал код, проверял на Windows 7 и Windows 8.1, пользователи — администраторы и нет, все работало норм.
Такие вопросы:
1) Нужно дожидаться завершения дочернего процесса чтобы выгрузить профиль? Или можно как-то это обойти?
2) может ли быть незагруженный профиль причиной проблем? Вроде когда я использовал CreateProcessWithLogonW (без флага LOGON_WITH_PROFILE) — проблем не было.
Здравствуйте, Somescout, Вы писали:
S>Такие вопросы: S>1) Нужно дожидаться завершения дочернего процесса чтобы выгрузить профиль? Или можно как-то это обойти? S>2) может ли быть незагруженный профиль причиной проблем? Вроде когда я использовал CreateProcessWithLogonW (без флага LOGON_WITH_PROFILE) — проблем не было.
Гм... Пересмотрел свой старый код — оказалось, что манипуляции с LogonUser/LsaLogonUser/LogonUserExExW вообще не нужны.
Вместо этого можно просто вызвать CreateProcessWithLogonW (с кодом LOGON_WITH_PROFILE или без). Ключевой момент — структуру STARTUPINFO
следует заполнить нулями, тогда функция сама проделает всю работу, необходимую для обеспечения процессу доступа к оконной
станции, рабочему столу и т.п. У нового процесса в токене есть все нужные SID-ы (и Logon SID совпадает).
А вот если самому писать в STARTUPINFO что-нибудь (например, GetStartupInfoW, lpDesktop = WinSta0\\Default и т.п.), тогда
запуск обламывается с кодом типа 0xC0000142...
Здравствуйте, okman, Вы писали:
O>Гм... Пересмотрел свой старый код — оказалось, что манипуляции с LogonUser/LsaLogonUser/LogonUserExExW вообще не нужны. O>Вместо этого можно просто вызвать CreateProcessWithLogonW (с кодом LOGON_WITH_PROFILE или без). Ключевой момент — структуру STARTUPINFO O>следует заполнить нулями, тогда функция сама проделает всю работу, необходимую для обеспечения процессу доступа к оконной O>станции, рабочему столу и т.п. У нового процесса в токене есть все нужные SID-ы (и Logon SID совпадает). O>А вот если самому писать в STARTUPINFO что-нибудь (например, GetStartupInfoW, lpDesktop = WinSta0\\Default и т.п.), тогда O>запуск обламывается с кодом типа 0xC0000142...
Дело в том, что мне вообще не нужен доступ к UI — я пытаюсь запустить консольное приложение (без создния окна), которому через перенаправление stdin нужно скормить XML с параметрами. Попытка просто запустить через Process.Start (использует как раз CreateProcessWithLogonW) из сервиса неудачна (процесс запускается, но conhost падает сразу после запуска и роняет за собой приложение). Хотя мне тут пришло в голову, что возможно надо сделать перенаправление не тослько stdin, но и out и err. Попробую завтра.
Здравствуйте, Somescout, Вы писали:
S>может ли быть незагруженный профиль причиной проблем? Вроде когда я использовал CreateProcessWithLogonW (без флага LOGON_WITH_PROFILE) — проблем не было.
Если не загрузить профиль — процесс не сможет работать с пользовательским кустом реестра HKEY_CURRENT_USER.
Хотя, может, ему это и не требуется...
А если не выгрузить — данные не будут синхронизированы: не будет RegFlushKey, да и в случае использования
сетевого профиля вероятны проблемы.
S>Нужно дожидаться завершения дочернего процесса чтобы выгрузить профиль? Или можно как-то это обойти?
В общем случае, нужно дожидаться завершения всех процессов, запущенных с данным токеном, после чего выгружать профиль.
Если дочерний процесс 100% всегда один, то можно просто поставить его хэндл в ожидание, а когда процесс завершится и
хэндл перейдет в сигнальное состояние, дернуть UnloadUserProfile. Но если он может порождать другие процессы и завершаться
раньше них, тогда... я не знаю как быть В Windows нету (ну или я не нашел) механизма, чтобы подписаться на удаление
объекта logon session (функция ядра SeRegisterLogonSessionTerminatedRoutine не в счет), а это позволило бы узнать,
когда больше не осталось процессов, запущенных с данным токеном, и безопасно выгрузить профиль.
У CreateProcessWithLogon и CreateProcessWithToken есть флаг LOGON_WITH_PROFILE и в документации написано:
"The profile is unloaded after the new process and all child processes it has created are terminated".
Мне стало интересно, как сама Windows решает проблему с выгрузкой профиля. Оказалось, что на Windows 7 служба
seclogon, которая обрабатывает вызовы этих двух функций, при создании нового процесса добавляет его в Job-объект и
затем отслеживает уничтожение всего Job-а. При уничтожении, среди прочего, вызывается UnloadUserProfile.
Очень любопытно: а что будет, если дочерний процесс попытается добавить себя в другой Job-объект?
Ведь вложенные Job-объекты поддерживаются только на Windows 8 и выше...
Для себя я пока остановился на таком варианте (у меня служба запущена от LocalSystem):
1. Служба запускает в сессии пользователя промежуточный процесс: OpenProcessToken -> DuplicateTokenEx ->
SetTokenInformation(TokenSessionId) -> CreateProcessAsUser.
2. Промежуточный процесс создает с помощью LogonUserExExW токен, добавляя туда S-1-2-0 и Logon SID
пользователя: WTSQueryUserToken -> GetTokenInformation(TokenLogonId) -> LogonUserExExW -> ...
3. CreateEnvironmentBlock и т.п.
4. Далее с полученным токеном вызывается CreateProcessWithToken с флагом LOGON_WITH_PROFILE.
В STARTUPINFO.lpDesktop пишется WinSta0\Default.
Здравствуйте, Somescout, Вы писали:
S>Дело в том, что мне вообще не нужен доступ к UI — я пытаюсь запустить консольное приложение (без создния окна)...
Дело не только в UI. Если в сессии пользователя создать приложение "не с тем" Logon SID, то оно не только UI не
сможет показывать, но и создавать/открывать объекты в пространстве имен сессии. Например:
Здравствуйте, okman, Вы писали:
O>Здравствуйте, Somescout, Вы писали:
S>>Дело в том, что мне вообще не нужен доступ к UI — я пытаюсь запустить консольное приложение (без создния окна)...
O>Дело не только в UI. Если в сессии пользователя создать приложение "не с тем" Logon SID, то оно не только UI не O>сможет показывать, но и создавать/открывать объекты в пространстве имен сессии. Например:
O>
То есть делает то же самое — пользователю, от которого делается запуск, даёт доступ к WinStation текущего приложения. Проверил — работает, и из сервиса, и из консоли (даже из под системного аккаунта работает).