воскресенье, 24 апреля 2011 г.

UAC и CreateProcessAsUser – добавим привилегий

В прошлой заметке речь шла о том, что представляет собой токен учётной записи с административными привилегиями при включенной службе Контроля Учётной записи Пользователя (UAC). Как после авторизации получить доступ к неограниченному токену и почему его нельзя использовать для запуска приложения, если у вас нет привилегии "Act as a part of operation system" (SeTcbPrivilege). Пришла пора рассмотреть альтернативный путь.

Акт второй - сетевой
Данный метод, также как и предыдущий, предполагает наличие у вызывающей стороны определённой привилегии, а именно: SeAssignPrimaryToken. Эта привилегия по умолчанию предоставлена только учётным записям локальной системы (LocalSystem) и локального сервиса (LocalService). Но она не обладает такой мощью, как SeTcbPrivilege, поэтому данное решение в большинстве случаев предпочтительнее. Привилегию можно назначит программно с помощью функции LsaAddAccountRights. Это в свою очередь предполагает, что приложение исполняется в elevation mode под учётной записью с административными правами.

Нужно помнить, что все вновь назначенные привилегии применяются к учётной записи только при последующем входе в систему. На активные сессии подобные назначения не влияют. Поэтому назначать привилегию пользователю, под которым в данный момент исполняется приложение, бессмысленно. Зато её можно назначить той учётной записи, которую мы хотим использовать для вызова CreateProcessAsUser. Если сделать это до выполнения функции LogonUser, то после авторизации мы получим токен с необходимой нам привилегией. Всё что нам останется сделать – это имперсонировать его и вызвать CreateProcessAsUser.

Как я уже говорил, ограничения UAC применяются только к учётным записям, осуществившим интерактивный вход в систему. В терминах WinAPI – это означает, что был произведён вызов функции LogonUser с указанием LOGON32_LOGON_INTERACTIVE в качестве типа входа (4-ый параметр). Альтернативный подход заключается в том, чтобы выполнить сетевой вход вместо интерактивного. В этом случае полученный токен не будет ограниченным даже для пользователей с административными правами. Для этого при вызове функции LogonUser необходимо в качестве типа входа указать LOGON32_LOGON_NETWORK.

Сетевой тип входа по сравнению с интерактивным имеет следующие преимущества и ограничения:
  • он выполняется значительно быстрее;
  • полученный токен является токеном имперсонирования (impersonation token), поэтому перед вызовом функции CreateProcessAsUser его необходимо преобразовать в основной токен (primary token) с помощью функции DuplicateTokenEx;
  • его нельзя использовать для обращения к защищённым сетевым ресурсам.

Таким образом, функция запуска приложения может выглядеть следующим образом:
DWORD
runAs(
        const std::wstring &appPath,
        const std::wstring &login,
        const std::wstring &password,
        const std::wstring &domain)
{
    std::wstring app = L"\"" + appPath + L"\"";

    // Assign the SeAssignPrimaryToken privilege for the user
    // which will be used to logon

    DWORD privError = addUserPrivilege(
        domain, login, SE_ASSIGNPRIMARYTOKEN_NAME);
    if (ERROR_SUCCESS != privError) {
        std::wcerr << L"Cannot assign the "
            L"SE_ASSIGNPRIMARYTOKEN_NAME privilege: "
            << privError;
        return privError;
    }

    // Logon
    HANDLE token = 0;
    BOOL logonRes = ::LogonUser(
        login.c_str(),
        domain.c_str(),
        password.c_str(),
        LOGON32_LOGON_NETWORK,
        LOGON32_PROVIDER_DEFAULT,
        &token);
    DWORD logonError = ::GetLastError();
    // The obtained token is unrestricted with all administrative
    // rights. Also it contains the SeAssignPrimaryToken
    // privilege and we can remove it from
    // the account now (this will not affect our token)

    privError = removeUserPrivilege(
        domain, login, SE_ASSIGNPRIMARYTOKEN_NAME);
    if (ERROR_SUCCESS != privError) {
        // this is strange, but we can continue...
        std::wcerr << L"Cannot remove the "
            L"SE_ASSIGNPRIMARYTOKEN_NAME privilege: "
            << privError;
    }

    // now check the logon result - may be it fails
    if (!logonRes) {
        std::wcerr << L"LogonUser fails: " << logonError;
        return logonError;
    }

    // impersonate the obtained token, so the
    // SeAssignPrimaryToken privilege will be available for our
    // application

    if (!::ImpersonateLoggedOnUser(token)) {
        DWORD err = ::GetLastError();
        std::wcerr << L"ImpersonateLoggedOnUser fails: " << err;
        ::CloseHandle(token);
        return err;
    }

    HANDLE createProcToken = 0;

    // convert the obtained token into primary
    if (!::DuplicateTokenEx(
            token,
            MAXIMUM_ALLOWED,
            0,
            SecurityDelegation,
            TokenPrimary,
            &createProcToken)) {
        DWORD err = ::GetLastError();
        std::wcerr << L"DuplicateTokenEx fails: " << err;
        ::RevertToSelf();
        ::CloseHandle(token);
        return err;
    }

    // start an application
    STARTUPINFO si = {0};
    si.cb = sizeof(STARTUPINFO);
    PROCESS_INFORMATION pi;

    DWORD result = ERROR_SUCCESS;
    if (!::CreateProcessAsUser(
            createProcToken,
            appPath.c_str(),
            (LPTSTR)(app.c_str()),
            0, 0, false, 0, 0, 0, &si, &pi)) {
        DWORD err = ::GetLastError();
        std::wcerr << L"CreateProcessAsUser fails: " << err;
        result = err;
    }

    // cleanup
    ::RevertToSelf();
    ::CloseHandle(createProcToken);
    ::CloseHandle(token);

    ::WaitForSingleObject(pi.hProcess, INFINITE);
    ::CloseHandle(pi.hProcess);
    ::CloseHandle(pi.hThread);

    return result;
}


Ну и на закуску. Если всё-таки ни одно из приведённых решений не подходит по тем или иным причинам, всегда можно явно запросить запуск приложения в elevated mode (программно вызвать функцию "Запуск от имени Администратора" / "Run as Administration"):
DWORD
RunWithElevation(
        HWND hwnd, // can be 0
        const std::wstring &app,
        const std::wstring &parameters = L"",
        const std::wstring &directory = L"")
{
    SHELLEXECUTEINFO executeInfo = {0};
    executeInfo.cbSize = sizeof(SHELLEXECUTEINFO);

    executeInfo.fMask = 0;
    executeInfo.hwnd = hwnd;

    // this instructs shell to display the
    // elevation agreement request

    executeInfo.lpVerb = L"runas";

    executeInfo.lpFile = app.c_str();
    executeInfo.lpParameters = parameters.empty()
        ? 0 : parameters.c_str();
    executeInfo.lpDirectory = directory.empty()
        ? 0 : directory.c_str();
    executeInfo.nShow = SW_NORMAL;

    if (!::ShellExecuteEx( &executeInfo )) {
        return ::GetLastError();
    }

    return ERROR_SUCCESS;
}

// ...

DWORD res = RunWithElevation(0, appPath);

Комментариев нет:

Отправить комментарий