From c2fba6f65132355fb6e4e9bcf7e12f29db1c9dbd Mon Sep 17 00:00:00 2001 From: Chase Payne Date: Mon, 27 Mar 2023 09:51:48 -0500 Subject: [PATCH] Add Support for Safely Elevating Administrator Privileges (#1036) --- CMakeLists.txt | 7 ++++ src/platform/windows/misc.cpp | 40 +++++++++++++++++--- tools/CMakeLists.txt | 8 ++++ tools/elevator.cpp | 70 +++++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 tools/elevator.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 96a19d51..0eaefdb6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -690,6 +690,7 @@ 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) @@ -801,6 +802,12 @@ 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_ELEVATION_DISPLAY_NAME "elevator") + set(CPACK_COMPONENT_ELEVATION_DESCRIPTION "CLI tool that assists with elevating \ + commands when permissions have been denied.") + set(CPACK_COMPONENT_ELEVATION_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.") diff --git a/src/platform/windows/misc.cpp b/src/platform/windows/misc.cpp index c89b0f2c..49f7ed0d 100644 --- a/src/platform/windows/misc.cpp +++ b/src/platform/windows/misc.cpp @@ -9,20 +9,22 @@ // prevent clang format from "optimizing" the header include order // clang-format off -#include +#include #include +#include +#include +#include +#include #include #include -#include -#include -#include -#include #include +#include // clang-format on #include "src/main.h" #include "src/platform/common.h" #include "src/utility.h" +#include // UDP_SEND_MSG_SIZE was added in the Windows 10 20H1 SDK #ifndef UDP_SEND_MSG_SIZE @@ -464,6 +466,34 @@ bp::child run_unprivileged(const std::string &cmd, boost::filesystem::path &work (LPSTARTUPINFOW)&startup_info, &process_info); + 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); + } + } + // 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. diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index c2121a7d..0ee64ddc 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -12,6 +12,13 @@ target_link_libraries(dxgi-info ${PLATFORM_LIBRARIES}) target_compile_options(dxgi-info PRIVATE ${SUNSHINE_COMPILE_OPTIONS}) +add_executable(elevator elevator.cpp) +set_target_properties(elevator PROPERTIES CXX_STANDARD 17) +target_link_libraries(elevator + shell32 + ${PLATFORM_LIBRARIES}) +target_compile_options(elevator PRIVATE ${SUNSHINE_COMPILE_OPTIONS}) + add_executable(audio-info audio.cpp) set_target_properties(audio-info PROPERTIES CXX_STANDARD 17) target_link_libraries(audio-info @@ -36,3 +43,4 @@ target_link_libraries(ddprobe d3d11 ${PLATFORM_LIBRARIES}) target_compile_options(ddprobe PRIVATE ${SUNSHINE_COMPILE_OPTIONS}) + diff --git a/tools/elevator.cpp b/tools/elevator.cpp new file mode 100644 index 00000000..8f449bf8 --- /dev/null +++ b/tools/elevator.cpp @@ -0,0 +1,70 @@ +#include +#include +#include + +/** + * @file elevator.cpp + * @brief A simple command line utility to run a given command with administrative privileges. + * + * This utility helps run a command with administrative privileges on Windows + * by leveraging the ShellExecuteExW function. The program accepts a command + * and optional arguments, then attempts to run the command with elevated + * privileges. If successful, it waits for the process to complete and + * returns the exit code of the launched process. + * + * @example + * To run the command prompt with administrative privileges, execute the following command: + * elevator.exe cmd + * + * To run a command, such as 'ipconfig /flushdns', with administrative privileges, execute: + * elevator.exe cmd /C "ipconfig /flushdns" + */ +int main(int argc, char *argv[]) { + // Check if the user provided at least one argument (the command to run) + if(argc < 2) { + std::cout << "Usage: " << argv[0] << " [arguments]" << std::endl; + return 1; + } + + // Convert the command and arguments from char* to wstring for use with ShellExecuteExW + std::wstring command = std::wstring(argv[1], argv[1] + strlen(argv[1])); + std::wstring arguments; + + // Concatenate the remaining arguments (if any) into a single wstring + for(int i = 2; i < argc; ++i) { + arguments += std::wstring(argv[i], argv[i] + strlen(argv[i])); + if(i < argc - 1) { + arguments += L" "; + } + } + + // Prepare the SHELLEXECUTEINFOW structure with the necessary information + SHELLEXECUTEINFOW info = { sizeof(SHELLEXECUTEINFOW) }; + info.lpVerb = L"runas"; // Request elevation + info.lpFile = command.c_str(); + info.lpParameters = arguments.empty() ? nullptr : arguments.c_str(); + info.nShow = SW_SHOW; + info.fMask = SEE_MASK_NOCLOSEPROCESS; // So we can wait for the process to finish + + // Attempt to execute the command with elevation + if(!ShellExecuteExW(&info)) { + std::cout << "Error: ShellExecuteExW failed with code " << GetLastError() << std::endl; + return 1; + } + + // Wait for the launched process to finish + WaitForSingleObject(info.hProcess, INFINITE); + + DWORD exitCode = 0; + + // Retrieve the exit code of the launched process + if(!GetExitCodeProcess(info.hProcess, &exitCode)) { + std::cout << "Error: GetExitCodeProcess failed with code " << GetLastError() << std::endl; + } + + // Close the process handle + CloseHandle(info.hProcess); + + // Return the exit code of the launched process + return exitCode; +}