diff --git a/DisplayMagician/ProcessUtils.cs b/DisplayMagician/ProcessUtils.cs
index a055651..f17600d 100644
--- a/DisplayMagician/ProcessUtils.cs
+++ b/DisplayMagician/ProcessUtils.cs
@@ -5,6 +5,7 @@ using System.Diagnostics;
using System.IO;
using System.Management;
using System.Runtime.InteropServices;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -15,6 +16,8 @@ namespace DisplayMagician
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
+ const uint SE_GROUP_INTEGRITY = 0x00000020;
+
[Flags]
public enum PROCESS_CREATION_FLAGS : UInt32
{
@@ -45,6 +48,177 @@ namespace DisplayMagician
REALTIME_PRIORITY_CLASS = 0x00000100,
}
+
+ public enum SaferLevel : uint
+ {
+ Disallowed = 0,
+ Untrusted = 0x1000,
+ Constrained = 0x10000,
+ NormalUser = 0x20000,
+ FullyTrusted = 0x40000
+ }
+
+ public enum SaferScope : uint
+ {
+ Machine = 1,
+ User = 2
+ }
+
+ [Flags]
+ public enum SaferOpenFlags : uint
+ {
+ Open = 1
+ }
+
+ public enum TOKEN_INFORMATION_CLASS
+ {
+ ///
+ /// The buffer receives a TOKEN_USER structure that contains the user account of the token.
+ ///
+ TokenUser = 1,
+
+ ///
+ /// The buffer receives a TOKEN_GROUPS structure that contains the group accounts associated with the token.
+ ///
+ TokenGroups,
+
+ ///
+ /// The buffer receives a TOKEN_PRIVILEGES structure that contains the privileges of the token.
+ ///
+ TokenPrivileges,
+
+ ///
+ /// The buffer receives a TOKEN_OWNER structure that contains the default owner security identifier (SID) for newly created objects.
+ ///
+ TokenOwner,
+
+ ///
+ /// The buffer receives a TOKEN_PRIMARY_GROUP structure that contains the default primary group SID for newly created objects.
+ ///
+ TokenPrimaryGroup,
+
+ ///
+ /// The buffer receives a TOKEN_DEFAULT_DACL structure that contains the default DACL for newly created objects.
+ ///
+ TokenDefaultDacl,
+
+ ///
+ /// The buffer receives a TOKEN_SOURCE structure that contains the source of the token. TOKEN_QUERY_SOURCE access is needed to retrieve this information.
+ ///
+ TokenSource,
+
+ ///
+ /// The buffer receives a TOKEN_TYPE value that indicates whether the token is a primary or impersonation token.
+ ///
+ TokenType,
+
+ ///
+ /// The buffer receives a SECURITY_IMPERSONATION_LEVEL value that indicates the impersonation level of the token. If the access token is not an impersonation token, the function fails.
+ ///
+ TokenImpersonationLevel,
+
+ ///
+ /// The buffer receives a TOKEN_STATISTICS structure that contains various token statistics.
+ ///
+ TokenStatistics,
+
+ ///
+ /// The buffer receives a TOKEN_GROUPS structure that contains the list of restricting SIDs in a restricted token.
+ ///
+ TokenRestrictedSids,
+
+ ///
+ /// The buffer receives a DWORD value that indicates the Terminal Services session identifier that is associated with the token.
+ ///
+ TokenSessionId,
+
+ ///
+ /// The buffer receives a TOKEN_GROUPS_AND_PRIVILEGES structure that contains the user SID, the group accounts, the restricted SIDs, and the authentication ID associated with the token.
+ ///
+ TokenGroupsAndPrivileges,
+
+ ///
+ /// Reserved.
+ ///
+ TokenSessionReference,
+
+ ///
+ /// The buffer receives a DWORD value that is nonzero if the token includes the SANDBOX_INERT flag.
+ ///
+ TokenSandBoxInert,
+
+ ///
+ /// Reserved.
+ ///
+ TokenAuditPolicy,
+
+ ///
+ /// The buffer receives a TOKEN_ORIGIN value.
+ ///
+ TokenOrigin,
+
+ ///
+ /// The buffer receives a TOKEN_ELEVATION_TYPE value that specifies the elevation level of the token.
+ ///
+ TokenElevationType,
+
+ ///
+ /// The buffer receives a TOKEN_LINKED_TOKEN structure that contains a handle to another token that is linked to this token.
+ ///
+ TokenLinkedToken,
+
+ ///
+ /// The buffer receives a TOKEN_ELEVATION structure that specifies whether the token is elevated.
+ ///
+ TokenElevation,
+
+ ///
+ /// The buffer receives a DWORD value that is nonzero if the token has ever been filtered.
+ ///
+ TokenHasRestrictions,
+
+ ///
+ /// The buffer receives a TOKEN_ACCESS_INFORMATION structure that specifies security information contained in the token.
+ ///
+ TokenAccessInformation,
+
+ ///
+ /// The buffer receives a DWORD value that is nonzero if virtualization is allowed for the token.
+ ///
+ TokenVirtualizationAllowed,
+
+ ///
+ /// The buffer receives a DWORD value that is nonzero if virtualization is enabled for the token.
+ ///
+ TokenVirtualizationEnabled,
+
+ ///
+ /// The buffer receives a TOKEN_MANDATORY_LABEL structure that specifies the token's integrity level.
+ ///
+ TokenIntegrityLevel,
+
+ ///
+ /// The buffer receives a DWORD value that is nonzero if the token has the UIAccess flag set.
+ ///
+ TokenUIAccess,
+
+ ///
+ /// The buffer receives a TOKEN_MANDATORY_POLICY structure that specifies the token's mandatory integrity policy.
+ ///
+ TokenMandatoryPolicy,
+
+ ///
+ /// The buffer receives the token's logon security identifier (SID).
+ ///
+ TokenLogonSid,
+
+ ///
+ /// The maximum value for this enumeration
+ ///
+ MaxTokenInfoClass
+ }
+
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct STARTUPINFOEX
{
@@ -92,6 +266,42 @@ namespace DisplayMagician
public int bInheritHandle;
}
+ [StructLayout(LayoutKind.Sequential)]
+ private struct SID_AND_ATTRIBUTES
+ {
+ public IntPtr Sid;
+ public uint Attributes;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct TOKEN_MANDATORY_LABEL
+ {
+ public SID_AND_ATTRIBUTES Label;
+ }
+
+ [DllImport("advapi32", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
+ private static extern bool SaferCreateLevel(SaferScope scope, SaferLevel level, SaferOpenFlags openFlags, out IntPtr pLevelHandle, IntPtr lpReserved);
+
+ [DllImport("advapi32", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
+ private static extern bool SaferComputeTokenFromLevel(IntPtr LevelHandle, IntPtr InAccessToken, out IntPtr OutAccessToken, int dwFlags, IntPtr lpReserved);
+
+ [DllImport("advapi32", SetLastError = true)]
+ private static extern bool SaferCloseLevel(IntPtr hLevelHandle);
+
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern bool ConvertStringSidToSid(string StringSid, out IntPtr ptrSid);
+
+ [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool CloseHandle(IntPtr hObject);
+
+ private static bool SafeCloseHandle(IntPtr hObject)
+ {
+ return (hObject == IntPtr.Zero) ? true : CloseHandle(hObject);
+ }
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ static extern IntPtr LocalFree(IntPtr hMem);
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
@@ -109,6 +319,27 @@ namespace DisplayMagician
IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFOEX lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
+ [DllImport("advapi32.dll", SetLastError = true)]
+ static extern Boolean SetTokenInformation(
+ IntPtr TokenHandle,
+ TOKEN_INFORMATION_CLASS TokenInformationClass,
+ IntPtr TokenInformation,
+ UInt32 TokenInformationLength);
+
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ static extern bool CreateProcessAsUser(
+ IntPtr hToken,
+ string lpApplicationName,
+ string lpCommandLine,
+ IntPtr lpProcessAttributes,
+ IntPtr lpThreadAttributes,
+ bool bInheritHandles,
+ uint dwCreationFlags,
+ IntPtr lpEnvironment,
+ string lpCurrentDirectory,
+ ref STARTUPINFO lpStartupInfo,
+ out PROCESS_INFORMATION lpProcessInformation);
+
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UpdateProcThreadAttribute(
@@ -124,9 +355,6 @@ namespace DisplayMagician
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeleteProcThreadAttributeList(IntPtr lpAttributeList);
- [DllImport("kernel32.dll", SetLastError = true)]
- private static extern bool CloseHandle(IntPtr hObject);
-
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint ResumeThread(IntPtr hThread);
@@ -192,13 +420,37 @@ namespace DisplayMagician
List runningProcesses = new List();
Process process = null;
PROCESS_INFORMATION processInfo;
+ bool usingChildProcess = false;
try
{
- if (CreateProcessWithPriority(executable, arguments, ProcessUtils.TranslatePriorityToClass(processPriority), out processInfo))
+ if (CreateProcessWithPriorityAsRestrictedUser(executable, arguments, ProcessUtils.TranslatePriorityToClass(processPriority), out processInfo))
{
if (processInfo.dwProcessId > 0)
{
- process = Process.GetProcessById(processInfo.dwProcessId);
+ try
+ {
+ process = Process.GetProcessById(processInfo.dwProcessId);
+ Task.Delay(500);
+ if (process.HasExited)
+ {
+ // it's a launcher! We need to look for children
+ List childProcesses = GetChildProcesses(process);
+ runningProcesses.AddRange(childProcesses);
+ usingChildProcess = true;
+ }
+ else
+ {
+ runningProcesses.Add(process);
+ }
+ }
+ catch(Exception ex)
+ {
+ // it's a launcher! We need to look for children
+ List childProcesses = GetChildProcesses(processInfo.dwProcessId);
+ runningProcesses.AddRange(childProcesses);
+ usingChildProcess = true;
+ }
+
}
else
{
@@ -211,9 +463,10 @@ namespace DisplayMagician
process = Process.Start(psi);
processInfo.hProcess = process.Handle;
processInfo.dwProcessId = process.Id;
- processInfo.dwThreadId = process.Threads[0].Id;
if (!process.HasExited)
{
+ processInfo.dwThreadId = process.Threads[0].Id;
+
// Change priority if we can (not always possible in this mode :(
try
{
@@ -286,20 +539,24 @@ namespace DisplayMagician
// Check the launched exe hasn't exited within 2 secs
- for (int secs = 0; secs <= (startTimeout * 1000); secs += 500)
+ if (!usingChildProcess)
{
- // If we have no more processes left then we're done!
- if (process.HasExited)
+ for (int secs = 0; secs <= (startTimeout * 1000); secs += 500)
{
- logger.Trace($"ProcessUtils/StartProcess: {executable} has exited early! It's likely to be a launcher! Trying to detect it's children.");
- // As the original process has left the building, we'll overwrite it with the children processes
- runningProcesses = GetChildProcesses(process);
- break;
+ // If we have no more processes left then we're done!
+ if (process.HasExited)
+ {
+ logger.Trace($"ProcessUtils/StartProcess: {executable} has exited early! It's likely to be a launcher! Trying to detect it's children.");
+ // As the original process has left the building, we'll overwrite it with the children processes
+ runningProcesses = GetChildProcesses(process);
+ break;
+ }
+ // Send a message to windows so that it doesn't think
+ // we're locked and try to kill us
+ System.Threading.Thread.CurrentThread.Join(0);
+ Thread.Sleep(500);
}
- // Send a message to windows so that it doesn't think
- // we're locked and try to kill us
- System.Threading.Thread.CurrentThread.Join(0);
- Thread.Sleep(500);
+
}
return runningProcesses;
@@ -316,9 +573,26 @@ namespace DisplayMagician
return children;
}
- public static bool CreateProcessWithPriority(string exeName, string cmdLine, ProcessPriorityClass priorityClass, out PROCESS_INFORMATION processInfo)
+ public static List GetChildProcesses(int processId)
+ {
+ List children = new List();
+ ManagementObjectSearcher mos = new ManagementObjectSearcher($"Select * From Win32_Process Where ParentProcessID={processId}");
+ foreach (ManagementObject mo in mos.Get())
+ {
+ children.Add(Process.GetProcessById(Convert.ToInt32(mo["ProcessID"])));
+ }
+ return children;
+ }
+
+ public static bool CreateProcessWithPriority(string fileName, string args, ProcessPriorityClass priorityClass, out PROCESS_INFORMATION processInfo)
{
PROCESS_CREATION_FLAGS processFlags = TranslatePriorityClassToFlags(priorityClass);
+ var cmd = new StringBuilder();
+ cmd.Append('"').Append(fileName).Append('"');
+ if (!string.IsNullOrWhiteSpace(args))
+ {
+ cmd.Append(' ').Append(args);
+ }
bool success = false;
PROCESS_INFORMATION pInfo = new PROCESS_INFORMATION();
var pSec = new SECURITY_ATTRIBUTES();
@@ -329,7 +603,7 @@ namespace DisplayMagician
sInfoEx.StartupInfo.cb = Marshal.SizeOf(sInfoEx);
try
{
- success = CreateProcess(exeName, cmdLine, ref pSec, ref tSec, false, processFlags, IntPtr.Zero, null, ref sInfoEx, out pInfo);
+ success = CreateProcess(fileName, cmd.ToString(), ref pSec, ref tSec, false, processFlags, IntPtr.Zero, null, ref sInfoEx, out pInfo);
}
catch (Exception ex)
{
@@ -339,7 +613,7 @@ namespace DisplayMagician
{
try
{
- success = CreateProcess(exeName, cmdLine, IntPtr.Zero, IntPtr.Zero, false, processFlags, IntPtr.Zero, null, ref sInfoEx, out pInfo);
+ success = CreateProcess(fileName, cmd.ToString(), IntPtr.Zero, IntPtr.Zero, false, processFlags, IntPtr.Zero, null, ref sInfoEx, out pInfo);
}
catch (Exception ex)
{
@@ -351,6 +625,117 @@ namespace DisplayMagician
return success;
}
+ /// Runs a process as a non-elevated version of the current user.
+ public static bool CreateProcessWithPriorityAsRestrictedUser(string fileName, string args, ProcessPriorityClass priorityClass, out PROCESS_INFORMATION processInfo)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ throw new ArgumentException("Value cannot be null or whitespace.", nameof(fileName));
+
+ var pi = new PROCESS_INFORMATION();
+ if (GetRestrictedSessionUserToken(out var hRestrictedToken))
+ {
+ try
+ {
+ var si = new STARTUPINFO();
+ var cmd = new StringBuilder();
+ cmd.Append('"').Append(fileName).Append('"');
+ if (!string.IsNullOrWhiteSpace(args))
+ {
+ cmd.Append(' ').Append(args);
+ }
+
+ if (!CreateProcessAsUser(
+ hRestrictedToken,
+ fileName,
+ cmd.ToString(),
+ IntPtr.Zero,
+ IntPtr.Zero,
+ true, // inherit handle
+ 0,
+ IntPtr.Zero,
+ Path.GetDirectoryName(fileName),
+ ref si,
+ out pi))
+ {
+ processInfo = pi;
+ return false;
+ }
+
+
+ }
+ finally
+ {
+ CloseHandle(hRestrictedToken);
+ }
+ processInfo = pi;
+ return true;
+ }
+ else
+ {
+ processInfo = pi;
+ return false;
+ }
+
+ }
+
+ // based on https://stackoverflow.com/a/16110126/862099
+ private static bool GetRestrictedSessionUserToken(out IntPtr token)
+ {
+ token = IntPtr.Zero;
+ if (!SaferCreateLevel(SaferScope.User, SaferLevel.NormalUser, SaferOpenFlags.Open, out var hLevel, IntPtr.Zero))
+ {
+ return false;
+ }
+
+ IntPtr hRestrictedToken = IntPtr.Zero;
+ TOKEN_MANDATORY_LABEL tml = default;
+ tml.Label.Sid = IntPtr.Zero;
+ IntPtr tmlPtr = IntPtr.Zero;
+
+ try
+ {
+ if (!SaferComputeTokenFromLevel(hLevel, IntPtr.Zero, out hRestrictedToken, 0, IntPtr.Zero))
+ {
+ return false;
+ }
+
+ // Set the token to medium integrity.
+ tml.Label.Attributes = SE_GROUP_INTEGRITY;
+ tml.Label.Sid = IntPtr.Zero;
+ if (!ConvertStringSidToSid("S-1-16-8192", out tml.Label.Sid))
+ {
+ return false;
+ }
+
+ tmlPtr = Marshal.AllocHGlobal(Marshal.SizeOf(tml));
+ Marshal.StructureToPtr(tml, tmlPtr, false);
+ if (!SetTokenInformation(hRestrictedToken,
+ TOKEN_INFORMATION_CLASS.TokenIntegrityLevel,
+ tmlPtr, (uint)Marshal.SizeOf(tml)))
+ {
+ return false;
+ }
+
+ token = hRestrictedToken;
+ hRestrictedToken = IntPtr.Zero; // make sure finally() doesn't close the handle
+ }
+ finally
+ {
+ SaferCloseLevel(hLevel);
+ SafeCloseHandle(hRestrictedToken);
+ if (tml.Label.Sid != IntPtr.Zero)
+ {
+ LocalFree(tml.Label.Sid);
+ }
+ if (tmlPtr != IntPtr.Zero)
+ {
+ Marshal.FreeHGlobal(tmlPtr);
+ }
+ }
+
+ return true;
+ }
+
public static void ResumeProcess(PROCESS_INFORMATION processInfo)
{
ResumeThread(processInfo.hThread);
@@ -492,7 +877,7 @@ namespace DisplayMagician
{
// Stop the process
processToStop.CloseMainWindow();
- if (!processToStop.WaitForExit(5000))
+ if (!processToStop.WaitForExit(1000))
{
logger.Trace($"ProcessUtils/StopProcess: Process {processToStop.StartInfo.FileName} wouldn't stop cleanly. Forcing program close.");
processToStop.Kill();
@@ -550,6 +935,10 @@ namespace DisplayMagician
{
logger.Debug($"ShortcutRepository/RunShortcut: Successfully stopped process {processToStop.StartInfo.FileName}");
}
+ else
+ {
+ logger.Warn($"ShortcutRepository/RunShortcut: Failed to stop process {processToStop.StartInfo.FileName} after main executable or game was exited by the user.");
+ }
}
}
catch (Exception ex)
diff --git a/DisplayMagician/Properties/AssemblyInfo.cs b/DisplayMagician/Properties/AssemblyInfo.cs
index d684a72..466bad3 100644
--- a/DisplayMagician/Properties/AssemblyInfo.cs
+++ b/DisplayMagician/Properties/AssemblyInfo.cs
@@ -26,8 +26,8 @@ using System.Resources;
[assembly: Guid("e4ceaf5e-ad01-4695-b179-31168eb74c48")]
// Version information
-[assembly: AssemblyVersion("2.1.0.190")]
-[assembly: AssemblyFileVersion("2.1.0.190")]
+[assembly: AssemblyVersion("2.1.0.195")]
+[assembly: AssemblyFileVersion("2.1.0.195")]
[assembly: NeutralResourcesLanguageAttribute( "en" )]
[assembly: CLSCompliant(true)]