Added support for 10.1.5 (READ THE README):

- Introduced new --dev command line option
- README: Read the new "IMPORTANT NOTE FOR LOCAL DEVELOPMENT & SERVER CONNECTIONS" section.

Properly kill the launcher and game client on pattern detection fails.
This commit is contained in:
Fabian
2023-05-21 00:56:25 +00:00
parent 318b1a0bbf
commit 221438ce2d
6 changed files with 69 additions and 33 deletions

View File

@@ -7,6 +7,14 @@ A game launcher for World of Warcraft that allows you to connect to custom serve
Please see our Open Source project [Documentation Repo](https://github.com/Arctium/Documentation)
### IMPORTANT NOTE FOR LOCAL DEVELOPMENT & SERVER CONNECTIONS
* LOCAL HOSTNAME & IP: `USE` the `--dev` command line parameter to to avoid issues with invalid certificate chains.
* EXTERNAL HOSTNAME:
* `DO NOT` use the `--dev` command line parameter.
* `USE` a valid certificate matching your authentication/bnet server host name.
* That certificate needs to be loaded by the authentication/bnet server too.
* EXTERNAL IP: `NOT SUPPORTED`
### Binary Releases
You can find signed binary releases at [Releases](https://github.com/Arctium/WoW-Launcher/releases)
@@ -63,6 +71,7 @@ Options:
--binary <binary>
--keepcache [default: True]
--staticseed
--dev [default: False], Required for local development without valid certificates.
-?, -h, --help Show help and usage information
Additional Arguments:

View File

@@ -4,7 +4,7 @@
using System.CommandLine.Builder;
using System.CommandLine.Parsing;
namespace Arctium.WoW.Launcher.Misc;
namespace Arctium.WoW.Launcher;
static class LaunchOptions
{
@@ -13,6 +13,7 @@ static class LaunchOptions
public static Option<string> GameBinary = new("--binary");
public static Option<bool> KeepCache = new("--keepcache", () => true);
public static Option<bool> UseStaticAuthSeed = new("--staticseed");
public static Option<bool> DevMode = new("--dev", () => false);
public static Parser Instance => new CommandLineBuilder(ConfigureCommandLine(RootCommand))
.UseHelp()
@@ -28,7 +29,8 @@ static class LaunchOptions
GamePath,
GameBinary,
KeepCache,
UseStaticAuthSeed
UseStaticAuthSeed,
DevMode
};
static Command ConfigureCommandLine(Command rootCommand)

View File

@@ -2,15 +2,13 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.CommandLine.Parsing;
using System.Reflection.PortableExecutable;
using static Arctium.WoW.Launcher.Misc.Helpers;
namespace Arctium.WoW.Launcher;
class Launcher
static class Launcher
{
public static CancellationTokenSource CancellationTokenSource => new();
public static readonly CancellationTokenSource CancellationTokenSource = new();
public static string PrepareGameLaunch(ParseResult commandLineResult)
{
@@ -91,7 +89,7 @@ class Launcher
return gameBinaryPath;
}
public static bool LaunchGame(string appPath, string gameCommandLine, bool useStaticAuthSeed)
public static bool LaunchGame(string appPath, string gameCommandLine, ParseResult commandLineResult)
{
var startupInfo = new StartupInfo();
var processInfo = new ProcessInformation();
@@ -101,7 +99,8 @@ class Launcher
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("Starting WoW client...");
var createSuccess = NativeWindows.CreateProcess(null, $"{appPath} {gameCommandLine}", 0, 0, false, 4, 0, new FileInfo(appPath)?.DirectoryName, ref startupInfo, out processInfo);
var createSuccess = NativeWindows.CreateProcess(null, $"{appPath} {gameCommandLine}", 0, 0, false, 4, 0, new FileInfo(appPath).DirectoryName,
ref startupInfo, out processInfo);
// On some systems we have to launch the game with the application name used.
if (!createSuccess)
@@ -117,10 +116,11 @@ class Launcher
// Resume the process to initialize it.
NativeWindows.NtResumeProcess(processInfo.ProcessHandle);
var mbi = new MemoryBasicInformation();
MemoryBasicInformation mbi;
// Wait for the memory region to be initialized.
while (NativeWindows.VirtualQueryEx(processInfo.ProcessHandle, memory.BaseAddress, out mbi, MemoryBasicInformation.Size) == 0 || mbi.RegionSize <= 0x1000)
while (NativeWindows.VirtualQueryEx(processInfo.ProcessHandle, memory.BaseAddress, out mbi, MemoryBasicInformation.Size) == 0 ||
mbi.RegionSize <= 0x1000)
{ }
if (mbi.BaseAddress != 0)
@@ -139,17 +139,24 @@ class Launcher
// We need to cache this here since we are using our RSA modulus as auth seed.
var modulusOffset = memory.Data.FindPattern(Patterns.Common.SignatureModulus);
if (clientVersion is (9, 2, 7, _) or (3, _, _, _) or (10, <= 1, _, _) and not (10, 1, 5, _))
{
Task.WaitAll(new[]
{
memory.PatchMemory(Patterns.Common.CertBundle, certBundleData, "Certificate Bundle"),
memory.PatchMemory(Patterns.Common.SignatureModulus, Patches.Common.SignatureModulus, "Certificate Signature RsaModulus")
}, CancellationTokenSource.Token);
}
// Wait for all direct memory patch tasks to complete,
Task.WaitAll(new[]
{
memory.PatchMemory(Patterns.Common.CertBundle, certBundleData, "Certificate Bundle"),
memory.PatchMemory(Patterns.Common.SignatureModulus, Patches.Common.SignatureModulus, "Certificate Signature RsaModulus"),
memory.PatchMemory(Patterns.Common.ConnectToModulus, Patches.Common.RsaModulus, "ConnectTo RsaModulus"),
// Recent clients have a different signing algorithm in EnterEncryptedMode.
(clientVersion is (9, 2, 7, _) or (3, _, _, _) or (10, _, _, _))
? memory.PatchMemory(Patterns.Common.CryptoEdPublicKey, Patches.Common.CryptoEdPublicKey, "GameCrypto Ed25519 PublicKey")
: memory.PatchMemory(Patterns.Common.CryptoRsaModulus, Patches.Common.RsaModulus, "GameCrypto RsaModulus"),
clientVersion is (9, 2, 7, _) or (3, _, _, _) or (10, _, _, _)
? memory.PatchMemory(Patterns.Common.CryptoEdPublicKey, Patches.Common.CryptoEdPublicKey, "GameCrypto Ed25519 PublicKey")
: memory.PatchMemory(Patterns.Common.CryptoRsaModulus, Patches.Common.RsaModulus, "GameCrypto RsaModulus"),
memory.PatchMemory(Patterns.Common.Portal, Patches.Common.Portal, "Login Portal"),
memory.PatchMemory(Patterns.Common.VersionUrl, versionPatch, "Version URL"),
@@ -162,13 +169,24 @@ class Launcher
WaitForUnpack(ref processInfo, memory, ref mbi, gameAppData);
#if x64
Task.WaitAll(new[]
if (clientVersion is (9, 2, 7, _) or (3, _, _, _) or (10, <= 1, _, _) and not (10, 1, 5, _))
{
memory.QueuePatch(Patterns.Windows.CertBundle, Patches.Windows.CertBundle, "CertBundle"),
memory.QueuePatch(Patterns.Windows.CertCommonName, Patches.Windows.CertCommonName, "CertCommonName", 5)
}, CancellationTokenSource.Token);
Task.WaitAll(new[]
{
memory.QueuePatch(Patterns.Windows.CertBundle, Patches.Windows.CertBundle, "CertBundle"),
memory.QueuePatch(Patterns.Windows.CertCommonName, Patches.Windows.CertCommonName, "CertCommonName", 5)
}, CancellationTokenSource.Token);
}
else if (commandLineResult.GetValueForOption(LaunchOptions.DevMode))
{
Task.WaitAll(new[]
{
memory.QueuePatch(Patterns.Windows.CertChain, Patches.Windows.CertChain, "CertChain"),
memory.QueuePatch(Patterns.Windows.CertCommonName, Patches.Windows.CertCommonName, "CertCommonName", 5)
}, CancellationTokenSource.Token);
}
if (useStaticAuthSeed)
if (commandLineResult.HasOption(LaunchOptions.UseStaticAuthSeed))
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("Static auth seed used. Be sure that the server you are connecting to supports it.");
@@ -186,12 +204,12 @@ class Launcher
Task.WaitAll(new[]
{
(clientVersion is (10, _, _, _))
? memory.QueuePatch(Patterns.Windows.LoadByFileIdAlternate, Patches.Windows.NoJump, "LoadByFileId", 3)
: memory.QueuePatch(Patterns.Windows.LoadByFileId, Patches.Windows.NoJump, "LoadByFileId", 6),
? memory.QueuePatch(Patterns.Windows.LoadByFileIdAlternate, Patches.Windows.NoJump, "LoadByFileId", 3)
: memory.QueuePatch(Patterns.Windows.LoadByFileId, Patches.Windows.NoJump, "LoadByFileId", 6),
(clientVersion is (10, _, _, _))
? memory.QueuePatch(Patterns.Windows.LoadByFilePathAlternate, Patches.Windows.NoJump, "LoadByFilePath", 3)
: memory.QueuePatch(Patterns.Windows.LoadByFilePath, Patches.Windows.NoJump, "LoadByFilePath", 3)
? memory.QueuePatch(Patterns.Windows.LoadByFilePathAlternate, Patches.Windows.NoJump, "LoadByFilePath", 3)
: memory.QueuePatch(Patterns.Windows.LoadByFilePath, Patches.Windows.NoJump, "LoadByFilePath", 3)
}, CancellationTokenSource.Token);
var (idAlloc, stringAlloc) = ModLoader.LoadFileMappings(processInfo.ProcessHandle);
@@ -234,9 +252,14 @@ class Launcher
}
}
}
// Only exit and do not print any exception messages to the console.
catch (OperationCanceledException)
{
NativeWindows.TerminateProcess(processInfo.ProcessHandle, 0);
}
// Just print out the exception we have and kill the game process.
catch (Exception ex)
{
// Just print out the exception we have and kill the game process.
Console.WriteLine(ex);
Console.WriteLine(ex.StackTrace);
@@ -279,19 +302,19 @@ class Launcher
{
#if x64
// Wait for client initialization.
var initOffset = memory?.Read(mbi.BaseAddress, (int)mbi.RegionSize)?.FindPattern(Patterns.Windows.Init) ?? 0;
var initOffset = memory.Read(mbi.BaseAddress, (int)mbi.RegionSize)?.FindPattern(Patterns.Windows.Init) ?? 0;
while (initOffset == 0)
{
initOffset = memory?.Read(mbi.BaseAddress, (int)mbi.RegionSize)?.FindPattern(Patterns.Windows.Init) ?? 0;
initOffset = memory.Read(mbi.BaseAddress, (int)mbi.RegionSize)?.FindPattern(Patterns.Windows.Init) ?? 0;
Console.WriteLine("Waiting for client initialization...");
}
initOffset += BitConverter.ToUInt32(memory.Read(initOffset + memory.BaseAddress + 2, 4), 0) + 10;
while (memory?.Read(initOffset + memory.BaseAddress, 1)?[0] == null ||
memory?.Read(initOffset + memory.BaseAddress, 1)?[0] == 0)
while (memory.Read(initOffset + memory.BaseAddress, 1)?[0] == null ||
memory.Read(initOffset + memory.BaseAddress, 1)?[0] == 0)
memory.Data = memory.Read(mbi.BaseAddress, (int)mbi.RegionSize);
#else
// Get PE header info for client initialization.
@@ -343,7 +366,6 @@ class Launcher
foreach (var a in remapOffsets)
{
var instructionStart = (int)a + 4;
var instructionEnd = (int)a + 4 + 6;
var instructions = new byte[6];
Buffer.BlockCopy(memory.Data, instructionStart, instructions, 0, 6);
@@ -352,7 +374,7 @@ class Launcher
if (WinMemory.IsUnconditionalJump(instructions))
continue;
var operandValue = 0;
int operandValue;
if (WinMemory.IsShortJump(instructions))
operandValue = instructions[1] + 2;
@@ -392,7 +414,6 @@ class Launcher
// Might need some better checks or future updates.
if (memory.Data[i - 3] == 0x48)
{
var iBytes = BitConverter.GetBytes(i);
var jumpBytes = new byte[] { 0xEB };
tempPatches.TryAdd($"ShortJump{i}", (i, jumpBytes));

View File

@@ -9,6 +9,7 @@ static class Windows
public static byte[] Integrity = { 0xC2, 0x00, 0x00 };
public static byte[] CertBundle = { 0x90, 0x90 };
public static byte[] CertCommonName = { 0xB0, 0x01 };
public static byte[] CertChain = { 0xB3, 0x01 };
public static byte[] ShortJump = { 0xEB };
public static byte[] NoJump = { 0x00, 0x00, 0x00, 0x00 };
public static byte[] AuthSeed = { 0x0F, 0x28, 0x05, 0xEF, 0xBE, 0xAD, 0xDE, 0x0F, 0x11, 0x02, 0xC3 };

View File

@@ -17,6 +17,9 @@ static class Windows
public static short[] CertBundle = { 0x75, 0x06, 0x48, -1, -1, 0x60, 0x5F, 0xC3 };
public static short[] CertCommonName = { 0x80, -1, 0x2A, 0x75, -1, 0x32, 0xC0, 0x48 };
// Developer mode.
public static short[] CertChain = { 0x32, 0xDB, 0xEB, 0x02, 0xB3, 0x01, 0x48, 0x83, -1, -1, 0x00, 0x00, 0x00, 0x00 };
// Auth seed function.
public static short[] AuthSeed = { 0x57, 0x6F, 0x57, 0x00, 0xE8, -1, -1, -1, -1, 0x48, 0x8D };

View File

@@ -16,7 +16,7 @@ LaunchOptions.RootCommand.SetHandler(context =>
var appPath = Launcher.PrepareGameLaunch(context.ParseResult);
var gameCommandLine = string.Join(" ", context.ParseResult.UnmatchedTokens);
if (string.IsNullOrEmpty(appPath) || !Launcher.LaunchGame(appPath, gameCommandLine, context.ParseResult.HasOption(LaunchOptions.UseStaticAuthSeed)))
if (string.IsNullOrEmpty(appPath) || !Launcher.LaunchGame(appPath, gameCommandLine, context.ParseResult))
WaitAndExit(5000);
});