using System; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using System.Threading; using Serilog; // ReSharper disable InconsistentNaming namespace Dalamud.Injector { /// /// Class responsible for stripping ACL protections from processes. /// public static class NativeAclFix { /// /// Start a process without ACL protections. /// /// The working directory. /// The path to the executable file. /// Arguments to pass to the executable file. /// Don't actually fix the ACL. /// Action to execute before the process is started. /// The started process. /// Thrown when a win32 error occurs. /// Thrown when the process did not start correctly. public static Process LaunchGame(string workingDir, string exePath, string arguments, bool dontFixAcl, Action beforeResume) { Process process = null; var psecDesc = IntPtr.Zero; if (!dontFixAcl) { var userName = Environment.UserName; var pExplicitAccess = default(PInvoke.EXPLICIT_ACCESS); PInvoke.BuildExplicitAccessWithName( ref pExplicitAccess, userName, PInvoke.STANDARD_RIGHTS_ALL | PInvoke.SPECIFIC_RIGHTS_ALL & ~PInvoke.PROCESS_VM_WRITE, PInvoke.GRANT_ACCESS, 0); if (PInvoke.SetEntriesInAcl(1, ref pExplicitAccess, IntPtr.Zero, out var newAcl) != 0) { throw new Win32Exception(Marshal.GetLastWin32Error()); } if (!PInvoke.InitializeSecurityDescriptor(out var secDesc, PInvoke.SECURITY_DESCRIPTOR_REVISION)) { throw new Win32Exception(Marshal.GetLastWin32Error()); } if (!PInvoke.SetSecurityDescriptorDacl(ref secDesc, true, newAcl, false)) { throw new Win32Exception(Marshal.GetLastWin32Error()); } psecDesc = Marshal.AllocHGlobal(Marshal.SizeOf()); Marshal.StructureToPtr(secDesc, psecDesc, true); } var lpProcessInformation = default(PInvoke.PROCESS_INFORMATION); try { var lpProcessAttributes = new PInvoke.SECURITY_ATTRIBUTES { nLength = Marshal.SizeOf(), lpSecurityDescriptor = psecDesc, bInheritHandle = false, }; var lpStartupInfo = new PInvoke.STARTUPINFO { cb = Marshal.SizeOf(), }; var compatLayerPrev = Environment.GetEnvironmentVariable("__COMPAT_LAYER"); Log.Verbose("Has preset __COMPAT_LAYER={CompatLayer}", compatLayerPrev); if (!string.IsNullOrEmpty(compatLayerPrev) && !compatLayerPrev.Contains("RunAsInvoker")) { Environment.SetEnvironmentVariable("__COMPAT_LAYER", $"RunAsInvoker {compatLayerPrev}"); } else if (string.IsNullOrEmpty(compatLayerPrev)) { Environment.SetEnvironmentVariable("__COMPAT_LAYER", "RunAsInvoker"); } try { Log.Information("Starting with __COMPAT_LAYER={CompatLayer}", Environment.GetEnvironmentVariable("__COMPAT_LAYER")); if (!PInvoke.CreateProcess( null, $"\"{exePath}\" {arguments}", ref lpProcessAttributes, IntPtr.Zero, false, PInvoke.CREATE_SUSPENDED, IntPtr.Zero, workingDir, ref lpStartupInfo, out lpProcessInformation)) { throw new Win32Exception(Marshal.GetLastWin32Error()); } } finally { Environment.SetEnvironmentVariable("__COMPAT_LAYER", compatLayerPrev); } if (!dontFixAcl) DisableSeDebug(lpProcessInformation.hProcess); process = new ExistingProcess(lpProcessInformation.hProcess); beforeResume?.Invoke(process); PInvoke.ResumeThread(lpProcessInformation.hThread); // Ensure that the game main window is prepared try { do { process.WaitForInputIdle(); Thread.Sleep(100); } while (TryFindGameWindow(process) == IntPtr.Zero); } catch (InvalidOperationException) { throw new GameExitedException(); } if (!dontFixAcl) CopyAclFromSelfToTargetProcess(lpProcessInformation.hProcess); } catch (Exception ex) { Log.Error(ex, "[NativeAclFix] Uncaught error during initialization, trying to kill process"); try { process?.Kill(); } catch (Exception killEx) { Log.Error(killEx, "[NativeAclFix] Could not kill process"); } throw; } finally { if (psecDesc != IntPtr.Zero) Marshal.FreeHGlobal(psecDesc); PInvoke.CloseHandle(lpProcessInformation.hThread); } return process; } /// /// Copies ACL of current process to the target process. /// /// Native handle to the target process. /// Thrown when a win32 error occurs. public static void CopyAclFromSelfToTargetProcess(IntPtr hProcess) { if (PInvoke.GetSecurityInfo( PInvoke.GetCurrentProcess(), PInvoke.SE_OBJECT_TYPE.SE_KERNEL_OBJECT, PInvoke.SECURITY_INFORMATION.DACL_SECURITY_INFORMATION, IntPtr.Zero, IntPtr.Zero, out var pACL, IntPtr.Zero, IntPtr.Zero) != 0) { throw new Win32Exception(Marshal.GetLastWin32Error()); } if (PInvoke.SetSecurityInfo( hProcess, PInvoke.SE_OBJECT_TYPE.SE_KERNEL_OBJECT, PInvoke.SECURITY_INFORMATION.DACL_SECURITY_INFORMATION | PInvoke.SECURITY_INFORMATION.UNPROTECTED_DACL_SECURITY_INFORMATION, IntPtr.Zero, IntPtr.Zero, pACL, IntPtr.Zero) != 0) { throw new Win32Exception(Marshal.GetLastWin32Error()); } } public static void ClaimSeDebug() { var hToken = PInvoke.INVALID_HANDLE_VALUE; try { if (!PInvoke.OpenThreadToken(PInvoke.GetCurrentThread(), PInvoke.TOKEN_QUERY | PInvoke.TOKEN_ADJUST_PRIVILEGES, false, out hToken)) { if (Marshal.GetLastWin32Error() != PInvoke.ERROR_NO_TOKEN) throw new Exception("ClaimSeDebug.OpenProcessToken#1", new Win32Exception(Marshal.GetLastWin32Error())); if (!PInvoke.ImpersonateSelf(PInvoke.SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation)) throw new Exception("ClaimSeDebug.ImpersonateSelf", new Win32Exception(Marshal.GetLastWin32Error())); if (!PInvoke.OpenThreadToken(PInvoke.GetCurrentThread(), PInvoke.TOKEN_QUERY | PInvoke.TOKEN_ADJUST_PRIVILEGES, false, out hToken)) throw new Exception("ClaimSeDebug.OpenProcessToken#2", new Win32Exception(Marshal.GetLastWin32Error())); } var luidDebugPrivilege = default(PInvoke.LUID); if (!PInvoke.LookupPrivilegeValue(null, PInvoke.SE_DEBUG_NAME, ref luidDebugPrivilege)) throw new Exception("ClaimSeDebug.LookupPrivilegeValue", new Win32Exception(Marshal.GetLastWin32Error())); var tpLookup = new PInvoke.TOKEN_PRIVILEGES() { PrivilegeCount = 1, Privileges = new PInvoke.LUID_AND_ATTRIBUTES[1] { new PInvoke.LUID_AND_ATTRIBUTES() { Luid = luidDebugPrivilege, Attributes = PInvoke.SE_PRIVILEGE_ENABLED, }, }, }; if (!PInvoke.AdjustTokenPrivileges(hToken, false, ref tpLookup, 0, IntPtr.Zero, IntPtr.Zero)) throw new Exception("ClaimSeDebug.AdjustTokenPrivileges", new Win32Exception(Marshal.GetLastWin32Error())); } finally { if (hToken != PInvoke.INVALID_HANDLE_VALUE && hToken != IntPtr.Zero) PInvoke.CloseHandle(hToken); } } private static void DisableSeDebug(IntPtr processHandle) { if (!PInvoke.OpenProcessToken(processHandle, PInvoke.TOKEN_QUERY | PInvoke.TOKEN_ADJUST_PRIVILEGES, out var tokenHandle)) { throw new Win32Exception(Marshal.GetLastWin32Error()); } var luidDebugPrivilege = default(PInvoke.LUID); if (!PInvoke.LookupPrivilegeValue(null, PInvoke.SE_DEBUG_NAME, ref luidDebugPrivilege)) { throw new Win32Exception(Marshal.GetLastWin32Error()); } var requiredPrivileges = new PInvoke.PRIVILEGE_SET { PrivilegeCount = 1, Control = PInvoke.PRIVILEGE_SET_ALL_NECESSARY, Privilege = new PInvoke.LUID_AND_ATTRIBUTES[1], }; requiredPrivileges.Privilege[0].Luid = luidDebugPrivilege; requiredPrivileges.Privilege[0].Attributes = PInvoke.SE_PRIVILEGE_ENABLED; if (!PInvoke.PrivilegeCheck(tokenHandle, ref requiredPrivileges, out bool bResult)) { throw new Win32Exception(Marshal.GetLastWin32Error()); } // SeDebugPrivilege is enabled; try disabling it if (bResult) { var tokenPrivileges = new PInvoke.TOKEN_PRIVILEGES { PrivilegeCount = 1, Privileges = new PInvoke.LUID_AND_ATTRIBUTES[1], }; tokenPrivileges.Privileges[0].Luid = luidDebugPrivilege; tokenPrivileges.Privileges[0].Attributes = PInvoke.SE_PRIVILEGE_REMOVED; if (!PInvoke.AdjustTokenPrivileges(tokenHandle, false, ref tokenPrivileges, 0, IntPtr.Zero, IntPtr.Zero)) { throw new Win32Exception(Marshal.GetLastWin32Error()); } } PInvoke.CloseHandle(tokenHandle); } private static IntPtr TryFindGameWindow(Process process) { IntPtr hwnd = IntPtr.Zero; while ((hwnd = PInvoke.FindWindowEx(IntPtr.Zero, hwnd, "FFXIVGAME", IntPtr.Zero)) != IntPtr.Zero) { PInvoke.GetWindowThreadProcessId(hwnd, out uint pid); if (pid == process.Id && PInvoke.IsWindowVisible(hwnd)) { break; } } return hwnd; } /// /// Exception thrown when the process has exited before a window could be found. /// public class GameExitedException : Exception { /// /// Initializes a new instance of the class. /// public GameExitedException() : base("Game exited prematurely.") { } } // Definitions taken from PInvoke.net (with some changes) [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "WINAPI conventions")] [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1121:Use built-in type alias", Justification = "WINAPI conventions")] [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1400:Access modifier should be declared", Justification = "WINAPI conventions")] [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "WINAPI conventions")] [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "WINAPI conventions")] [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "WINAPI conventions")] private static class PInvoke { #region Constants public static readonly IntPtr INVALID_HANDLE_VALUE = new(-1); public const string SE_DEBUG_NAME = "SeDebugPrivilege"; public const UInt32 STANDARD_RIGHTS_ALL = 0x001F0000; public const UInt32 SPECIFIC_RIGHTS_ALL = 0x0000FFFF; public const UInt32 PROCESS_VM_WRITE = 0x0020; public const UInt32 GRANT_ACCESS = 1; public const UInt32 SECURITY_DESCRIPTOR_REVISION = 1; public const UInt32 CREATE_SUSPENDED = 0x00000004; public const UInt32 TOKEN_QUERY = 0x0008; public const UInt32 TOKEN_ADJUST_PRIVILEGES = 0x0020; public const UInt32 PRIVILEGE_SET_ALL_NECESSARY = 1; public const UInt32 SE_PRIVILEGE_ENABLED = 0x00000002; public const UInt32 SE_PRIVILEGE_REMOVED = 0x00000004; public const UInt32 ERROR_NO_TOKEN = 0x000003F0; public enum MULTIPLE_TRUSTEE_OPERATION { NO_MULTIPLE_TRUSTEE, TRUSTEE_IS_IMPERSONATE, } public enum TRUSTEE_FORM { TRUSTEE_IS_SID, TRUSTEE_IS_NAME, TRUSTEE_BAD_FORM, TRUSTEE_IS_OBJECTS_AND_SID, TRUSTEE_IS_OBJECTS_AND_NAME, } public enum TRUSTEE_TYPE { TRUSTEE_IS_UNKNOWN, TRUSTEE_IS_USER, TRUSTEE_IS_GROUP, TRUSTEE_IS_DOMAIN, TRUSTEE_IS_ALIAS, TRUSTEE_IS_WELL_KNOWN_GROUP, TRUSTEE_IS_DELETED, TRUSTEE_IS_INVALID, TRUSTEE_IS_COMPUTER, } public enum SE_OBJECT_TYPE { SE_UNKNOWN_OBJECT_TYPE, SE_FILE_OBJECT, SE_SERVICE, SE_PRINTER, SE_REGISTRY_KEY, SE_LMSHARE, SE_KERNEL_OBJECT, SE_WINDOW_OBJECT, SE_DS_OBJECT, SE_DS_OBJECT_ALL, SE_PROVIDER_DEFINED_OBJECT, SE_WMIGUID_OBJECT, SE_REGISTRY_WOW64_32KEY, } [Flags] public enum SECURITY_INFORMATION { OWNER_SECURITY_INFORMATION = 1, GROUP_SECURITY_INFORMATION = 2, DACL_SECURITY_INFORMATION = 4, SACL_SECURITY_INFORMATION = 8, UNPROTECTED_SACL_SECURITY_INFORMATION = 0x10000000, UNPROTECTED_DACL_SECURITY_INFORMATION = 0x20000000, PROTECTED_SACL_SECURITY_INFORMATION = 0x40000000, } public enum SECURITY_IMPERSONATION_LEVEL { SecurityAnonymous, SecurityIdentification, SecurityImpersonation, SecurityDelegation } #endregion #region Methods [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern void BuildExplicitAccessWithName( ref EXPLICIT_ACCESS pExplicitAccess, string pTrusteeName, uint accessPermissions, uint accessMode, uint inheritance); [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern int SetEntriesInAcl( int cCountOfExplicitEntries, ref EXPLICIT_ACCESS pListOfExplicitEntries, IntPtr oldAcl, out IntPtr newAcl); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool InitializeSecurityDescriptor( out SECURITY_DESCRIPTOR pSecurityDescriptor, uint dwRevision); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool SetSecurityDescriptorDacl( ref SECURITY_DESCRIPTOR pSecurityDescriptor, bool bDaclPresent, IntPtr pDacl, bool bDaclDefaulted); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern bool CreateProcess( string lpApplicationName, string lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, UInt32 dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool CloseHandle(IntPtr hObject); [DllImport("kernel32.dll", SetLastError = true)] public static extern uint ResumeThread(IntPtr hThread); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool ImpersonateSelf( SECURITY_IMPERSONATION_LEVEL impersonationLevel ); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool OpenProcessToken( IntPtr processHandle, UInt32 desiredAccess, out IntPtr tokenHandle); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool OpenThreadToken( IntPtr ThreadHandle, uint DesiredAccess, bool OpenAsSelf, out IntPtr TokenHandle); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, ref LUID lpLuid); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool PrivilegeCheck( IntPtr clientToken, ref PRIVILEGE_SET requiredPrivileges, out bool pfResult); [DllImport("advapi32.dll", SetLastError = true)] public static extern bool AdjustTokenPrivileges( IntPtr tokenHandle, bool disableAllPrivileges, ref TOKEN_PRIVILEGES newState, int cbPreviousState, IntPtr previousState, IntPtr cbOutPreviousState); [DllImport("advapi32.dll", SetLastError = true)] public static extern uint GetSecurityInfo( IntPtr handle, SE_OBJECT_TYPE objectType, SECURITY_INFORMATION securityInfo, IntPtr pSidOwner, IntPtr pSidGroup, out IntPtr pDacl, IntPtr pSacl, IntPtr pSecurityDescriptor); [DllImport("advapi32.dll", SetLastError = true)] public static extern uint SetSecurityInfo( IntPtr handle, SE_OBJECT_TYPE objectType, SECURITY_INFORMATION securityInfo, IntPtr psidOwner, IntPtr psidGroup, IntPtr pDacl, IntPtr pSacl); [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr GetCurrentProcess(); [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr GetCurrentThread(); [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr hWndChildAfter, string className, IntPtr windowTitle); [DllImport("user32.dll", SetLastError = true)] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool IsWindowVisible(IntPtr hWnd); #endregion #region Structures [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 0)] public struct TRUSTEE : IDisposable { public IntPtr pMultipleTrustee; public MULTIPLE_TRUSTEE_OPERATION MultipleTrusteeOperation; public TRUSTEE_FORM TrusteeForm; public TRUSTEE_TYPE TrusteeType; private IntPtr ptstrName; public string Name => Marshal.PtrToStringAuto(this.ptstrName) ?? string.Empty; #pragma warning disable CA1416 void IDisposable.Dispose() { if (this.ptstrName != IntPtr.Zero) Marshal.Release(this.ptstrName); } #pragma warning restore CA1416 } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 0)] public struct EXPLICIT_ACCESS { uint grfAccessPermissions; uint grfAccessMode; uint grfInheritance; TRUSTEE Trustee; } [StructLayout(LayoutKind.Sequential)] public struct SECURITY_DESCRIPTOR { public byte Revision; public byte Sbz1; public UInt16 Control; public IntPtr Owner; public IntPtr Group; public IntPtr Sacl; public IntPtr Dacl; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct STARTUPINFO { public Int32 cb; public string lpReserved; public string lpDesktop; public string lpTitle; public Int32 dwX; public Int32 dwY; public Int32 dwXSize; public Int32 dwYSize; public Int32 dwXCountChars; public Int32 dwYCountChars; public Int32 dwFillAttribute; public Int32 dwFlags; public Int16 wShowWindow; public Int16 cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } [StructLayout(LayoutKind.Sequential)] public struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public int dwProcessId; public UInt32 dwThreadId; } [StructLayout(LayoutKind.Sequential)] public struct SECURITY_ATTRIBUTES { public int nLength; public IntPtr lpSecurityDescriptor; public bool bInheritHandle; } [StructLayout(LayoutKind.Sequential)] public struct LUID { public UInt32 LowPart; public Int32 HighPart; } [StructLayout(LayoutKind.Sequential)] public struct PRIVILEGE_SET { public UInt32 PrivilegeCount; public UInt32 Control; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] public LUID_AND_ATTRIBUTES[] Privilege; } public struct LUID_AND_ATTRIBUTES { public LUID Luid; public UInt32 Attributes; } [StructLayout(LayoutKind.Sequential)] public struct TOKEN_PRIVILEGES { public UInt32 PrivilegeCount; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] public LUID_AND_ATTRIBUTES[] Privileges; } #endregion } } }