Skip to content

Commit

Permalink
[Core/RemoteClient] Allow creation of MTGO process from services
Browse files Browse the repository at this point in the history
This requires enabling 'Replace a process level token' for the user account running this process. Refer to the below resource for more info:
https://learn.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/replace-a-process-level-token
  • Loading branch information
Qonfused committed Jan 3, 2024
1 parent dba2ca9 commit d834fae
Show file tree
Hide file tree
Showing 2 changed files with 286 additions and 7 deletions.
253 changes: 253 additions & 0 deletions MTGOSDK.Win32/src/Utilities/ProcessUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
/** @file
Copyright (c) 2023, Cory Bennett. All rights reserved.
SPDX-License-Identifier: Apache-2.0
**/

using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;


namespace MTGOSDK.Win32.Utilities;

public static class ProcessUtilities
{
public static Process? RunAsDesktopUser(string fileName, string arguments, bool hideWindow = false)

Check warning on line 16 in MTGOSDK.Win32/src/Utilities/ProcessUtilities.cs

View workflow job for this annotation

GitHub Actions / MSBuild Runner

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
//
// Enable SeIncreaseQuotaPrivilege in this process.
//
IntPtr hProcessToken = IntPtr.Zero;
try
{
IntPtr process = GetCurrentProcess();
if (!OpenProcessToken(process, 0x0020, ref hProcessToken))
return null;

TOKEN_PRIVILEGES tkp = new()
{
PrivilegeCount = 1,
Privileges = new LUID_AND_ATTRIBUTES[1]
};

if (!LookupPrivilegeValue(null, "SeIncreaseQuotaPrivilege", ref tkp.Privileges[0].Luid))
return null;

tkp.Privileges[0].Attributes = 0x00000002;

if (!AdjustTokenPrivileges(hProcessToken, false, ref tkp, 0, IntPtr.Zero, IntPtr.Zero))
return null;
}
finally
{
CloseHandle(hProcessToken);
}

//
// Get an HWND representing the desktop shell.
//
// This will fail if the shell is not running (crashed or terminated), or
// if the default shell has been replaced with a custom shell.
//
IntPtr hwnd = GetShellWindow();
if (hwnd == IntPtr.Zero)
return null;

IntPtr hShellProcess = IntPtr.Zero;
IntPtr hShellProcessToken = IntPtr.Zero;
IntPtr hPrimaryToken = IntPtr.Zero;
try
{
// Get the PID of the desktop shell process.
uint dwPID;
if (GetWindowThreadProcessId(hwnd, out dwPID) == 0)
return null;

// Open the desktop shell process in order to query it (get the token)
hShellProcess = OpenProcess(ProcessAccessFlags.QueryInformation, false, dwPID);
if (hShellProcess == IntPtr.Zero)
return null;

// Get the process token of the desktop shell.
if (!OpenProcessToken(hShellProcess, 0x0002, ref hShellProcessToken))
return null;

uint 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 null;

// Start the target process with the new token.
STARTUPINFO si = new();
if (hideWindow)
{
si.dwFlags = 0x00000001;
si.wShowWindow = 0;
}

PROCESS_INFORMATION pi = new();
if (!CreateProcessWithTokenW(
hPrimaryToken,
0,
fileName,
$"\"{fileName}\" {arguments}",
0,
IntPtr.Zero,
Path.GetDirectoryName(fileName)!,
ref si,
out pi
))
{
// Get the last error and display it.
int error = Marshal.GetLastWin32Error();
return null;
}

return Process.GetProcessById(pi.dwProcessId);
}
finally
{
CloseHandle(hShellProcessToken);
CloseHandle(hPrimaryToken);
CloseHandle(hShellProcess);
}
}

#region Win32 Interop

private struct TOKEN_PRIVILEGES
{
public uint PrivilegeCount;

[MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
public LUID_AND_ATTRIBUTES[] Privileges;
}

[StructLayout(LayoutKind.Sequential, Pack = 4)]
private struct LUID_AND_ATTRIBUTES
{
public LUID Luid;
public uint Attributes;
}

[StructLayout(LayoutKind.Sequential)]
private struct LUID
{
public readonly uint LowPart;
public readonly 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 readonly IntPtr hProcess;
public readonly IntPtr hThread;
public readonly int dwProcessId;
public readonly int dwThreadId;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct STARTUPINFO
{
public readonly int cb;
public readonly string lpReserved;
public readonly string lpDesktop;
public readonly string lpTitle;
public readonly int dwX;
public readonly int dwY;
public readonly int dwXSize;
public readonly int dwYSize;
public readonly int dwXCountChars;
public readonly int dwYCountChars;
public readonly int dwFillAttribute;
public int dwFlags;
public short wShowWindow;
public readonly short cbReserved2;
public readonly IntPtr lpReserved2;
public readonly IntPtr hStdInput;
public readonly IntPtr hStdOutput;
public readonly IntPtr hStdError;
}

[DllImport("kernel32.dll", ExactSpelling = true)]
private static extern IntPtr GetCurrentProcess();

[DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
private static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr phtok);

[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool LookupPrivilegeValue(string? host, string name, ref LUID pluid);

Check warning on line 225 in MTGOSDK.Win32/src/Utilities/ProcessUtilities.cs

View workflow job for this annotation

GitHub Actions / MSBuild Runner

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

[DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
private static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall, ref TOKEN_PRIVILEGES newst, int len, IntPtr prev, IntPtr relen);

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr hObject);


[DllImport("user32.dll")]
private static extern IntPtr GetShellWindow();

[DllImport("user32.dll", SetLastError = true)]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(ProcessAccessFlags processAccess, bool bInheritHandle, uint processId);

[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess, IntPtr lpTokenAttributes, SECURITY_IMPERSONATION_LEVEL impersonationLevel, TOKEN_TYPE tokenType,
out IntPtr phNewToken);

[DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool CreateProcessWithTokenW(IntPtr hToken, int dwLogonFlags, string lpApplicationName, string lpCommandLine, int dwCreationFlags, IntPtr lpEnvironment,
string lpCurrentDirectory, [In] ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);

#endregion
}
40 changes: 33 additions & 7 deletions MTGOSDK/src/Core/RemoteClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

using MTGOSDK.Core.Reflection;
using MTGOSDK.Core.Exceptions;
using MTGOSDK.Win32.Utilities;


namespace MTGOSDK.Core;
Expand Down Expand Up @@ -112,14 +113,39 @@ public static async Task<bool> StartProcess()
try { using var p = MTGOProcess(); p.Kill(); p.WaitForExit(); } catch { }

// Start MTGO using the ClickOnce application manifest uri.
using var process = new Process();
process.StartInfo = new ProcessStartInfo()
try
{
FileName = "rundll32.exe",
Arguments = $"dfshim.dll,ShOpenVerbApplication {ApplicationUri}",
};
process.Start();
process.WaitForExit();
//
// This makes a call to CreateProcessAsUser from the Win32 API to start the
// process as the current user. This requires the SeIncreaseQuotaPrivilege
// (and potentially SE_ASSIGNPRIMARYTOKEN_NAME if not assignable).
//
// This requires enabling 'Replace a process level token' for the user
// account running this process. Refer to the below resource for more info:
// https://learn.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/replace-a-process-level-token
//
// using var process = ProcessHandler.CreateProcessAsUser(
using var process = ProcessUtilities.RunAsDesktopUser(
"rundll32.exe",
$"dfshim.dll,ShOpenVerbApplication {ApplicationUri}"
);
process.WaitForExit();
}
catch
{
//
// Fall back to starting the process assuming the process has proper
// permissions set to start as the current user (or is now set).
//
using var process = new Process();
process.StartInfo = new ProcessStartInfo()
{
FileName = "rundll32.exe",
Arguments = $"dfshim.dll,ShOpenVerbApplication {ApplicationUri}",
};
process.Start();
process.WaitForExit();
}

//
// Check for ClickOnce installation or updates and wait for it to finish.
Expand Down

0 comments on commit d834fae

Please sign in to comment.