Здравствуйте, kovbas, Вы писали:
K>Родил следующий код:
...
K>Трассировка показывет, что все в порядке. Запускаю службу под пользователем с системной учетной записью.
K>Как <Обнаружитель интерактивных служб> реагировал на ShowWindow(), так и реагирует.
K>ВОПРОС: Как правильно подменить рабочий стол системных служб на рабочий стол залогиненного пользователя в Windows 7 / Vista / 2008
Сама постановка вопроса немного некорректна.
У служб вообще нету своего рабочего стола, а в Vista и выше они выполняются в
разных с пользовательскими процессами сессиях. И не забывайте, что сервис — почти как
консольный процесс, то есть никакие оконные сообщения до него не дойдут.
Могу посоветовать запустить из службы дочерний процесс от имени пользователя и весь
GUI-интерфейс организовать внутри этого процесса.
Но для того, чтобы писать код в этой области, нужно хотя бы в общих чертах
разбираться в таких понятиях, как пользовательская сессия (консольная или
терминальная), оконная станция, рабочий стол, контекст и маркер безопасности.
И вообще, понимание системы безопасности Windows тоже очень желательно, иначе
можно постоянно наступать на одни и те же грабли, не находя причин.
Очень рекомендую разобраться во всей этой "кухне".
Есть две разных схемы запуска дочерних интерактивных процессов из служб.
Первая из них довольно тривиальна (ее мы и рассмотрим подробно).
Если вкратце, то суть ее в том, чтобы получить маркер безопасности
пользователя и создать процесс с этим маркером. При этом не нужно проводить
никаких манипуляций с рабочими столами и оконными станциями, так как вся
нужная информация уже содержится внутри этого маркера.
Созданный процесс, за исключением очень незначительных деталей, ничем не
отличается от других, запущенных пользователем.
Единственная важная деталь — профиль пользователя должен быть загружен, иначе
созданному процессу будут недоступны кое-какие данные из Documents And
Settings и ветки реестра HKEY_CURRENT_USER.
Вторая схема более сложная и извращенная.
Ее в каком-то смысле можно назвать стыковкой — новый интерактивный процесс
создается в сессии пользователя, но при этом сохраняет все атрибуты безопасности
службы (LocalSystem), которая его запустила. Это более "веселенько" получается,
потому что порожденный процесс уже может взаимодействовать с рабочим столом
пользователя, отправлять и принимать оконные сообщения и так далее.
Для создания такого процесса требуется несколько не очень очевидных действий с
маркером безопасности (DuplicateToken, SetTokenInformation с ключом TokenSessionId и
некоторые другие). Разумеется, нет нужды говорить, что это является потенциальной
дырой в безопасности. Поэтому обычно рекомендуется не использовать для нового
процесса системный контекст, а создавать специальный ограниченный маркер
(см. CreateRestrictedToken). Если кого-то заинтересует, могу описать подробно.
Вот функция, которая запускает стандартный Windows-калькулятор на
рабочем столе определенного пользователя.
| | Скрытый текст |
| | /************************************************************************
Функция StartCalculator.
Открывает стандартный калькулятор (calc.exe) на рабочем столе
определенного пользователя. Предназначена только для запуска из службы (LocalSystem).
Возвращает TRUE в случае успеха и FALSE в случае ошибки.
*************************************************************************/
BOOL
_stdcall
StartCalculator()
{
// Определяется Id активной пользовательской сессии.
// Значение 0xFFFFFFFF означает что консольной сессии не существует.
// Идентификатор сессии лучше всего получать в обработчике
// событий службы, см. RegisterServiceCtrlHandler(Ex).
// Там же можно определить консольная сессия или терминальная.
// См. SERVICE_CONTROL_SESSIONCHANGE, WM_WTSSESSION_CHANGE,
// WTSSESSION_NOTIFICATION, а также WTSQuerySessionInformation.
DWORD IdSession = WTSGetActiveConsoleSessionId();
if (IdSession == 0xFFFFFFFF)
{
return FALSE;
}
// Для того, чтобы действовать от имени пользователя, нужно
// получить так называемый маркер безопасности, представляющий
// этого пользователя в системе. Для этого существует функция
// WTSQueryUserToken, она требует высоких привилегий, которые
// обычно есть только у сервисов LocalSystem.
HANDLE hUserToken;
if (WTSQueryUserToken(IdSession, &hUserToken) == FALSE)
{
return FALSE;
}
// Перед созданием процесса в контексте пользователя необходимо
// выполнить некоторые дополнительные действия.
// Во-первых, нужно загрузить профиль пользователя - без этого
// новому процессу будут недоступны данные, лежащие в папках
// профиля и ключе реестра HKEY_CURRENT_USER.
// Для загрузки профиля нужно знать имя пользователя.
// Узнать его можно с помощью функции WTSQuerySessionInformation с
// ключом WTSUserName, но дело в том, что при эта функция зависит
// от службы терминалов, и если та еще не успела загрузиться (а такое
// бывает нередко при включении компьютера и быстром входе в систему),
// вызов WTSQuerySessionInformation вернет ошибку.
// Это имеет значение, когда запуск интерактивного процесса должен
// выполняться сразу же после входа пользователя.
// Мы поступим по-другому - временно переключимся в контекст
// безопасности пользователя и оттуда вызовем GetUserName.
// Замечу, что без подмены маркера безопасности вызов GetUserName
// будет возвращать имя SYSTEM (это распостраненная ошибка при попытке
// получить имя пользователя из службы).
// В MSDN сказано, что максимальная длина имени пользователя - 20 символов.
// Добавим еще один для нулевого завершающего символа.
wchar_t pUserName[21];
DWORD cchUserNameBuffer = 21;
// Теперь данный поток временно будет действовать от имени пользователя.
// Нужно учитывать, что при этом заимствуется также его контекст безопасности.
// То есть, доступ к критическим системным объектам будет ограничен.
if (ImpersonateLoggedOnUser(hUserToken) == FALSE)
{
CloseHandle(hUserToken);
return FALSE;
}
if (GetUserNameW(pUserName, &cchUserNameBuffer) == FALSE)
{
RevertToSelf();
CloseHandle(hUserToken);
return FALSE;
}
// Возвращаем системный контекст "на место".
RevertToSelf();
// Заполняем нужные структуры и загружаем профиль пользователя.
PROFILEINFOW prof;
ZeroMemory(&prof, sizeof (prof));
prof.dwSize = sizeof (prof);
prof.lpUserName = pUserName;
prof.dwFlags = PI_NOUI;
if (LoadUserProfileW(hUserToken, &prof) == FALSE)
{
CloseHandle(hUserToken);
return FALSE;
}
// Последнее приготовление - нужно подготовить переменные окружения.
// Подробнее можно почитать в MSDN-справке к функции CreateProcessAsUser.
void *pEnvBlock;
if (CreateEnvironmentBlock(&pEnvBlock, hUserToken, FALSE) == FALSE)
{
UnloadUserProfile(hUserToken, prof.hProfile);
CloseHandle(hUserToken);
return FALSE;
}
// Создание процесса почти ничем не отличается от стандартного способа с
// CreateProcess, только нужно указать маркер безопасности, от имени
// которого этот процесс будет создан.
STARTUPINFOW si;
GetStartupInfoW(&si);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_SHOWNORMAL;
si.lpDesktop = L"WinSta0\\Default";
wchar_t pCmdLine[] = {L"calc.exe"};
PROCESS_INFORMATION pi;
if (CreateProcessAsUserW(hUserToken,
NULL,
pCmdLine,
NULL,
NULL,
FALSE,
NORMAL_PRIORITY_CLASS | CREATE_UNICODE_ENVIRONMENT,
pEnvBlock,
NULL,
&si,
&pi) == FALSE)
{
DestroyEnvironmentBlock(pEnvBlock);
UnloadUserProfile(hUserToken, prof.hProfile);
CloseHandle(hUserToken);
return FALSE;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
DestroyEnvironmentBlock(pEnvBlock);
// Эта функция должна вызываться при завершении созданного процесса.
// UnloadUserProfile(hUserToken, prof.hProfile);
CloseHandle(hUserToken);
// Собственно, все !
return TRUE;
}
|
| | |
Испытано практически на XP/Vista/Seven/Server2008, 32-64 бита.