mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-01-03 14:23:40 +01:00
197 lines
7.7 KiB
C#
197 lines
7.7 KiB
C#
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 static void Test()
|
|
{
|
|
//
|
|
}
|
|
|
|
public void Launch(string exePath, string? commandLine)
|
|
{
|
|
commandLine = commandLine ?? "";
|
|
|
|
|
|
//throw new NotImplementedException("TODO");
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="pid"></param>
|
|
/// <exception cref="BootstrapException">
|
|
/// Thrown when it could not relaunch FINAL FANTASY XIV or inject Dalamud.
|
|
/// </exception>
|
|
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 = GameProcess.Open(pid);
|
|
|
|
var exePath = process.GetImageFilePath();
|
|
|
|
var argument = process.GetGameArguments();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var encryptedArgument = EncryptArgument(argument.ToString());
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
//process.Terminate();
|
|
}
|
|
|
|
private static string EncryptArgument(string argument)
|
|
{
|
|
// for testing purpose
|
|
return argument;
|
|
|
|
//
|
|
// var tick = (uint)Environment.TickCount;
|
|
// var key = tick & 0xFFFF_0000; // only the high nibble is used
|
|
|
|
// var encryptedArgument = new EncryptedArgument(argument, key);
|
|
// return encryptedArgument.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Injects Dalamud into the process. See remarks for process state prerequisites.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// TODO: CREATE_SUSPENDED -> entrypoint explainations
|
|
/// </remarks>
|
|
/// <param name="pid">A process id to inject Dalamud into.</param>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recovers a key used in encrypting process arguments.
|
|
/// </summary>
|
|
/// <returns>A key recovered from the time when the process was created.</returns>
|
|
/// <remarks>
|
|
/// 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!)
|
|
/// </remarks>
|
|
private uint GetArgumentEncryptionKey()
|
|
{
|
|
var createdTime = m_process.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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads command-line arguments from the game and decrypts them if necessary.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// Command-line arguments that looks like this:
|
|
/// /DEV.TestSID =ABCD /UserPath =C:\Examples
|
|
/// </returns>
|
|
public string GetGameArguments()
|
|
{
|
|
var processArguments = m_process.GetProcessArguments();
|
|
|
|
// arg[0] is a path to exe(normally), arg[1] is actual stuff.
|
|
if (processArguments.Length < 2)
|
|
{
|
|
throw new ProcessException($"Process {m_process.GetProcessId()} only have {processArguments.Length} arguments. It must have atleast 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;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|