XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEWarframe

【WinUI】基于Warframe游戏本体的公开服务器以及安装器

公开
关注 0 Fork 0 Star 0
UTF-8
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<NativeHook> 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<int>)memory)() != 42) return 2;
            hook = new NativeHook(memory, (nint)(delegate* unmanaged<int>)&SelfTestTarget);
            hook.Enable();
            if (((delegate* unmanaged<int>)memory)() != 99) return 3;
            if (((delegate* unmanaged<int>)hook.Trampoline)() != 42) return 4;
            hook.Rollback(); hook = null;
            if (((delegate* unmanaged<int>)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<int>)(memory + 32))() != 42) return 7;
            hook = NativeHook.RedirectCall(memory + 36, (nint)(delegate* unmanaged<int>)&SelfTestTarget);
            hook.Enable();
            if (((delegate* unmanaged<int>)(memory + 32))() != 99 || ((delegate* unmanaged<int>)hook.Trampoline)() != 42) return 8;
            hook.Rollback(); hook = null;
            if (((delegate* unmanaged<int>)(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<nint, nint, nint, nint, void>)&SelfTestObserver);
            hook.Enable();
            if (((delegate* unmanaged<nint, nint, nint, nint, nint>)(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<byte>((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<nint, GameString*, GameString*, byte, nint, nint, void>)&Post0);
            post1 = Hook("Post1", (nint)(delegate* unmanaged<nint, GameString*, GameString*, byte, nint, nint, void>)&Post1);
            get0 = Hook("Get0", (nint)(delegate* unmanaged<GameString*, nint, nint, void>)&Get0);
            get1 = Hook("Get1", (nint)(delegate* unmanaged<GameString*, nint, nint, void>)&Get1);
            rsa = Hook("RsaEncrypt", (nint)(delegate* unmanaged<int, byte*, byte*, nint, int, int>)&RsaEncrypt);
            Hook("DownloadManifest", (nint)(delegate* unmanaged<nint, nint, nint, nint, nint, nint, int>)&DownloadManifest);
            Hook("VerifyCertificate", (nint)(delegate* unmanaged<nint, int>)&AcceptCertificate);
            Hook("VerifyHost", (nint)(delegate* unmanaged<nint, nint, nint, nint, int>)&AcceptHost);
            Hook("VerifyWorldState", (nint)(delegate* unmanaged<nint, nint, int>)&AcceptWorldState);
            Hook("NrsAnalyze", (nint)(delegate* unmanaged<nint, void>)&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<byte>((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<byte>(from, 40).Fill((byte)'A'); new Span<byte>(to, 128).Fill((byte)'A'); return 128;
        }
        return ((delegate* unmanaged<int, byte*, byte*, nint, int, int>)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<nint, GameString*, GameString*, byte, nint, nint, void>)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<GameString*, nint, nint, void>)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<byte> 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<byte>(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<byte>(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<byte*, byte*, void>)createString)(buffer, input);
                data.CopyTo(new Span<byte>(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<nint, void>)freeString)(*(nint*)buffer);
                buffer[15] = 15;
            }
        }
    }
}