First (probably) working process creation upgrade

I have completely rewritten the process creation logic for the start programs to handle launchers, and applications that require higher level privileges. It doesn't quite handle asking for UAC if the programn requires it, but that's something I can add at a later date. At the moment it seems to work a lot more reliably than the last set of code.
This commit is contained in:
Terry MacDonald 2021-11-07 17:34:55 +13:00
parent 9200b82e29
commit a1f9bb5e2f
2 changed files with 412 additions and 23 deletions

View File

@ -5,6 +5,7 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Management; using System.Management;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -15,6 +16,8 @@ namespace DisplayMagician
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
const uint SE_GROUP_INTEGRITY = 0x00000020;
[Flags] [Flags]
public enum PROCESS_CREATION_FLAGS : UInt32 public enum PROCESS_CREATION_FLAGS : UInt32
{ {
@ -45,6 +48,177 @@ namespace DisplayMagician
REALTIME_PRIORITY_CLASS = 0x00000100, 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
{
/// <summary>
/// The buffer receives a TOKEN_USER structure that contains the user account of the token.
/// </summary>
TokenUser = 1,
/// <summary>
/// The buffer receives a TOKEN_GROUPS structure that contains the group accounts associated with the token.
/// </summary>
TokenGroups,
/// <summary>
/// The buffer receives a TOKEN_PRIVILEGES structure that contains the privileges of the token.
/// </summary>
TokenPrivileges,
/// <summary>
/// The buffer receives a TOKEN_OWNER structure that contains the default owner security identifier (SID) for newly created objects.
/// </summary>
TokenOwner,
/// <summary>
/// The buffer receives a TOKEN_PRIMARY_GROUP structure that contains the default primary group SID for newly created objects.
/// </summary>
TokenPrimaryGroup,
/// <summary>
/// The buffer receives a TOKEN_DEFAULT_DACL structure that contains the default DACL for newly created objects.
/// </summary>
TokenDefaultDacl,
/// <summary>
/// The buffer receives a TOKEN_SOURCE structure that contains the source of the token. TOKEN_QUERY_SOURCE access is needed to retrieve this information.
/// </summary>
TokenSource,
/// <summary>
/// The buffer receives a TOKEN_TYPE value that indicates whether the token is a primary or impersonation token.
/// </summary>
TokenType,
/// <summary>
/// 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.
/// </summary>
TokenImpersonationLevel,
/// <summary>
/// The buffer receives a TOKEN_STATISTICS structure that contains various token statistics.
/// </summary>
TokenStatistics,
/// <summary>
/// The buffer receives a TOKEN_GROUPS structure that contains the list of restricting SIDs in a restricted token.
/// </summary>
TokenRestrictedSids,
/// <summary>
/// The buffer receives a DWORD value that indicates the Terminal Services session identifier that is associated with the token.
/// </summary>
TokenSessionId,
/// <summary>
/// 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.
/// </summary>
TokenGroupsAndPrivileges,
/// <summary>
/// Reserved.
/// </summary>
TokenSessionReference,
/// <summary>
/// The buffer receives a DWORD value that is nonzero if the token includes the SANDBOX_INERT flag.
/// </summary>
TokenSandBoxInert,
/// <summary>
/// Reserved.
/// </summary>
TokenAuditPolicy,
/// <summary>
/// The buffer receives a TOKEN_ORIGIN value.
/// </summary>
TokenOrigin,
/// <summary>
/// The buffer receives a TOKEN_ELEVATION_TYPE value that specifies the elevation level of the token.
/// </summary>
TokenElevationType,
/// <summary>
/// The buffer receives a TOKEN_LINKED_TOKEN structure that contains a handle to another token that is linked to this token.
/// </summary>
TokenLinkedToken,
/// <summary>
/// The buffer receives a TOKEN_ELEVATION structure that specifies whether the token is elevated.
/// </summary>
TokenElevation,
/// <summary>
/// The buffer receives a DWORD value that is nonzero if the token has ever been filtered.
/// </summary>
TokenHasRestrictions,
/// <summary>
/// The buffer receives a TOKEN_ACCESS_INFORMATION structure that specifies security information contained in the token.
/// </summary>
TokenAccessInformation,
/// <summary>
/// The buffer receives a DWORD value that is nonzero if virtualization is allowed for the token.
/// </summary>
TokenVirtualizationAllowed,
/// <summary>
/// The buffer receives a DWORD value that is nonzero if virtualization is enabled for the token.
/// </summary>
TokenVirtualizationEnabled,
/// <summary>
/// The buffer receives a TOKEN_MANDATORY_LABEL structure that specifies the token's integrity level.
/// </summary>
TokenIntegrityLevel,
/// <summary>
/// The buffer receives a DWORD value that is nonzero if the token has the UIAccess flag set.
/// </summary>
TokenUIAccess,
/// <summary>
/// The buffer receives a TOKEN_MANDATORY_POLICY structure that specifies the token's mandatory integrity policy.
/// </summary>
TokenMandatoryPolicy,
/// <summary>
/// The buffer receives the token's logon security identifier (SID).
/// </summary>
TokenLogonSid,
/// <summary>
/// The maximum value for this enumeration
/// </summary>
MaxTokenInfoClass
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct STARTUPINFOEX public struct STARTUPINFOEX
{ {
@ -92,6 +266,42 @@ namespace DisplayMagician
public int bInheritHandle; 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")] [DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)] [return: MarshalAs(UnmanagedType.Bool)]
@ -109,6 +319,27 @@ namespace DisplayMagician
IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFOEX lpStartupInfo, IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFOEX lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation); 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)] [DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)] [return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UpdateProcThreadAttribute( private static extern bool UpdateProcThreadAttribute(
@ -124,9 +355,6 @@ namespace DisplayMagician
[return: MarshalAs(UnmanagedType.Bool)] [return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeleteProcThreadAttributeList(IntPtr lpAttributeList); private static extern bool DeleteProcThreadAttributeList(IntPtr lpAttributeList);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll", SetLastError = true)] [DllImport("kernel32.dll", SetLastError = true)]
private static extern uint ResumeThread(IntPtr hThread); private static extern uint ResumeThread(IntPtr hThread);
@ -192,13 +420,37 @@ namespace DisplayMagician
List<Process> runningProcesses = new List<Process>(); List<Process> runningProcesses = new List<Process>();
Process process = null; Process process = null;
PROCESS_INFORMATION processInfo; PROCESS_INFORMATION processInfo;
bool usingChildProcess = false;
try try
{ {
if (CreateProcessWithPriority(executable, arguments, ProcessUtils.TranslatePriorityToClass(processPriority), out processInfo)) if (CreateProcessWithPriorityAsRestrictedUser(executable, arguments, ProcessUtils.TranslatePriorityToClass(processPriority), out processInfo))
{ {
if (processInfo.dwProcessId > 0) 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<Process> 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<Process> childProcesses = GetChildProcesses(processInfo.dwProcessId);
runningProcesses.AddRange(childProcesses);
usingChildProcess = true;
}
} }
else else
{ {
@ -211,9 +463,10 @@ namespace DisplayMagician
process = Process.Start(psi); process = Process.Start(psi);
processInfo.hProcess = process.Handle; processInfo.hProcess = process.Handle;
processInfo.dwProcessId = process.Id; processInfo.dwProcessId = process.Id;
processInfo.dwThreadId = process.Threads[0].Id;
if (!process.HasExited) if (!process.HasExited)
{ {
processInfo.dwThreadId = process.Threads[0].Id;
// Change priority if we can (not always possible in this mode :( // Change priority if we can (not always possible in this mode :(
try try
{ {
@ -286,20 +539,24 @@ namespace DisplayMagician
// Check the launched exe hasn't exited within 2 secs // 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! for (int secs = 0; secs <= (startTimeout * 1000); secs += 500)
if (process.HasExited)
{ {
logger.Trace($"ProcessUtils/StartProcess: {executable} has exited early! It's likely to be a launcher! Trying to detect it's children."); // If we have no more processes left then we're done!
// As the original process has left the building, we'll overwrite it with the children processes if (process.HasExited)
runningProcesses = GetChildProcesses(process); {
break; 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; return runningProcesses;
@ -316,9 +573,26 @@ namespace DisplayMagician
return children; return children;
} }
public static bool CreateProcessWithPriority(string exeName, string cmdLine, ProcessPriorityClass priorityClass, out PROCESS_INFORMATION processInfo) public static List<Process> GetChildProcesses(int processId)
{
List<Process> children = new List<Process>();
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); 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; bool success = false;
PROCESS_INFORMATION pInfo = new PROCESS_INFORMATION(); PROCESS_INFORMATION pInfo = new PROCESS_INFORMATION();
var pSec = new SECURITY_ATTRIBUTES(); var pSec = new SECURITY_ATTRIBUTES();
@ -329,7 +603,7 @@ namespace DisplayMagician
sInfoEx.StartupInfo.cb = Marshal.SizeOf(sInfoEx); sInfoEx.StartupInfo.cb = Marshal.SizeOf(sInfoEx);
try 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) catch (Exception ex)
{ {
@ -339,7 +613,7 @@ namespace DisplayMagician
{ {
try 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) catch (Exception ex)
{ {
@ -351,6 +625,117 @@ namespace DisplayMagician
return success; 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) public static void ResumeProcess(PROCESS_INFORMATION processInfo)
{ {
ResumeThread(processInfo.hThread); ResumeThread(processInfo.hThread);
@ -492,7 +877,7 @@ namespace DisplayMagician
{ {
// Stop the process // Stop the process
processToStop.CloseMainWindow(); 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."); logger.Trace($"ProcessUtils/StopProcess: Process {processToStop.StartInfo.FileName} wouldn't stop cleanly. Forcing program close.");
processToStop.Kill(); processToStop.Kill();
@ -550,6 +935,10 @@ namespace DisplayMagician
{ {
logger.Debug($"ShortcutRepository/RunShortcut: Successfully stopped process {processToStop.StartInfo.FileName}"); 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) catch (Exception ex)

View File

@ -26,8 +26,8 @@ using System.Resources;
[assembly: Guid("e4ceaf5e-ad01-4695-b179-31168eb74c48")] [assembly: Guid("e4ceaf5e-ad01-4695-b179-31168eb74c48")]
// Version information // Version information
[assembly: AssemblyVersion("2.1.0.190")] [assembly: AssemblyVersion("2.1.0.195")]
[assembly: AssemblyFileVersion("2.1.0.190")] [assembly: AssemblyFileVersion("2.1.0.195")]
[assembly: NeutralResourcesLanguageAttribute( "en" )] [assembly: NeutralResourcesLanguageAttribute( "en" )]
[assembly: CLSCompliant(true)] [assembly: CLSCompliant(true)]