using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using XFEWarframe.Client; using XFEWarframe.Client.Runtime; namespace XFEWarframe.Bridge; public static unsafe class Bootstrap { private static ClientConfiguration config = new(); private static string logFile = ""; private static readonly object LogLock = new(); private static readonly List Hooks = []; private static nint post0, post1, get0, get1, rsa, createString, freeString; private static nint assetDownloader; private static int initialized; private static readonly RuntimeLogBuffer RuntimeLogs = new(); private static RuntimeControlServer? runtime; [UnmanagedCallersOnly(EntryPoint = "XfeSelfTest", CallConvs = [typeof(CallConvStdcall)])] public static uint SelfTest(nint unused) { var memory = Native.VirtualAlloc(0, 4096, 0x3000, 0x40); if (memory == 0) return 1; NativeHook? hook = null; try { // Read a RIP-relative constant outside the overwritten prologue, then return it + 1. var code = Enumerable.Repeat((byte)0x90, 64).ToArray(); new byte[] { 0x8B,0x05,26,0,0,0,0x83,0xC0,1,0xC3 }.CopyTo(code, 0); BitConverter.TryWriteBytes(code.AsSpan(32), 41); NativeHook.Write(memory, code); if (((delegate* unmanaged)memory)() != 42) return 2; hook = new NativeHook(memory, (nint)(delegate* unmanaged)&SelfTestTarget); hook.Enable(); if (((delegate* unmanaged)memory)() != 99) return 3; if (((delegate* unmanaged)hook.Trampoline)() != 42) return 4; hook.Rollback(); hook = null; if (((delegate* unmanaged)memory)() != 42) return 5; // A real CALL relocation must preserve its return address and original target. byte[] caller = [0x48,0x83,0xEC,0x28,0xE8,23,0,0,0,0x48,0x83,0xC4,0x28,0xC3]; NativeHook.Write(memory + 64, [0xB8,42,0,0,0,0xC3]); NativeHook.Write(memory + 32, caller); if (((delegate* unmanaged)(memory + 32))() != 42) return 7; hook = NativeHook.RedirectCall(memory + 36, (nint)(delegate* unmanaged)&SelfTestTarget); hook.Enable(); if (((delegate* unmanaged)(memory + 32))() != 99 || ((delegate* unmanaged)hook.Trampoline)() != 42) return 8; hook.Rollback(); hook = null; if (((delegate* unmanaged)(memory + 32))() != 42) return 9; NativeHook.Write(memory + 128, [0x48,0x89,0xC8,0x48,0x01,0xD0,0x4C,0x01,0xC0,0x4C,0x01,0xC8,0xC3,0x90,0x90,0x90]); observedArguments = 0; hook = NativeHook.Observe(memory + 128, (nint)(delegate* unmanaged)&SelfTestObserver); hook.Enable(); if (((delegate* unmanaged)(memory + 128))(1,2,3,4) != 10 || observedArguments != 10) return 30; hook.Rollback(); hook = null; return ExperimentalAbilityRuntime.SelfTest(); } catch { return 6; } finally { hook?.Rollback(); Native.VirtualFree(memory, 0, 0x8000); } } [UnmanagedCallersOnly] private static int SelfTestTarget() => 99; private static nint observedArguments; [UnmanagedCallersOnly] private static void SelfTestObserver(nint a, nint b, nint c, nint d) => observedArguments = a + b + c + d; [UnmanagedCallersOnly(EntryPoint = "XfeInitialize", CallConvs = [typeof(CallConvStdcall)])] public static uint Initialize(nint configurationPath) { if (Interlocked.Exchange(ref initialized, 1) != 0) return 2; try { var path = Marshal.PtrToStringUni(configurationPath) ?? throw new InvalidDataException("Missing configuration path."); var root = Path.GetDirectoryName(path)!; Directory.CreateDirectory(Path.Combine(root, "logs")); logFile = Path.Combine(root, "logs", $"client-{DateTime.UtcNow:yyyyMMdd-HHmmss}-{Environment.ProcessId}.log"); config = JsonSerializer.Deserialize(File.ReadAllText(path), ClientJson.Default.ClientConfiguration) ?? throw new InvalidDataException("Empty configuration."); if (config.SchemaVersion != 1) throw new InvalidDataException("Unsupported configuration schema."); config.ServerUrl = ClientProtocol.NormalizeServer(config.ServerUrl); config.GameServerUrl = ClientProtocol.NormalizeServer(config.GameServerUrl); if (!config.GameServerUrl.StartsWith("https://", StringComparison.Ordinal)) throw new InvalidDataException("The game connection requires HTTPS. Update the launcher and server."); if (!string.Equals(Path.GetFileName(Environment.ProcessPath), "Warframe.x64.exe", StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("This component must be loaded by the Warframe launcher."); var image = Native.GetModuleHandle(null); foreach (var (name, rva) in config.Addresses) { var expected = Convert.FromHexString(config.Prologues[name]); if (rva < 0 || !new ReadOnlySpan((void*)(image + rva), expected.Length).SequenceEqual(expected)) throw new InvalidDataException("Executable signature changed: " + name); } nint Address(string name) => image + config.Addresses[name]; createString = Address("StringCreate"); freeString = Address("StringFree"); post0 = Hook("Post0", (nint)(delegate* unmanaged)&Post0); post1 = Hook("Post1", (nint)(delegate* unmanaged)&Post1); get0 = Hook("Get0", (nint)(delegate* unmanaged)&Get0); get1 = Hook("Get1", (nint)(delegate* unmanaged)&Get1); rsa = Hook("RsaEncrypt", (nint)(delegate* unmanaged)&RsaEncrypt); Hook("DownloadManifest", (nint)(delegate* unmanaged)&DownloadManifest); Hook("VerifyCertificate", (nint)(delegate* unmanaged)&AcceptCertificate); Hook("VerifyHost", (nint)(delegate* unmanaged)&AcceptHost); Hook("VerifyWorldState", (nint)(delegate* unmanaged)&AcceptWorldState); Hook("NrsAnalyze", (nint)(delegate* unmanaged)&SkipNrs); ExperimentalAbilityRuntime.Prepare(image, config, Hooks, Log); GameTestRuntime.Prepare(image, config, Hooks, Log); foreach (var hook in Hooks) hook.Enable(); NativeHook.Write(Address("LevelCap") + 6, [0xEB]); Log($"Ready | build={config.BuildLabel} | server={config.ServerUrl} | hooks={Hooks.Count}"); GameBranding.Start(config); try { runtime = new RuntimeControlServer(Path.GetDirectoryName(root)!, config, RuntimeLogs, value => GameBranding.OverlayDisabled = value.DisableOverlay, multiAbilitiesAvailable: ExperimentalAbilityRuntime.Available, gameControl: GameTestRuntime.Available ? GameTestRuntime.Dispatch : null); Log("Runtime API ready at " + runtime.Session.Endpoint); } catch (Exception ex) { Log("Runtime API unavailable: " + ex.GetType().Name); } return 0; nint Hook(string name, nint callback) { var hook = new NativeHook(Address(name), callback); Hooks.Add(hook); return hook.Trampoline; } } catch (Exception ex) { Log("Initialization failed: " + ex); foreach (var hook in Hooks.AsEnumerable().Reverse()) { try { hook.Rollback(); } catch { } } return 1; } } private static void Log(string message) { RuntimeLogs.Add(message); try { lock (LogLock) if (logFile.Length > 0) File.AppendAllText(logFile, $"{DateTime.UtcNow:O} {message}\n"); } catch { } } private static void Fail(Exception ex) { Log("Request processing failed: " + ex.GetType().Name + ": " + ex.Message); Native.ExitProcess(1); } [UnmanagedCallersOnly] private static int AcceptCertificate(nint context) => 1; [UnmanagedCallersOnly] private static int AcceptHost(nint a, nint b, nint c, nint d) => 0; [UnmanagedCallersOnly] private static int AcceptWorldState(nint a, nint b) => 1; [UnmanagedCallersOnly] private static void SkipNrs(nint instance) { } [UnmanagedCallersOnly] private static int DownloadManifest(nint a, nint b, nint c, nint d, nint e, nint f) { assetDownloader = a; Log("Using installed cache manifest."); return 0; } private static string RequestBuildLabel(string originalUrl) { var path = new Uri(originalUrl).AbsolutePath; if (!path.EndsWith("/login.php", StringComparison.OrdinalIgnoreCase) && !path.EndsWith("/worldState.php", StringComparison.OrdinalIgnoreCase)) return config.BuildLabel; if (assetDownloader == 0) throw new InvalidDataException("Game cache manifest has not initialized."); // AssetDownloader's virtual slot 7 is the same manifest-hash getter used by // the game's login version check. Validate the field accessor before reading it. var getter = *(nint*)(*(nint*)assetDownloader + 7 * sizeof(nint)); var code = new ReadOnlySpan((void*)getter, 8); if (code[0] != 0x48 || code[1] != 0x8D || code[2] != 0x81 || code[7] != 0xC3) throw new InvalidDataException("Unsupported cache manifest hash accessor."); var offset = System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(code[3..7]); if (offset is < 0x10 or > 0x4000) throw new InvalidDataException("Invalid cache manifest hash offset."); var hash = ((GameString*)(assetDownloader + offset))->Text(); return ClientProtocol.BuildIdentity(config.BuildLabel, hash); } [UnmanagedCallersOnly] private static int RsaEncrypt(int length, byte* from, byte* to, nint key, int padding) { if (length == 40 && padding == 4) { new Span(from, 40).Fill((byte)'A'); new Span(to, 128).Fill((byte)'A'); return 128; } return ((delegate* unmanaged)rsa)(length, from, to, key, padding); } [UnmanagedCallersOnly] private static void Post0(nint a, GameString* u, GameString* b, byte t, nint e, nint f) => Post(post0, a, u, b, t, e, f); [UnmanagedCallersOnly] private static void Post1(nint a, GameString* u, GameString* b, byte t, nint e, nint f) => Post(post1, a, u, b, t, e, f); [UnmanagedCallersOnly] private static void Get0(GameString* u, nint a, nint b) => Get(get0, u, a, b); [UnmanagedCallersOnly] private static void Get1(GameString* u, nint a, nint b) => Get(get1, u, a, b); private static void Post(nint original, nint a, GameString* url, GameString* body, byte type, nint e, nint f) { var newUrl = GameString.Empty; var newBody = GameString.Empty; try { var originalUrl = url->Text(); var target = ClientProtocol.Redirect(originalUrl, config.GameServerUrl, RequestBuildLabel(originalUrl)); Log("POST " + new Uri(target).AbsolutePath); // Never log query strings, credentials or request bodies. newUrl.Assign(Encoding.UTF8.GetBytes(target)); if (type == 2) { newBody.Assign(ClientProtocol.DecodeProtected(body->Data())); body = &newBody; type = 1; } ((delegate* unmanaged)original)(a, &newUrl, body, type, e, f); } catch (Exception ex) { Fail(ex); } finally { newUrl.Free(); newBody.Free(); } } private static void Get(nint original, GameString* url, nint a, nint b) { var newUrl = GameString.Empty; try { var originalUrl = url->Text(); var target = ClientProtocol.Redirect(originalUrl, config.GameServerUrl, RequestBuildLabel(originalUrl)); Log("GET " + new Uri(target).AbsolutePath); newUrl.Assign(Encoding.UTF8.GetBytes(target)); ((delegate* unmanaged)original)(&newUrl, a, b); } catch (Exception ex) { Fail(ex); } finally { newUrl.Free(); } } [StructLayout(LayoutKind.Sequential, Size = 16)] internal struct GameString { public fixed byte Buffer[16]; public static GameString Empty { get { var result = new GameString(); result.Buffer[15] = 15; return result; } } public ReadOnlySpan Data() { fixed (byte* buffer = Buffer) { var size = buffer[15] == 255 ? *(int*)(buffer + 8) & 0x0FFFFFFF : 15 - buffer[15]; if (size < 0 || size > 32 * 1024 * 1024) throw new InvalidDataException("Invalid game string length."); return new ReadOnlySpan(buffer[15] == 255 ? *(byte**)buffer : buffer, size); } } public string Text() => Encoding.UTF8.GetString(Data()); // Use only for strings owned by this component, after the engine has copied them. public void ClearAndFree() { var length = Data().Length; fixed (byte* buffer = Buffer) System.Security.Cryptography.CryptographicOperations.ZeroMemory(new Span(buffer[15] == 255 ? *(byte**)buffer : buffer, length)); Free(); } public void Assign(byte[] data) { var spaces = new byte[data.Length + 1]; spaces.AsSpan(0, data.Length).Fill((byte)' '); fixed (byte* buffer = Buffer) fixed (byte* input = spaces) { ((delegate* unmanaged)createString)(buffer, input); data.CopyTo(new Span(buffer[15] == 255 ? *(byte**)buffer : buffer, data.Length)); } } public void Free() { fixed (byte* buffer = Buffer) { if (buffer[15] == 255 && (*(ulong*)(buffer + 8) & 0xFFFFFFF0000000UL) != 0xFFFFFFF0000000UL) ((delegate* unmanaged)freeString)(*(nint*)buffer); buffer[15] = 15; } } } }