diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 7e7a8dd1..4b715a8e 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -24,7 +24,7 @@ jobs: strategy: matrix: - targetplatform: [x86] + targetplatform: [x64] runs-on: windows-latest @@ -60,6 +60,7 @@ jobs: uses: actions/setup-dotnet@v5 with: dotnet-version: '10.x' + architecture: x64 - name: Setup MSBuild.exe uses: microsoft/setup-msbuild@v2.0.0 @@ -72,9 +73,7 @@ jobs: dotnet user-secrets --project Daybreak\Daybreak.csproj set ApmUri "${{ secrets.ApmUri }}" - name: Restore project - run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration /p:RuntimeIdentifier=$env:RuntimeIdentifier --property:SolutionDir=$GITHUB_WORKSPACE - env: - RuntimeIdentifier: win-${{ matrix.targetplatform }} + run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration --property:SolutionDir=$GITHUB_WORKSPACE - name: Build Daybreak project run: dotnet build Daybreak -c $env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE @@ -85,9 +84,7 @@ jobs: echo "::set-env name=Version::$version" - name: Create publish launcher files - run: dotnet publish .\Daybreak\Daybreak.csproj -c $env:Configuration -r $env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=false --self-contained true -o .\Publish - env: - RuntimeIdentifier: win-${{ matrix.targetplatform }} + run: dotnet publish .\Daybreak\Daybreak.csproj -c $env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=false --self-contained true -o .\Publish - name: Pack publish files run: | diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a34180dc..e69c3a5b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,7 +20,7 @@ jobs: strategy: matrix: - targetplatform: [x86] + targetplatform: [x64] runs-on: windows-latest @@ -41,13 +41,12 @@ jobs: uses: actions/setup-dotnet@v5 with: dotnet-version: '10.x' - architecture: x86 + architecture: x64 - name: Setup MSBuild.exe uses: microsoft/setup-msbuild@v2.0.0 - name: Restore the Wpf application to populate the obj folder - run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration /p:RuntimeIdentifier=$env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE + run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE env: - Configuration: Debug - RuntimeIdentifier: win-${{ matrix.targetplatform }} \ No newline at end of file + Configuration: Debug \ No newline at end of file diff --git a/Daybreak.7ZipExtractor/Daybreak.7ZipExtractor.csproj b/Daybreak.7ZipExtractor/Daybreak.7ZipExtractor.csproj deleted file mode 100644 index f865d331..00000000 --- a/Daybreak.7ZipExtractor/Daybreak.7ZipExtractor.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - $(TargetFrameworkVersion) - Daybreak._7ZipExtractor - enable - x64 - - - - - - - diff --git a/Daybreak.7ZipExtractor/Program.cs b/Daybreak.7ZipExtractor/Program.cs deleted file mode 100644 index 0397464a..00000000 --- a/Daybreak.7ZipExtractor/Program.cs +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Extractor is in a different executable because of memory limitations. Daybreak needs to be compiled for x86 and as such has a memory limit of 2GB. - * 7Zip algorithm may require more than 2GB memory to unpack large zip files. - * As such, Daybreak.7ZipExtractor is x64. - */ -using SharpCompress.Archives; - -if (args.Length != 2) -{ - return -1; -} - -var archivePath = args[0]; -var destinationDirectory = args[1]; - -using var fileStream = new FileStream(archivePath, FileMode.Open); -using var archive = ArchiveFactory.Open(fileStream); -var progress = 0d; -var count = archive.Entries.Count(); -var reader = archive.ExtractAllEntries(); -while (reader.MoveToNextEntry()) -{ - var entry = reader.Entry; - if (entry.Key is null) - { - continue; - } - - if (entry.IsDirectory) - { - var directoryName = Path.Combine(Path.GetFullPath(destinationDirectory), entry.Key); - Directory.CreateDirectory(directoryName); - continue; - } - - var fileName = Path.Combine(Path.GetFullPath(destinationDirectory), entry.Key); - using var entryStream = new FileStream(fileName, FileMode.Create); - using (var stream = reader.OpenEntryStream()) - { - stream.CopyTo(entryStream); - } - - progress += 1d / count; - Console.WriteLine($"{progress} {fileName}"); -} - -return 0; diff --git a/Daybreak.Injector/Daybreak.Injector.csproj b/Daybreak.Injector/Daybreak.Injector.csproj new file mode 100644 index 00000000..e7767811 --- /dev/null +++ b/Daybreak.Injector/Daybreak.Injector.csproj @@ -0,0 +1,23 @@ + + + + Exe + $(TargetFrameworkVersionWindows) + enable + enable + x86 + true + win-x86 + true + true + + + + + + + + + + + diff --git a/Daybreak.Injector/ProcessInjector.cs b/Daybreak.Injector/ProcessInjector.cs new file mode 100644 index 00000000..088bba52 --- /dev/null +++ b/Daybreak.Injector/ProcessInjector.cs @@ -0,0 +1,138 @@ +using Daybreak.Shared.Models; +using Daybreak.Shared.Utils; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace Daybreak.Injector; + +public static class ProcessInjector +{ + public static InjectorResponses.InjectResult InjectWithApi(Process process, string pathToDll) + { + var modulefullpath = Path.GetFullPath(pathToDll); + + if (!File.Exists(modulefullpath)) + { + Console.WriteLine("Dll to inject not found"); + return InjectorResponses.InjectResult.InvalidDllPath; + } + + var hKernel32 = NativeMethods.GetModuleHandle("kernel32.dll"); + if (hKernel32 == IntPtr.Zero) + { + Console.WriteLine("Unable to get a handle of kernel32.dll"); + return InjectorResponses.InjectResult.InvalidKernel32; + } + + var hLoadLib = NativeMethods.GetProcAddress(hKernel32, "LoadLibraryW"); + if (hLoadLib == IntPtr.Zero) + { + Console.WriteLine("Unable to get the address of LoadLibraryW"); + return InjectorResponses.InjectResult.InvalidLoadLibraryW; + } + + var hStringBuffer = NativeMethods.VirtualAllocEx(process.Handle, IntPtr.Zero, new IntPtr(2 * (modulefullpath.Length + 1)), + 0x3000 /* MEM_COMMIT | MEM_RESERVE */, 0x4 /* PAGE_READWRITE */); + if (hStringBuffer == IntPtr.Zero) + { + Console.WriteLine("Unable to allocate memory for module path"); + return InjectorResponses.InjectResult.ModulePathAllocationFailed; + } + + WriteWString(process, hStringBuffer, modulefullpath); + if (ReadWString(process, hStringBuffer, 260) != modulefullpath) + { + Console.WriteLine("Module path string is not correct"); + return InjectorResponses.InjectResult.DllPathWriteFailed; + } + + var hThread = NativeMethods.CreateRemoteThread(process.Handle, IntPtr.Zero, 0, hLoadLib, hStringBuffer, 0, out _); + if (hThread == IntPtr.Zero) + { + Console.WriteLine("Unable to create remote thread"); + return InjectorResponses.InjectResult.CreateRemoteThreadFailed; + } + + var threadResult = NativeMethods.WaitForSingleObject(hThread, 30000u); + if (threadResult is 0x102 or 0xFFFFFFFF /* WAIT_FAILED */) + { + Console.WriteLine($"Exception occurred while waiting for the remote thread. Result is {threadResult}"); + return InjectorResponses.InjectResult.RemoteThreadTimeout; + } + + var dllResult = NativeMethods.GetExitCodeThread(hThread, out _); + if (dllResult == 0) + { + Console.WriteLine($"Injected dll returned non-success status code {dllResult}"); + return InjectorResponses.InjectResult.DllExitCodeFailed; + } + + var memoryFreeResult = NativeMethods.VirtualFreeEx(process.Handle, hStringBuffer, 0, 0x8000 /* MEM_RELEASE */); + if (!memoryFreeResult) + { + Console.WriteLine($"Failed to free dll memory"); + } + + return memoryFreeResult + ? InjectorResponses.InjectResult.Success + : InjectorResponses.InjectResult.MemoryFreeFailed; + } + + private static void WriteBytes(Process process, IntPtr address, byte[] data) + { + var size = data.Length; + var buffer = Marshal.AllocHGlobal(size); + Marshal.Copy(data, 0, buffer, size); + + NativeMethods.WriteProcessMemory( + process.Handle, + address, + buffer, + size, + out _); + + Marshal.FreeHGlobal(buffer); + } + + private static void WriteWString(Process process, IntPtr address, string data) + { + WriteBytes(process, address, Encoding.Unicode.GetBytes(data)); + } + + private static string ReadWString(Process process, IntPtr address, int maxsize, Encoding? encoding = null) + { + encoding ??= Encoding.Unicode; + var rawbytes = ReadBytes(process, address, maxsize); + if (rawbytes.Length == 0) + { + return ""; + } + + var ret = encoding.GetString(rawbytes); + if (ret.Contains('\0')) + { + ret = ret[..ret.IndexOf('\0')]; + } + + return ret; + } + + private static byte[] ReadBytes(Process process, IntPtr address, int size) + { + var buffer = Marshal.AllocHGlobal(size); + + NativeMethods.ReadProcessMemory(process.Handle, + address, + buffer, + size, + out _ + ); + + var ret = new byte[size]; + Marshal.Copy(buffer, ret, 0, size); + Marshal.FreeHGlobal(buffer); + + return ret; + } +} diff --git a/Daybreak.Injector/ProcessLauncher.cs b/Daybreak.Injector/ProcessLauncher.cs new file mode 100644 index 00000000..a40a7f35 --- /dev/null +++ b/Daybreak.Injector/ProcessLauncher.cs @@ -0,0 +1,248 @@ +using Daybreak.Shared.Models; +using Daybreak.Shared.Utils; +using Microsoft.Win32; +using System.Diagnostics; +using System.Runtime.InteropServices; +using static Daybreak.Shared.Utils.NativeMethods; + +namespace Daybreak.Injector; + +public sealed class ProcessLauncher +{ + public static InjectorResponses.LaunchResult LaunchGuildWars( + string path, + string args, + bool elevated, + out int threadId, + out int processId) + { + var sw = Stopwatch.StartNew(); + Process? process; + processId = LaunchClient(path, string.Join(" ", args), elevated, out threadId); + if (processId is 0) + { + Console.WriteLine("Failed to launch GuildWars process."); + return InjectorResponses.LaunchResult.LaunchFailed; + } + + do + { + if (sw.Elapsed.TotalSeconds > 10) + { + Console.WriteLine("Timed out waiting for GuildWars process to start."); + return InjectorResponses.LaunchResult.LaunchTimeout; + } + + process = Process.GetProcessById(processId); + Thread.Sleep(100); + } while (process is null); + + if (!McPatch(process.Handle)) + { + var lastErr = Marshal.GetLastWin32Error(); + Console.WriteLine($"Failed to patch GuildWars process. Error code: {lastErr}"); + return InjectorResponses.LaunchResult.PatchFailed; + } + + return InjectorResponses.LaunchResult.Success; + } + + private static int LaunchClient(string path, string args, bool elevated, out int threadId) + { + var commandLine = $"\"{path}\" {args}"; + threadId = 0; + + ProcessInformation procinfo; + var startinfo = new StartupInfo + { + cb = Marshal.SizeOf() + }; + var saProcess = new SecurityAttributes(); + saProcess.nLength = (uint)Marshal.SizeOf(saProcess); + var saThread = new SecurityAttributes(); + saThread.nLength = (uint)Marshal.SizeOf(saThread); + + var lastDirectory = Directory.GetCurrentDirectory(); + Directory.SetCurrentDirectory(Path.GetDirectoryName(path)!); + + SetRegistryValue(@"Software\ArenaNet\Guild Wars", "Path", path); + SetRegistryValue(@"Software\ArenaNet\Guild Wars", "Src", path); + + if (!elevated) + { + if (!SaferCreateLevel(SaferLevelScope.User, SaferLevel.NormalUser, SaferOpen.Open, out var hLevel, IntPtr.Zero)) + { + Debug.WriteLine("SaferCreateLevel"); + return 0; + } + + if (!SaferComputeTokenFromLevel(hLevel, IntPtr.Zero, out var hRestrictedToken, 0, IntPtr.Zero)) + { + Debug.WriteLine("SaferComputeTokenFromLevel"); + return 0; + } + + SaferCloseLevel(hLevel); + + TokenMandatoryLabel tml; + tml.Label.Attributes = 0x20; // SE_GROUP_INTEGRITY + if (!ConvertStringSidToSid("S-1-16-8192", out tml.Label.Sid)) + { + CloseHandle(hRestrictedToken); + Debug.WriteLine("ConvertStringSidToSid"); + } + + if (!SetTokenInformation( + hRestrictedToken, + TokenInformationClass.TokenIntegrityLevel, + ref tml, + (uint)Marshal.SizeOf(tml) + GetLengthSid(tml.Label.Sid))) + { + LocalFree(tml.Label.Sid); + CloseHandle(hRestrictedToken); + return 0; + } + + LocalFree(tml.Label.Sid); + if (!CreateProcessAsUser( + hRestrictedToken, + null!, + commandLine, + ref saProcess, + ref saProcess, + false, + (uint)CreationFlags.CreateSuspended, + IntPtr.Zero, + null!, + ref startinfo, + out procinfo)) + { + var error = Marshal.GetLastWin32Error(); + Debug.WriteLine($"CreateProcessAsUser {error}"); + CloseHandle(procinfo.hThread); + return 0; + } + + CloseHandle(hRestrictedToken); + } + else + { + if (!CreateProcess( + null!, + commandLine, + ref saProcess, + ref saThread, + false, + (uint)CreationFlags.CreateSuspended, + IntPtr.Zero, + null!, + ref startinfo, + out procinfo)) + { + var error = Marshal.GetLastWin32Error(); + Debug.WriteLine($"CreateProcess {error}"); + _ = ResumeThread(procinfo.hThread); + CloseHandle(procinfo.hThread); + return 0; + } + } + + Directory.SetCurrentDirectory(lastDirectory); + + threadId = procinfo.dwThreadId; + CloseHandle(procinfo.hThread); + CloseHandle(procinfo.hProcess); + return procinfo.dwProcessId; + } + + /// + /// https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs + /// + private static bool McPatch(IntPtr processHandle) + { + byte[] sigPatch = + [ + 0x56, 0x57, 0x68, 0x00, 0x01, 0x00, 0x00, 0x89, 0x85, 0xF4, 0xFE, 0xFF, 0xFF, 0xC7, 0x00, 0x00, 0x00, 0x00, + 0x00 + ]; + var moduleBase = GetProcessModuleBase(processHandle); + var gwdata = new byte[0x48D000]; + + if (!NativeMethods.ReadProcessMemory(processHandle, moduleBase, gwdata, gwdata.Length, out _)) + { + return false; + } + + var idx = SearchBytes(gwdata, sigPatch); + + if (idx == -1) + { + return false; + } + + var mcpatch = moduleBase + idx - 0x1A; + + byte[] payload = [0x31, 0xC0, 0x90, 0xC3]; + + return NativeMethods.WriteProcessMemory(processHandle, mcpatch, payload, payload.Length, out _); + } + + /// + /// https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs + /// + private static int SearchBytes(Memory haystack, Memory needle) + { + var len = needle.Length; + var limit = haystack.Length - len; + for (var i = 0; i <= limit; i++) + { + var k = 0; + for (; k < len; k++) + { + if (needle.Span[k] != haystack.Span[i + k]) + { + break; + } + } + + if (k == len) + { + return i; + } + } + + return -1; + } + + /// + /// https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs + /// + private static IntPtr GetProcessModuleBase(IntPtr process) + { + if (NativeMethods.NtQueryInformationProcess(process, NativeMethods.ProcessInfoClass.ProcessBasicInformation, out var pbi, + Marshal.SizeOf(), out _) != 0) + { + return IntPtr.Zero; + } + + var buffer = new byte[Marshal.SizeOf()]; + + if (!NativeMethods.ReadProcessMemory(process, pbi.PebBaseAddress, buffer, Marshal.SizeOf(), out _)) + { + return IntPtr.Zero; + } + + PEB peb = new() + { + ImageBaseAddress = (IntPtr)BitConverter.ToInt32(buffer, 8) + }; + + return peb.ImageBaseAddress + 0x1000; + } + + private static void SetRegistryValue(string registryPath, string valueName, object newValue) + { + using var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(registryPath); + key.SetValue(valueName, newValue, RegistryValueKind.String); + } +} diff --git a/Daybreak.Injector/Program.cs b/Daybreak.Injector/Program.cs new file mode 100644 index 00000000..7c644713 --- /dev/null +++ b/Daybreak.Injector/Program.cs @@ -0,0 +1,239 @@ +using Daybreak.Injector; +using Daybreak.Shared.Models; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +static void PrintUsage() +{ + Console.WriteLine("======================================================"); + Console.WriteLine("Usage: Daybreak.Injector [mode] "); + Console.WriteLine("Modes: "); + Console.WriteLine("- winapi"); + Console.WriteLine("- stub"); + Console.WriteLine("- launch"); + Console.WriteLine("- resume"); + Console.WriteLine("Examples:"); + Console.WriteLine("1) Daybreak.Injector winapi 1234 C:\\path\\to\\dll.dll"); + Console.WriteLine("2) Daybreak.Injector stub 1234 entryPoint C:\\path\\to\\dll.dll"); + Console.WriteLine("3) Daybreak.Injector launch true C:\\path\\to\\dll.dll arg1 arg2 arg3"); + Console.WriteLine("4) Daybreak.Injector resume 1234"); + Console.WriteLine("======================================================"); +} + +static bool TryParseInjectWinApiArgs( + string[] args, + [NotNullWhen(true)] out Process? process, + [NotNullWhen(true)] out string? dllPath, + out InjectorResponses.InjectResult exitCode) +{ + process = default; + dllPath = default; + if (!int.TryParse(args[1], out var processId)) + { + PrintUsage(); + exitCode = InjectorResponses.InjectResult.InvalidProcess; + return false; + } + + if (args.Length < 3) + { + PrintUsage(); + exitCode = InjectorResponses.InjectResult.InvalidArgs; + return false; + } + + dllPath = Path.GetFullPath(args[2]); + process = Process.GetProcessById(processId); + if (process is null) + { + Console.WriteLine($"Process {processId} could not be found"); + exitCode = InjectorResponses.InjectResult.InvalidProcess; + return false; + } + + + if (!File.Exists(dllPath)) + { + Console.WriteLine($"DLL path {dllPath} could not be found"); + exitCode = InjectorResponses.InjectResult.InvalidDllPath; + return false; + } + + exitCode = 0; + return true; +} + +static bool TryParseInjectStubArgs( + string[] args, + [NotNullWhen(true)] out Process? process, + [NotNullWhen(true)] out string? dllPath, + [NotNullWhen(true)] out string? entryPoint, + out InjectorResponses.InjectResult exitCode) +{ + process = default; + dllPath = default; + entryPoint = default; + if (!int.TryParse(args[1], out var processId)) + { + PrintUsage(); + exitCode = InjectorResponses.InjectResult.InvalidProcess; + return false; + } + + if (args.Length < 4) + { + PrintUsage(); + exitCode = InjectorResponses.InjectResult.InvalidArgs; + return false; + } + + entryPoint = args[2]; + dllPath = Path.GetFullPath(args[3]); + process = Process.GetProcessById(processId); + if (process is null) + { + Console.WriteLine($"Process {processId} could not be found"); + exitCode = InjectorResponses.InjectResult.InvalidProcess; + return false; + } + + + if (!File.Exists(dllPath)) + { + Console.WriteLine($"DLL path {dllPath} could not be found"); + exitCode = InjectorResponses.InjectResult.InvalidDllPath; + return false; + } + + exitCode = 0; + return true; +} + +static bool TryParseLaunchArgs( + string[] args, + [NotNullWhen(true)] out string? gwPath, + [NotNullWhen(true)] out string? gwArgs, + [NotNullWhen(true)] out bool? elevated, + out InjectorResponses.LaunchResult exitCode) +{ + gwPath = default; + gwArgs = default; + elevated = default; + if (args.Length < 3) + { + PrintUsage(); + exitCode = InjectorResponses.LaunchResult.InvalidArgs; + return false; + } + + gwPath = Path.GetFullPath(args[2]); + if (!bool.TryParse(args[1], out var parsedElevated)) + { + PrintUsage(); + exitCode = InjectorResponses.LaunchResult.InvalidElevated; + return false; + } + + elevated = parsedElevated; + gwArgs = args.Length > 3 ? string.Join(" ", args.Skip(3)) : string.Empty; + + if (!File.Exists(gwPath)) + { + Console.WriteLine($"Guild Wars path {gwPath} could not be found"); + exitCode = InjectorResponses.LaunchResult.InvalidPath; + return false; + } + + exitCode = 0; + return true; +} + +static bool TryParseThreadResumeArgs( + string[] args, + [NotNullWhen(true)] out IntPtr? threadHwnd, + out InjectorResponses.ResumeResult exitCode) +{ + threadHwnd = default; + if (args.Length < 2) + { + PrintUsage(); + exitCode = InjectorResponses.ResumeResult.InvalidArgs; + return false; + } + + if (!IntPtr.TryParse(args[1], out var threadInt)) + { + PrintUsage(); + exitCode = InjectorResponses.ResumeResult.InvalidThreadHandle; + return false; + } + + threadHwnd = threadInt; + exitCode = InjectorResponses.ResumeResult.Success; + return true; +} + +if (args.Length < 1) +{ + PrintUsage(); + return (int)InjectorResponses.GenericResults.InvalidArgs; +} + +var mode = args[0]; + +switch (mode) +{ + case "winapi": + { + if (!TryParseInjectWinApiArgs(args, out var process, out var dllPath, out var parseResult)) + { + return (int)parseResult; + } + + Console.WriteLine($"Starting WinAPI injection. Process {process.Id}. DllPath: {dllPath}"); + return (int)ProcessInjector.InjectWithApi(process, dllPath); + } + case "stub": + { + if (!TryParseInjectStubArgs(args, out var process, out var dllPath, out var entryPoint, out var parseResult)) + { + return (int)parseResult; + } + + Console.WriteLine($"Starting stub injection. Process {process.Id}. EntryPoint {entryPoint}. DllPath: {dllPath}"); + var result = StubInjector.Inject(process, dllPath, entryPoint, out var exitCode); + if (result is not InjectorResponses.InjectResult.Success) + { + return (int)result; + } + + Console.WriteLine($"Stub returned {exitCode}"); + return exitCode; + } + case "launch": + { + if (!TryParseLaunchArgs(args, out var gwPath, out var gwArgs, out var elevated, out var parseResult)) + { + return (int)parseResult; + } + + Console.WriteLine($"Launching Guild Wars. Path {gwPath}. Elevated {elevated}. Args {gwArgs}"); + var result = ProcessLauncher.LaunchGuildWars(gwPath, gwArgs, elevated.Value, out var threadHandle, out var processId); + Console.WriteLine($"ThreadHandle: {threadHandle}"); + Console.WriteLine($"ProcessId: {processId}"); + return (int)result; + } + case "resume": + { + if (!TryParseThreadResumeArgs(args, out var threadHwnd, out var parseResult)) + { + return (int)parseResult; + } + + Console.WriteLine($"Resuming thread {threadHwnd.Value}"); + return (int) ThreadResumer.Resume(threadHwnd.Value); + } + default: + PrintUsage(); + return (int)InjectorResponses.GenericResults.InvalidMode; +} diff --git a/Daybreak.Injector/StubInjector.cs b/Daybreak.Injector/StubInjector.cs new file mode 100644 index 00000000..62f4ad1b --- /dev/null +++ b/Daybreak.Injector/StubInjector.cs @@ -0,0 +1,183 @@ +using Daybreak.Shared.Models; +using Daybreak.Shared.Utils; +using Reloaded.Assembler; +using System.Diagnostics; +using System.Text; + +namespace Daybreak.Injector; + +public static class StubInjector +{ + /// + /// ebp+8 is the dll path pointer + /// 0xDEADBEEF is a placeholder to be patched later for LoadLibraryA + /// 0xFEEDF00D is a placeholder to be patched later for GetProcAddress + /// + const string Asm = @" +use32 +push ebp +mov ebp, esp + +mov esi, [ebp+8] ; ESI = &INJECT_DATA (kept unchanged) + +; -------- LoadLibraryA(dllPath) ------------------------------ +push dword [esi] ; dllPath +mov eax, 0xDEADBEEF ; patched → LoadLibraryA +call eax ; EAX = hModule + +; -------- GetProcAddress(hModule, funcName) ------------------ +push dword [esi+4] ; funcName +push eax ; hModule (still in EAX) +mov eax, 0xFEEDF00D ; patched → GetProcAddress +call eax ; EAX = exported EntryPoint + +; -------- call EntryPoint() ---------------------------------- +call eax ; calls EntryPoint +; xor eax, eax ; thread exit-code 0 + +; -------- return EntryPoint() response ----------------------- +leave ; = mov esp, ebp / pop ebp +ret 4 ; stdcall: pop lpParameter +"; + + public static InjectorResponses.InjectResult Inject(Process target, string dllPath, string entryPoint, out int exitCode) + { + var hProcess = NativeMethods.OpenProcess( + NativeMethods.ProcessAccessFlags.All, false, (uint)target.Id); + + exitCode = 0; + if (hProcess is 0) + { + Console.WriteLine($"Failed to inject with stub. Could not open process by id {target.Id}"); + return InjectorResponses.InjectResult.InvalidProcess; + } + + using var assembler = new Assembler(); + var stubBytes = assembler.Assemble(Asm); + + // patch placeholders + var hKernel = NativeMethods.GetModuleHandle("kernel32.dll"); + if (hKernel is 0) + { + Console.WriteLine("Failed to inject with stub. Could not get handle of kernel32.dll"); + return InjectorResponses.InjectResult.InvalidKernel32; + } + + var pLoadLibA = NativeMethods.GetProcAddress(hKernel, "LoadLibraryA"); + if (pLoadLibA is 0) + { + Console.WriteLine("Failed to inject with stub. Could not get address of LoadLibraryA"); + return InjectorResponses.InjectResult.InvalidLoadLibraryA; + } + + var pGetProc = NativeMethods.GetProcAddress(hKernel, "GetProcAddress"); + if (pGetProc is 0) + { + Console.WriteLine("Failed to inject with stub. Could not get address of GetProcAddress"); + return InjectorResponses.InjectResult.InvalidGetProcAddress; + } + + Patch(stubBytes, 0xDEADBEEF, pLoadLibA); + Patch(stubBytes, 0xFEEDF00D, pGetProc); + + var dllBytes = Encoding.ASCII.GetBytes(dllPath + '\0'); + var funcBytes = Encoding.ASCII.GetBytes(entryPoint + '\0'); + var remoteDll = AllocWrite(hProcess, dllBytes, NativeMethods.MemoryProtection.PAGE_READWRITE); + if (remoteDll is 0) + { + Console.WriteLine("Failed to inject with stub. Could not allocate memory for dll path"); + return InjectorResponses.InjectResult.DllPathWriteFailed; + } + + var remoteFunc = AllocWrite(hProcess, funcBytes, NativeMethods.MemoryProtection.PAGE_READWRITE); + if (remoteFunc is 0) + { + Console.WriteLine("Failed to inject with stub. Could not allocate memory for function name"); + return InjectorResponses.InjectResult.FunctionNameWriteFailed; + } + + var injectData = new byte[IntPtr.Size * 2]; + BitConverter.GetBytes(remoteDll.ToInt32()).CopyTo(injectData, 0); + BitConverter.GetBytes(remoteFunc.ToInt32()).CopyTo(injectData, 4); + var remoteStruct = AllocWrite(hProcess, injectData, NativeMethods.MemoryProtection.PAGE_READWRITE); + if (remoteStruct is 0) + { + Console.WriteLine("Failed to inject with stub. Could not allocate memory for injectData"); + return InjectorResponses.InjectResult.InjectDataWriteFailed; + } + + var remoteStub = AllocWrite(hProcess, stubBytes, NativeMethods.MemoryProtection.PAGE_EXECUTE_READ_WRITE); + if (remoteStub is 0) + { + Console.WriteLine("Failed to inject with stub. Could not allocate memory for stub"); + return InjectorResponses.InjectResult.StubAllocationFailed; + } + + var hThread = NativeMethods.CreateRemoteThread( + hProcess, IntPtr.Zero, 0, + remoteStub, + remoteStruct, + 0, out _); + + if (hThread is 0) + { + Console.WriteLine("Failed to inject with stub. Could not create remote thread"); + return InjectorResponses.InjectResult.CreateRemoteThreadFailed; + } + + var waitResult = NativeMethods.WaitForSingleObject(hThread, 10000); // Wait up to 10 seconds + if (waitResult is 0) // WAIT_OBJECT_0 - thread completed + { + // Get the thread exit code, which will be our port number + if (NativeMethods.GetExitCodeThread(hThread, out var moduleExitCode) > 0) + { + Console.WriteLine($"Thread completed with result: {moduleExitCode}"); + exitCode = (int)moduleExitCode; + } + else + { + Console.WriteLine("Failed to get thread exit code"); + } + } + else + { + Console.WriteLine("Thread did not complete within timeout period"); + return InjectorResponses.InjectResult.RemoteThreadTimeout; + } + + return InjectorResponses.InjectResult.Success; + } + + private static void Patch(byte[] blob, uint marker, IntPtr value) + { + byte m0 = (byte)(marker & 0xFF); + byte m1 = (byte)((marker >> 8) & 0xFF); + byte m2 = (byte)((marker >> 16) & 0xFF); + byte m3 = (byte)((marker >> 24) & 0xFF); + + for (int i = 0; i <= blob.Length - 4; i++) + { + if (blob[i] == m0 && blob[i + 1] == m1 && + blob[i + 2] == m2 && blob[i + 3] == m3) + { + BitConverter.GetBytes(value.ToInt32()).CopyTo(blob, i); + return; + } + } + + throw new Exception($"Marker 0x{marker:X8} not found"); + } + + private static IntPtr AllocWrite(IntPtr hProcess, byte[] data, + NativeMethods.MemoryProtection protect) + { + var addr = NativeMethods.VirtualAllocEx(hProcess, IntPtr.Zero, + (nint)data.Length, + (uint)(NativeMethods.AllocationType.Commit | + NativeMethods.AllocationType.Reserve), + (uint)protect); + NativeMethods.WriteProcessMemory(hProcess, addr, data, + data.Length, out _); + return addr; + } +} diff --git a/Daybreak.Injector/ThreadResumer.cs b/Daybreak.Injector/ThreadResumer.cs new file mode 100644 index 00000000..d7b6c62f --- /dev/null +++ b/Daybreak.Injector/ThreadResumer.cs @@ -0,0 +1,37 @@ +using Daybreak.Shared.Models; +using Daybreak.Shared.Utils; +using System.Runtime.InteropServices; + +namespace Daybreak.Injector; + +public static class ThreadResumer +{ + public static InjectorResponses.ResumeResult Resume(IntPtr threadId) + { + if (threadId == 0) + { + return InjectorResponses.ResumeResult.InvalidThreadHandle; + } + + var threadHandle = NativeMethods.OpenThread(NativeMethods.ThreadAccess.SuspendResume, false, (uint)threadId); + if (threadHandle == IntPtr.Zero) + { + var openErr = Marshal.GetLastWin32Error(); + Console.WriteLine($"OpenThread failed. Error: {openErr}"); + return InjectorResponses.ResumeResult.InvalidThreadHandle; + } + + var result = NativeMethods.ResumeThread(threadHandle); + Console.WriteLine($"ResumeThread result: {result}"); + NativeMethods.CloseHandle(threadHandle); + + if (result == uint.MaxValue) + { + var resumeErr = Marshal.GetLastWin32Error(); + Console.WriteLine($"ResumeThread failed. Error: {resumeErr}"); + return InjectorResponses.ResumeResult.InvalidThreadHandle; + } + + return InjectorResponses.ResumeResult.Success; + } +} diff --git a/Daybreak.Installer/Daybreak.Installer.csproj b/Daybreak.Installer/Daybreak.Installer.csproj index f1f53e9e..2eacc080 100644 --- a/Daybreak.Installer/Daybreak.Installer.csproj +++ b/Daybreak.Installer/Daybreak.Installer.csproj @@ -5,11 +5,12 @@ $(TargetFrameworkVersion) enable enable - AnyCPU;x86 + x64 True - win-x86 + win-x64 true true + true diff --git a/Daybreak.Installer/Program.cs b/Daybreak.Installer/Program.cs index 9c11ef5e..ddabdda3 100644 --- a/Daybreak.Installer/Program.cs +++ b/Daybreak.Installer/Program.cs @@ -1,10 +1,7 @@ -// See https://aka.ms/new-console-template for more information -using System.Diagnostics; +using System.Diagnostics; using System.IO.Compression; using System.Text; -var cts = new CancellationTokenSource(); - static void RenderProgressBar(int currentStep, int totalSteps, int barSize) { Console.CursorLeft = 0; // Reset cursor position to the start of the line @@ -20,9 +17,6 @@ static void RenderProgressBar(int currentStep, int totalSteps, int barSize) Console.Write($"{pctComplete:P0}"); // Display the percentage completed } -const string tempFile = "tempfile.zip"; -const string updatePkg = "update.pkg"; -const string executableName = "Daybreak.exe"; Console.Title = "Daybreak Installer"; Console.WriteLine("Starting installation..."); while (Process.GetProcesses().Where(p => p.ProcessName == "Daybreak").FirstOrDefault()?.HasExited is false) @@ -37,6 +31,27 @@ while (Process.GetProcessesByName("gw").FirstOrDefault()?.HasExited is false) await Task.Delay(5000); } +if (args.Length < 1) +{ + Console.WriteLine("No working directory specified"); + Console.ReadKey(); + return; +} + +var workingDirectory = args[0]; +if (!Directory.Exists(workingDirectory)) +{ + Console.WriteLine("Working directory does not exist"); + Console.ReadKey(); + return; +} + +Console.WriteLine($"Working directory: {workingDirectory}"); + +var tempFile = Path.GetFullPath("tempfile.zip", workingDirectory); +var updatePkg = Path.GetFullPath("update.pkg", workingDirectory); +var executableName = Path.GetFullPath("Daybreak.exe", workingDirectory); + if (File.Exists(updatePkg)) { Console.WriteLine("Unpacking files..."); @@ -76,7 +91,7 @@ if (File.Exists(updatePkg)) var binarySize = BitConverter.ToInt32(sizeBuffer.Span); var fileInfo = new FileInfo(relativePath); fileInfo.Directory!.Create(); - using var destinationStream = new FileStream(relativePath, FileMode.Create); + using var destinationStream = new FileStream(Path.GetFullPath(workingDirectory, relativePath), FileMode.Create); while(binarySize > 0) { var toRead = Math.Min(binarySize, copyBuffer.Length); @@ -98,7 +113,7 @@ else if (File.Exists(tempFile)) Console.WriteLine("Unpacking files..."); try { - ZipFile.ExtractToDirectory(tempFile, AppContext.BaseDirectory, true); + ZipFile.ExtractToDirectory(tempFile, Path.GetFullPath(workingDirectory), true); } catch { diff --git a/Daybreak.Shared/Models/InjectorResponses.cs b/Daybreak.Shared/Models/InjectorResponses.cs new file mode 100644 index 00000000..65b72d49 --- /dev/null +++ b/Daybreak.Shared/Models/InjectorResponses.cs @@ -0,0 +1,55 @@ +namespace Daybreak.Shared.Models; + +public static class InjectorResponses +{ + public enum GenericResults + { + Success = 0, + InvalidArgs = -1, + InvalidMode = -2, + InvalidInjector = -3, + } + + public enum InjectResult + { + Success = 0, + InvalidArgs = -1, + InvalidInjector = -3, + InvalidProcess = -101, + InvalidDllPath = -102, + InvalidKernel32 = -103, + InvalidLoadLibraryW = -104, + ModulePathAllocationFailed = -105, + WriteMemoryFailed = -106, + CreateRemoteThreadFailed = -107, + RemoteThreadTimeout = -108, + DllExitCodeFailed = -109, + MemoryFreeFailed = -110, + InvalidGetProcAddress = -111, + DllPathWriteFailed = -112, + FunctionNameWriteFailed = -113, + InjectDataWriteFailed = -114, + StubAllocationFailed = -115, + InvalidLoadLibraryA = -116, + } + + public enum LaunchResult + { + Success = 0, + InvalidArgs = -1, + InvalidInjector = -3, + InvalidPath = -301, + InvalidElevated = -302, + LaunchTimeout = -303, + PatchFailed = -304, + LaunchFailed = -305, + } + + public enum ResumeResult + { + Success = 0, + InvalidArgs = -1, + InvalidInjector = -3, + InvalidThreadHandle = -400, + } +} diff --git a/Daybreak.Shared/Services/Injection/IDaybreakInjector.cs b/Daybreak.Shared/Services/Injection/IDaybreakInjector.cs new file mode 100644 index 00000000..7bafe705 --- /dev/null +++ b/Daybreak.Shared/Services/Injection/IDaybreakInjector.cs @@ -0,0 +1,12 @@ +using Daybreak.Shared.Models; + +namespace Daybreak.Shared.Services.Injection; + +public interface IDaybreakInjector +{ + bool InjectorAvailable(); + Task InjectWinApi(int processId, string dllPath, CancellationToken cancellationToken); + Task InjectStub(int processId, string dllPath, string entryPoint, CancellationToken cancellationToken); + Task<(InjectorResponses.LaunchResult ExitCode, int ThreadHandle, int ProcessId)> Launch(string executablePath, bool elevated, string[] args, CancellationToken cancellationToken); + Task Resume(int threadhandle, CancellationToken cancellationToken); +} diff --git a/Daybreak.Shared/Services/Injection/IStubInjector.cs b/Daybreak.Shared/Services/Injection/IStubInjector.cs index 03d78891..f0a6ed26 100644 --- a/Daybreak.Shared/Services/Injection/IStubInjector.cs +++ b/Daybreak.Shared/Services/Injection/IStubInjector.cs @@ -3,5 +3,9 @@ namespace Daybreak.Shared.Services.Injection; public interface IStubInjector { - bool Inject(Process target, string dllPath, out int exitCode); + /// + /// Injects the specified DLL into the target process using an asm stub. Only used for + /// + /// + Task Inject(Process target, string dllPath, string entryPoint, CancellationToken cancellationToken); } diff --git a/Daybreak.Shared/Utils/NativeMethods.cs b/Daybreak.Shared/Utils/NativeMethods.cs index a3ae5ca4..59ce652c 100644 --- a/Daybreak.Shared/Utils/NativeMethods.cs +++ b/Daybreak.Shared/Utils/NativeMethods.cs @@ -10,6 +10,13 @@ public static class NativeMethods public const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; public const uint ENABLE_PROCESSED_OUTPUT = 0x0001; + public const int WM_NCHITTEST = 0x0084; + public const int HTCLIENT = 1; + public const int HTCAPTION = 2; + public const int HTMINBUTTON = 8; + public const int HTMAXBUTTON = 9; + public const int HTCLOSE = 20; + public const uint WM_KEYDOWN = 0x0100; public const uint SWP_SHOWWINDOW = 0x0040; public const nint HWND_TOPMOST = -1; @@ -18,7 +25,6 @@ public static class NativeMethods public const uint LIST_MODULES_32BIT = 0x01; public const int WM_SYSCOMMAND = 0x112; public const int WM_NCLBUTTONDOWN = 0x00A1; - public const int HTCAPTION = 2; public const int SC_MOVE = 0xF010; public const int SC_SIZE = 0xF000; @@ -46,6 +52,20 @@ public static class NativeMethods DWMWCP_ROUNDSMALL = 3 } + [Flags] + public enum ThreadAccess : uint + { + Terminate = 0x0001, + SuspendResume = 0x0002, + GetContext = 0x0008, + SetContext = 0x0010, + SetInformation = 0x0020, + QueryInformation = 0x0040, + SetThreadToken = 0x0080, + Impersonate = 0x0100, + DirectImpersonation = 0x0200 + } + [Flags] public enum AllocationType : uint { @@ -561,4 +581,7 @@ public static class NativeMethods [DllImport("dwmapi.dll", CharSet = CharSet.Unicode, PreserveSig = false)] public static extern void DwmSetWindowAttribute(IntPtr hWnd, DWMWINDOWATTRIBUTE attribute, ref DWM_WINDOW_CORNER_PREFERENCE pvAttribute, uint cbAttribute); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern nint OpenThread(ThreadAccess dwDesiredAccess, bool bInheritHandle, uint dwThreadId); } diff --git a/Daybreak.Tests/Daybreak.Tests.csproj b/Daybreak.Tests/Daybreak.Tests.csproj index c1be186e..c7e89185 100644 --- a/Daybreak.Tests/Daybreak.Tests.csproj +++ b/Daybreak.Tests/Daybreak.Tests.csproj @@ -3,8 +3,8 @@ $(TargetFrameworkVersionWindows) false - x86 - win-x86 + x64 + win-x64 diff --git a/Daybreak.slnx b/Daybreak.slnx index 1432207d..e0afa7da 100644 --- a/Daybreak.slnx +++ b/Daybreak.slnx @@ -1,8 +1,6 @@ - - @@ -25,28 +23,20 @@ - - - + + - + + + + - - - - - - - + + - - - - - - + diff --git a/Daybreak/Configuration/ProjectConfiguration.cs b/Daybreak/Configuration/ProjectConfiguration.cs index 9508b44b..b7115eb8 100644 --- a/Daybreak/Configuration/ProjectConfiguration.cs +++ b/Daybreak/Configuration/ProjectConfiguration.cs @@ -42,7 +42,6 @@ using Daybreak.Services.ReShade; using Daybreak.Services.ReShade.Notifications; using Daybreak.Services.Screens; using Daybreak.Services.Screenshots; -using Daybreak.Services.SevenZip; using Daybreak.Services.Shortcuts; using Daybreak.Services.Startup; using Daybreak.Services.Startup.Actions; @@ -242,7 +241,7 @@ public class ProjectConfiguration : PluginConfigurationBase services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -273,6 +272,7 @@ public class ProjectConfiguration : PluginConfigurationBase services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Daybreak/Daybreak.csproj b/Daybreak/Daybreak.csproj index 7c782d38..cda020cf 100644 --- a/Daybreak/Daybreak.csproj +++ b/Daybreak/Daybreak.csproj @@ -7,30 +7,21 @@ true true enable - x86;x64 + x64 true preview Daybreak.ico true true cfb2a489-db80-448d-a969-80270f314c46 - win-x86 + win-x64 true en - - - Default - RemoveDuplicateStaticWebAssets;$(ResolveStaticWebAssetsInputsDependsOn) - - - - - - - + + true + @@ -57,7 +48,6 @@ - @@ -70,6 +60,7 @@ + @@ -100,8 +91,8 @@ --> - + @@ -118,9 +109,9 @@ - - - + + + - + + + - - - + - + - - - + + + - + - + - + diff --git a/Daybreak/Launch/Launcher.cs b/Daybreak/Launch/Launcher.cs index c29685a1..574bbcee 100644 --- a/Daybreak/Launch/Launcher.cs +++ b/Daybreak/Launch/Launcher.cs @@ -54,7 +54,13 @@ public sealed class Launcher : BlazorHybridApplication private static readonly ProgressUpdate ProgressFinished = new(1.0, "Finished"); public static Launcher Instance { get; private set; } = default!; + +#if DEBUG public override bool DevToolsEnabled { get; } = true; +#else + public override bool DevToolsEnabled { get; } = false; +#endif + public override string HostPage { get; } = "wwwroot/Index.html"; public override bool ShowTitleBar => false; @@ -76,11 +82,6 @@ public sealed class Launcher : BlazorHybridApplication AllocateAnsiConsole(); #endif - // Ensure GPU acceleration is enabled in WebView2 - // This environment variable is read by WebView2 before creating the browser process - Environment.SetEnvironmentVariable("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", - "--enable-gpu --enable-gpu-rasterization --enable-zero-copy --ignore-gpu-blocklist"); - Instance = new Launcher(args); RegisterExtraEncodingProviders(); return LaunchMainWindow(); @@ -140,6 +141,8 @@ public sealed class Launcher : BlazorHybridApplication { e.WebView.CoreWebView2.ProcessFailed += this.CoreWebView2_ProcessFailed; Global.CoreWebView2 = e.WebView.CoreWebView2; + this.logger?.LogInformation("WebView2 initialized with version {version}", e.WebView.CoreWebView2.Environment.BrowserVersionString); + this.logger?.LogInformation("Process: {architecture}", Environment.Is64BitProcess ? "x64" : "x86"); base.Host_BlazorWebViewInitialized(e); } diff --git a/Daybreak/Services/Api/DaybreakApiService.cs b/Daybreak/Services/Api/DaybreakApiService.cs index 180aa8ae..47d2686c 100644 --- a/Daybreak/Services/Api/DaybreakApiService.cs +++ b/Daybreak/Services/Api/DaybreakApiService.cs @@ -28,8 +28,9 @@ public sealed class DaybreakApiService( ILogger scopedApiLogger) : IDaybreakApiService { + private const string EntryPoint = "ThreadInit"; private const string LocalHost = "localhost"; - private const string DaybreakApiName = "Daybreak.API.dll"; + private const string DaybreakApiName = "Api/Daybreak.API.dll"; private const string ProcessIdPlaceholder = "{PID}"; private const string DaybreakApiServiceName = $"daybreak-api-{ProcessIdPlaceholder}"; private const string ServiceSubType = "daybreak-api"; @@ -173,7 +174,7 @@ public sealed class DaybreakApiService( } public Task OnGuildWarsCreated(GuildWarsCreatedContext guildWarsCreatedContext, CancellationToken cancellationToken) => - Task.Factory.StartNew(() => this.InjectWithStub(guildWarsCreatedContext.ApplicationLauncherContext), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); + this.InjectWithStub(guildWarsCreatedContext.ApplicationLauncherContext, cancellationToken); public Task OnGuildWarsStarted(GuildWarsStartedContext guildWarsStartedContext, CancellationToken cancellationToken) => Task.CompletedTask; @@ -193,9 +194,9 @@ public sealed class DaybreakApiService( } public Task OnGuildWarsRunning(GuildWarsRunningContext guildWarsRunningContext, CancellationToken cancellationToken) - => Task.Factory.StartNew(() => this.InjectWithStub(guildWarsRunningContext.ApplicationLauncherContext), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); + => this.InjectWithStub(guildWarsRunningContext.ApplicationLauncherContext, cancellationToken); - private void InjectWithStub(ApplicationLauncherContext context) + private async Task InjectWithStub(ApplicationLauncherContext context, CancellationToken cancellationToken) { var scopedLogger = this.logger.CreateScopedLogger(); var dllName = @@ -215,7 +216,8 @@ public sealed class DaybreakApiService( } scopedLogger.LogInformation("Injecting {dllName} into {processId}", dllName, context.Process.Id); - if (!this.stubInjector.Inject(context.Process, dllName, out var port)) + var result = await this.stubInjector.Inject(context.Process, dllName, EntryPoint, cancellationToken); + if (result < 0) { this.notificationService.NotifyError( "Daybreak API Failure", @@ -224,6 +226,7 @@ public sealed class DaybreakApiService( return; } + var port = result; if (port <= 0) { this.notificationService.NotifyError( diff --git a/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs b/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs index 66437248..a53c1187 100644 --- a/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs +++ b/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs @@ -5,6 +5,7 @@ using Daybreak.Shared.Models.LaunchConfigurations; using Daybreak.Shared.Models.Mods; using Daybreak.Shared.Services.ApplicationLauncher; using Daybreak.Shared.Services.ExecutableManagement; +using Daybreak.Shared.Services.Injection; using Daybreak.Shared.Services.Mods; using Daybreak.Shared.Services.Notifications; using Daybreak.Shared.Services.Privilege; @@ -28,6 +29,7 @@ using static Daybreak.Shared.Utils.NativeMethods; namespace Daybreak.Services.ApplicationLauncher; internal sealed class ApplicationLauncher( + IDaybreakInjector daybreakInjector, IGuildWarsExecutableManager guildWarsExecutableManager, INotificationService notificationService, ILiveOptions launcherOptions, @@ -42,6 +44,7 @@ internal sealed class ApplicationLauncher( private static readonly TimeSpan LaunchTimeout = TimeSpan.FromMinutes(1); + private readonly IDaybreakInjector daybreakInjector = daybreakInjector.ThrowIfNull(); private readonly IGuildWarsExecutableManager guildWarsExecutableManager = guildWarsExecutableManager.ThrowIfNull(); private readonly INotificationService notificationService = notificationService.ThrowIfNull(); private readonly ILiveOptions launcherOptions = launcherOptions.ThrowIfNull(); @@ -279,21 +282,29 @@ internal sealed class ApplicationLauncher( } } - var pId = LaunchClient(executable, string.Join(" ", args), this.privilegeManager.AdminPrivileges, out var clientHandle); - do + var launchResult = await this.daybreakInjector.Launch(executable, this.privilegeManager.AdminPrivileges, [.. args], cancellationToken); + if ((int)launchResult.ExitCode < 0) { - process = Process.GetProcessById(pId); - await Task.Delay(100, cancellationToken); - } while (process is null); + scopedLogger.LogError("Failed to launch Guild Wars via injector with launch result {launchResult}", launchResult); + this.notificationService.NotifyError( + title: "Failed to launch Guild Wars", + description: $"Injector failed to launch Guild Wars with launch result {launchResult}. Check logs for details"); + return default; + } - if (!McPatch(process.Handle)) + var threadHandle = launchResult.ThreadHandle; + var pId = launchResult.ProcessId; + if (threadHandle <= 0 || pId <= 0) { - var lastErr = Marshal.GetLastWin32Error(); - scopedLogger.LogError("Failed to patch GuildWars process. Error code: {lastErr}", lastErr); + scopedLogger.LogError("Invalid process or thread handle returned from injector. ProcessId: {pId}, ThreadHandle: {threadHandle}", pId, threadHandle); + this.notificationService.NotifyError( + title: "Failed to launch Guild Wars", + description: "Injector returned invalid process or thread handle. Check logs for details"); return default; } // Reset launch context with the launched process + process = Process.GetProcessById(pId); applicationLauncherContext = new ApplicationLauncherContext { ExecutablePath = executable, Process = process, ProcessId = (uint)pId }; var guildWarsCreatedContext = new GuildWarsCreatedContext { ApplicationLauncherContext = applicationLauncherContext, CancelStartup = false }; foreach (var mod in mods) @@ -330,10 +341,14 @@ internal sealed class ApplicationLauncher( } } - if (clientHandle != IntPtr.Zero) + var resumeResult = await this.daybreakInjector.Resume(threadHandle, cancellationToken); + if (resumeResult < 0) { - _ = ResumeThread(clientHandle); - CloseHandle(clientHandle); + scopedLogger.LogError("Failed to resume Guild Wars process via injector with resume result {resumeResult}", resumeResult); + this.notificationService.NotifyError( + title: "Failed to launch Guild Wars", + description: $"Injector failed to resume Guild Wars with resume result {resumeResult}. Check logs for details"); + return default; } var sw = Stopwatch.StartNew(); @@ -616,186 +631,6 @@ internal sealed class ApplicationLauncher( } } - /// - /// Launches the Guild Wars in suspended state. This fixes a lot of the injection issues - /// Once everything is injected, the process is resumed. - /// Source: https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs - /// - private static int LaunchClient(string path, string args, bool elevated, out IntPtr hThread) - { - var commandLine = $"\"{path}\" {args}"; - hThread = IntPtr.Zero; - - ProcessInformation procinfo; - var startinfo = new StartupInfo - { - cb = Marshal.SizeOf() - }; - var saProcess = new SecurityAttributes(); - saProcess.nLength = (uint)Marshal.SizeOf(saProcess); - var saThread = new SecurityAttributes(); - saThread.nLength = (uint)Marshal.SizeOf(saThread); - - var lastDirectory = Directory.GetCurrentDirectory(); - Directory.SetCurrentDirectory(Path.GetDirectoryName(path)!); - - SetRegistryValue(@"Software\ArenaNet\Guild Wars", "Path", path); - SetRegistryValue(@"Software\ArenaNet\Guild Wars", "Src", path); - - if (!elevated) - { - if (!SaferCreateLevel(SaferLevelScope.User, SaferLevel.NormalUser, SaferOpen.Open, out var hLevel, - IntPtr.Zero)) - { - Debug.WriteLine("SaferCreateLevel"); - return 0; - } - - if (!SaferComputeTokenFromLevel(hLevel, IntPtr.Zero, out var hRestrictedToken, 0, IntPtr.Zero)) - { - Debug.WriteLine("SaferComputeTokenFromLevel"); - return 0; - } - - SaferCloseLevel(hLevel); - - // Set the token to medium integrity. - - TokenMandatoryLabel tml; - tml.Label.Attributes = 0x20; // SE_GROUP_INTEGRITY - if (!ConvertStringSidToSid("S-1-16-8192", out tml.Label.Sid)) - { - CloseHandle(hRestrictedToken); - Debug.WriteLine("ConvertStringSidToSid"); - } - - if (!SetTokenInformation(hRestrictedToken, TokenInformationClass.TokenIntegrityLevel, ref tml, - (uint)Marshal.SizeOf(tml) + GetLengthSid(tml.Label.Sid))) - { - LocalFree(tml.Label.Sid); - CloseHandle(hRestrictedToken); - return 0; - } - - LocalFree(tml.Label.Sid); - if (!CreateProcessAsUser(hRestrictedToken, null!, commandLine, ref saProcess, - ref saProcess, false, (uint)CreationFlags.CreateSuspended, IntPtr.Zero, - null!, ref startinfo, out procinfo)) - { - var error = Marshal.GetLastWin32Error(); - Debug.WriteLine($"CreateProcessAsUser {error}"); - CloseHandle(procinfo.hThread); - return 0; - } - - CloseHandle(hRestrictedToken); - } - else - { - if (!CreateProcess(null!, commandLine, ref saProcess, - ref saThread, false, (uint)CreationFlags.CreateSuspended, IntPtr.Zero, - null!, ref startinfo, out procinfo)) - { - var error = Marshal.GetLastWin32Error(); - Debug.WriteLine($"CreateProcess {error}"); - _ = ResumeThread(procinfo.hThread); - CloseHandle(procinfo.hThread); - return 0; - } - } - - Directory.SetCurrentDirectory(lastDirectory); - - CloseHandle(procinfo.hProcess); - hThread = procinfo.hThread; - return procinfo.dwProcessId; - } - - /// - /// https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs - /// - private static bool McPatch(IntPtr processHandle) - { - byte[] sigPatch = - [ - 0x56, 0x57, 0x68, 0x00, 0x01, 0x00, 0x00, 0x89, 0x85, 0xF4, 0xFE, 0xFF, 0xFF, 0xC7, 0x00, 0x00, 0x00, 0x00, - 0x00 - ]; - var moduleBase = GetProcessModuleBase(processHandle); - var gwdata = new byte[0x48D000]; - - if (!NativeMethods.ReadProcessMemory(processHandle, moduleBase, gwdata, gwdata.Length, out _)) - { - return false; - } - - var idx = SearchBytes(gwdata, sigPatch); - - if (idx == -1) - { - return false; - } - - var mcpatch = moduleBase + idx - 0x1A; - - byte[] payload = [0x31, 0xC0, 0x90, 0xC3]; - - return NativeMethods.WriteProcessMemory(processHandle, mcpatch, payload, payload.Length, out _); - } - - /// - /// https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs - /// - private static int SearchBytes(Memory haystack, Memory needle) - { - var len = needle.Length; - var limit = haystack.Length - len; - for (var i = 0; i <= limit; i++) - { - var k = 0; - for (; k < len; k++) - { - if (needle.Span[k] != haystack.Span[i + k]) - { - break; - } - } - - if (k == len) - { - return i; - } - } - - return -1; - } - - /// - /// https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs - /// - private static IntPtr GetProcessModuleBase(IntPtr process) - { - if (NativeMethods.NtQueryInformationProcess(process, NativeMethods.ProcessInfoClass.ProcessBasicInformation, out var pbi, - Marshal.SizeOf(), out _) != 0) - { - return IntPtr.Zero; - } - - var buffer = new byte[Marshal.SizeOf()]; - - if (!NativeMethods.ReadProcessMemory(process, pbi.PebBaseAddress, buffer, Marshal.SizeOf(), out _)) - { - return IntPtr.Zero; - } - - PEB peb = new() - { - ImageBaseAddress = (IntPtr)BitConverter.ToInt32(buffer, 8) - }; - - return peb.ImageBaseAddress + 0x1000; - } - private static List GetRootWindowsOfProcess(int pid) { var rootWindows = GetChildWindows(IntPtr.Zero); @@ -850,12 +685,6 @@ internal sealed class ApplicationLauncher( return true; } - private static void SetRegistryValue(string registryPath, string valueName, object newValue) - { - using var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(registryPath); - key.SetValue(valueName, newValue, RegistryValueKind.String); - } - private static string[]? PopulateCommandLineArgs(string argName, string? argValue) { if (argValue is null || argValue.IsNullOrWhiteSpace()) diff --git a/Daybreak/Services/Injection/DaybreakInjector.cs b/Daybreak/Services/Injection/DaybreakInjector.cs new file mode 100644 index 00000000..05477822 --- /dev/null +++ b/Daybreak/Services/Injection/DaybreakInjector.cs @@ -0,0 +1,165 @@ +using Daybreak.Shared.Models; +using Daybreak.Shared.Services.Injection; +using Daybreak.Shared.Utils; +using Microsoft.Extensions.Logging; +using System.Diagnostics; +using System.Extensions.Core; +using System.IO; + +namespace Daybreak.Services.Injection; + +public class DaybreakInjector( + ILogger logger) + : IDaybreakInjector +{ + private const string ExecutableName = "Injector/Daybreak.Injector.exe"; + + private readonly ILogger logger = logger; + + public bool InjectorAvailable() + { + var executablePath = PathUtils.GetAbsolutePathFromRoot(ExecutableName); + return File.Exists(executablePath); + } + + public async Task InjectWinApi(int processId, string dllPath, CancellationToken cancellationToken) + { + var scopedLogger = this.logger.CreateScopedLogger(); + if (!this.InjectorAvailable()) + { + scopedLogger.LogError("Injector not available at path {InjectorPath}", PathUtils.GetAbsolutePathFromRoot(ExecutableName)); + return InjectorResponses.InjectResult.InvalidInjector; + } + + var (output, error, exitCode) = await LaunchInjector( + [ + "winapi", + processId.ToString(), + $"\"{dllPath}\"" + ], + cancellationToken); + + scopedLogger.LogInformation("Injector exit code: {exitCode}\nInjector output: {output}\nInjector error: {error}", exitCode, output ?? string.Empty, error ?? string.Empty); + return (InjectorResponses.InjectResult)exitCode; + } + + public async Task InjectStub(int processId, string dllPath, string entryPoint, CancellationToken cancellationToken) + { + var scopedLogger = this.logger.CreateScopedLogger(); + if (!this.InjectorAvailable()) + { + scopedLogger.LogError("Injector not available at path {InjectorPath}", PathUtils.GetAbsolutePathFromRoot(ExecutableName)); + return InjectorResponses.InjectResult.InvalidInjector; + } + + var (output, error, exitCode) = await LaunchInjector( + [ + "stub", + processId.ToString(), + $"\"{entryPoint}\"", + $"\"{dllPath}\"" + ], + cancellationToken); + + scopedLogger.LogInformation("Injector exit code: {exitCode}\nInjector output: {output}\nInjector error: {error}", exitCode, output ?? string.Empty, error ?? string.Empty); + return (InjectorResponses.InjectResult)exitCode; + } + + public async Task<(InjectorResponses.LaunchResult ExitCode, int ThreadHandle, int ProcessId)> Launch(string executablePath, bool elevated, string[] args, CancellationToken cancellationToken) + { + var scopedLogger = this.logger.CreateScopedLogger(); + if (!this.InjectorAvailable()) + { + scopedLogger.LogError("Injector not available at path {InjectorPath}", PathUtils.GetAbsolutePathFromRoot(ExecutableName)); + return (InjectorResponses.LaunchResult.InvalidInjector, -1, -1); + } + + var (output, error, exitCode) = await LaunchInjector( + [ + "launch", + elevated.ToString(), + $"\"{executablePath}\"", + string.Join(' ', args) + ], + cancellationToken); + + scopedLogger.LogInformation("Injector exit code: {exitCode}\nInjector output: {output}\nInjector error: {error}", exitCode, output ?? string.Empty, error ?? string.Empty); + var lines = output?.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries) ?? []; + var processId = 0; + var threadHandle = 0; + foreach (var line in lines) + { + if (line.StartsWith("ProcessId: ")) + { + var processIdString = line["ProcessId: ".Length..]; + _ = int.TryParse(processIdString, out processId); + } + else if (line.StartsWith("ThreadHandle: ")) + { + var threadHandleString = line["ThreadHandle: ".Length..]; + _ = int.TryParse(threadHandleString, out threadHandle); + } + } + + return ((InjectorResponses.LaunchResult)exitCode, threadHandle, processId); + } + + public async Task Resume(int threadhandle, CancellationToken cancellationToken) + { + var scopedLogger = this.logger.CreateScopedLogger(); + if (!this.InjectorAvailable()) + { + scopedLogger.LogError("Injector not available at path {InjectorPath}", PathUtils.GetAbsolutePathFromRoot(ExecutableName)); + return InjectorResponses.ResumeResult.InvalidInjector; + } + + var (output, error, exitCode) = await LaunchInjector( + [ + "resume", + threadhandle.ToString() + ], + cancellationToken); + + scopedLogger.LogInformation("Injector exit code: {exitCode}\nInjector output: {output}\nInjector error: {error}", exitCode, output ?? string.Empty, error ?? string.Empty); + return (InjectorResponses.ResumeResult)exitCode; + } + + private static async Task<(string? Output, string? Error, int ExitCode)> LaunchInjector(string[] arguments, CancellationToken cancellationToken) + { + var executablePath = PathUtils.GetAbsolutePathFromRoot(ExecutableName); + if (!File.Exists(executablePath)) + { + throw new InvalidOperationException($"Could not find injector {executablePath}"); + } + + try + { + var startInfo = new ProcessStartInfo + { + FileName = executablePath, + Arguments = string.Join(' ', arguments), + WorkingDirectory = Path.GetDirectoryName(executablePath), + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + using var process = new Process + { + StartInfo = startInfo, + }; + + process.Start(); + await process.WaitForExitAsync(cancellationToken); + + var output = await process.StandardOutput.ReadToEndAsync(cancellationToken); + var error = await process.StandardError.ReadToEndAsync(cancellationToken); + return (output, error, process.ExitCode); + } + catch(Exception) + { + throw; + } + } +} diff --git a/Daybreak/Services/Injection/Models/InjectionResult.cs b/Daybreak/Services/Injection/Models/InjectionResult.cs new file mode 100644 index 00000000..69f033e1 --- /dev/null +++ b/Daybreak/Services/Injection/Models/InjectionResult.cs @@ -0,0 +1,5 @@ +namespace Daybreak.Services.Injection.Models; + +internal sealed record InjectionResult(int ExitCode, string Output, string Error) +{ +} diff --git a/Daybreak/Services/Injection/ProcessInjector.cs b/Daybreak/Services/Injection/ProcessInjector.cs index 9eb9171e..3679d385 100644 --- a/Daybreak/Services/Injection/ProcessInjector.cs +++ b/Daybreak/Services/Injection/ProcessInjector.cs @@ -1,148 +1,28 @@ using Daybreak.Shared.Services.Injection; -using Daybreak.Shared.Utils; using Microsoft.Extensions.Logging; using System.Core.Extensions; using System.Diagnostics; using System.Extensions; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; +using System.Extensions.Core; namespace Daybreak.Services.Injection; internal sealed class ProcessInjector( + IDaybreakInjector daybreakInjector, ILogger logger) : IProcessInjector { + private readonly IDaybreakInjector daybreakInjector = daybreakInjector.ThrowIfNull(); private readonly ILogger logger = logger.ThrowIfNull(); - public Task Inject(Process process, string pathToDll, CancellationToken cancellationToken) + public async Task Inject(Process process, string pathToDll, CancellationToken cancellationToken) { - return Task.Factory.StartNew(() => this.InjectWithApi(process, pathToDll), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); - } - - private bool InjectWithApi(Process process, string pathToDll) - { - var scopedLogger = this.logger.CreateScopedLogger(nameof(this.InjectWithApi), pathToDll); - var modulefullpath = Path.GetFullPath(pathToDll); - - if (!File.Exists(modulefullpath)) + var scopedLogger = this.logger.CreateScopedLogger(); + var result = await this.daybreakInjector.InjectWinApi(process.Id, pathToDll, cancellationToken); + if (result < 0) { - scopedLogger.LogError("Dll to inject not found"); + scopedLogger.LogError("Failed to inject DLL into process {ProcessId} with error code {ErrorCode}", process.Id, result); return false; } - var hKernel32 = NativeMethods.GetModuleHandle("kernel32.dll"); - if (hKernel32 == IntPtr.Zero) - { - scopedLogger.LogError("Unable to get a handle of kernel32.dll"); - return false; - } - - var hLoadLib = NativeMethods.GetProcAddress(hKernel32, "LoadLibraryW"); - if (hLoadLib == IntPtr.Zero) - { - scopedLogger.LogError("Unable to get the address of LoadLibraryW"); - return false; - } - - var hStringBuffer = NativeMethods.VirtualAllocEx(process.Handle, IntPtr.Zero, new IntPtr(2 * (modulefullpath.Length + 1)), - 0x3000 /* MEM_COMMIT | MEM_RESERVE */, 0x4 /* PAGE_READWRITE */); - if (hStringBuffer == IntPtr.Zero) - { - scopedLogger.LogError("Unable to allocate memory for module path"); - return false; - } - - WriteWString(process, hStringBuffer, modulefullpath); - if (ReadWString(process, hStringBuffer, 260) != modulefullpath) - { - scopedLogger.LogError("Module path string is not correct"); - return false; - } - - var hThread = NativeMethods.CreateRemoteThread(process.Handle, IntPtr.Zero, 0, hLoadLib, hStringBuffer, 0, out _); - if (hThread == IntPtr.Zero) - { - scopedLogger.LogError("Unable to create remote thread"); - return false; - } - - var threadResult = NativeMethods.WaitForSingleObject(hThread, 30000u); - if (threadResult is 0x102 or 0xFFFFFFFF /* WAIT_FAILED */) - { - scopedLogger.LogError($"Exception occurred while waiting for the remote thread. Result is {threadResult}"); - return false; - } - - var dllResult = NativeMethods.GetExitCodeThread(hThread, out _); - if (dllResult == 0) - { - scopedLogger.LogError($"Injected dll returned non-success status code {dllResult}"); - return false; - } - - var memoryFreeResult = NativeMethods.VirtualFreeEx(process.Handle, hStringBuffer, 0, 0x8000 /* MEM_RELEASE */); - if (!memoryFreeResult) - { - scopedLogger.LogError($"Failed to free dll memory"); - } - - return memoryFreeResult; - } - - private static void WriteBytes(Process process, IntPtr address, byte[] data) - { - var size = data.Length; - var buffer = Marshal.AllocHGlobal(size); - Marshal.Copy(data, 0, buffer, size); - - NativeMethods.WriteProcessMemory( - process.Handle, - address, - buffer, - size, - out _); - - Marshal.FreeHGlobal(buffer); - } - - private static void WriteWString(Process process, IntPtr address, string data) - { - WriteBytes(process, address, Encoding.Unicode.GetBytes(data)); - } - - private static string ReadWString(Process process, IntPtr address, int maxsize, Encoding? encoding = null) - { - encoding ??= Encoding.Unicode; - var rawbytes = ReadBytes(process, address, maxsize); - if (rawbytes.Length == 0) - { - return ""; - } - - var ret = encoding.GetString(rawbytes); - if (ret.Contains('\0')) - { - ret = ret[..ret.IndexOf('\0')]; - } - - return ret; - } - - private static byte[] ReadBytes(Process process, IntPtr address, int size) - { - var buffer = Marshal.AllocHGlobal(size); - - NativeMethods.ReadProcessMemory(process.Handle, - address, - buffer, - size, - out _ - ); - - var ret = new byte[size]; - Marshal.Copy(buffer, ret, 0, size); - Marshal.FreeHGlobal(buffer); - - return ret; + return true; } } diff --git a/Daybreak/Services/Injection/StubInjector.cs b/Daybreak/Services/Injection/StubInjector.cs index 69e79bbe..00dfef0c 100644 --- a/Daybreak/Services/Injection/StubInjector.cs +++ b/Daybreak/Services/Injection/StubInjector.cs @@ -1,190 +1,28 @@ using Daybreak.Shared.Services.Injection; -using Daybreak.Shared.Utils; using Microsoft.Extensions.Logging; -using Reloaded.Assembler; using System.Core.Extensions; using System.Diagnostics; using System.Extensions.Core; -using System.Text; namespace Daybreak.Services.Injection; -public sealed class StubInjector( +internal sealed class StubInjector( + IDaybreakInjector daybreakInjector, ILogger logger) : IStubInjector { - const string ListenerEntryPoint = "ThreadInit"; + private readonly IDaybreakInjector daybreakInjector = daybreakInjector.ThrowIfNull(); + private readonly ILogger logger = logger; - /// - /// ebp+8 is the dll path pointer - /// 0xDEADBEEF is a placeholder to be patched later for LoadLibraryA - /// 0xFEEDF00D is a placeholder to be patched later for GetProcAddress - /// - const string Asm = @" -use32 -push ebp -mov ebp, esp - -mov esi, [ebp+8] ; ESI = &INJECT_DATA (kept unchanged) - -; -------- LoadLibraryA(dllPath) ------------------------------ -push dword [esi] ; dllPath -mov eax, 0xDEADBEEF ; patched → LoadLibraryA -call eax ; EAX = hModule - -; -------- GetProcAddress(hModule, funcName) ------------------ -push dword [esi+4] ; funcName -push eax ; hModule (still in EAX) -mov eax, 0xFEEDF00D ; patched → GetProcAddress -call eax ; EAX = exported EntryPoint - -; -------- call EntryPoint() ---------------------------------- -call eax ; calls EntryPoint -; xor eax, eax ; thread exit-code 0 - -; -------- return EntryPoint() response ----------------------- -leave ; = mov esp, ebp / pop ebp -ret 4 ; stdcall: pop lpParameter -"; - - private readonly ILogger logger = logger.ThrowIfNull(); - - public bool Inject(Process target, string dllPath, out int exitCode) + public async Task Inject(Process target, string dllPath, string entryPoint, CancellationToken cancellationToken) { - var scopedLogger = this.logger.CreateScopedLogger(flowIdentifier: dllPath); - var hProcess = NativeMethods.OpenProcess( - NativeMethods.ProcessAccessFlags.All, false, (uint)target.Id); - - exitCode = 0; - if (hProcess is 0) + var scopedLogger = this.logger.CreateScopedLogger(); + var result = await this.daybreakInjector.InjectStub(target.Id, dllPath, entryPoint, cancellationToken); + if (result < 0) { - scopedLogger.LogError("Failed to inject with stub. Could not open process by id {processId}", target.Id); - return false; + scopedLogger.LogError("Failed to inject DLL into process {ProcessId} with error code {ErrorCode}", target.Id, result); + return (int)result; } - using var assembler = new Assembler(); - var stubBytes = assembler.Assemble(Asm); - - // patch placeholders - var hKernel = NativeMethods.GetModuleHandle("kernel32.dll"); - if (hKernel is 0) - { - scopedLogger.LogError("Failed to inject with stub. Could not get handle of kernel32.dll"); - return false; - } - - var pLoadLibA = NativeMethods.GetProcAddress(hKernel, "LoadLibraryA"); - if (pLoadLibA is 0) - { - scopedLogger.LogError("Failed to inject with stub. Could not get address of LoadLibraryA"); - return false; - } - - var pGetProc = NativeMethods.GetProcAddress(hKernel, "GetProcAddress"); - if (pGetProc is 0) - { - scopedLogger.LogError("Failed to inject with stub. Could not get address of GetProcAddress"); - return false; - } - - Patch(stubBytes, 0xDEADBEEF, pLoadLibA); - Patch(stubBytes, 0xFEEDF00D, pGetProc); - - var dllBytes = Encoding.ASCII.GetBytes(dllPath + '\0'); - var funcBytes = Encoding.ASCII.GetBytes(ListenerEntryPoint + '\0'); - var remoteDll = AllocWrite(hProcess, dllBytes, NativeMethods.MemoryProtection.PAGE_READWRITE); - if (remoteDll is 0) - { - scopedLogger.LogError("Failed to inject with stub. Could not allocate memory for dll path"); - return false; - } - - var remoteFunc = AllocWrite(hProcess, funcBytes, NativeMethods.MemoryProtection.PAGE_READWRITE); - if (remoteFunc is 0) - { - scopedLogger.LogError("Failed to inject with stub. Could not allocate memory for function name"); - return false; - } - - var injectData = new byte[IntPtr.Size * 2]; - BitConverter.GetBytes(remoteDll.ToInt32()).CopyTo(injectData, 0); - BitConverter.GetBytes(remoteFunc.ToInt32()).CopyTo(injectData, 4); - var remoteStruct = AllocWrite(hProcess, injectData, NativeMethods.MemoryProtection.PAGE_READWRITE); - if (remoteStruct is 0) - { - scopedLogger.LogError("Failed to inject with stub. Could not allocate memory for injectData"); - return false; - } - - var remoteStub = AllocWrite(hProcess, stubBytes, NativeMethods.MemoryProtection.PAGE_EXECUTE_READ_WRITE); - if (remoteStub is 0) - { - scopedLogger.LogError("Failed to inject with stub. Could not allocate memory for stub"); - return false; - } - - var hThread = NativeMethods.CreateRemoteThread( - hProcess, IntPtr.Zero, 0, - remoteStub, - remoteStruct, - 0, out _); - - if (hThread is 0) - { - scopedLogger.LogError("Failed to inject with stub. Could not create remote thread"); - return false; - } - - var waitResult = NativeMethods.WaitForSingleObject(hThread, 10000); // Wait up to 10 seconds - if (waitResult is 0) // WAIT_OBJECT_0 - thread completed - { - // Get the thread exit code, which will be our port number - if (NativeMethods.GetExitCodeThread(hThread, out var moduleExitCode) > 0) - { - scopedLogger.LogDebug("Thread completed with result: {result}", moduleExitCode); - exitCode = (int)moduleExitCode; - } - else - { - scopedLogger.LogWarning("Failed to get thread exit code"); - } - } - else - { - scopedLogger.LogWarning("Thread did not complete within timeout period"); - } - - return true; - } - - private static void Patch(byte[] blob, uint marker, IntPtr value) - { - byte m0 = (byte)(marker & 0xFF); - byte m1 = (byte)((marker >> 8) & 0xFF); - byte m2 = (byte)((marker >> 16) & 0xFF); - byte m3 = (byte)((marker >> 24) & 0xFF); - - for (int i = 0; i <= blob.Length - 4; i++) - { - if (blob[i] == m0 && blob[i + 1] == m1 && - blob[i + 2] == m2 && blob[i + 3] == m3) - { - BitConverter.GetBytes(value.ToInt32()).CopyTo(blob, i); - return; - } - } - - throw new Exception($"Marker 0x{marker:X8} not found"); - } - - private static IntPtr AllocWrite(IntPtr hProcess, byte[] data, - NativeMethods.MemoryProtection protect) - { - var addr = NativeMethods.VirtualAllocEx(hProcess, IntPtr.Zero, - (nint)data.Length, - (uint)(NativeMethods.AllocationType.Commit | - NativeMethods.AllocationType.Reserve), - (uint)protect); - NativeMethods.WriteProcessMemory(hProcess, addr, data, - data.Length, out _); - return addr; + scopedLogger.LogInformation("Successfully injected DLL into process {ProcessId}. Stub response: {ExitCode}", target.Id, result); + return (int)result; } } diff --git a/Daybreak/Services/MDns/MDomainRegistrar.cs b/Daybreak/Services/MDns/MDomainRegistrar.cs index 7600e89f..4a91858d 100644 --- a/Daybreak/Services/MDns/MDomainRegistrar.cs +++ b/Daybreak/Services/MDns/MDomainRegistrar.cs @@ -168,7 +168,7 @@ public sealed class MDomainRegistrar( { // split "instance._http._tcp.local." → ["instance", "_http", "_tcp", "local"] var labels = srvRecord.Target.Labels; - if (labels.Count < 2) return "http"; + if (labels.Count <= 2) return "http"; return labels[^3] switch { diff --git a/Daybreak/Services/SevenZip/SevenZipExtractor.cs b/Daybreak/Services/SevenZip/SevenZipExtractor.cs index cc7c923b..f5fd6067 100644 --- a/Daybreak/Services/SevenZip/SevenZipExtractor.cs +++ b/Daybreak/Services/SevenZip/SevenZipExtractor.cs @@ -1,55 +1,41 @@ using Daybreak.Shared.Services.SevenZip; -using Daybreak.Shared.Utils; using Microsoft.Extensions.Logging; +using SevenZipExtractor; using System.Core.Extensions; -using System.Diagnostics; using System.Extensions; +using System.IO; namespace Daybreak.Services.SevenZip; internal sealed class SevenZipExtractor( ILogger logger) : ISevenZipExtractor { - private const string ExtractorExeName = "Daybreak.7ZipExtractor.exe"; - private readonly ILogger logger = logger.ThrowIfNull(); public async Task ExtractToDirectory(string sourceFile, string destinationDirectory, Action progressTracker, CancellationToken cancellationToken) { var scopedLogger = this.logger.CreateScopedLogger(nameof(this.ExtractToDirectory), sourceFile); - using var process = new Process + try { - StartInfo = new ProcessStartInfo + var root = Path.GetFullPath(destinationDirectory); + using var archive = new ArchiveFile(sourceFile); + var total = archive.Entries.Count; + var processed = 0; + await Task.Factory.StartNew(() => { - FileName = PathUtils.GetAbsolutePathFromRoot(ExtractorExeName), - Arguments = $"\"{sourceFile}\" \"{destinationDirectory}\"", - RedirectStandardOutput = true, - CreateNoWindow = true, - UseShellExecute = false - }, - EnableRaisingEvents = true - }; - - process!.OutputDataReceived += (sender, args) => + archive.Extract(entry => + { + progressTracker((double)processed / total, entry.FileName); + processed++; + return Path.Combine(root, entry.FileName); + }); + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); + return true; + } + catch (Exception ex) { - if (args.Data is null) - { - return; - } - - var tokens = args.Data.Split(' '); - if (!double.TryParse(tokens[0], out var progress)) - { - } - - var fileName = string.Join(' ', tokens.Skip(1)); - progressTracker(progress, fileName); - }; - - process.Start(); - process.BeginOutputReadLine(); - await process.WaitForExitAsync(cancellationToken); - process.CancelOutputRead(); - return process.ExitCode == 0; + scopedLogger.LogError(ex, "Failed to extract archive {SourceFile} to {DestinationDirectory}", sourceFile, destinationDirectory); + return false; + } } } diff --git a/Daybreak/Services/Updater/ApplicationUpdater.cs b/Daybreak/Services/Updater/ApplicationUpdater.cs index b29fda5b..cddcffdf 100644 --- a/Daybreak/Services/Updater/ApplicationUpdater.cs +++ b/Daybreak/Services/Updater/ApplicationUpdater.cs @@ -34,8 +34,8 @@ internal sealed class ApplicationUpdater( ILogger logger) : IApplicationUpdater, IApplicationLifetimeService { private const string UpdatePkgSubPath = "update.pkg"; - private const string TempInstallerFileNameSubPath = "Daybreak.Installer.Temp.exe"; - private const string InstallerFileNameSubPath = "Daybreak.Installer.exe"; + private const string TempInstallerFileNameSubPath = "Installer/Daybreak.Installer.Temp.exe"; + private const string InstallerFileNameSubPath = "Installer/Daybreak.Installer.exe"; private const string UpdatedKey = "LauncherUpdating"; private const string TempFileSubPath = "tempfile.zip"; private const string VersionTag = "{VERSION}"; @@ -436,7 +436,8 @@ internal sealed class ApplicationUpdater( { StartInfo = new ProcessStartInfo { - FileName = InstallerFileName + FileName = InstallerFileName, + Arguments = PathUtils.GetRootFolder() } }; this.logger.LogDebug("Launching installer"); diff --git a/Daybreak/Views/App.razor.cs b/Daybreak/Views/App.razor.cs index b47c1972..1920f434 100644 --- a/Daybreak/Views/App.razor.cs +++ b/Daybreak/Views/App.razor.cs @@ -30,14 +30,6 @@ public sealed class AppViewModel { private const string IssueUrl = "https://github.com/gwdevhub/Daybreak/issues/new"; - // WM_NCHITTEST constants - private const int WM_NCHITTEST = 0x0084; - private const int HTCLIENT = 1; - private const int HTCAPTION = 2; - private const int HTMINBUTTON = 8; - private const int HTMAXBUTTON = 9; - private const int HTCLOSE = 20; - private readonly IOptionsProvider optionsProvider; private readonly IMenuServiceProducer menuServiceProducer; private readonly IMenuServiceButtonHandler menuServiceButtonHandler; @@ -162,14 +154,14 @@ public sealed class AppViewModel /// private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { - if (msg == WM_NCHITTEST) + if (msg == NativeMethods.WM_NCHITTEST) { // Get the mouse position from lParam var x = (short)(lParam.ToInt32() & 0xFFFF); var y = (short)((lParam.ToInt32() >> 16) & 0xFFFF); // Convert screen coordinates to window coordinates - var point = new System.Windows.Point(x, y); + var point = new Point(x, y); point = this.blazorHostWindow.PointFromScreen(point); // Define the title bar height (should match the CSS title bar height of 40px) @@ -182,7 +174,7 @@ public sealed class AppViewModel if (point.Y >= 0 && point.Y < titleBarHeight) { handled = true; - return new IntPtr(HTCLIENT); + return new IntPtr(NativeMethods.HTCLIENT); } } diff --git a/Directory.Packages.props b/Directory.Packages.props index 63ad1dff..4dbae70e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,8 +15,10 @@ - - + + + + @@ -49,7 +51,8 @@ - + + @@ -64,9 +67,11 @@ - + + + \ No newline at end of file