diff --git a/README.md b/README.md index c33506d..de29c56 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ Your renamed *WinSW.exe* binary also accepts the following commands: * `Started` to indicate the service is currently running * `Stopped` to indicate that the service is installed but not currently running. +Most commands require Administrator privileges to execute. Since v2.8, WinSW will prompt for UAC in non-elevated sessions. + ## Documentation User documentation: diff --git a/src/Core/ServiceWrapper/Main.cs b/src/Core/ServiceWrapper/Main.cs index 8003e2c..e58fa5b 100644 --- a/src/Core/ServiceWrapper/Main.cs +++ b/src/Core/ServiceWrapper/Main.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using System.Runtime.InteropServices; using System.Security.AccessControl; +using System.Security.Principal; using System.ServiceProcess; using System.Text; using System.Text.RegularExpressions; @@ -589,6 +591,26 @@ namespace winsw args = args.GetRange(2, args.Count - 2); } + bool elevated; + if (args[0] == "/elevated") + { + elevated = true; + + _ = SigIntHelper.FreeConsole(); + _ = SigIntHelper.AttachConsole(SigIntHelper.ATTACH_PARENT_PROCESS); + + args = args.GetRange(1, args.Count - 1); + } + else if (Environment.OSVersion.Version.Major == 5) + { + // Windows XP + elevated = true; + } + else + { + elevated = IsProcessElevated(); + } + switch (args[0].ToLower()) { case "install": @@ -647,6 +669,12 @@ namespace winsw void Install() { + if (!elevated) + { + Elevate(); + return; + } + Log.Info("Installing the service with id '" + descriptor.Id + "'"); // Check if the service exists @@ -738,6 +766,12 @@ namespace winsw void Uninstall() { + if (!elevated) + { + Elevate(); + return; + } + Log.Info("Uninstalling the service with id '" + descriptor.Id + "'"); if (s is null) { @@ -777,6 +811,12 @@ namespace winsw void Start() { + if (!elevated) + { + Elevate(); + return; + } + Log.Info("Starting the service with id '" + descriptor.Id + "'"); if (s is null) ThrowNoSuchService(); @@ -800,6 +840,12 @@ namespace winsw void Stop() { + if (!elevated) + { + Elevate(); + return; + } + Log.Info("Stopping the service with id '" + descriptor.Id + "'"); if (s is null) ThrowNoSuchService(); @@ -823,6 +869,12 @@ namespace winsw void Restart() { + if (!elevated) + { + Elevate(); + return; + } + Log.Info("Restarting the service with id '" + descriptor.Id + "'"); if (s is null) ThrowNoSuchService(); @@ -841,6 +893,11 @@ namespace winsw void RestartSelf() { + if (!elevated) + { + throw new UnauthorizedAccessException("Access is denied."); + } + Log.Info("Restarting the service with id '" + descriptor.Id + "'"); // run restart from another process group. see README.md for why this is useful. @@ -865,6 +922,12 @@ namespace winsw void Test() { + if (!elevated) + { + Elevate(); + return; + } + WrapperService wsvc = new WrapperService(descriptor); wsvc.OnStart(args.ToArray()); Thread.Sleep(1000); @@ -873,12 +936,52 @@ namespace winsw void TestWait() { + if (!elevated) + { + Elevate(); + return; + } + WrapperService wsvc = new WrapperService(descriptor); wsvc.OnStart(args.ToArray()); Console.WriteLine("Press any key to stop the service..."); Console.Read(); wsvc.OnStop(); } + + // [DoesNotReturn] + void Elevate() + { + using Process current = Process.GetCurrentProcess(); + + ProcessStartInfo startInfo = new ProcessStartInfo + { + UseShellExecute = true, + Verb = "runas", + FileName = current.MainModule.FileName, +#if NETCOREAPP + Arguments = "/elevated " + string.Join(' ', args), +#elif !NET20 + Arguments = "/elevated " + string.Join(" ", args), +#else + Arguments = "/elevated " + string.Join(" ", args.ToArray()), +#endif + WindowStyle = ProcessWindowStyle.Hidden, + }; + + try + { + using Process elevated = Process.Start(startInfo); + + elevated.WaitForExit(); + Environment.Exit(elevated.ExitCode); + } + catch (Win32Exception e) when (e.NativeErrorCode == Errors.ERROR_CANCELLED) + { + Log.Fatal(e.Message); + Environment.Exit(e.ErrorCode); + } + } } private static void InitLoggers(ServiceDescriptor d, bool enableCLILogging) @@ -941,6 +1044,40 @@ namespace winsw appenders.ToArray()); } + private static unsafe bool IsProcessElevated() + { + IntPtr process = Kernel32.GetCurrentProcess(); + if (!Advapi32.OpenProcessToken(process, TokenAccessLevels.Read, out IntPtr token)) + { + ThrowWin32Exception("Failed to open process token."); + } + + try + { + if (!Advapi32.GetTokenInformation( + token, + TOKEN_INFORMATION_CLASS.TokenElevation, + out TOKEN_ELEVATION elevation, + sizeof(TOKEN_ELEVATION), + out _)) + { + ThrowWin32Exception("Failed to get token information"); + } + + return elevation.TokenIsElevated != 0; + } + finally + { + _ = Kernel32.CloseHandle(token); + } + + static void ThrowWin32Exception(string message) + { + Win32Exception inner = new Win32Exception(); + throw new Win32Exception(inner.NativeErrorCode, message + ' ' + inner.Message); + } + } + private static string ReadPassword() { StringBuilder buf = new StringBuilder(); diff --git a/src/Core/ServiceWrapper/winsw.csproj b/src/Core/ServiceWrapper/winsw.csproj index e90deb5..071de8f 100644 --- a/src/Core/ServiceWrapper/winsw.csproj +++ b/src/Core/ServiceWrapper/winsw.csproj @@ -5,6 +5,7 @@ net20;net40;net461;netcoreapp3.1 latest enable + true Windows Service Wrapper Allows arbitrary process to run as a Windows service by wrapping it. diff --git a/src/Core/WinSWCore/Native/Advapi32.cs b/src/Core/WinSWCore/Native/Advapi32.cs index 6604f33..6c87fdd 100755 --- a/src/Core/WinSWCore/Native/Advapi32.cs +++ b/src/Core/WinSWCore/Native/Advapi32.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Runtime.InteropServices; using System.Security.AccessControl; +using System.Security.Principal; using System.Text; // ReSharper disable InconsistentNaming @@ -321,6 +322,20 @@ namespace winsw.Native [DllImport(Advapi32LibraryName, SetLastError = false)] internal static extern uint LsaNtStatusToWinError(uint status); + + [DllImport(Advapi32LibraryName, SetLastError = true)] + public static extern bool OpenProcessToken( + IntPtr ProcessHandle, + TokenAccessLevels DesiredAccess, + out IntPtr TokenHandle); + + [DllImport(Advapi32LibraryName, SetLastError = true)] + public static extern bool GetTokenInformation( + IntPtr TokenHandle, + TOKEN_INFORMATION_CLASS TokenInformationClass, + out TOKEN_ELEVATION TokenInformation, + int TokenInformationLength, + out int ReturnLength); } // http://msdn.microsoft.com/en-us/library/windows/desktop/bb545671(v=vs.85).aspx @@ -603,4 +618,14 @@ namespace winsw.Native { public string lpDescription; } + + public enum TOKEN_INFORMATION_CLASS + { + TokenElevation = 20, + } + + public struct TOKEN_ELEVATION + { + public uint TokenIsElevated; + } } diff --git a/src/Core/WinSWCore/Native/Errors.cs b/src/Core/WinSWCore/Native/Errors.cs new file mode 100644 index 0000000..2dfe908 --- /dev/null +++ b/src/Core/WinSWCore/Native/Errors.cs @@ -0,0 +1,7 @@ +namespace winsw.Native +{ + public static class Errors + { + public const int ERROR_CANCELLED = 1223; + } +} diff --git a/src/Core/WinSWCore/Native/Kernel32.cs b/src/Core/WinSWCore/Native/Kernel32.cs index 32528c1..bd93b91 100755 --- a/src/Core/WinSWCore/Native/Kernel32.cs +++ b/src/Core/WinSWCore/Native/Kernel32.cs @@ -28,6 +28,12 @@ namespace winsw.Native string? lpCurrentDirectory, in STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); + + [DllImport(Kernel32LibraryName)] + public static extern IntPtr GetCurrentProcess(); + + [DllImport(Kernel32LibraryName)] + public static extern bool CloseHandle(IntPtr hObject); } [StructLayout(LayoutKind.Sequential)] diff --git a/src/Core/WinSWCore/Util/SigIntHelper.cs b/src/Core/WinSWCore/Util/SigIntHelper.cs index b8ede86..558cad1 100644 --- a/src/Core/WinSWCore/Util/SigIntHelper.cs +++ b/src/Core/WinSWCore/Util/SigIntHelper.cs @@ -9,13 +9,15 @@ namespace winsw.Util { private static readonly ILog Logger = LogManager.GetLogger(typeof(SigIntHelper)); + public const int ATTACH_PARENT_PROCESS = -1; + private const string Kernel32LibraryName = "kernel32.dll"; [DllImport(Kernel32LibraryName, SetLastError = true)] - private static extern bool AttachConsole(uint dwProcessId); + public static extern bool AttachConsole(int dwProcessId); [DllImport(Kernel32LibraryName, SetLastError = true)] - private static extern bool FreeConsole(); + public static extern bool FreeConsole(); [DllImport(Kernel32LibraryName)] private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate? HandlerRoutine, bool Add); @@ -44,7 +46,7 @@ namespace winsw.Util /// True if the process shut down successfully to the SIGINT, false if it did not. public static bool SendSIGINTToProcess(Process process, TimeSpan shutdownTimeout) { - if (AttachConsole((uint)process.Id)) + if (AttachConsole(process.Id)) { // Disable Ctrl-C handling for our program _ = SetConsoleCtrlHandler(null, true);