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