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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection.PortableExecutable;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Win32.SafeHandles;
using XFEEDToolBox.Core.Models;

namespace XFEEDToolBox.Core.Plugins;

/// <summary>Loads a hash-checked, separately versioned IPC extension without restarting the game.</summary>
public static class RuntimeInteractionModule
{
    private sealed record Manifest(int Schema,string Name,string File,string Sha256,string GameSha256);
    private static readonly SemaphoreSlim Gate=new(1,1);
    public static async Task LoadAsync(int processId,string gameHash,int owningThread,string packageDirectory,CancellationToken token)
    {
        if(!OperatingSystem.IsWindows()||!Environment.Is64BitProcess)throw new PlatformNotSupportedException("游戏交互扩展需要 Windows x64。");
        if(owningThread<=0||gameHash.Length!=64)throw new InvalidDataException("游戏交互身份不完整。");
        await Gate.WaitAsync(token);
        try
        {
            var manifest=JsonSerializer.Deserialize<Manifest>(await System.IO.File.ReadAllTextAsync(Path.Combine(packageDirectory,"interaction.json"),token),ToolboxSettings.JsonOptions);
            if(manifest is not {Schema:1,Name:"XFEEDToolBox.Interaction",File:"XFEEDInteraction.dll"}||manifest.GameSha256!=gameHash||manifest.Sha256.Length!=64)throw new InvalidDataException("游戏交互扩展清单不匹配。");
            var source=Path.GetFullPath(Path.Combine(packageDirectory,manifest.File));
            static async Task<string> Hash(string path,CancellationToken token){await using var file=System.IO.File.OpenRead(path);return Convert.ToHexStringLower(await SHA256.HashDataAsync(file,token));}
            if(await Hash(source,token)!=manifest.Sha256)throw new InvalidDataException("游戏交互扩展校验失败。");
            using var game=Process.GetProcessById(processId);var start=game.StartTime.ToUniversalTime();var image=game.MainModule?.FileName;
            if(image is null||!Path.GetFileName(image).Equals("EliteDangerous64.exe",StringComparison.OrdinalIgnoreCase)||await Hash(image,token)!=gameHash)throw new InvalidDataException("运行的游戏构建不匹配。");
            // Versioned paths keep loaded modules immutable across toolbox updates.
            var directory=Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEEDToolBox","Interaction",manifest.Sha256);
            Directory.CreateDirectory(directory);var module=Path.Combine(directory,manifest.File);
            if(System.IO.File.Exists(module)){if(await Hash(module,token)!=manifest.Sha256)throw new InvalidDataException("已缓存的交互扩展已变更。");}
            else System.IO.File.Copy(source,module,false);
            // A packaged host can virtualise LocalAppData while a Steam-launched
            // game runs outside that package. Send the actual backing path to
            // the other process, rather than a host-only logical cache path.
            module=PhysicalModulePath(module);
            if(await Hash(module,token)!=manifest.Sha256)throw new InvalidDataException("实际交互扩展文件校验失败。");
            game.Refresh();if(game.HasExited||game.StartTime.ToUniversalTime()!=start)throw new InvalidOperationException("选定的游戏进程已变更。");
            var loaded=game.Modules.Cast<ProcessModule>().SingleOrDefault(m=>string.Equals(m.FileName,module,StringComparison.OrdinalIgnoreCase));
            using var handle=new Handle(OpenProcess(0x043A,false,processId));
            if(handle.IsInvalid)throw new Win32Exception(Marshal.GetLastWin32Error());
            if(loaded is null)
            {
                var local=NativeLibrary.Load("kernel32.dll");var function=NativeLibrary.GetExport(local,"LoadLibraryW");
                nint remoteFunction=0;
                using var self=Process.GetCurrentProcess();
                var owner=self.Modules.Cast<ProcessModule>().Single(m=>function>=m.BaseAddress&&function<m.BaseAddress+m.ModuleMemorySize);
                var remoteOwner=game.Modules.Cast<ProcessModule>().Single(m=>string.Equals(Path.GetFileName(m.FileName),Path.GetFileName(owner.FileName),StringComparison.OrdinalIgnoreCase));
                remoteFunction=remoteOwner.BaseAddress+(function-owner.BaseAddress);
                var text=Encoding.Unicode.GetBytes(module+'\0');var address=VirtualAllocEx(handle.Value,0,(nuint)text.Length,0x3000,0x04);
                if(address==0)throw new Win32Exception(Marshal.GetLastWin32Error());
                var mayFree=true;
                try
                {
                    if(!WriteProcessMemory(handle.Value,address,text,(nuint)text.Length,out var written)||written!=(nuint)text.Length)throw new Win32Exception(Marshal.GetLastWin32Error());
                    // After dispatch, always wait for the loader before freeing its argument.
                    // No cancellation can free a path still being read by the game loader.
                    token.ThrowIfCancellationRequested();
                    try{_ = await CallAsync(handle.Value,remoteFunction,address);}
                    catch(TimeoutException){mayFree=false;throw;}
                }
                finally{if(mayFree)VirtualFreeEx(handle.Value,address,0,0x8000);NativeLibrary.Free(local);}
                game.Refresh();loaded=game.Modules.Cast<ProcessModule>().SingleOrDefault(m=>string.Equals(m.FileName,module,StringComparison.OrdinalIgnoreCase));
                if(loaded is null)throw new InvalidOperationException("游戏未加载交互扩展。");
            }
            if(game.HasExited||game.StartTime.ToUniversalTime()!=start)throw new InvalidOperationException("游戏进程已结束。");
            var export=ExportRva(module,"XFEInteractionStart");
            if(await CallAsync(handle.Value,loaded.BaseAddress+export,owningThread)!=1)throw new InvalidOperationException("游戏交互扩展初始化失败。");
        }
        finally{Gate.Release();}
    }
    private static string PhysicalModulePath(string path)
    {
        using var file=System.IO.File.OpenRead(path);var buffer=new StringBuilder(32768);
        var length=GetFinalPathNameByHandle(file.SafeFileHandle,buffer,(uint)buffer.Capacity,0);
        if(length==0)throw new Win32Exception(Marshal.GetLastWin32Error());
        if(length>=buffer.Capacity)throw new IOException("交互扩展实际路径过长。");
        var resolved=buffer.ToString();
        if(resolved.StartsWith(@"\\?\UNC\",StringComparison.OrdinalIgnoreCase))resolved=@"\\"+resolved[8..];
        else if(resolved.StartsWith(@"\\?\",StringComparison.Ordinal))resolved=resolved[4..];
        if(!Path.IsPathFullyQualified(resolved)||!Path.GetFileName(resolved).Equals("XFEEDInteraction.dll",StringComparison.OrdinalIgnoreCase))
            throw new InvalidDataException("交互扩展实际文件身份无效。");
        return resolved;
    }
    private static int ExportRva(string path,string name)
    {
        using var file=System.IO.File.OpenRead(path);using var pe=new PEReader(file);var export=pe.PEHeaders.PEHeader?.ExportTableDirectory??throw new InvalidDataException("交互模块缺少 PE 头。");
        var table=pe.GetSectionData(export.RelativeVirtualAddress).GetContent(0,40).ToArray();
        var count=BitConverter.ToInt32(table,24);if(count is <1 or >256)throw new InvalidDataException("交互模块导出表无效。");
        var names=pe.GetSectionData(BitConverter.ToInt32(table,32)).GetContent(0,count*4).ToArray();var ordinals=pe.GetSectionData(BitConverter.ToInt32(table,36)).GetContent(0,count*2).ToArray();
        for(var i=0;i<count;i++)
        {
            var bytes=pe.GetSectionData(BitConverter.ToInt32(names,i*4)).GetContent(0,Math.Min(128,pe.GetSectionData(BitConverter.ToInt32(names,i*4)).Length)).ToArray();var end=Array.IndexOf(bytes,(byte)0);
            if(end<0||Encoding.ASCII.GetString(bytes,0,end)!=name)continue;
            var ordinal=BitConverter.ToUInt16(ordinals,i*2);var functions=BitConverter.ToInt32(table,28);
            var rva=BitConverter.ToInt32(pe.GetSectionData(functions+ordinal*4).GetContent(0,4).ToArray());
            if(rva<=0||rva>=export.RelativeVirtualAddress&&rva<export.RelativeVirtualAddress+export.Size)throw new InvalidDataException("不接受转发的交互模块入口。");return rva;
        }
        throw new InvalidDataException("交互模块缺少启动入口。");
    }
    private static async Task<uint> CallAsync(nint process,nint function,nint argument)
    {
        using var thread=new Handle(CreateRemoteThread(process,0,0,function,argument,0,out _));if(thread.IsInvalid)throw new Win32Exception(Marshal.GetLastWin32Error());
        var clock=Stopwatch.StartNew();
        while(true){var state=WaitForSingleObject(thread.Value,0);if(state==0)break;if(state!=258)throw new Win32Exception(Marshal.GetLastWin32Error());if(clock.Elapsed>TimeSpan.FromSeconds(10))throw new TimeoutException("游戏加载交互扩展超时;保留仍在使用的参数。");await Task.Delay(20);}
        if(!GetExitCodeThread(thread.Value,out var code))throw new Win32Exception(Marshal.GetLastWin32Error());return code;
    }
    private sealed class Handle(nint value):IDisposable {public nint Value=>value;public bool IsInvalid=>value==0||value==new nint(-1);public void Dispose(){if(!IsInvalid)CloseHandle(value);}}
    [DllImport("kernel32",SetLastError=true)]private static extern nint OpenProcess(uint access,bool inherit,int processId);
    [DllImport("kernel32",CharSet=CharSet.Unicode,SetLastError=true)]private static extern uint GetFinalPathNameByHandle(SafeFileHandle file,StringBuilder path,uint length,uint flags);
    [DllImport("kernel32",SetLastError=true)]private static extern bool CloseHandle(nint handle);
    [DllImport("kernel32",SetLastError=true)]private static extern nint VirtualAllocEx(nint process,nint address,nuint size,uint allocation,uint protect);
    [DllImport("kernel32",SetLastError=true)]private static extern bool VirtualFreeEx(nint process,nint address,nuint size,uint freeType);
    [DllImport("kernel32",SetLastError=true)]private static extern bool WriteProcessMemory(nint process,nint address,byte[] bytes,nuint count,out nuint written);
    [DllImport("kernel32",SetLastError=true)]private static extern nint CreateRemoteThread(nint process,nint attributes,nuint stack,nint function,nint argument,uint flags,out uint threadId);
    [DllImport("kernel32",SetLastError=true)]private static extern uint WaitForSingleObject(nint handle,uint milliseconds);
    [DllImport("kernel32",SetLastError=true)]private static extern bool GetExitCodeThread(nint thread,out uint exitCode);
}