diff --git a/AppWithPrivileges/App.xaml b/AppWithPrivileges/App.xaml new file mode 100644 index 0000000..7011b70 --- /dev/null +++ b/AppWithPrivileges/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/AppWithPrivileges/App.xaml.cs b/AppWithPrivileges/App.xaml.cs new file mode 100644 index 0000000..c38f72a --- /dev/null +++ b/AppWithPrivileges/App.xaml.cs @@ -0,0 +1,14 @@ +using System.Configuration; +using System.Data; +using System.Windows; + +namespace AppWithPrivileges +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } + +} diff --git a/AppWithPrivileges/AppWithPrivileges.csproj b/AppWithPrivileges/AppWithPrivileges.csproj new file mode 100644 index 0000000..ae10916 --- /dev/null +++ b/AppWithPrivileges/AppWithPrivileges.csproj @@ -0,0 +1,12 @@ + + + + WinExe + net6.0-windows + enable + enable + true + app.manifest + + + diff --git a/AppWithPrivileges/AssemblyInfo.cs b/AppWithPrivileges/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/AppWithPrivileges/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/AppWithPrivileges/MainWindow.xaml b/AppWithPrivileges/MainWindow.xaml new file mode 100644 index 0000000..48f4a00 --- /dev/null +++ b/AppWithPrivileges/MainWindow.xaml @@ -0,0 +1,15 @@ + + + + + diff --git a/AppWithPrivileges/MainWindow.xaml.cs b/AppWithPrivileges/MainWindow.xaml.cs new file mode 100644 index 0000000..8f14c53 --- /dev/null +++ b/AppWithPrivileges/MainWindow.xaml.cs @@ -0,0 +1,24 @@ +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace AppWithPrivileges +{ + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class MainWindow : Window + { + public MainWindow() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/AppWithPrivileges/app.manifest b/AppWithPrivileges/app.manifest new file mode 100644 index 0000000..438ee0d --- /dev/null +++ b/AppWithPrivileges/app.manifest @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DemoModernService/DemoModernService.cs b/DemoModernService/DemoModernService.cs index c30d601..649ce1e 100644 --- a/DemoModernService/DemoModernService.cs +++ b/DemoModernService/DemoModernService.cs @@ -1,6 +1,7 @@  using Microsoft.Extensions.Hosting; using murrayju.ProcessExtensions; +using System.Diagnostics; namespace DemoModernService; @@ -8,6 +9,12 @@ internal class DemoModernService : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - ProcessExtensions.StartProcessAsCurrentUser("calc.exe"); + string appPath = "AppWithPrivileges.exe"; + + string solutionDirectory = Directory.GetParent(AppDomain.CurrentDomain.BaseDirectory).Parent.Parent.Parent.Parent.FullName; + string targetProjectName = Path.GetFileNameWithoutExtension(appPath); + string workDir = Path.Combine(solutionDirectory, targetProjectName, "bin", "Debug", "net6.0-windows"); + + ProcessExtensions.LaunchProcess(appPath: appPath, workDir: workDir); } } \ No newline at end of file diff --git a/DemoModernService/DemoModernService.csproj b/DemoModernService/DemoModernService.csproj index fda8c1f..8d21618 100644 --- a/DemoModernService/DemoModernService.csproj +++ b/DemoModernService/DemoModernService.csproj @@ -8,7 +8,7 @@ - + diff --git a/DemoService/App.config b/DemoService/App.config index aee9adf..4bfa005 100644 --- a/DemoService/App.config +++ b/DemoService/App.config @@ -1,6 +1,6 @@ - + - + - \ No newline at end of file + diff --git a/DemoService/DemoService.cs b/DemoService/DemoService.cs index f8c9174..0478175 100644 --- a/DemoService/DemoService.cs +++ b/DemoService/DemoService.cs @@ -12,7 +12,7 @@ public DemoService() protected override void OnStart(string[] args) { - ProcessExtensions.StartProcessAsCurrentUser("calc.exe"); + ProcessExtensions.LaunchProcess("calc.exe"); } protected override void OnStop() diff --git a/DemoService/DemoService.csproj b/DemoService/DemoService.csproj index d1f0ec5..1bf7a06 100644 --- a/DemoService/DemoService.csproj +++ b/DemoService/DemoService.csproj @@ -8,10 +8,11 @@ Exe demo DemoService - v4.8.1 + v4.8 512 true true + AnyCPU @@ -38,6 +39,9 @@ ..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8.1\System.Configuration.Install.dll + + ..\packages\System.Security.Principal.Windows.5.0.0\lib\net461\System.Security.Principal.Windows.dll + ..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8.1\System.ServiceProcess.dll @@ -68,6 +72,7 @@ Always + diff --git a/DemoService/packages.config b/DemoService/packages.config new file mode 100644 index 0000000..5e8760a --- /dev/null +++ b/DemoService/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ProcessExtensions/ProcessExtensions.cs b/ProcessExtensions/ProcessExtensions.cs index e7dcc65..3eed107 100644 --- a/ProcessExtensions/ProcessExtensions.cs +++ b/ProcessExtensions/ProcessExtensions.cs @@ -1,6 +1,7 @@ -using System; -using System.IO; +using System.IO; +using System; using System.Runtime.InteropServices; +using System.Security.Principal; namespace murrayju.ProcessExtensions { @@ -21,7 +22,12 @@ public static class ProcessExtensions private const uint INVALID_SESSION_ID = 0xFFFFFFFF; private static readonly IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero; private const int STARTF_USESHOWWINDOW = 0x00000001; - + + private const uint TOKEN_ALL_ACCESS = 0xF01FF; + + private static IntPtr executionToken = IntPtr.Zero; + public static uint activeSessionId = INVALID_SESSION_ID; + #endregion #region DllImports @@ -47,7 +53,14 @@ private static extern bool DuplicateTokenEx( IntPtr lpThreadAttributes, int TokenType, int ImpersonationLevel, - ref IntPtr DuplicateTokenHandle); + out IntPtr DuplicateTokenHandle); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool SetTokenInformation( + IntPtr tokenHandle, + TokenInformationClass tokenInformationClass, + ref int tokenInformation, + int tokenInformationLength); [DllImport("userenv.dll", SetLastError = true)] private static extern bool CreateEnvironmentBlock(ref IntPtr lpEnvironment, IntPtr hToken, bool bInherit); @@ -62,8 +75,6 @@ private static extern bool DuplicateTokenEx( [DllImport("kernel32.dll")] private static extern uint WTSGetActiveConsoleSessionId(); - [DllImport("Wtsapi32.dll")] - private static extern uint WTSQueryUserToken(uint SessionId, ref IntPtr phToken); [DllImport("wtsapi32.dll", SetLastError = true)] private static extern int WTSEnumerateSessions( @@ -158,6 +169,11 @@ private enum TOKEN_TYPE TokenImpersonation = 2 } + private enum TokenInformationClass + { + TokenSessionId = 12 + } + [StructLayout(LayoutKind.Sequential)] private struct WTS_SESSION_INFO { @@ -171,12 +187,13 @@ private struct WTS_SESSION_INFO #endregion + // Gets the user token from the currently active session - private static bool GetSessionUserToken(ref IntPtr phUserToken) + private static bool GetSessionUserToken(ref IntPtr userToken, bool asAdmin) { var bResult = false; var hImpersonationToken = IntPtr.Zero; - var activeSessionId = INVALID_SESSION_ID; + var pSessionInfo = IntPtr.Zero; var sessionCount = 0; @@ -206,42 +223,61 @@ private static bool GetSessionUserToken(ref IntPtr phUserToken) activeSessionId = WTSGetActiveConsoleSessionId(); } - if (WTSQueryUserToken(activeSessionId, ref hImpersonationToken) != 0) + // Convert the execution token to a primary token + if(asAdmin) { - // Convert the impersonation token to a primary token - bResult = DuplicateTokenEx(hImpersonationToken, 0, IntPtr.Zero, - (int)SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, (int)TOKEN_TYPE.TokenPrimary, - ref phUserToken); - - CloseHandle(hImpersonationToken); + bResult = DuplicateTokenEx(userToken, ((uint)TokenAccessLevels.AllAccess), IntPtr.Zero, + (int)TokenImpersonationLevel.Impersonation, (int)TOKEN_TYPE.TokenPrimary, + out executionToken); } + else + { + bResult = DuplicateTokenEx(userToken, 0, IntPtr.Zero, + (int)TokenImpersonationLevel.Impersonation, (int)TOKEN_TYPE.TokenPrimary, + out executionToken); + } + + CloseHandle(userToken); return bResult; } - public static bool StartProcessAsCurrentUser(string appPath, string cmdLine = null, string workDir = null, bool visible = true) + public static bool LaunchProcess(string appPath, string cmdLine = null, string workDir = null, bool visible = true) { - var hUserToken = IntPtr.Zero; + var hUserToken = WindowsIdentity.GetCurrent().Token; var startInfo = new STARTUPINFO(); var procInfo = new PROCESS_INFORMATION(); var pEnv = IntPtr.Zero; int iResultOfCreateProcessAsUser; + bool systemAccount = WindowsIdentity.GetCurrent().IsSystem; + + startInfo.cb = Marshal.SizeOf(typeof(STARTUPINFO)); try { - if (!GetSessionUserToken(ref hUserToken)) + //workDir = Path.GetDirectoryName(appPath); + if (!GetSessionUserToken(ref hUserToken, systemAccount)) { throw new ProcessCreationException("StartProcessAsCurrentUser: GetSessionUserToken failed."); } + int sessionId = (int)activeSessionId; + if (systemAccount && !SetTokenInformation(executionToken, TokenInformationClass.TokenSessionId, ref sessionId, sizeof(int))) + { + throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), "Failed to set token information."); + } + + PROCESS_INFORMATION processInfo = new PROCESS_INFORMATION(); + + // Allow the app to be interactive with the desktop uint dwCreationFlags = CREATE_UNICODE_ENVIRONMENT | (uint)(visible ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW); startInfo.dwFlags = STARTF_USESHOWWINDOW; startInfo.wShowWindow = (short)(visible ? SW.SW_SHOW : SW.SW_HIDE); startInfo.lpDesktop = "winsta0\\default"; - if (!CreateEnvironmentBlock(ref pEnv, hUserToken, false)) + if (!CreateEnvironmentBlock(ref pEnv, executionToken, false)) { throw new ProcessCreationException("StartProcessAsCurrentUser: CreateEnvironmentBlock failed."); } @@ -251,7 +287,7 @@ public static bool StartProcessAsCurrentUser(string appPath, string cmdLine = nu Directory.SetCurrentDirectory(workDir); } - if (!CreateProcessAsUser(hUserToken, + if (!CreateProcessAsUser(executionToken, appPath, // Application Name cmdLine, // Command Line IntPtr.Zero, @@ -271,7 +307,7 @@ public static bool StartProcessAsCurrentUser(string appPath, string cmdLine = nu } finally { - CloseHandle(hUserToken); + CloseHandle(executionToken); if (pEnv != IntPtr.Zero) { DestroyEnvironmentBlock(pEnv); @@ -284,4 +320,6 @@ public static bool StartProcessAsCurrentUser(string appPath, string cmdLine = nu } } -} + + +} \ No newline at end of file diff --git a/ProcessExtensions/ProcessExtensions.csproj b/ProcessExtensions/ProcessExtensions.csproj index dbdcea4..77a6280 100644 --- a/ProcessExtensions/ProcessExtensions.csproj +++ b/ProcessExtensions/ProcessExtensions.csproj @@ -4,4 +4,9 @@ netstandard2.0 + + + + + diff --git a/README.md b/README.md index 07d9bc9..9f96a0a 100644 --- a/README.md +++ b/README.md @@ -37,3 +37,5 @@ Similarly, the `DemoModernService` project uses .NET 8.0, and a build will copy For either version, CD to the bin directory and run `createService` to install and start the service. It will launch `calc.exe` as soon as it starts. After that, run `deleteService` to stop and uninstall the service. +## Major Addition +Now you can run apps that require administrative privileges using the SetTokenInformation which can be used to update the session Id in which the app runs and hence show the app UI, note that you need to set the workDir parameter to the working directory which contains the app itself. See [this stack overflow answer](https://stackoverflow.com/questions/33212984/createprocessasuser-with-elevated-privileges) diff --git a/solution.sln b/solution.sln index 1877f69..7026a8a 100644 --- a/solution.sln +++ b/solution.sln @@ -14,6 +14,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppWithPrivileges", "AppWithPrivileges\AppWithPrivileges.csproj", "{FB499E9E-A4E8-4201-9FD9-E70A0C80A9DD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,6 +34,10 @@ Global {75EE3B51-5A96-473C-9E68-C36B728A6E2B}.Debug|Any CPU.Build.0 = Debug|Any CPU {75EE3B51-5A96-473C-9E68-C36B728A6E2B}.Release|Any CPU.ActiveCfg = Release|Any CPU {75EE3B51-5A96-473C-9E68-C36B728A6E2B}.Release|Any CPU.Build.0 = Release|Any CPU + {FB499E9E-A4E8-4201-9FD9-E70A0C80A9DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB499E9E-A4E8-4201-9FD9-E70A0C80A9DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB499E9E-A4E8-4201-9FD9-E70A0C80A9DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB499E9E-A4E8-4201-9FD9-E70A0C80A9DD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE