Skip to content

Instantly share code, notes, and snippets.

@smourier
Created January 6, 2024 18:54
Show Gist options
  • Save smourier/70cc9208b47534ba475279a4554180ce to your computer and use it in GitHub Desktop.
Save smourier/70cc9208b47534ba475279a4554180ce to your computer and use it in GitHub Desktop.
public static void RunAsDesktopUser(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
throw new ArgumentException("Value cannot be null or whitespace.", nameof(fileName));
// To start process as shell user you will need to carry out these steps:
// 1. Enable the SeIncreaseQuotaPrivilege in your current token
// 2. Get an HWND representing the desktop shell (GetShellWindow)
// 3. Get the Process ID(PID) of the process associated with that window(GetWindowThreadProcessId)
// 4. Open that process(OpenProcess)
// 5. Get the access token from that process (OpenProcessToken)
// 6. Make a primary token with that token(DuplicateTokenEx)
// 7. Start the new process with that primary token(CreateProcessWithTokenW)
var hProcessToken = IntPtr.Zero;
// Enable SeIncreaseQuotaPrivilege in this process. (This won't work if current process is not elevated.)
try
{
var process = GetCurrentProcess();
if (!OpenProcessToken(process, 0x0020, ref hProcessToken))
return;
var tkp = new TOKEN_PRIVILEGES
{
PrivilegeCount = 1,
Privileges = new LUID_AND_ATTRIBUTES[1]
};
if (!LookupPrivilegeValue(null, "SeIncreaseQuotaPrivilege", ref tkp.Privileges[0].Luid))
return;
tkp.Privileges[0].Attributes = 0x00000002;
if (!AdjustTokenPrivileges(hProcessToken, false, tkp, 0, IntPtr.Zero, IntPtr.Zero))
return;
}
finally
{
CloseHandle(hProcessToken);
}
// Get an HWND representing the desktop shell.
// CAVEATS: This will fail if the shell is not running (crashed or terminated), or the default shell has been
// replaced with a custom shell. This also won't return what you probably want if Explorer has been terminated and
// restarted elevated.
var hwnd = GetShellWindow();
if (hwnd == IntPtr.Zero)
return;
var hShellProcess = IntPtr.Zero;
var hShellProcessToken = IntPtr.Zero;
var hPrimaryToken = IntPtr.Zero;
try
{
// Get the PID of the desktop shell process.
uint dwPID;
if (GetWindowThreadProcessId(hwnd, out dwPID) == 0)
return;
// Open the desktop shell process in order to query it (get the token)
hShellProcess = OpenProcess(ProcessAccessFlags.QueryInformation, false, dwPID);
if (hShellProcess == IntPtr.Zero)
return;
// Get the process token of the desktop shell.
if (!OpenProcessToken(hShellProcess, 0x0002, ref hShellProcessToken))
return;
var dwTokenRights = 395U;
// Duplicate the shell's process token to get a primary token.
// Based on experimentation, this is the minimal set of rights required for CreateProcessWithTokenW (contrary to current documentation).
if (!DuplicateTokenEx(hShellProcessToken, dwTokenRights, IntPtr.Zero, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, out hPrimaryToken))
return;
// Start the target process with the new token.
var si = new STARTUPINFO();
var pi = new PROCESS_INFORMATION();
if (!CreateProcessWithTokenW(hPrimaryToken, 0, fileName, "", 0, IntPtr.Zero, Path.GetDirectoryName(fileName), ref si, out pi))
{
var gle = Marshal.GetLastWin32Error();
var msg = new Win32Exception(gle).Message;
MessageBox.Show(msg);
return;
}
}
finally
{
CloseHandle(hShellProcessToken);
CloseHandle(hPrimaryToken);
CloseHandle(hShellProcess);
}
}
#region Interop
[CustomMarshaller(typeof(TOKEN_PRIVILEGES), MarshalMode.ManagedToUnmanagedIn, typeof(TOKEN_PRIVILEGESMarshaller))]
private unsafe static class TOKEN_PRIVILEGESMarshaller
{
public struct Unmanaged
{
public uint PrivilegeCount;
public LUID_AND_ATTRIBUTES* Privileges;
}
public static Unmanaged ConvertToUnmanaged(TOKEN_PRIVILEGES managed)
{
var unmanaged = new Unmanaged
{
PrivilegeCount = managed.PrivilegeCount,
Privileges = ArrayMarshaller<LUID_AND_ATTRIBUTES, LUID_AND_ATTRIBUTES>.AllocateContainerForUnmanagedElements(managed.Privileges, out var count)
};
ArrayMarshaller<LUID_AND_ATTRIBUTES, LUID_AND_ATTRIBUTES>.GetManagedValuesSource(managed.Privileges)
.CopyTo(ArrayMarshaller<LUID_AND_ATTRIBUTES, LUID_AND_ATTRIBUTES>
.GetUnmanagedValuesDestination(unmanaged.Privileges, count));
return unmanaged;
}
public static void Free(Unmanaged unmanaged) => ArrayMarshaller<LUID_AND_ATTRIBUTES, LUID_AND_ATTRIBUTES>.Free(unmanaged.Privileges);
}
[NativeMarshalling(typeof(TOKEN_PRIVILEGESMarshaller))]
private struct TOKEN_PRIVILEGES
{
public uint PrivilegeCount;
//[MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] // you can keep that for compat reasons
public LUID_AND_ATTRIBUTES[] Privileges;
}
[StructLayout(LayoutKind.Sequential)]
private struct LUID_AND_ATTRIBUTES
{
public LUID Luid;
public uint Attributes;
}
[StructLayout(LayoutKind.Sequential)]
private struct LUID
{
public uint LowPart;
public int HighPart;
}
[Flags]
private enum ProcessAccessFlags : uint
{
All = 0x001F0FFF,
Terminate = 0x00000001,
CreateThread = 0x00000002,
VirtualMemoryOperation = 0x00000008,
VirtualMemoryRead = 0x00000010,
VirtualMemoryWrite = 0x00000020,
DuplicateHandle = 0x00000040,
CreateProcess = 0x000000080,
SetQuota = 0x00000100,
SetInformation = 0x00000200,
QueryInformation = 0x00000400,
QueryLimitedInformation = 0x00001000,
Synchronize = 0x00100000
}
private enum SECURITY_IMPERSONATION_LEVEL
{
SecurityAnonymous,
SecurityIdentification,
SecurityImpersonation,
SecurityDelegation
}
private enum TOKEN_TYPE
{
TokenPrimary = 1,
TokenImpersonation
}
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct STARTUPINFO
{
public int cb;
public IntPtr lpReserved;
public IntPtr lpDesktop;
public IntPtr lpTitle;
public int dwX;
public int dwY;
public int dwXSize;
public int dwYSize;
public int dwXCountChars;
public int dwYCountChars;
public int dwFillAttribute;
public int dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[LibraryImport("kernel32")]
private static partial IntPtr GetCurrentProcess();
[LibraryImport("advapi32", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool OpenProcessToken(IntPtr h, int acc, ref IntPtr phtok);
[LibraryImport("advapi32", SetLastError = true, StringMarshalling = StringMarshalling.Utf16, EntryPoint = "LookupPrivilegeValueW")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool LookupPrivilegeValue(string host, string name, ref LUID pluid);
[LibraryImport("advapi32", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool AdjustTokenPrivileges(IntPtr htok, [MarshalAs(UnmanagedType.Bool)] bool disall, TOKEN_PRIVILEGES newst, int len, IntPtr prev, IntPtr relen);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool CloseHandle(IntPtr hObject);
[LibraryImport("user32.dll")]
private static partial IntPtr GetShellWindow();
[LibraryImport("user32.dll", SetLastError = true)]
private static partial uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial IntPtr OpenProcess(ProcessAccessFlags processAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint processId);
[LibraryImport("advapi32", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess, IntPtr lpTokenAttributes, SECURITY_IMPERSONATION_LEVEL impersonationLevel, TOKEN_TYPE tokenType, out IntPtr phNewToken);
[LibraryImport("advapi32", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool CreateProcessWithTokenW(IntPtr hToken, int dwLogonFlags, string lpApplicationName, string lpCommandLine, int dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment