Add registry patch for -launcherlogin usage.

Some code cleanups.
This commit is contained in:
Fabian
2021-10-07 22:53:41 +02:00
parent dc93bda08d
commit 56155433c1
10 changed files with 115 additions and 176 deletions

View File

@@ -15,6 +15,7 @@
<FileVersion>9.0.0.0</FileVersion>
<Copyright>Arctium</Copyright>
<Platforms>x64</Platforms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
@@ -32,12 +33,6 @@
<PackageReference Include="Microsoft.DotNet.ILCompiler" Version="7.0.0-*" />
</ItemGroup>
<ItemGroup>
<Folder Include="IO\" />
<Folder Include="Constants\" />
<Folder Include="Misc\" />
</ItemGroup>
<ItemGroup>
<TrimmerRootAssembly Include="mscorlib" />
<TrimmerRootAssembly Include="System.Runtime" />

View File

@@ -1,5 +1,5 @@
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the proje root for full license information.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using static Arctium.WoW.Launcher.Misc.NativeWindows;
@@ -7,24 +7,39 @@ namespace Arctium.WoW.Launcher.IO;
class WinMemory
{
public byte[] Data { get; private set; }
public byte[] Data { get; set; }
public nint ProcessHandle { get; }
public nint BaseAddress { get; }
ProcessBasicInformation peb;
public WinMemory(nint processHandle)
public WinMemory(ProcessInformation processInformation, FileInfo fileInfo)
{
ProcessHandle = processHandle;
ProcessHandle = processInformation.ProcessHandle;
if (processHandle == 0)
if (processInformation.ProcessHandle == 0)
throw new InvalidOperationException("No valid process found.");
BaseAddress = ReadImageBaseFromPEB(processHandle);
BaseAddress = ReadImageBaseFromPEB(processInformation.ProcessHandle);
if (BaseAddress == 0)
throw new InvalidOperationException("Error while reading PEB data.");
Data = Read(BaseAddress, (int)fileInfo.Length);
}
public void RefreshMemoryData(int size)
{
// Reset previous memory data.
Data = Array.Empty<byte>();
while (Data?.Length == 0)
{
Console.WriteLine("Refreshing client data...");
Data = Read(BaseAddress, size);
}
}
public nint Read(nint address)
@@ -88,7 +103,7 @@ class WinMemory
return (int)length;
}
public void Write(nint address, byte[] data, MemProtection newProtection = MemProtection.ExecuteReadWrite)
public void Write(nint address, byte[] data, MemProtection newProtection = MemProtection.ReadWrite)
{
try
{
@@ -106,7 +121,39 @@ class WinMemory
}
}
public void Write(long address, byte[] data, MemProtection newProtection = MemProtection.ExecuteReadWrite) => Write((nint)address, data, newProtection);
public void Write(long address, byte[] data, MemProtection newProtection = MemProtection.ReadWrite) => Write((nint)address, data, newProtection);
public Task PatchMemory(short[] pattern, byte[] patch, string patchName)
{
Console.WriteLine($"[{patchName}] Patching...");
long patchOffset = Data.FindPattern(pattern, BaseAddress);
// No result for the given pattern.
if (patchOffset == 0)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"[{patchName}] No result found.");
Console.WriteLine("Press any key to continue...");
Console.ReadKey();
}
while (Read(patchOffset, patch.Length)?.SequenceEqual(patch) == false)
Write(patchOffset, patch);
Console.Write($"[{patchName}]");
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(" Done.");
Console.ForegroundColor = ConsoleColor.Gray;
Console.WriteLine();
return Task.CompletedTask;
}
public bool RemapAndPatch(nint viewAddress, int viewSize, Dictionary<string, (long, byte[])> patches)
{
@@ -170,10 +217,8 @@ class WinMemory
// Unmap them writeable view, it's not longer needed.
NtUnmapViewOfSection(ProcessHandle, viewBase2);
var mbi = new MemoryBasicInformation();
// Check if the allocation protections is the right one.
if (VirtualQueryEx(ProcessHandle, BaseAddress, out mbi, mbi.Size) != 0 && mbi.AllocationProtect == MemProtection.ExecuteRead)
if (VirtualQueryEx(ProcessHandle, BaseAddress, out MemoryBasicInformation mbi, MemoryBasicInformation.Size) != 0 && mbi.AllocationProtect == MemProtection.ExecuteRead)
{
// Also check if we can change the page protection.
if (!VirtualProtectEx(ProcessHandle, BaseAddress, 0x4000, (uint)MemProtection.ReadWrite, out var oldProtect))
@@ -198,9 +243,7 @@ class WinMemory
public bool RemapAndPatch(Dictionary<string, (long, byte[])> patches)
{
var mbi = new MemoryBasicInformation();
if (VirtualQueryEx(ProcessHandle, BaseAddress, out mbi, mbi.Size) != 0)
if (VirtualQueryEx(ProcessHandle, BaseAddress, out MemoryBasicInformation mbi, MemoryBasicInformation.Size) != 0)
return RemapAndPatch(mbi.BaseAddress, (int)mbi.RegionSize, patches);
return false;

View File

@@ -70,6 +70,8 @@ static class Extensions
} while ((matchList.Count < maxMatches || match < maxOffset) && match != 0);
return matchList;
}
return matchList;
}
public static short[] ToPattern(this string data) => Encoding.UTF8.GetBytes(data).Select(b => (short)b).ToArray();
}

View File

@@ -1,5 +1,5 @@
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the proje root for full license information.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Patches;

View File

@@ -1,5 +1,5 @@
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the proje root for full license information.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Patches;
@@ -9,4 +9,7 @@ static class Windows
public static byte[] CertBundle = { 0x90, 0x90 };
public static byte[] CertCommonName = { 0xB0, 0x01 };
public static byte[] ShortJump = { 0xEB };
// Registry entry used for -launcherlogin.
public static byte[] LauncherLogin = Encoding.UTF8.GetBytes(@"Software\Custom Game Server Dev\Battle.net\Launch Options\");
}

View File

@@ -1,11 +1,11 @@
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the proje root for full license information.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Patterns;
static class Common
{
public static short[] CertBundle = Encoding.UTF8.GetBytes("{\"Created\":").Select(b => (short)b).ToArray();
public static short[] CertBundle = "{\"Created\":".ToPattern();
public static short[] ConnectToModulus = { 0x91, 0xD5, 0x9B, 0xB7, 0xD4, 0xE1, 0x83, 0xA5 };
public static short[] SignatureModulus = { 0x35, 0xFF, 0x17, 0xE7, 0x33, 0xC4, 0xD3, 0xD4 };
public static short[] ChangeProtocolModulus = { 0x71, 0xFD, 0xFA, 0x60, 0x14, 0x0D, 0xF2, 0x05 };

View File

@@ -1,5 +1,5 @@
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the proje root for full license information.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Arctium.WoW.Launcher.Patterns;
@@ -18,4 +18,7 @@ static class Windows
public static short[] CertCommonName = { 0x80, 0xF9, 0x2A, 0x75, -1, 0x32, 0xC0, 0x48, 0x8B };
public static short[] CertSignatureMagic = { 0x3B, 0x0D, -1, -1, -1, -1, 0x0F, 0x85, -1, -1, -1, -1, 0x48, 0x8D, 0x15, -1, -1, -1, -1, 0x48, 0x8D, -1, -1, -1, -1, 0x00, 0x00, 0xE8, -1, -1, -1, -1, 0x48 };
public static short[] CertSignature = { 0x74, -1, 0x4C, 0x8B, -1, 0x08, 0x48, 0x8B, -1, 0x48, 0x8B, -1, 0x49, 0x81, -1, 0xFC };
// Registry entry used for -launcherlogin.
public static short[] LauncherLogin = @"Software\Blizzard Entertainment\Battle.net\Launch Options\".ToPattern();
}

View File

@@ -1,5 +1,5 @@
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the proje root for full license information.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using static Arctium.WoW.Launcher.Misc.Helpers;
@@ -12,8 +12,6 @@ class Program
static bool keepCache = false;
static string consoleArgs = string.Empty;
static Dictionary<string, (long, byte[])> patches = new();
static void Main(string[] args)
{
PrintHeader("WoW Client Launcher");
@@ -125,121 +123,28 @@ class Program
// On some systems we have to launch the game with the application name used.
if (!createSuccess)
createSuccess = NativeWindows.CreateProcess(appPath, $"- {consoleArgs}", 0, 0, false, 4U, 0, null, ref startupInfo, out processInfo);
createSuccess = NativeWindows.CreateProcess(appPath, $" {consoleArgs}", 0, 0, false, 4U, 0, null, ref startupInfo, out processInfo);
// Start process with suspend flags.
if (createSuccess)
{
var memory = new WinMemory(processInfo.ProcessHandle);
var appFileInfo = new FileInfo(appPath);
var memory = new WinMemory(processInfo, appFileInfo);
// Get cert bundle offset from file.
var bundleOffset = File.ReadAllBytes(appPath).FindPattern(Patterns.Common.CertBundle, memory.BaseAddress).ToNint();
var sectionOffset = memory.Read(bundleOffset, 0x10000).FindPattern(Patterns.Common.CertBundle);
byte[] certBundleData = Convert.FromBase64String(Patches.Common.CertBundleData);
bundleOffset = (bundleOffset + sectionOffset).ToNint();
// Build the version URL from the game binary build.
int wowBuild = GetVersionValueFromClient(appPath, 0);
byte[] versionPatch = Patches.Common.GetVersionUrl(wowBuild);
var certBundleData = Convert.FromBase64String(Patches.Common.CertBundleData);
var originalBundleLength = memory.ReadDataLength(bundleOffset, "NGIS");
if (originalBundleLength == -1)
{
Console.WriteLine("Can't get cert bundle data length.");
WaitAndExit(5000);
}
Console.WriteLine("Patching cert bundle data...");
// Zero the original cert bundle data.
while (memory.Read(bundleOffset, 1)?[0] != 0)
memory.Write(bundleOffset, new byte[originalBundleLength + 4 + 256]);
// Be sure that the modulus is written before the client is initialized.
while (memory.Read(bundleOffset, 1)?[0] != certBundleData[0])
memory.Write(bundleOffset, certBundleData);
Console.WriteLine("Done");
Console.WriteLine("Patching Signature modulus...");
// Get ConnectTo RSA modulus offset from file.
var modulusOffset = File.ReadAllBytes(appPath).FindPattern(Patterns.Common.SignatureModulus, memory.BaseAddress).ToNint();
sectionOffset = memory.Read(modulusOffset, 0x10000).FindPattern(Patterns.Common.SignatureModulus);
modulusOffset = (modulusOffset + sectionOffset).ToNint();
// Be sure that the modulus is written before the client is initialized.
while (memory.Read(modulusOffset, 1)?[0] != Patches.Common.SignatureModulus[0])
memory.Write(modulusOffset, Patches.Common.SignatureModulus);
Console.WriteLine("Done");
Console.WriteLine("Patching ConnectTo modulus...");
// Get ConnectTo RSA modulus offset from file.
modulusOffset = File.ReadAllBytes(appPath).FindPattern(Patterns.Common.ConnectToModulus, memory.BaseAddress).ToNint();
sectionOffset = memory.Read(modulusOffset, 0x10000).FindPattern(Patterns.Common.ConnectToModulus);
modulusOffset = (modulusOffset + sectionOffset).ToNint();
// Be sure that the modulus is written before the client is initialized.
while (memory.Read(modulusOffset, 1)?[0] != Patches.Common.Modulus[0])
memory.Write(modulusOffset, Patches.Common.Modulus);
Console.WriteLine("Done");
Console.WriteLine("Patching ChangeProtocol modulus...");
// Get ChangeProtocol RSA modulus offset from file.
modulusOffset = File.ReadAllBytes(appPath).FindPattern(Patterns.Common.ChangeProtocolModulus, memory.BaseAddress).ToNint();
sectionOffset = memory.Read(modulusOffset, 0x10000).FindPattern(Patterns.Common.ChangeProtocolModulus);
modulusOffset = (modulusOffset + sectionOffset).ToNint();
// Be sure that the modulus is written before the client is initialized.
while (memory.Read(modulusOffset, 1)?[0] != Patches.Common.Modulus[0])
memory.Write(modulusOffset, Patches.Common.Modulus);
Console.WriteLine("Done");
Console.WriteLine("Patching portal...");
// Portal patch
nint portalOffset = 0;
// Get portal offset from file.
portalOffset = File.ReadAllBytes(appPath).FindPattern(Patterns.Common.Portal, memory.BaseAddress).ToNint();
sectionOffset = memory.Read(portalOffset, 0x2000).FindPattern(Patterns.Common.Portal);
portalOffset = (portalOffset + sectionOffset).ToNint();
// Be sure that the portal is written before the client is initialized.
while (memory.Read(portalOffset, 1)?[0] != Patches.Common.Portal[0])
memory.Write(portalOffset, Patches.Common.Portal);
Console.WriteLine("Done");
Console.WriteLine("Patching version url...");
// Version patch
nint versionOffset = 0;
// Get version offset from file.
versionOffset = File.ReadAllBytes(appPath).FindPattern(Patterns.Common.VersionUrl, memory.BaseAddress).ToNint();
if (versionOffset == 0)
{
Console.WriteLine($"Can't find {nameof(versionOffset)}. Custom version URL is used!!!");
Console.WriteLine("Done.");
}
else
{
sectionOffset = memory.Read(versionOffset, 0x2000).FindPattern(Patterns.Common.VersionUrl);
versionOffset = (versionOffset + sectionOffset).ToNint();
var wowBuild = GetVersionValueFromClient(appPath, 0);
var versionPatch = Patches.Common.GetVersionUrl(wowBuild);
while (memory.Read(versionOffset, 1)?[0] != versionPatch[0])
memory.Write(versionOffset, versionPatch);
Console.WriteLine("Done");
}
// Wait for all direct memory patch tasks to complete,
Task.WaitAll(memory.PatchMemory(Patterns.Common.CertBundle, certBundleData, "Certificate Bundle"),
memory.PatchMemory(Patterns.Common.SignatureModulus, Patches.Common.SignatureModulus, "Certificate Signature Modulus"),
memory.PatchMemory(Patterns.Common.ConnectToModulus, Patches.Common.Modulus, "ConnectTo Modulus"),
memory.PatchMemory(Patterns.Common.ChangeProtocolModulus, Patches.Common.Modulus, "ChangeProtocol (GameCrypt) Modulus"),
memory.PatchMemory(Patterns.Common.Portal, Patches.Common.Portal, "Login Portal"),
memory.PatchMemory(Patterns.Common.VersionUrl, versionPatch, "Version URL"),
memory.PatchMemory(Patterns.Windows.LauncherLogin, Patches.Windows.LauncherLogin, "Launcher Login Registry"));
// Resume the process to initialize it.
NativeWindows.NtResumeProcess(processInfo.ProcessHandle);
@@ -247,26 +152,20 @@ class Program
var mbi = new MemoryBasicInformation();
// Wait for the memory region to be initialized.
while (NativeWindows.VirtualQueryEx(processInfo.ProcessHandle, memory.BaseAddress, out mbi, mbi.Size) == 0 || (int)mbi.RegionSize <= 0x1000)
while (NativeWindows.VirtualQueryEx(processInfo.ProcessHandle, memory.BaseAddress, out mbi, MemoryBasicInformation.Size) == 0 || mbi.RegionSize <= 0x1000)
{ }
if (mbi.BaseAddress != 0)
{
var binary = Array.Empty<byte>();
var patches = new Dictionary<string, (long Address, byte[] Data)>();
PrepareAntiCrash(memory, binary, ref mbi, ref processInfo);
PrepareAntiCrash(memory, patches, ref mbi, ref processInfo);
// Recheck binary data.
while (binary?.Length == 0)
{
Console.WriteLine("Waiting for client data...");
binary = memory.Read(mbi.BaseAddress, (int)mbi.RegionSize);
}
memory.RefreshMemoryData((int)mbi.RegionSize);
// Get patch locations.
var certBundleOffset = binary.FindPattern(Patterns.Windows.CertBundle);
var certCommonNameOffset = binary.FindPattern(Patterns.Windows.CertCommonName);
var certBundleOffset = memory.Data.FindPattern(Patterns.Windows.CertBundle);
var certCommonNameOffset = memory.Data.FindPattern(Patterns.Windows.CertCommonName);
if (certBundleOffset == 0 || certCommonNameOffset == 0)
{
@@ -322,7 +221,7 @@ class Program
}
}
static void PrepareAntiCrash(WinMemory memory, byte[] binary, ref MemoryBasicInformation mbi, ref ProcessInformation processInfo)
static void PrepareAntiCrash(WinMemory memory, Dictionary<string, (long, byte[])> patches, ref MemoryBasicInformation mbi, ref ProcessInformation processInfo)
{
// Wait for client initialization.
var initOffset = memory?.Read(mbi.BaseAddress, (int)mbi.RegionSize)?.FindPattern(Patterns.Windows.Init) ?? 0;
@@ -338,35 +237,29 @@ class Program
while (memory?.Read(initOffset + memory.BaseAddress, 1)?[0] == null ||
memory?.Read(initOffset + memory.BaseAddress, 1)?[0] == 0)
binary = memory.Read(mbi.BaseAddress, (int)mbi.RegionSize);
memory.Data = memory.Read(mbi.BaseAddress, (int)mbi.RegionSize);
// Recheck binary data.
while (binary?.Length == 0)
{
Console.WriteLine("Waiting for client data...");
binary = memory.Read(mbi.BaseAddress, (int)mbi.RegionSize);
}
memory.RefreshMemoryData((int)mbi.RegionSize);
// Suspend the process and handle the patches.
NativeWindows.NtSuspendProcess(processInfo.ProcessHandle);
// Get Integrity check locations
var integrityOffsets = binary.FindPattern(Patterns.Windows.Integrity, int.MaxValue, (int)mbi.RegionSize).ToArray();
var integrityOffsets = memory.Data.FindPattern(Patterns.Windows.Integrity, int.MaxValue, (int)mbi.RegionSize).ToArray();
// Encrypt integrity offsets and patches and add them to the patch list.
for (var i = 0; i < integrityOffsets.Length; i++)
patches[$"Integrity{i}"] = (integrityOffsets[i], Patches.Windows.Integrity);
// Get Integrity check locations
var integrityOffsets2 = binary.FindPattern(Patterns.Windows.Integrity2, int.MaxValue, (int)mbi.RegionSize).ToArray();
var integrityOffsets2 = memory.Data.FindPattern(Patterns.Windows.Integrity2, int.MaxValue, (int)mbi.RegionSize).ToArray();
// Encrypt integrity offsets and patches and add them to the patch list.
for (var i = 0; i < integrityOffsets2.Length; i++)
patches[$"Integrity{integrityOffsets.Length + i}"] = (integrityOffsets2[i], Patches.Windows.Integrity);
// Get Remap check locations.
var remapOffsets = binary.FindPattern(Patterns.Windows.Remap, int.MaxValue, (int)mbi.RegionSize);
var remapOffsets = memory.Data.FindPattern(Patterns.Windows.Remap, int.MaxValue, (int)mbi.RegionSize);
var lastAddress = 0;
foreach (var a in remapOffsets)
@@ -375,7 +268,7 @@ class Program
var instructionEnd = (int)a + 4 + 6;
var instructions = new byte[6];
Buffer.BlockCopy(binary, instructionStart, instructions, 0, 6);
Buffer.BlockCopy(memory.Data, instructionStart, instructions, 0, 6);
// Skip unconditional jumps.
if (memory.IsUnconditionalJump(instructions))
@@ -394,32 +287,32 @@ class Program
var tempPatches = new ConcurrentDictionary<string, (long, byte[])>();
// Find all references of real code parts inside the remap check functions.
Parallel.For(lastAddress, binary.Length, i =>
Parallel.For(lastAddress, memory.Data.Length, i =>
{
if (memory.IsJump(binary, i))
if (memory.IsJump(memory.Data, i))
{
var jumpOperand = BitConverter.ToInt32(binary, i + 2);
var jumpOperand = BitConverter.ToInt32(memory.Data, i + 2);
var jumpSize = (int)jumpToValue - i - 6;
if (jumpOperand == jumpSize)
{
// Add 1 because we patch the instruction start.
// This results in a shorter overall instruction length.
var jumpBytes = new byte[] { 0xE9 }.Concat(BitConverter.GetBytes(jumpSize + 1)).ToArray();
// Add 1 because we patch the instruction start.
// This results in a shorter overall instruction length.
var jumpBytes = new byte[] { 0xE9 }.Concat(BitConverter.GetBytes(jumpSize + 1)).ToArray();
tempPatches.TryAdd($"Jump{i}", (i, jumpBytes));
}
}
else if (memory.IsShortJump(binary, i))
else if (memory.IsShortJump(memory.Data, i))
{
var jumpOperand = binary[i + 1];
var jumpOperand = memory.Data[i + 1];
var jumpSize = (int)jumpToValue - i - 2;
if (jumpOperand == jumpSize)
{
// Check for 0x48 here. This is an indicator for the test instructions.
// Might need some better checks or future updates.
if (binary[i - 3] == 0x48)
// Check for 0x48 here. This is an indicator for the test instructions.
// Might need some better checks or future updates.
if (memory.Data[i - 3] == 0x48)
{
var iBytes = BitConverter.GetBytes(i);
var jumpBytes = new byte[] { 0xEB };

View File

@@ -14,5 +14,5 @@ struct MemoryBasicInformation
public MemProtection Protect;
public MemType Type;
public int Size => Marshal.SizeOf(typeof(MemoryBasicInformation));
public static int Size => Marshal.SizeOf(typeof(MemoryBasicInformation));
}

View File

@@ -1,5 +1,5 @@
// Copyright (c) Arctium.
// Licensed under the MIT license. See LICENSE file in the proje root for full license information.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
global using Arctium.WoW.Launcher.Constants;
global using Arctium.WoW.Launcher.IO;