mirror of
https://github.com/gwdevhub/Daybreak.git
synced 2026-07-15 15:19:57 +00:00
This commit is contained in:
@@ -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: |
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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 -> $(InstallerOutput)"></Message>
|
||||
|
||||
<Message Importance="high" Text="📦 Daybreak.7ZipExtractor -> $(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 -> $(DependencyOutput)"></Message>
|
||||
|
||||
<Exec Command="dotnet publish ..\Daybreak.Installer\Daybreak.Installer.csproj -c $(Configuration) -o $(DependencyOutput)" />
|
||||
<Message Importance="high" Text="📦 Daybreak.Injector -> $(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 -> $(DependencyOutput)"></Message>
|
||||
<Message Importance="high" Text="📦 Daybreak.API -> $(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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user