using System; using System.IO; using System.IO.Pipes; using System.Text.RegularExpressions; using CoreHook.BinaryInjection.RemoteInjection; using CoreHook.BinaryInjection.RemoteInjection.Configuration; using CoreHook.IPC.Platform; using Dalamud.Bootstrap.SqexArg; namespace Dalamud.Bootstrap { public sealed class Bootstrapper { private readonly BootstrapperOptions m_options; public Bootstrapper(BootstrapperOptions options) { m_options = options; } public void Launch(string exePath, string? commandLine) { commandLine = commandLine ?? ""; //throw new NotImplementedException("TODO"); } /// /// /// /// /// /// Thrown when it could not relaunch FINAL FANTASY XIV or inject Dalamud. /// public void Relaunch(uint pid) { // TODO // 1. Open process `pid` with handle // 2. Read command arguments (requires reading PEB) // 3. Construct new arguments // 3.1 Decrypt arguments acquired from step.2 (possible key space is very small so it's feasible to do this) // 3.2 Manipulate arguments as needed // 3.3 Re-encrypt arguments with new timestamp // 4 Launch a new process with new argument which was computed from step.3 // 4.1 Create process with CREATE_SUSPENDED // 4.2 Figure out entry-point of ffxiv_dx11.exe // 4.3 Insert a hook on entry-point to wait for user-mode process initialization to be finished, but not the code from ffxiv_dx11.exe // - This can be implemented in a such way that constantly checking program counter from `GetThreadContext` returns a value we expect. // Before you might ask: Yes, this is not the cleanest method I could come up with per se, but hey it gives far less headache to actually implement! // 5 Attempt to inject into that process. // 6. If all succeeded, terminate the old process. // // delegate Step 3 to 5 to Launch() maybe? // Acquire the process handle and read the command line using var process = Process.Open(pid); var exePath = process.GetImageFilePath(); var argument = ReadArgumentFromProcess(process); var newTick = (uint)Environment.TickCount; var newKey = newTick & 0xFFFF_0000; // only the high nibble is used var newArgument = argument .Remove("T") .Add("T", $"{newTick}") .ToString(); var encryptedArgument = new EncryptedArgument(newArgument, newKey); // TODO: launch new exe with the argument from encryptedArgument.ToString() // TODO: we also need to figure out where the exe is located // This is just for poc purpose. System.Diagnostics.Process.Start(exePath, encryptedArgument.ToString()); process.Terminate(); } private static uint RecoverKey(Process gameProcess) { var createdTime = gameProcess.GetCreationTime(); var currentDt = DateTime.Now; var currentTick = Environment.TickCount; var delta = currentDt - createdTime; var createdTick = (uint)currentTick - (uint)delta.TotalMilliseconds; // only the high nibble is used. return createdTick & 0xFFFF_0000; } private static ArgumentBuilder ReadArgumentFromProcess(Process process) { var arguments = process.ReadArguments(); if (arguments.Length < 2) { throw new BootstrapException($"Process id {process.GetPid()} does not have any arguments to parse."); } var argument = arguments[1]; if (EncryptedArgument.TryParse(argument, out var encryptedArgument)) { var key = RecoverKey(process); argument = encryptedArgument.Decrypt(key); } return ArgumentBuilder.Parse(argument); } /// /// Injects Dalamud into the process. See remarks for process state prerequisites. /// /// /// TODO: CREATE_SUSPENDED -> entrypoint explainations /// /// A process id to inject Dalamud into. public void Inject(uint pid) { // Please keep in mind that this config values (especially ClrRootPath) assumes that // Dalamud is compiled as self-contained to avoid requiring people to pre-install specific .NET Core version. // https://docs.microsoft.com/en-us/dotnet/core/deploying/ var corehookConfig = new RemoteInjectorConfiguration { InjectionPipeName = $"Dalamud-{pid}-CoreHook", ClrRootPath = m_options.BinaryDirectory, // `dotnet.runtimeconfig.json` is not needed for self-contained app. ClrBootstrapLibrary = Path.Combine(m_options.BinaryDirectory, "CoreHook.CoreLoad.dll"), DetourLibrary = Path.Combine(m_options.BinaryDirectory, "corehook64.dll"), HostLibrary = Path.Combine(m_options.BinaryDirectory, "coreload64.dll"), PayloadLibrary = Path.Combine(m_options.BinaryDirectory, "Dalamud.dll"), VerboseLog = false, }; try { RemoteInjector.Inject((int)pid, corehookConfig, new PipePlatform(), m_options.RootDirectory); } catch (Exception ex) { var exMessage = $"Failed to inject Dalamud library into the process id {pid}."; // Could not inject Dalamud for whatever reason; it could be process is not actually running, insufficient os privilege, or whatever the thing SE put in their game; // Therefore there's not much we can do on this side; You have to trobleshoot by yourself somehow. throw new BootstrapException(exMessage, ex); } } } internal sealed class PipePlatform : IPipePlatform { public NamedPipeServerStream CreatePipeByName(string pipeName, string serverName = ".") { return new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous, 0x10000, 0x10000); } } }