Skip to content

Commit 1a8334f

Browse files
committed
Add RunAsDesktopUser helper method
1 parent 2c1f161 commit 1a8334f

File tree

2 files changed

+151
-0
lines changed

2 files changed

+151
-0
lines changed

Flow.Launcher.Infrastructure/NativeMethods.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,18 @@ LOCALE_TRANSIENT_KEYBOARD4
6666
SHParseDisplayName
6767
SHOpenFolderAndSelectItems
6868
CoTaskMemFree
69+
70+
OpenProcessToken
71+
GetCurrentProcess
72+
LookupPrivilegeValue
73+
SE_INCREASE_QUOTA_NAME
74+
CloseHandle
75+
TOKEN_PRIVILEGES
76+
AdjustTokenPrivileges
77+
GetShellWindow
78+
GetWindowThreadProcessId
79+
OpenProcess
80+
GetProcessId
81+
DuplicateTokenEx
82+
CreateProcessWithTokenW
83+
STARTUPINFO

Flow.Launcher.Infrastructure/Win32Helper.cs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
using Windows.Win32;
1919
using Windows.Win32.Foundation;
2020
using Windows.Win32.Graphics.Dwm;
21+
using Windows.Win32.Security;
22+
using Windows.Win32.System.Threading;
2123
using Windows.Win32.UI.Input.KeyboardAndMouse;
2224
using Windows.Win32.UI.Shell.Common;
2325
using Windows.Win32.UI.WindowsAndMessaging;
@@ -802,6 +804,140 @@ public static bool IsAdministrator()
802804
return principal.IsInRole(WindowsBuiltInRole.Administrator);
803805
}
804806

807+
/// <summary>
808+
/// Inspired by <see href="https://github.com/jay/RunAsDesktopUser">
809+
/// Document: <see href="https://learn.microsoft.com/en-us/archive/blogs/aaron_margosis/faq-how-do-i-start-a-program-as-the-desktop-user-from-an-elevated-app">
810+
/// </summary>
811+
public static unsafe bool RunAsDesktopUser(string app, string cmdLine, string currentDir, out string errorInfo)
812+
{
813+
STARTUPINFOW si = new();
814+
PROCESS_INFORMATION pi = new();
815+
errorInfo = string.Empty;
816+
HANDLE hShellProcess = HANDLE.Null, hShellProcessToken = HANDLE.Null, hPrimaryToken = HANDLE.Null;
817+
HWND hwnd;
818+
uint dwPID;
819+
820+
// 1. Enable the SeIncreaseQuotaPrivilege in your current token
821+
if (!PInvoke.OpenProcessToken(PInvoke.GetCurrentProcess_SafeHandle(), TOKEN_ACCESS_MASK.TOKEN_ADJUST_PRIVILEGES, out var hProcessToken))
822+
{
823+
errorInfo = $"OpenProcessToken failed: {Marshal.GetLastWin32Error()}";
824+
return false;
825+
}
826+
827+
if (!PInvoke.LookupPrivilegeValue(null, PInvoke.SE_INCREASE_QUOTA_NAME, out var luid))
828+
{
829+
errorInfo = $"LookupPrivilegeValue failed: {Marshal.GetLastWin32Error()}";
830+
hProcessToken.Dispose();//PInvoke.CloseHandle(hProcessToken);
831+
return false;
832+
}
833+
834+
var tp = new TOKEN_PRIVILEGES
835+
{
836+
PrivilegeCount = 1,
837+
Privileges = new()
838+
{
839+
e0 = new LUID_AND_ATTRIBUTES
840+
{
841+
Luid = luid,
842+
Attributes = TOKEN_PRIVILEGES_ATTRIBUTES.SE_PRIVILEGE_ENABLED
843+
}
844+
}
845+
};
846+
847+
PInvoke.AdjustTokenPrivileges(hProcessToken, false, &tp, 0, null, null);
848+
var lastError = Marshal.GetLastWin32Error();
849+
hProcessToken.Dispose();//PInvoke.CloseHandle(hProcessToken);
850+
851+
if (lastError != 0)
852+
{
853+
errorInfo = $"AdjustTokenPrivileges failed: {lastError}";
854+
return false;
855+
}
856+
857+
retry:
858+
// 2. Get an HWND representing the desktop shell
859+
hwnd = PInvoke.GetShellWindow();
860+
if (hwnd == HWND.Null)
861+
{
862+
errorInfo = "No desktop shell is present.";
863+
return false;
864+
}
865+
866+
// 3. Get the Process ID (PID) of the process associated with that window
867+
_ = PInvoke.GetWindowThreadProcessId(hwnd, &dwPID);
868+
if (dwPID == 0)
869+
{
870+
errorInfo = "Unable to get PID of desktop shell.";
871+
return false;
872+
}
873+
874+
// 4. Open that process
875+
hShellProcess = PInvoke.OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_INFORMATION, false, dwPID);
876+
if (hShellProcess == IntPtr.Zero)
877+
{
878+
errorInfo = $"Can't open desktop shell process: {Marshal.GetLastWin32Error()}";
879+
return false;
880+
}
881+
882+
if (hwnd != PInvoke.GetShellWindow())
883+
{
884+
PInvoke.CloseHandle(hShellProcess);
885+
goto retry;
886+
}
887+
888+
_ = PInvoke.GetWindowThreadProcessId(hwnd, &dwPID);
889+
if (dwPID != PInvoke.GetProcessId(hShellProcess))
890+
{
891+
PInvoke.CloseHandle(hShellProcess);
892+
goto retry;
893+
}
894+
895+
// 5. Get the access token from that process
896+
if (!PInvoke.OpenProcessToken(hShellProcess, TOKEN_ACCESS_MASK.TOKEN_DUPLICATE, &hShellProcessToken))
897+
{
898+
errorInfo = $"Can't get process token of desktop shell: {Marshal.GetLastWin32Error()}";
899+
goto cleanup;
900+
}
901+
902+
// 6. Make a primary token with that token
903+
var tokenRights = TOKEN_ACCESS_MASK.TOKEN_QUERY | TOKEN_ACCESS_MASK.TOKEN_ASSIGN_PRIMARY |
904+
TOKEN_ACCESS_MASK.TOKEN_DUPLICATE | TOKEN_ACCESS_MASK.TOKEN_ADJUST_DEFAULT |
905+
TOKEN_ACCESS_MASK.TOKEN_ADJUST_SESSIONID;
906+
if (!PInvoke.DuplicateTokenEx(hShellProcessToken, tokenRights, null, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, &hPrimaryToken))
907+
{
908+
errorInfo = $"Can't get primary token: {Marshal.GetLastWin32Error()}";
909+
goto cleanup;
910+
}
911+
912+
// 7. Start the new process with that primary token
913+
fixed (char* appPtr = app)
914+
fixed (char* cmdLinePtr = cmdLine)
915+
fixed (char* currentDirPtr = currentDir)
916+
{
917+
if (!PInvoke.CreateProcessWithToken(hPrimaryToken,
918+
0 /*CREATE_PROCESS_LOGON_FLAGS.LOGON_WITH_PROFILE*/,
919+
appPtr,
920+
cmdLinePtr,
921+
0/*PROCESS_CREATION_FLAGS.CREATE_NEW_CONSOLE*/,
922+
null,
923+
currentDirPtr,
924+
&si,
925+
&pi))
926+
{
927+
errorInfo = $"CreateProcessWithTokenW failed: {Marshal.GetLastWin32Error()}";
928+
goto cleanup;
929+
}
930+
}
931+
932+
return true;
933+
934+
cleanup:
935+
if (hShellProcessToken != HANDLE.Null) PInvoke.CloseHandle(hShellProcessToken);
936+
if (hPrimaryToken != HANDLE.Null) PInvoke.CloseHandle(hPrimaryToken);
937+
if (hShellProcess != HANDLE.Null) PInvoke.CloseHandle(hShellProcess);
938+
return false;
939+
}
940+
805941
#endregion
806942
}
807943
}

0 commit comments

Comments
 (0)