using Dalamud.Bootstrap.OS; using Dalamud.Bootstrap.OS.Windows; using Dalamud.Bootstrap.OS.Windows.Raw; using Dalamud.Bootstrap.SqexArg; using Microsoft.Win32.SafeHandles; using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Text; namespace Dalamud.Bootstrap { public sealed class GameProcess : IDisposable { private const uint OpenProcessRights = (uint) ( PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_INFORMATION | PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME | PROCESS_ACCESS_RIGHTS.PROCESS_TERMINATE | PROCESS_ACCESS_RIGHTS.PROCESS_VM_OPERATION | PROCESS_ACCESS_RIGHTS.PROCESS_VM_READ | PROCESS_ACCESS_RIGHTS.PROCESS_VM_WRITE ); private IntPtr m_handle; public GameProcess(IntPtr handle) { m_handle = handle; } ~GameProcess() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(true); } private void Dispose(bool disposing) { if (m_handle != IntPtr.Zero) { Kernel32.CloseHandle(m_handle); m_handle = IntPtr.Zero; } } private static IntPtr OpenProcessHandleRaw(uint pid, uint access) { var handle = Kernel32.OpenProcess(access, false, pid); if (handle == IntPtr.Zero) { ProcessException.ThrowLastOsError(); } return handle; } private static IntPtr OpenProcessHandle(uint pid, uint access) { var processDacHandle = OpenProcessHandleRaw(pid, (uint)(PROCESS_ACCESS_RIGHTS.READ_CONTROL | PROCESS_ACCESS_RIGHTS.WRITE_DAC)); try { return RelaxProcessHandle(processDacHandle, access, (_) => { var handle = OpenProcessHandleRaw(pid, access); return handle; }); } finally { Kernel32.CloseHandle(processDacHandle); } } public static GameProcess Open(uint pid) { var handle = OpenProcessHandle(pid, OpenProcessRights); try { return new GameProcess(handle); } catch { // If something bad thing happens in .ctor we need to close the handle to avoid leak. Kernel32.CloseHandle(handle); throw; } } /// /// Temporary grants access rights to the handle. /// /// A handle to set access rights on it. Must be SE_KERNEL_OBJECT /// An access right to grant. /// A function to execute while temporary access is granted. /// A return type. /// A value returned from the scope function. private static T RelaxProcessHandle(IntPtr handle, uint access, Func scope) { // relax shit unsafe { T result; uint error; SECURITY_DESCRIPTOR* pSecurityDescOrig; ACL* pDaclOrig; error = Advapi32.GetSecurityInfo( handle, SE_OBJECT_TYPE.SE_KERNEL_OBJECT, SECURITY_INFORMATION.DACL_SECURITY_INFORMATION, null, null, &pDaclOrig, null, &pSecurityDescOrig ); if (error != 0) { throw new ProcessException($"Could not get security info. (Error {error})"); } try { EXPLICIT_ACCESS_W explictAccess; ACL* pRelaxedAcl; Advapi32.BuildExplicitAccessWithNameW(&explictAccess, Environment.UserName, access, ACCESS_MODE.SET_ACCESS, 0 /* NO_INHERITANCE */); error = Advapi32.SetEntriesInAclW(1, &explictAccess, null, &pRelaxedAcl); if (error != 0) { throw new ProcessException($"Could not set security info. (Error {error})"); } error = Advapi32.SetSecurityInfo( handle, SE_OBJECT_TYPE.SE_KERNEL_OBJECT, SECURITY_INFORMATION.DACL_SECURITY_INFORMATION, null, null, pRelaxedAcl, null ); if (error != 0) { throw new ProcessException(); } result = scope(handle); } finally { // Restore permission; also we don't care about an error for now Advapi32.SetSecurityInfo( handle, SE_OBJECT_TYPE.SE_KERNEL_OBJECT, SECURITY_INFORMATION.DACL_SECURITY_INFORMATION, null, null, pDaclOrig, null ); Kernel32.LocalFree(pSecurityDescOrig); } return result; } } private static uint CreateCommandLineKey() { return (uint)(Environment.TickCount & 0xFFFF_0000); } public static GameProcess Create(GameProcessCreationOptions options) { unsafe { SECURITY_ATTRIBUTES processAttr, threadAttr; STARTUPINFOW startupInfo = default; PROCESS_INFORMATION processInfo = default; uint creationFlag = default; var key = CreateCommandLineKey(); var commandLine = BuildCommandLine(options.Arguments, key); var currentDirectory = Path.Combine(Directory.GetParent(Path.GetDirectoryName(options.ImagePath)).FullName, "boot"); // this is fucked var environments = BuildEnvironments(options.Environments); if (options.CreateSuspended) { creationFlag |= (uint)PROCESS_CREATION_FLAGS.CREATE_SUSPENDED; } if (!Kernel32.CreateProcessW( options.ImagePath, commandLine, &processAttr, &threadAttr, false, creationFlag, /* env */, currentDirectory, &startupInfo, &processInfo )) { ProcessException.ThrowLastOsError(); } Kernel32.CloseHandle(//////////////////// fucking thread ) return new GameProcess(processInfo.hProcess); } } private static string BuildCommandLine(IDictionary arguments, uint key) { var builder = new SqexArgBuilder(arguments); return builder.Build(key); } private static byte[] BuildEnvironments(IDictionary? environments) { } /// /// Reads process memory. /// /// /// A number of bytes that is actually read. /// /// /// Thrown when failed to read memory. /// public int ReadMemory(IntPtr address, Span destination) { unsafe { fixed (byte* pDest = destination) { if (!Kernel32.ReadProcessMemory(m_handle, address, pDest, (IntPtr)destination.Length, out var bytesRead)) { ProcessException.ThrowLastOsError(); } // This is okay because destination will never be longer than int.Max return bytesRead.ToInt32(); } } } /// /// Reads the exact number of bytes required to fill the buffer. /// /// /// Thrown when failed to read memory. /// public void ReadMemoryExact(IntPtr address, Span destination) { var totalBytesRead = 0; while (totalBytesRead < destination.Length) { var bytesRead = ReadMemory(address + totalBytesRead, destination[totalBytesRead..]); if (bytesRead == 0) { // prolly page fault; there's not much we can do here var readBeginAddr = address.ToInt64() + totalBytesRead; var readEndAddr = address.ToInt64() + destination.Length; ProcessException.ThrowLastOsError(); } totalBytesRead += bytesRead; } } /// /// Thrown when failed to read memory. /// public byte[] ReadMemoryExact(IntPtr address, int length) { var buffer = new byte[length]; ReadMemoryExact(address, buffer); return buffer; } /// /// Thrown when failed to read memory. /// public void ReadMemoryExact(IntPtr address, ref T value) where T : unmanaged { var span = MemoryMarshal.CreateSpan(ref value, 1); // span should never leave this function since it has unbounded lifetime. var buffer = MemoryMarshal.AsBytes(span); ReadMemoryExact(address, buffer); } private IntPtr GetPebAddress() { unsafe { PROCESS_BASIC_INFORMATION info = default; var status = Ntdll.NtQueryInformationProcess( m_handle, PROCESSINFOCLASS.ProcessBasicInformation, &info, Marshal.SizeOf(), (IntPtr*)IntPtr.Zero ); if (!status.Success) { throw new ProcessException($"Could not query information on process. (Status: {status})"); } return info.PebBaseAddress; } } /// /// Reads command-line arguments from the process. /// public string[] GetProcessArguments() { PEB peb = default; RTL_USER_PROCESS_PARAMETERS procParam = default; // Find where the command line is allocated var pebAddr = GetPebAddress(); ReadMemoryExact(pebAddr, ref peb); ReadMemoryExact(peb.ProcessParameters, ref procParam); // Read the command line (which is utf16-like string) var commandLine = ReadMemoryExact(procParam.CommandLine.Buffer, procParam.CommandLine.Length); return ParseCommandLineToArguments(commandLine); } /// /// Returns a time when the process was started. /// public DateTime GetCreationTime() { unsafe { FILETIME creationTime, exitTime, kernelTime, userTime; if (!Kernel32.GetProcessTimes(m_handle, &creationTime, &exitTime, &kernelTime, &userTime)) { ProcessException.ThrowLastOsError(); } return creationTime.ToDateTime(); } } private string[] ParseCommandLineToArguments(ReadOnlySpan commandLine) { unsafe { char** argv; int argc; fixed (byte* pCommandLine = commandLine) { argv = Shell32.CommandLineToArgvW(pCommandLine, out argc); } if (argv == null) { ProcessException.ThrowLastOsError(); } // NOTE: argv must be deallocated via LocalFree when we're done try { var arguments = new string[argc]; for (var i = 0; i < argc; i++) { arguments[i] = new string(argv[i]); } return arguments; } finally { Kernel32.LocalFree(argv); } } } public string GetImageFilePath() { var buffer = new StringBuilder(300); // From docs: // On input, specifies the size of the lpExeName buffer, in characters. // On success, receives the number of characters written to the buffer, not including the null-terminating character. var size = buffer.Capacity; if (!Kernel32.QueryFullProcessImageNameW(m_handle, 0, buffer, ref size)) { ProcessException.ThrowLastOsError(); } return buffer.ToString(); } /// /// Recovers a key used in encrypting process arguments. /// /// A key recovered from the time when the process was created. /// /// This is possible because the key to encrypt arguments is just a high nibble value from GetTickCount() at the time when the process was created. /// (Thanks Wintermute!) /// private uint GetArgumentEncryptionKey() { var createdTime = GetCreationTime(); // Get current tick var currentDt = DateTime.Now; var currentTick = Environment.TickCount; // We know that GetTickCount() is just a system uptime in milliseconds. var delta = currentDt - createdTime; var createdTick = (uint)currentTick - (uint)delta.TotalMilliseconds; // only the high nibble is used. return createdTick & 0xFFFF_0000; } public uint Id => Kernel32.GetProcessId(m_handle); } }