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.Runtime.InteropServices; using System.Text; namespace Dalamud.Bootstrap { public sealed class GameProcess : IDisposable { private const uint OpenProcessRights = 0; 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 OpenProcessHandle(uint pid, uint access) { var handle = Kernel32.OpenProcess(access, false, pid); if (handle == IntPtr.Zero) { ProcessException.ThrowLastOsError(); } return handle; } public static GameProcess Open(uint pid) { var secHandle = OpenProcessHandle(pid, (uint)(PROCESS_ACCESS_RIGHTS.READ_CONTROL | PROCESS_ACCESS_RIGHTS.WRITE_DAC)); try { // We can get VM_WRITE this way return RelaxProcessHandle(secHandle, OpenProcessRights, (_) => { var handle = OpenProcessHandle(pid, OpenProcessRights); return new GameProcess(handle); }); } finally { Kernel32.CloseHandle(secHandle); } } /// /// 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; } } /// /// 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, sizeof(PROCESS_BASIC_INFORMATION), (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() { FILETIME creationTime, exitTime, kernelTime, userTime; unsafe { 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; } /// /// Reads command-line arguments from the game and decrypts them if necessary. /// /// /// Command-line arguments that looks like this: /// /DEV.TestSID =ABCD /UserPath =C:\Examples /// public ArgumentBuilder GetGameArguments() { var processArguments = GetProcessArguments(); // arg[0] is a path to exe(normally), arg[1] is actual stuff. if (processArguments.Length < 2) { throw new ProcessException($"There's only {processArguments.Length} process arguments. It must have at least 2 arguments."); } // We're interested in argument that contains session id var argument = processArguments[1]; // If it's encrypted, we need to decrypt it first if (EncryptedArgument.TryParse(argument, out var encryptedArgument)) { var key = GetArgumentEncryptionKey(); argument = encryptedArgument.Decrypt(key); } return argument; } } }