Extract injector to its own executable. Convert Daybreak to x64 (#1228) (Closes #1222 #1223)

This commit is contained in:
2026-01-07 21:01:24 +01:00
parent 0ec1c9c96f
commit f838d16543
32 changed files with 1308 additions and 705 deletions
+4 -7
View File
@@ -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: |
+4 -5
View File
@@ -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 }}
Configuration: Debug
@@ -1,15 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(TargetFrameworkVersion)</TargetFramework>
<RootNamespace>Daybreak._7ZipExtractor</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Platforms>x64</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SharpCompress" />
</ItemGroup>
</Project>
-47
View File
@@ -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;
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(TargetFrameworkVersionWindows)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>x86</Platforms>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<PublishAot>true</PublishAot>
<StripSymbols>true</StripSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Reloaded.Assembler" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Daybreak.Shared\Daybreak.Shared.csproj" />
</ItemGroup>
</Project>
+138
View File
@@ -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;
}
}
+248
View File
@@ -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<StartupInfo>()
};
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;
}
/// <summary>
/// https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs
/// </summary>
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 _);
}
/// <summary>
/// https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs
/// </summary>
private static int SearchBytes(Memory<byte> haystack, Memory<byte> 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;
}
/// <summary>
/// https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs
/// </summary>
private static IntPtr GetProcessModuleBase(IntPtr process)
{
if (NativeMethods.NtQueryInformationProcess(process, NativeMethods.ProcessInfoClass.ProcessBasicInformation, out var pbi,
Marshal.SizeOf<ProcessBasicInformation>(), out _) != 0)
{
return IntPtr.Zero;
}
var buffer = new byte[Marshal.SizeOf<PEB>()];
if (!NativeMethods.ReadProcessMemory(process, pbi.PebBaseAddress, buffer, Marshal.SizeOf<PEB>(), 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);
}
}
+239
View File
@@ -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] <params>");
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;
}
+183
View File
@@ -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
{
/// <summary>
/// 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
/// </summary>
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;
}
}
+37
View File
@@ -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;
}
}
+3 -2
View File
@@ -5,11 +5,12 @@
<TargetFramework>$(TargetFrameworkVersion)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>AnyCPU;x86</Platforms>
<Platforms>x64</Platforms>
<Self-Contained>True</Self-Contained>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishAot>true</PublishAot>
<StripSymbols>true</StripSymbols>
<PublishSingleFile>true</PublishSingleFile>
</PropertyGroup>
</Project>
+24 -9
View File
@@ -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
{
@@ -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,
}
}
@@ -0,0 +1,12 @@
using Daybreak.Shared.Models;
namespace Daybreak.Shared.Services.Injection;
public interface IDaybreakInjector
{
bool InjectorAvailable();
Task<InjectorResponses.InjectResult> InjectWinApi(int processId, string dllPath, CancellationToken cancellationToken);
Task<InjectorResponses.InjectResult> 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<InjectorResponses.ResumeResult> Resume(int threadhandle, CancellationToken cancellationToken);
}
@@ -3,5 +3,9 @@
namespace Daybreak.Shared.Services.Injection;
public interface IStubInjector
{
bool Inject(Process target, string dllPath, out int exitCode);
/// <summary>
/// Injects the specified DLL into the target process using an asm stub. Only used for
/// </summary>
/// <returns></returns>
Task<int> Inject(Process target, string dllPath, string entryPoint, CancellationToken cancellationToken);
}
+24 -1
View File
@@ -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);
}
+2 -2
View File
@@ -3,8 +3,8 @@
<PropertyGroup>
<TargetFramework>$(TargetFrameworkVersionWindows)</TargetFramework>
<IsPackable>false</IsPackable>
<Platforms>x86</Platforms>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<Platforms>x64</Platforms>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
+9 -19
View File
@@ -1,8 +1,6 @@
<Solution>
<Configurations>
<Platform Name="Any CPU" />
<Platform Name="x64" />
<Platform Name="x86" />
</Configurations>
<Folder Name="/Pipelines/">
<File Path=".github/dependabot.yml" />
@@ -25,28 +23,20 @@
<File Path="LICENSE.md" />
<File Path="README.md" />
</Folder>
<Project Path="Daybreak.7ZipExtractor/Daybreak.7ZipExtractor.csproj">
<Platform Solution="*|x86" Project="x64" />
<Platform Solution="Debug|x64" Project="x64" />
<Project Path="Daybreak/Daybreak.csproj">
<Platform Project="x64" />
</Project>
<Project Path="Daybreak.API/Daybreak.API.csproj">
<Platform Solution="Debug|x86" Project="x86" />
<Platform Project="x86" />
</Project>
<Project Path="Daybreak.Injector/Daybreak.Injector.csproj">
<Platform Project="x86" />
</Project>
<Project Path="Daybreak.Installer/Daybreak.Installer.csproj">
<Platform Solution="*|x86" Project="x86" />
<Platform Solution="Debug|Any CPU" Project="x86" />
<Platform Solution="Debug|x64" Project="x86" />
<Platform Solution="Release|x64" Project="x64" />
</Project>
<Project Path="Daybreak.Shared/Daybreak.Shared.csproj">
<Platform Solution="Debug|Any CPU" Project="x86" />
<Platform Project="x64" />
</Project>
<Project Path="Daybreak.Shared/Daybreak.Shared.csproj" />
<Project Path="Daybreak.Tests/Daybreak.Tests.csproj">
<Platform Solution="*|x64" Project="x64" />
<Platform Solution="*|x86" Project="x86" />
</Project>
<Project Path="Daybreak/Daybreak.csproj">
<Platform Solution="*|x64" Project="x64" />
<Platform Solution="*|x86" Project="x86" />
<Platform Project="x64" />
</Project>
</Solution>
@@ -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<IPluginsService, PluginsService>();
services.AddSingleton<ISplashScreenService, SplashScreenService>();
services.AddSingleton<IGuildWarsExecutableManager, GuildWarsExecutableManager>();
services.AddSingleton<ISevenZipExtractor, SevenZipExtractor>();
services.AddSingleton<ISevenZipExtractor, Daybreak.Services.SevenZip.SevenZipExtractor>();
services.AddSingleton<IOptionsSynchronizationService, OptionsSynchronizationService>();
services.AddSingleton<IMDomainNameService, MDomainNameService>();
services.AddSingleton<IMDomainRegistrar, MDomainRegistrar>();
@@ -273,6 +272,7 @@ public class ProjectConfiguration : PluginConfigurationBase
services.AddScoped<IRegistryService, RegistryService>();
services.AddScoped<IEventNotifierService, EventNotifierService>();
services.AddScoped<IToolboxClient, ToolboxClient>();
services.AddScoped<IDaybreakInjector, DaybreakInjector>();
services.AddScoped<IProcessInjector, ProcessInjector>();
services.AddScoped<IStubInjector, StubInjector>();
services.AddScoped<ILaunchConfigurationService, LaunchConfigurationService>();
+25 -31
View File
@@ -7,30 +7,21 @@
<UseWPF>true</UseWPF>
<ScopedCssEnabled>true</ScopedCssEnabled>
<Nullable>enable</Nullable>
<Platforms>x86;x64</Platforms>
<Platforms>x64</Platforms>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<LangVersion>preview</LangVersion>
<ApplicationIcon>Daybreak.ico</ApplicationIcon>
<IncludePackageReferencesDuringMarkupCompilation>true</IncludePackageReferencesDuringMarkupCompilation>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<UserSecretsId>cfb2a489-db80-448d-a969-80270f314c46</UserSecretsId>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<!-- Static web assets configuration for .NET 10 Blazor Hybrid -->
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
<ResolveStaticWebAssetsInputsDependsOn>RemoveDuplicateStaticWebAssets;$(ResolveStaticWebAssetsInputsDependsOn)</ResolveStaticWebAssetsInputsDependsOn>
</PropertyGroup>
<!-- Remove duplicate static web assets from referenced projects -->
<Target Name="RemoveDuplicateStaticWebAssets" BeforeTargets="ResolveStaticWebAssetsInputs;GenerateStaticWebAssetsManifest">
<ItemGroup>
<!-- Remove duplicate blazor.modules.json and blazor.webview.js from non-primary sources -->
<StaticWebAsset Remove="@(StaticWebAsset)"
Condition="'%(SourceId)' != '$(MSBuildProjectName)' And ($([System.String]::new('%(RelativePath)').Contains('blazor.modules')) Or $([System.String]::new('%(RelativePath)').Contains('blazor.webview')))" />
</ItemGroup>
</Target>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<StripSymbols>true</StripSymbols>
</PropertyGroup>
<Target Name="AddUserSecrets" BeforeTargets="PrepareForBuild" Condition=" '$(UserSecretsId)' != '' ">
<PropertyGroup>
@@ -57,7 +48,6 @@
<PackageReference Include="OpenTelemetry.Exporter.Console" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
<PackageReference Include="Reloaded.Assembler" />
<PackageReference Include="WpfExtended.SourceGeneration" />
<PackageReference Include="WpfScreenHelper" />
<PackageReference Include="PeNet" />
@@ -70,6 +60,7 @@
<PackageReference Include="Microsoft.CorrelationVector" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="SevenZipExtractor" />
</ItemGroup>
<ItemGroup>
@@ -100,8 +91,8 @@
-->
<ItemGroup>
<UpToDateCheckInput Include="..\Daybreak.API\**\*.cs" />
<UpToDateCheckInput Include="..\Daybreak.7ZipExtractor\**\*.cs" />
<UpToDateCheckInput Include="..\Daybreak.Installer\**\*.cs" />
<UpToDateCheckInput Include="..\Daybreak.Injector\**\*.cs" />
<UpToDateCheckInput Include="..\Daybreak.wiki\**\*.*" Exclude="..\Daybreak.wiki\.git\**" />
</ItemGroup>
@@ -118,9 +109,9 @@
</ItemGroup>
<ItemGroup>
<DaybreakExtractorSources Include="..\Daybreak.7ZipExtractor\**\*.cs" />
<DaybreakExtractorSources Include="..\Daybreak.7ZipExtractor\Daybreak.7ZipExtractor.csproj" />
<DaybreakExtractorSources Include="..\Daybreak.Shared\**\*.cs" />
<DaybreakInjectorSources Include="..\Daybreak.Injector\**\*.cs" />
<DaybreakInjectorSources Include="..\Daybreak.Injector\Daybreak.Injector.csproj" />
<DaybreakInjectorSources Include="..\Daybreak.Shared\**\*.cs" />
</ItemGroup>
<!--
@@ -133,31 +124,34 @@
<BaseBuildDir>$(ProjectDir)bin\$(Platform)\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\</BaseBuildDir>
<DependencyOutput Condition="'$(PublishDir)' != ''">$(PublishDir)</DependencyOutput>
<DependencyOutput Condition="'$(PublishDir)' == ''">$(BaseBuildDir)</DependencyOutput>
<InstallerOutput>$(DependencyOutput)\Installer\</InstallerOutput>
<InjectorOutput>$(DependencyOutput)\Injector\</InjectorOutput>
<ApiOutput>$(DependencyOutput)\Api\</ApiOutput>
</PropertyGroup>
<!--
The targets below build Daybreak.7ZipExtractor.csproj, Daybreak.Installer.csproj and Daybreak.API.csproj
The targets below build Daybreak.7ZipExtractor.csproj, Daybreak.Installer.csproj, Daybreak.Injector and Daybreak.API.csproj
and places the resulting binaries in the same folder as Daybreak.exe.
These work with the normal build, as well as with the dotnet publish
-->
<Target Name="PublishDaybreak7ZipExtractor" AfterTargets="Build" Inputs="@(DaybreakExtractorSources)" Outputs="$(OutDir)Daybreak.7ZipExtractor.exe">
<Target Name="PublishDaybreakInstaller" AfterTargets="Build" Inputs="@(DaybreakInstallerSources)" Outputs="$(InstallerOutput)Daybreak.Installer.exe">
<Message Importance="high" Text="📦 Daybreak.Installer -&gt; $(InstallerOutput)"></Message>
<Message Importance="high" Text="📦 Daybreak.7ZipExtractor -&gt; $(DependencyOutput)"></Message>
<Exec Command="dotnet publish ..\Daybreak.7ZipExtractor\Daybreak.7ZipExtractor.csproj -c $(Configuration) -o $(DependencyOutput)" />
<Exec Command="dotnet publish ..\Daybreak.Installer\Daybreak.Installer.csproj -c $(Configuration) -o $(InstallerOutput)" />
</Target>
<Target Name="PublishDaybreakInstaller" AfterTargets="Build" Inputs="@(DaybreakInstallerSources)" Outputs="$(OutDir)Daybreak.Installer.exe">
<Target Name="PublishDaybreakInjector" AfterTargets="Build" Inputs="@(DaybreakInjectorSources)" Outputs="$(InjectorOutput)Daybreak.Injector.exe">
<Message Importance="high" Text="📦 Daybreak.Installer -&gt; $(DependencyOutput)"></Message>
<Exec Command="dotnet publish ..\Daybreak.Installer\Daybreak.Installer.csproj -c $(Configuration) -o $(DependencyOutput)" />
<Message Importance="high" Text="📦 Daybreak.Injector -&gt; $(InjectorOutput)"></Message>
<Exec Command="dotnet publish ..\Daybreak.Injector\Daybreak.Injector.csproj -c $(Configuration) -o $(InjectorOutput)" />
</Target>
<Target Name="PublishDaybreakAPI" AfterTargets="Build" Inputs="@(DaybreakApiSources)" Outputs="$(OutDir)Daybreak.API.dll">
<Target Name="PublishDaybreakAPI" AfterTargets="Build" Inputs="@(DaybreakApiSources)" Outputs="$(ApiOutput)Daybreak.API.dll">
<Message Importance="high" Text="📦 Daybreak.API -&gt; $(DependencyOutput)"></Message>
<Message Importance="high" Text="📦 Daybreak.API -&gt; $(ApiOutput)"></Message>
<Exec Command="dotnet publish ..\Daybreak.API\Daybreak.API.csproj -c $(Configuration) -o $(DependencyOutput)" />
<Exec Command="dotnet publish ..\Daybreak.API\Daybreak.API.csproj -c $(Configuration) -o $(ApiOutput)" />
</Target>
</Project>
+8 -5
View File
@@ -54,7 +54,13 @@ public sealed class Launcher : BlazorHybridApplication<App>
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<App>
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<App>
{
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);
}
+8 -5
View File
@@ -28,8 +28,9 @@ public sealed class DaybreakApiService(
ILogger<ScopedApiContext> 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(
@@ -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> 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 = 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(
}
}
/// <summary>
/// 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
/// </summary>
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<StartupInfo>()
};
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;
}
/// <summary>
/// https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs
/// </summary>
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 _);
}
/// <summary>
/// https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs
/// </summary>
private static int SearchBytes(Memory<byte> haystack, Memory<byte> 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;
}
/// <summary>
/// https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs
/// </summary>
private static IntPtr GetProcessModuleBase(IntPtr process)
{
if (NativeMethods.NtQueryInformationProcess(process, NativeMethods.ProcessInfoClass.ProcessBasicInformation, out var pbi,
Marshal.SizeOf<ProcessBasicInformation>(), out _) != 0)
{
return IntPtr.Zero;
}
var buffer = new byte[Marshal.SizeOf<PEB>()];
if (!NativeMethods.ReadProcessMemory(process, pbi.PebBaseAddress, buffer, Marshal.SizeOf<PEB>(), out _))
{
return IntPtr.Zero;
}
PEB peb = new()
{
ImageBaseAddress = (IntPtr)BitConverter.ToInt32(buffer, 8)
};
return peb.ImageBaseAddress + 0x1000;
}
private static List<IntPtr> 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())
@@ -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<DaybreakInjector> logger)
: IDaybreakInjector
{
private const string ExecutableName = "Injector/Daybreak.Injector.exe";
private readonly ILogger<DaybreakInjector> logger = logger;
public bool InjectorAvailable()
{
var executablePath = PathUtils.GetAbsolutePathFromRoot(ExecutableName);
return File.Exists(executablePath);
}
public async Task<InjectorResponses.InjectResult> 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<InjectorResponses.InjectResult> 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<InjectorResponses.ResumeResult> 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;
}
}
}
@@ -0,0 +1,5 @@
namespace Daybreak.Services.Injection.Models;
internal sealed record InjectionResult(int ExitCode, string Output, string Error)
{
}
+9 -129
View File
@@ -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<ProcessInjector> logger) : IProcessInjector
{
private readonly IDaybreakInjector daybreakInjector = daybreakInjector.ThrowIfNull();
private readonly ILogger<ProcessInjector> logger = logger.ThrowIfNull();
public Task<bool> Inject(Process process, string pathToDll, CancellationToken cancellationToken)
public async Task<bool> 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;
}
}
+12 -174
View File
@@ -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<StubInjector> logger) : IStubInjector
{
const string ListenerEntryPoint = "ThreadInit";
private readonly IDaybreakInjector daybreakInjector = daybreakInjector.ThrowIfNull();
private readonly ILogger<StubInjector> logger = logger;
/// <summary>
/// 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
/// </summary>
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<StubInjector> logger = logger.ThrowIfNull();
public bool Inject(Process target, string dllPath, out int exitCode)
public async Task<int> 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;
}
}
+1 -1
View File
@@ -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
{
+21 -35
View File
@@ -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<SevenZipExtractor> logger) : ISevenZipExtractor
{
private const string ExtractorExeName = "Daybreak.7ZipExtractor.exe";
private readonly ILogger<SevenZipExtractor> logger = logger.ThrowIfNull();
public async Task<bool> ExtractToDirectory(string sourceFile, string destinationDirectory, Action<double, string> 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;
}
}
}
@@ -34,8 +34,8 @@ internal sealed class ApplicationUpdater(
ILogger<ApplicationUpdater> 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");
+3 -11
View File
@@ -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
/// </summary>
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);
}
}
+9 -4
View File
@@ -15,8 +15,10 @@
<PackageVersion Include="MeaMod.DNS" Version="1.0.71" />
<PackageVersion Include="MemoryPack" Version="1.21.4" />
<PackageVersion Include="MemoryPack.Generator" Version="1.21.4" />
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="10.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebView.Wpf" Version="9.0.120" /> <!-- Pinned. TODO: #1226 New version of Microsoft.AspNetCore.Components.WebView.Wpf is unstable -->
<!-- Pinned. TODO: #1226 New version of Microsoft.AspNetCore.Components.WebView.Wpf is unstable -->
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="9.0.10" />
<!-- Pinned. TODO: #1226 New version of Microsoft.AspNetCore.Components.WebView.Wpf is unstable -->
<PackageVersion Include="Microsoft.AspNetCore.Components.WebView.Wpf" Version="9.0.120" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
<PackageVersion Include="Microsoft.Authentication.WebAssembly.Msal" Version="9.0.8" />
<PackageVersion Include="Microsoft.CorrelationVector" Version="1.0.42" />
@@ -49,7 +51,8 @@
<PackageVersion Include="Serilog.Settings.Configuration" Version="10.0.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="SharpCompress" Version="0.43.0" />
<PackageVersion Include="SevenZipExtractor" Version="1.0.19" />
<PackageVersion Include="SharpCompress" Version="0.44.0" />
<PackageVersion Include="Slim" Version="1.11.2" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.Swagger" Version="10.1.0" />
@@ -64,9 +67,11 @@
<PackageVersion Include="SystemExtensions.NetStandard.DependencyInjection" Version="1.6.9" />
<PackageVersion Include="SystemExtensions.NetStandard.Generators" Version="0.1.6" />
<PackageVersion Include="TrailBlazr" Version="0.3.1" />
<PackageVersion Include="WpfExtended.Blazor" Version="0.8.1" /> <!-- Pinned. TODO: #1226 New version of Microsoft.AspNetCore.Components.WebView.Wpf is unstable -->
<!-- Pinned. TODO: #1226 New version of Microsoft.AspNetCore.Components.WebView.Wpf is unstable -->
<PackageVersion Include="WpfExtended.Blazor" Version="0.8.1" />
<PackageVersion Include="WpfExtended.SourceGeneration" Version="0.3.1" />
<PackageVersion Include="WpfScreenHelper" Version="2.1.1" />
<PackageVersion Include="zgabi.ManagedXZ" Version="1.0.2" />
<PackageVersion Include="ZLinq" Version="1.5.4" />
</ItemGroup>
</Project>