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)]