From 430a43969892098f40e18d378c9e9a30fed11e0f Mon Sep 17 00:00:00 2001 From: Chase Payne Date: Sat, 29 Apr 2023 00:22:01 -0500 Subject: [PATCH] Elevated Commands Redesign (#1123) --- CMakeLists.txt | 40 +- docs/source/about/app_examples.rst | 29 ++ src/config.cpp | 10 +- src/config.h | 10 +- src/httpcommon.cpp | 2 +- src/platform/common.h | 2 +- src/platform/linux/misc.cpp | 3 +- src/platform/macos/misc.mm | 3 +- src/platform/windows/misc.cpp | 485 ++++++++++++++--------- src/platform/windows/misc.h | 1 - src/process.cpp | 52 ++- src/process.h | 1 + src/system_tray.cpp | 2 +- src_assets/common/assets/web/apps.html | 202 +++++++--- src_assets/common/assets/web/config.html | 73 ++-- tools/CMakeLists.txt | 7 - tools/elevator.cpp | 71 ---- 17 files changed, 568 insertions(+), 425 deletions(-) delete mode 100644 tools/elevator.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 53d809c5..30ec64c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -639,6 +639,8 @@ if(WIN32) set_target_properties(sunshine PROPERTIES LINK_SEARCH_START_STATIC 1) set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll") find_library(ZLIB ZLIB1) + list(APPEND SUNSHINE_EXTERNAL_LIBRARIES + Wtsapi32.lib) endif() target_link_libraries(sunshine ${SUNSHINE_EXTERNAL_LIBRARIES} ${EXTRA_LIBS}) @@ -694,7 +696,6 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h install(TARGETS dxgi-info RUNTIME DESTINATION "tools" COMPONENT dxgi) install(TARGETS audio-info RUNTIME DESTINATION "tools" COMPONENT audio) install(TARGETS sunshinesvc RUNTIME DESTINATION "tools" COMPONENT sunshinesvc) - install(TARGETS elevator RUNTIME DESTINATION "tools" COMPONENT elevator) # Mandatory tools install(TARGETS ddprobe RUNTIME DESTINATION "tools" COMPONENT application) @@ -730,15 +731,16 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h # Extra install commands # Restores permissions on the install directory # Migrates config files from the root into the new config folder - # Sets permissions on the config folder so that we can write in it # Install service SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${CPACK_NSIS_EXTRA_INSTALL_COMMANDS} IfSilent +2 0 ExecShell 'open' 'https://sunshinestream.readthedocs.io/' - nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\" /reset' + nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\" /reset /T' + nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\\config\\\\credentials\\\" /inheritance:r' + nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\\config\\\\credentials\\\" \ + /grant:r Administrators:\\\(OI\\\)\\\(CI\\\)\\\(F\\\)' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\migrate-config.bat\\\"' - nsExec::ExecToLog 'icacls \\\"$INSTDIR\\\\config\\\" /grant:r Users:\\\(OI\\\)\\\(CI\\\)\\\(F\\\)' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\add-firewall-rule.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-service.bat\\\"' nsExec::ExecToLog '\\\"$INSTDIR\\\\scripts\\\\install-vigembus.bat\\\"' @@ -763,21 +765,6 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h NoDelete: ") - # Adding an option for the start menu and PATH - # TODO: it asks to add it to the PATH but is not working https://gitlab.kitware.com/cmake/cmake/-/issues/15635 - set(CPACK_NSIS_MODIFY_PATH "OFF") - set(CPACK_NSIS_EXECUTABLES_DIRECTORY ".") - # This will be shown on the installed apps Windows settings - set(CPACK_NSIS_INSTALLED_ICON_NAME "${CMAKE_PROJECT_NAME}.exe") - set(CPACK_NSIS_CREATE_ICONS_EXTRA - "${CPACK_NSIS_CREATE_ICONS_EXTRA} - CreateShortCut '\$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${CMAKE_PROJECT_NAME} (Foreground Mode).lnk' \ - '\$INSTDIR\\\\${CMAKE_PROJECT_NAME}.exe' - ") - set(CPACK_NSIS_DELETE_ICONS_EXTRA - "${CPACK_NSIS_DELETE_ICONS_EXTRA} - Delete '\$SMPROGRAMS\\\\$MUI_TEMP\\\\${CMAKE_PROJECT_NAME} (Foreground Mode).lnk' - ") # Checking for previous installed versions set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL "ON") @@ -811,12 +798,6 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h set(CPACK_COMPONENT_AUDIO_DESCRIPTION "CLI tool providing information about sound devices.") set(CPACK_COMPONENT_AUDIO_GROUP "tools") - # elevation tool - set(CPACK_COMPONENT_ELEVATOR_DISPLAY_NAME "elevator") - set(CPACK_COMPONENT_ELEVATOR_DESCRIPTION "CLI tool that assists with elevating \ - commands when permissions have been denied.") - set(CPACK_COMPONENT_ELEVATOR_GROUP "tools") - # display tool set(CPACK_COMPONENT_DXGI_DISPLAY_NAME "dxgi-info") set(CPACK_COMPONENT_DXGI_DESCRIPTION "CLI tool providing information about graphics cards and displays.") @@ -824,14 +805,19 @@ if(WIN32) # see options at: https://cmake.org/cmake/help/latest/cpack_gen/nsis.h # service set(CPACK_COMPONENT_SUNSHINESVC_DISPLAY_NAME "sunshinesvc") - set(CPACK_COMPONENT_SUNSHINESVC_DESCRIPTION "CLI tool providing ability to enable/disable the Sunshine service.") + set(CPACK_COMPONENT_SUNSHINESVC_DESCRIPTION "Installs sunshine as a service as SYSTEM, \ + so that it can allow usage before logon and other benefits.") set(CPACK_COMPONENT_SUNSHINESVC_GROUP "tools") + # This is required because we've changed the ACL for config to require admin to protect against EoP exploits. + set(CPACK_COMPONENT_SUNSHINESVC_REQUIRED true) # service scripts set(CPACK_COMPONENT_SERVICE_DISPLAY_NAME "service-scripts") - set(CPACK_COMPONENT_SERVICE_DESCRIPTION "Scripts to enable/disable the service.") + set(CPACK_COMPONENT_SERVICE_DESCRIPTION "Scripts for installing and enabling the service.") set(CPACK_COMPONENT_SERVICE_GROUP "scripts") set(CPACK_COMPONENT_SERVICE_DEPENDS sunshinesvc) + # This is required because we've changed the ACL for config to require admin to protect against EoP exploits. + set(CPACK_COMPONENT_SERVICE_REQUIRED true) # firewall scripts set(CPACK_COMPONENT_FIREWALL_DISPLAY_NAME "firewall-scripts") diff --git a/docs/source/about/app_examples.rst b/docs/source/about/app_examples.rst index dbeac1ac..0e390297 100644 --- a/docs/source/about/app_examples.rst +++ b/docs/source/about/app_examples.rst @@ -187,3 +187,32 @@ Changing Resolution and Refresh Rate (Windows) .. Tip:: You can change your host resolution to match the client resolution automatically using the `Nonary/ResolutionAutomation `_ project. + + +Elevating Commands (Windows) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you've installed Sunshine as a service (default), you can now specify if a command should be elevated with adminsitrative privileges. +Simply enable the elevated option in the WEB UI, or add it to the JSON configuration. +This is an option for both prep-cmd and regular commands and will launch the process with the current user without a UAC prompt. + +.. Note:: It's important to write the values "true" and "false" as string values, not as the typical true/false values in most JSON. + +**Example** + .. code-block:: json + + { + "name": "Game With AntiCheat that Requires Admin", + "output": "", + "cmd": "ping 127.0.0.1", + "exclude-global-prep-cmd": "false", + "elevated": "true", + "prep-cmd": [ + { + "do": "powershell.exe -command \"Start-Streaming\"", + "undo": "powershell.exe -command \"Stop-Streaming\"", + "elevated": "false" + } + ], + "image-path": "" + } diff --git a/src/config.cpp b/src/config.cpp index 1e4276df..60961cd6 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -816,12 +816,14 @@ namespace config { boost::property_tree::read_json(jsonStream, jsonTree); for (auto &[_, prep_cmd] : jsonTree.get_child("prep_cmd"s)) { - auto do_cmd = prep_cmd.get("do"s); - auto undo_cmd = prep_cmd.get("undo"s); + auto do_cmd = prep_cmd.get_optional("do"s); + auto undo_cmd = prep_cmd.get_optional("undo"s); + auto elevated = prep_cmd.get_optional("elevated"s); input.emplace_back( - std::move(do_cmd), - std::move(undo_cmd)); + std::move(do_cmd.value_or("")), + std::move(undo_cmd.value_or("")), + std::move(elevated.value_or(false))); } } diff --git a/src/config.h b/src/config.h index fc77748b..90b3fbee 100644 --- a/src/config.h +++ b/src/config.h @@ -119,14 +119,14 @@ namespace config { } struct prep_cmd_t { - prep_cmd_t(std::string &&do_cmd, std::string &&undo_cmd): - do_cmd(std::move(do_cmd)), undo_cmd(std::move(undo_cmd)) {} - explicit prep_cmd_t(std::string &&do_cmd): - do_cmd(std::move(do_cmd)) {} + prep_cmd_t(std::string &&do_cmd, std::string &&undo_cmd, bool &&elevated): + do_cmd(std::move(do_cmd)), undo_cmd(std::move(undo_cmd)), elevated(std::move(elevated)) {} + explicit prep_cmd_t(std::string &&do_cmd, bool &&elevated): + do_cmd(std::move(do_cmd)), elevated(std::move(elevated)) {} std::string do_cmd; std::string undo_cmd; + bool elevated; }; - struct sunshine_t { int min_log_level; std::bitset flags; diff --git a/src/httpcommon.cpp b/src/httpcommon.cpp index 24810f51..31054a05 100644 --- a/src/httpcommon.cpp +++ b/src/httpcommon.cpp @@ -89,7 +89,7 @@ namespace http { pt::write_json(file, outputTree); } catch (std::exception &e) { - BOOST_LOG(error) << "generating user credentials: "sv << e.what(); + BOOST_LOG(error) << "error writing to the credentials file, perhaps try this again as an administrator? Details: "sv << e.what(); return -1; } diff --git a/src/platform/common.h b/src/platform/common.h index fa8104dc..6b6f3f1e 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -382,7 +382,7 @@ namespace platf { display_names(mem_type_e hwdevice_type); boost::process::child - run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, boost::process::environment &env, FILE *file, std::error_code &ec, boost::process::group *group); + run_command(bool elevated, const std::string &cmd, boost::filesystem::path &working_dir, boost::process::environment &env, FILE *file, std::error_code &ec, boost::process::group *group); enum class thread_priority_e : int { low, diff --git a/src/platform/linux/misc.cpp b/src/platform/linux/misc.cpp index ba41e625..63179277 100644 --- a/src/platform/linux/misc.cpp +++ b/src/platform/linux/misc.cpp @@ -159,8 +159,7 @@ namespace platf { } bp::child - run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { - BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv; + run_command(bool elevated, const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { if (!group) { if (!file) { return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec); diff --git a/src/platform/macos/misc.mm b/src/platform/macos/misc.mm index 3d863729..2f16f4ba 100644 --- a/src/platform/macos/misc.mm +++ b/src/platform/macos/misc.mm @@ -158,8 +158,7 @@ namespace platf { } bp::child - run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { - BOOST_LOG(warning) << "run_unprivileged() is not yet implemented for this platform. The new process will run with Sunshine's permissions."sv; + run_command(bool elevated, const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { if (!group) { if (!file) { return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec); diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index 5335cae2..af6b4a26 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -19,6 +19,8 @@ #include #include #include +#include +#include // clang-format on #include "src/main.h" @@ -200,103 +202,99 @@ namespace platf { return std::string(buffer, bytes); } - HANDLE - duplicate_shell_token() { - // Get the shell window (will usually be owned by explorer.exe) - HWND shell_window = GetShellWindow(); - if (!shell_window) { - BOOST_LOG(error) << "No shell window found. Is explorer.exe running?"sv; - return NULL; - } - - // Open a handle to the explorer.exe process - DWORD shell_pid; - GetWindowThreadProcessId(shell_window, &shell_pid); - HANDLE shell_process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, shell_pid); - if (!shell_process) { - BOOST_LOG(error) << "Failed to open shell process: "sv << GetLastError(); - return NULL; - } - - // Open explorer's token to clone for process creation - HANDLE shell_token; - BOOL ret = OpenProcessToken(shell_process, TOKEN_DUPLICATE, &shell_token); - CloseHandle(shell_process); - if (!ret) { - BOOST_LOG(error) << "Failed to open shell process token: "sv << GetLastError(); - return NULL; - } - - // Duplicate the token to make it usable for process creation - HANDLE new_token; - ret = DuplicateTokenEx(shell_token, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &new_token); - CloseHandle(shell_token); - if (!ret) { - BOOST_LOG(error) << "Failed to duplicate shell process token: "sv << GetLastError(); - return NULL; - } - - return new_token; - } - - PTOKEN_USER - get_token_user(HANDLE token) { - DWORD return_length; - if (GetTokenInformation(token, TokenUser, NULL, 0, &return_length) || GetLastError() != ERROR_INSUFFICIENT_BUFFER) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "Failed to get token information size: "sv << winerr; - return nullptr; - } - - auto user = (PTOKEN_USER) HeapAlloc(GetProcessHeap(), 0, return_length); - if (!user) { - return nullptr; - } - - if (!GetTokenInformation(token, TokenUser, user, return_length, &return_length)) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "Failed to get token information: "sv << winerr; - HeapFree(GetProcessHeap(), 0, user); - return nullptr; - } - - return user; - } - - void - free_token_user(PTOKEN_USER user) { - HeapFree(GetProcessHeap(), 0, user); - } - bool - is_token_same_user_as_process(HANDLE other_token) { - HANDLE process_token; - if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &process_token)) { - auto winerr = GetLastError(); - BOOST_LOG(error) << "Failed to open process token: "sv << winerr; - return false; + IsUserAdmin(HANDLE user_token) { + WINBOOL ret; + SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY; + PSID AdministratorsGroup; + ret = AllocateAndInitializeSid( + &NtAuthority, + 2, + SECURITY_BUILTIN_DOMAIN_RID, + DOMAIN_ALIAS_RID_ADMINS, + 0, 0, 0, 0, 0, 0, + &AdministratorsGroup); + if (ret) { + if (!CheckTokenMembership(user_token, AdministratorsGroup, &ret)) { + ret = false; + BOOST_LOG(error) << "Failed to verify token membership for administrative access: " << GetLastError(); + } + FreeSid(AdministratorsGroup); } - - auto process_user = get_token_user(process_token); - CloseHandle(process_token); - if (!process_user) { - return false; + else { + BOOST_LOG(error) << "Unable to allocate SID to check administrative access: " << GetLastError(); } - auto token_user = get_token_user(other_token); - if (!token_user) { - free_token_user(process_user); - return false; - } - - bool ret = EqualSid(process_user->User.Sid, token_user->User.Sid); - - free_token_user(process_user); - free_token_user(token_user); - return ret; } + /** + * @brief A function to obtain the current sessions user's primary token with elevated privileges + * + * @return The users token, if user has admin capability it will be elevated. If not, it will return back a limited token. On error, nullptrs + */ + HANDLE + retrieve_users_token(bool elevated) { + DWORD consoleSessionId; + HANDLE userToken; + TOKEN_ELEVATION_TYPE elevationType; + DWORD dwSize; + + // Get the session ID of the active console session + consoleSessionId = WTSGetActiveConsoleSessionId(); + if (0xFFFFFFFF == consoleSessionId) { + // If there is no active console session, log a warning and return null + BOOST_LOG(warning) << "There isn't an active user session, therefore it is not possible to execute commands under the users profile."; + return nullptr; + } + + // Get the user token for the active console session + if (!WTSQueryUserToken(consoleSessionId, &userToken)) { + BOOST_LOG(debug) << "QueryUserToken failed, this would prevent commands from launching under the users profile."; + return nullptr; + } + + // We need to know if this is an elevated token or not. + // Get the elevation type of the user token + // Elevation - Default: User is not an admin, UAC enabled/disabled does not matter. + // Elevation - Limited: User is an admin, has UAC enabled. + // Elevation - Full: User is an admin, has UAC disabled. + if (!GetTokenInformation(userToken, TokenElevationType, &elevationType, sizeof(TOKEN_ELEVATION_TYPE), &dwSize)) { + BOOST_LOG(debug) << "Retrieving token information failed: " << GetLastError(); + CloseHandle(userToken); + return nullptr; + } + + // User is currently not an administrator + // The documentation for this scenario is conflicting, so we'll double check to see if user is actually an admin. + if (elevated && (elevationType == TokenElevationTypeDefault && !IsUserAdmin(userToken))) { + // We don't have to strip the token or do anything here, but let's give the user a warning so they're aware what is happening. + BOOST_LOG(warning) << "This command requires elevation and the current user account logged in does not have administrator rights. " + << "For security reasons Sunshine will retain the same access level as the current user and will not elevate it."; + } + + // User has a limited token, this means they have UAC enabled and is an Administrator + if (elevated && elevationType == TokenElevationTypeLimited) { + TOKEN_LINKED_TOKEN linkedToken; + // Retrieve the administrator token that is linked to the limited token + if (!GetTokenInformation(userToken, TokenLinkedToken, reinterpret_cast(&linkedToken), sizeof(TOKEN_LINKED_TOKEN), &dwSize)) { + // If the retrieval failed, log an error message and return null + BOOST_LOG(error) << "Retrieving linked token information failed: " << GetLastError(); + CloseHandle(userToken); + + // There is no scenario where this should be hit, except for an actual error. + return nullptr; + } + + // Since we need the elevated token, we'll replace it with their administrative token. + CloseHandle(userToken); + userToken = linkedToken.LinkedToken; + } + + // We don't need to do anything for TokenElevationTypeFull users here, because they're already elevated. + return userToken; + } + bool merge_user_environment_block(bp::environment &env, HANDLE shell_token) { // Get the target user's environment block @@ -334,6 +332,42 @@ namespace platf { return true; } + /** + +@brief Check if the current process is running with system-level privileges. +@return true if the current process has system-level privileges, false otherwise. +*/ + bool + is_running_as_system() { + BOOL ret; + PSID SystemSid; + DWORD dwSize = SECURITY_MAX_SID_SIZE; + + // Allocate memory for the SID structure + SystemSid = LocalAlloc(LMEM_FIXED, dwSize); + if (SystemSid == nullptr) { + BOOST_LOG(error) << "Failed to allocate memory for the SID structure: " << GetLastError(); + return false; + } + + // Create a SID for the local system account + ret = CreateWellKnownSid(WinLocalSystemSid, nullptr, SystemSid, &dwSize); + if (ret) { + // Check if the current process token contains this SID + if (!CheckTokenMembership(nullptr, SystemSid, &ret)) { + BOOST_LOG(error) << "Failed to check token membership: " << GetLastError(); + ret = false; + } + } + else { + BOOST_LOG(error) << "Failed to create a SID for the local system account. This may happen if the system is out of memory or if the SID buffer is too small: " << GetLastError(); + } + + // Free the memory allocated for the SID structure + LocalFree(SystemSid); + return ret; + } + // Note: This does NOT append a null terminator void append_string_to_environment_block(wchar_t *env_block, int &offset, const std::wstring &wstr) { @@ -395,46 +429,114 @@ namespace platf { HeapFree(GetProcessHeap(), 0, list); } + /** + * @brief Creates a bp::child object from the results of launching a process + * + * @param process_launched A boolean indicating whether the launch was successful or not + * @param cmd The command that was used to launch the process + * @param ec A reference to an std::error_code object that will store any error that occurred during the launch + * @param process_info A reference to a PROCESS_INFORMATION structure that contains information about the new process + * @param group A pointer to a bp::group object that will add the new process to its group, if not null + * @return A bp::child object representing the new process, or an empty bp::child object if the launch failed or an error occurred + */ bp::child - run_unprivileged(const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { - HANDLE shell_token = duplicate_shell_token(); - if (!shell_token) { - // This can happen if the shell has crashed. Fail the launch rather than risking launching with - // Sunshine's permissions unmodified. - ec = std::make_error_code(std::errc::no_such_process); - return bp::child(); - } - - auto token_close = util::fail_guard([shell_token]() { - CloseHandle(shell_token); + create_boost_child_from_results(bool process_launched, const std::string &cmd, std::error_code &ec, PROCESS_INFORMATION &process_info, bp::group *group) { + // Use RAII to ensure the process is closed when we're done with it, even if there was an error. + auto close_process_handles = util::fail_guard([process_launched, process_info]() { + if (process_launched) { + CloseHandle(process_info.hThread); + CloseHandle(process_info.hProcess); + } }); - // Populate env with user-specific environment variables - if (!merge_user_environment_block(env, shell_token)) { - ec = std::make_error_code(std::errc::not_enough_memory); + if (ec) { + // If there was an error, return an empty bp::child object return bp::child(); } - // Most Win32 APIs can't consume UTF-8 strings directly, so we must convert them into UTF-16 - std::wstring wcmd = utf8_to_wide_string(cmd); - std::wstring env_block = create_environment_block(env); - std::wstring start_dir = utf8_to_wide_string(working_dir.string()); + if (process_launched) { + // If the launch was successful, create a new bp::child object representing the new process + auto child = bp::child((bp::pid_t) process_info.dwProcessId); + if (group) { + // If a group was provided, add the new process to the group + group->add(child); + } + BOOST_LOG(info) << cmd << " running with PID "sv << child.id(); + return child; + } + else { + auto winerror = GetLastError(); + BOOST_LOG(error) << "Failed to launch process: "sv << winerror; + ec = std::make_error_code(std::errc::invalid_argument); + // We must NOT attach the failed process here, since this case can potentially be induced by ACL + // manipulation (denying yourself execute permission) to cause an escalation of privilege. + // So to protect ourselves against that, we'll return an empty child process instead. + return bp::child(); + } + } + + /** + * @brief Impersonate the current user, invoke the callback function, then returns back to system context. + * + * @param user_token A handle to the user's token that was obtained from the shell + * @param callback A function that will be executed while impersonating the user + * @return An std::error_code object that will store any error that occurred during the impersonation + */ + std::error_code + impersonate_current_user(HANDLE user_token, std::function callback) { + std::error_code ec; + // Impersonate the user when launching the process. This will ensure that appropriate access + // checks are done against the user token, not our SYSTEM token. It will also allow network + // shares and mapped network drives to be used as launch targets, since those credentials + // are stored per-user. + if (!ImpersonateLoggedOnUser(user_token)) { + auto winerror = GetLastError(); + // Log the failure of impersonating the user and its error code + BOOST_LOG(error) << "Failed to impersonate user: "sv << winerror; + ec = std::make_error_code(std::errc::permission_denied); + return ec; + } + + // Execute the callback function while impersonating the user + callback(); + + // End impersonation of the logged on user. If this fails (which is extremely unlikely), + // we will be running with an unknown user token. The only safe thing to do in that case + // is terminate ourselves. + if (!RevertToSelf()) { + auto winerror = GetLastError(); + // Log the failure of reverting to self and its error code + BOOST_LOG(fatal) << "Failed to revert to self after impersonation: "sv << winerror; + std::abort(); + } + + return ec; + } + + /** + * @brief A function to create a STARTUPINFOEXW structure for launching a process + * + * @param file A pointer to a FILE object that will be used as the standard output and error for the new process, or null if not needed + * @param ec A reference to an std::error_code object that will store any error that occurred during the creation of the structure + * @return A STARTUPINFOEXW structure that contains information about how to launch the new process + */ + STARTUPINFOEXW + create_startup_info(FILE *file, std::error_code &ec) { + // Initialize a zeroed-out STARTUPINFOEXW structure and set its size STARTUPINFOEXW startup_info = {}; startup_info.StartupInfo.cb = sizeof(startup_info); // Allocate a process attribute list with space for 1 element startup_info.lpAttributeList = allocate_proc_thread_attr_list(1); if (startup_info.lpAttributeList == NULL) { + // If the allocation failed, set ec to an appropriate error code and return the structure ec = std::make_error_code(std::errc::not_enough_memory); - return bp::child(); + return startup_info; } - auto attr_list_free = util::fail_guard([list = startup_info.lpAttributeList]() { - free_proc_thread_attr_list(list); - }); - if (file) { + // If a file was provided, get its handle and use it as the standard output and error for the new process HANDLE log_file_handle = (HANDLE) _get_osfhandle(_fileno(file)); // Populate std handles if the caller gave us a log file to use @@ -454,74 +556,84 @@ namespace platf { NULL); } - // If we're running with the same user account as the shell, just use CreateProcess(). - // This will launch the child process elevated if Sunshine is elevated. - PROCESS_INFORMATION process_info; + return startup_info; + } + + /** + * @brief Runs a command on the users profile + * + * This function launches a child process as the user, using the current user's environment + * and a specific working directory. If the launch is successful, a `bp::child` object representing the new + * process is returned. Otherwise, an error code is returned. + * + * @param elevated Specify to elevate the process or not + * @param cmd The command to run + * @param working_dir The working directory for the new process + * @param env The environment variables to use for the new process + * @param file A file object to redirect the child process's output to (may be nullptr) + * @param ec An error code, set to indicate any errors that occur during the launch process + * @param group A pointer to a `bp::group` object to which the new process should belong (may be nullptr) + * + * @return A `bp::child` object representing the new process, or an empty `bp::child` object if the launch fails + */ + bp::child + run_command(bool elevated, const std::string &cmd, boost::filesystem::path &working_dir, bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) { BOOL ret; - if (!is_token_same_user_as_process(shell_token)) { - // Impersonate the user when launching the process. This will ensure that appropriate access - // checks are done against the user token, not our SYSTEM token. It will also allow network - // shares and mapped network drives to be used as launch targets, since those credentials - // are stored per-user. - if (!ImpersonateLoggedOnUser(shell_token)) { - auto winerror = GetLastError(); - BOOST_LOG(error) << "Failed to impersonate user: "sv << winerror; - ec = std::make_error_code(std::errc::permission_denied); + // Convert cmd, env, and working_dir to the appropriate character sets for Win32 APIs + std::wstring wcmd = utf8_to_wide_string(cmd); + std::wstring env_block = create_environment_block(env); + std::wstring start_dir = utf8_to_wide_string(working_dir.string()); + + STARTUPINFOEXW startup_info = create_startup_info(file, ec); + PROCESS_INFORMATION process_info; + + if (ec) { + // In the event that startup_info failed, return a blank child process. + return bp::child(); + } + + // Use RAII to ensure the attribute list is freed when we're done with it + auto attr_list_free = util::fail_guard([list = startup_info.lpAttributeList]() { + free_proc_thread_attr_list(list); + }); + + if (is_running_as_system()) { + // Duplicate the current user's token + HANDLE user_token = retrieve_users_token(elevated); + if (!user_token) { + // Fail the launch rather than risking launching with Sunshine's permissions unmodified. + ec = std::make_error_code(std::errc::no_such_process); return bp::child(); } - // Launch the process with the duplicated shell token. - // Set CREATE_BREAKAWAY_FROM_JOB to avoid the child being killed if SunshineSvc.exe is terminated. - // Set CREATE_NEW_CONSOLE to avoid writing stdout to Sunshine's log if 'file' is not specified. - ret = CreateProcessAsUserW(shell_token, - NULL, - (LPWSTR) wcmd.c_str(), - NULL, - NULL, - !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), - EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB, - env_block.data(), - start_dir.empty() ? NULL : start_dir.c_str(), - (LPSTARTUPINFOW) &startup_info, - &process_info); + // Use RAII to ensure the shell token is closed when we're done with it + auto token_close = util::fail_guard([user_token]() { + CloseHandle(user_token); + }); - if (!ret) { - auto error = GetLastError(); - - if (error == 740) { - BOOST_LOG(info) << "Could not execute previous command because it required elevation. Running the command again with elevation, for security reasons this will prompt user interaction."sv; - startup_info.StartupInfo.wShowWindow = SW_HIDE; - startup_info.StartupInfo.dwFlags = startup_info.StartupInfo.dwFlags | STARTF_USESHOWWINDOW; - std::wstring elevated_command = L"tools\\elevator.exe "; - elevated_command += wcmd; - - // For security reasons, Windows enforces that an application can have only one "interactive thread" responsible for processing user input and managing the user interface (UI). - // Since UAC prompts are interactive, we cannot have a UAC prompt while Sunshine is already running because it would block the thread. - // To work around this issue, we will launch a separate process that will elevate the command, which will prompt the user to confirm the elevation. - // This is our intended behavior: to require interaction before elevating the command. - ret = CreateProcessAsUserW(shell_token, - nullptr, - (LPWSTR) elevated_command.c_str(), - nullptr, - nullptr, - !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), - EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB, - env_block.data(), - start_dir.empty() ? nullptr : start_dir.c_str(), - (LPSTARTUPINFOW) &startup_info, - &process_info); - } + // Populate env with user-specific environment variables + if (!merge_user_environment_block(env, user_token)) { + ec = std::make_error_code(std::errc::not_enough_memory); + return bp::child(); } - // End impersonation of the logged on user. If this fails (which is extremely unlikely), - // we will be running with an unknown user token. The only safe thing to do in that case - // is terminate ourselves. - if (!RevertToSelf()) { - auto winerror = GetLastError(); - BOOST_LOG(fatal) << "Failed to revert to self after impersonation: "sv << winerror; - std::abort(); - } + // Open the process as the current user account, elevation is handled in the token itself. + ec = impersonate_current_user(user_token, [&]() { + ret = CreateProcessAsUserW(user_token, + NULL, + (LPWSTR) wcmd.c_str(), + NULL, + NULL, + !!(startup_info.StartupInfo.dwFlags & STARTF_USESTDHANDLES), + EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB, + env_block.data(), + start_dir.empty() ? NULL : start_dir.c_str(), + (LPSTARTUPINFOW) &startup_info, + &process_info); + }); } + // Otherwise, launch the process using CreateProcessW() + // This will inherit the elevation of whatever the user launched Sunshine with. else { ret = CreateProcessW(NULL, (LPWSTR) wcmd.c_str(), @@ -535,30 +647,8 @@ namespace platf { &process_info); } - if (ret) { - // Since we are always spawning a process with a less privileged token than ourselves, - // bp::child() should have no problem opening it with any access rights it wants. - auto child = bp::child((bp::pid_t) process_info.dwProcessId); - if (group) { - group->add(child); - } - - // Only close handles after bp::child() has opened the process. If the process terminates - // quickly, the PID could be reused if we close the process handle. - CloseHandle(process_info.hThread); - CloseHandle(process_info.hProcess); - - BOOST_LOG(info) << cmd << " running with PID "sv << child.id(); - return child; - } - else { - // We must NOT try bp::child() here, since this case can potentially be induced by ACL - // manipulation (denying yourself execute permission) to cause an escalation of privilege. - auto winerror = GetLastError(); - BOOST_LOG(error) << "Failed to launch process: "sv << winerror; - ec = std::make_error_code(std::errc::invalid_argument); - return bp::child(); - } + // Use the results of the launch to create a bp::child object + return create_boost_child_from_results(ret, cmd, ec, process_info, group); } void @@ -900,5 +990,4 @@ namespace platf { return std::make_unique(flow_id); } - } // namespace platf \ No newline at end of file diff --git a/src/platform/windows/misc.h b/src/platform/windows/misc.h index 4bcd31fd..6fe68ac2 100644 --- a/src/platform/windows/misc.h +++ b/src/platform/windows/misc.h @@ -1,6 +1,5 @@ #ifndef SUNSHINE_WINDOWS_MISC_H #define SUNSHINE_WINDOWS_MISC_H - #include #include #include diff --git a/src/process.cpp b/src/process.cpp index 6053e73b..f8875452 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -125,16 +125,21 @@ namespace proc { }); for (; _app_prep_it != std::end(_app.prep_cmds); ++_app_prep_it) { - auto &cmd = _app_prep_it->do_cmd; + auto &cmd = *_app_prep_it; + + // Skip empty commands + if (cmd.do_cmd.empty()) { + continue; + } boost::filesystem::path working_dir = _app.working_dir.empty() ? - find_working_directory(cmd, _env) : + find_working_directory(cmd.do_cmd, _env) : boost::filesystem::path(_app.working_dir); - BOOST_LOG(info) << "Executing Do Cmd: ["sv << cmd << ']'; - auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec, nullptr); + BOOST_LOG(info) << "Executing Do Cmd: ["sv << cmd.do_cmd << ']'; + auto child = platf::run_command(cmd.elevated, cmd.do_cmd, working_dir, _env, _pipe.get(), ec, nullptr); if (ec) { - BOOST_LOG(error) << "Couldn't run ["sv << cmd << "]: System: "sv << ec.message(); + BOOST_LOG(error) << "Couldn't run ["sv << cmd.do_cmd << "]: System: "sv << ec.message(); return -1; } @@ -142,7 +147,7 @@ namespace proc { auto ret = child.exit_code(); if (ret != 0) { - BOOST_LOG(error) << '[' << cmd << "] failed with code ["sv << ret << ']'; + BOOST_LOG(error) << '[' << cmd.do_cmd << "] failed with code ["sv << ret << ']'; return -1; } } @@ -152,7 +157,7 @@ namespace proc { find_working_directory(cmd, _env) : boost::filesystem::path(_app.working_dir); BOOST_LOG(info) << "Spawning ["sv << cmd << "] in ["sv << working_dir << ']'; - auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec, nullptr); + auto child = platf::run_command(_app.elevated, cmd, working_dir, _env, _pipe.get(), ec, nullptr); if (ec) { BOOST_LOG(warning) << "Couldn't spawn ["sv << cmd << "]: System: "sv << ec.message(); } @@ -170,7 +175,7 @@ namespace proc { find_working_directory(_app.cmd, _env) : boost::filesystem::path(_app.working_dir); BOOST_LOG(info) << "Executing: ["sv << _app.cmd << "] in ["sv << working_dir << ']'; - _process = platf::run_unprivileged(_app.cmd, working_dir, _env, _pipe.get(), ec, &_process_handle); + _process = platf::run_command(_app.elevated, _app.cmd, working_dir, _env, _pipe.get(), ec, &_process_handle); if (ec) { BOOST_LOG(warning) << "Couldn't run ["sv << _app.cmd << "]: System: "sv << ec.message(); return -1; @@ -208,17 +213,17 @@ namespace proc { _app_id = -1; for (; _app_prep_it != _app_prep_begin; --_app_prep_it) { - auto &cmd = (_app_prep_it - 1)->undo_cmd; + auto &cmd = *(_app_prep_it - 1); - if (cmd.empty()) { + if (cmd.undo_cmd.empty()) { continue; } boost::filesystem::path working_dir = _app.working_dir.empty() ? - find_working_directory(cmd, _env) : + find_working_directory(cmd.undo_cmd, _env) : boost::filesystem::path(_app.working_dir); - BOOST_LOG(info) << "Executing Undo Cmd: ["sv << cmd << ']'; - auto child = platf::run_unprivileged(cmd, working_dir, _env, _pipe.get(), ec, nullptr); + BOOST_LOG(info) << "Executing Undo Cmd: ["sv << cmd.undo_cmd << ']'; + auto child = platf::run_command(cmd.elevated, cmd.undo_cmd, working_dir, _env, _pipe.get(), ec, nullptr); if (ec) { BOOST_LOG(warning) << "System: "sv << ec.message(); @@ -481,6 +486,7 @@ namespace proc { auto cmd = app_node.get_optional("cmd"s); auto image_path = app_node.get_optional("image-path"s); auto working_dir = app_node.get_optional("working-dir"s); + auto elevated = app_node.get_optional("elevated"s); std::vector prep_cmds; if (!exclude_global_prep.value_or(false)) { @@ -489,7 +495,10 @@ namespace proc { auto do_cmd = parse_env_val(this_env, prep_cmd.do_cmd); auto undo_cmd = parse_env_val(this_env, prep_cmd.undo_cmd); - prep_cmds.emplace_back(std::move(do_cmd), std::move(undo_cmd)); + prep_cmds.emplace_back( + std::move(do_cmd), + std::move(undo_cmd), + std::move(prep_cmd.elevated)); } } @@ -498,15 +507,14 @@ namespace proc { prep_cmds.reserve(prep_cmds.size() + prep_nodes.size()); for (auto &[_, prep_node] : prep_nodes) { - auto do_cmd = parse_env_val(this_env, prep_node.get("do"s)); + auto do_cmd = prep_node.get_optional("do"s); auto undo_cmd = prep_node.get_optional("undo"s); + auto elevated = prep_node.get_optional("elevated"); - if (undo_cmd) { - prep_cmds.emplace_back(std::move(do_cmd), parse_env_val(this_env, *undo_cmd)); - } - else { - prep_cmds.emplace_back(std::move(do_cmd)); - } + prep_cmds.emplace_back( + parse_env_val(this_env, do_cmd.value_or("")), + parse_env_val(this_env, undo_cmd.value_or("")), + std::move(elevated.value_or(false))); } } @@ -536,6 +544,8 @@ namespace proc { ctx.image_path = parse_env_val(this_env, *image_path); } + ctx.elevated = elevated.value_or(false); + auto possible_ids = calculate_app_id(name, ctx.image_path, i++); if (ids.count(std::get<0>(possible_ids)) == 0) { // Avoid using index to generate id if possible diff --git a/src/process.h b/src/process.h index 12f4b5d9..418ed3a7 100644 --- a/src/process.h +++ b/src/process.h @@ -48,6 +48,7 @@ namespace proc { std::string output; std::string image_path; std::string id; + bool elevated; }; class proc_t { diff --git a/src/system_tray.cpp b/src/system_tray.cpp index b25d6d38..8de4baf5 100644 --- a/src/system_tray.cpp +++ b/src/system_tray.cpp @@ -62,7 +62,7 @@ namespace system_tray { boost::process::environment _env = boost::this_process::environment(); std::error_code ec; - auto child = platf::run_unprivileged(cmd, working_dir, _env, nullptr, ec, nullptr); + auto child = platf::run_command(false, cmd, working_dir, _env, nullptr, ec, nullptr); if (ec) { BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message(); } diff --git a/src_assets/common/assets/web/apps.html b/src_assets/common/assets/web/apps.html index 8908c50c..f3243036 100644 --- a/src_assets/common/assets/web/apps.html +++ b/src_assets/common/assets/web/apps.html @@ -15,9 +15,11 @@ {{app.name}} - + @@ -56,29 +58,51 @@ -
-
- - -
- Enable/Disable the execution of Global Prep Commands for this application. -
+
+ + +
+ Enable/Disable the execution of Global Prep Commands for this + application.
+
+
- A list of commands to be run before/after this application.
- If any of the prep-commands fail, starting the application is aborted + A list of commands to be run before/after this application.
+ If any of the prep-commands fail, starting the application is aborted.
- +
+ +
+
- - - + + + + + + - + +
DoUndo
Do Command Undo Command + Run as Admin +
+
+ + +
+
+
-
@@ -170,9 +205,28 @@ v-model="editForm['working-dir']" />
- The working directory that should be passed to the process. - For example, some applications use the working directory to search for configuration files. - If not set, Sunshine will default to the parent directory of the command + The working directory that should be passed to the process. For + example, some applications use the working directory to search for + configuration files. If not set, Sunshine will default to the parent + directory of the command +
+
+ +
+ + +
+ This can be necessary for some applications that require administrator + permissions to run properly.
@@ -180,36 +234,60 @@
- -