Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

wow-patcher

World of Warcraft client patcher for TrinityCore-based private servers.

What It Does

wow-patcher modifies WoW client binaries on disk to enable connections to private servers. It replaces embedded URLs and cryptographic keys with your server’s configuration.

Key difference: This tool modifies files on disk. It does not modify running processes (unlike runtime patchers).

Supported Clients

  • Retail
  • Classic
  • Classic Era

Supported Platforms

  • Windows (PE binaries)
  • macOS (Mach-O binaries)
  • Linux (ELF binaries)

How It Works

  1. Reads the WoW executable
  2. Detects client type and version
  3. Replaces embedded URLs (portal, version, CDNs, cert bundle download)
  4. Replaces cryptographic keys (RSA modulus for bundle signature verification, Ed25519 public key)
  5. Injects a custom cert bundle into builds that embed one
  6. Writes patched executable to new file

Quick Start

CLI

cargo build --release
./target/release/wow-patcher -l /path/to/Wow.exe -o Wow-patched.exe

Library

#![allow(unused)]
fn main() {
use wow_patcher::Patcher;

Patcher::new("Wow.exe")
    .output("Wow-patched.exe")
    .trinity_core_keys()
    .patch()?;
}

Next Steps

Usage

Installation

Build from source:

cargo build --release

The binary will be at target/release/wow-patcher.

Basic Command

wow-patcher -l /path/to/Wow.exe -o Wow-patched.exe

Arguments

ArgumentDescriptionRequiredDefault
-l, --warcraft-exePath to WoW executableYes (auto-detected on macOS)-
-o, --output-fileOutput file pathNoArctium
--bgs-portal-domainPortal hostname suffix (max 10 bytes)Nowowemu.dev
--cert-bundleCert bundle file to embed (≤ 32761 bytes)No-
--cert-bundle-urlCert bundle download URL (≤ 59 bytes)No-

Optional Flags

FlagDescription
-h, --helpShow help message
-v, --verbosePrint detailed output
-n, --dry-runPreview changes without writing
-s, --strip-binary-codesignRemove macOS code signing (default: true)

Custom Keys

Use TrinityCore defaults:

wow-patcher -l Wow.exe -o Wow-patched.exe

Load keys from files:

wow-patcher -l Wow.exe -o Wow-patched.exe \
  --rsa-file /path/to/rsa.bin \
  --ed25519-file /path/to/ed25519.bin

Load keys from hex strings:

wow-patcher -l Wow.exe -o Wow-patched.exe \
  --rsa-hex "91D59BB7D4E183A5..." \
  --ed25519-hex "15D618BD7DB577BD..."

Custom CDN

Replace version and CDN URLs:

wow-patcher -l Wow.exe -o Wow-patched.exe \
  --version-url "https://my-cdn.example.com/versions" \
  --cdns-url "https://my-cdn.example.com/cdns"

Cert Bundle

For clients that download the bundle at startup (1.13.2), redirect the download URL:

wow-patcher -l Wow.exe -o Wow-patched.exe \
  --rsa-file bundle-signing-modulus.bin \
  --cert-bundle-url "http://my-server.example.com/bnet/bundle"

For clients with an embedded bundle (1.14.x / 2.5.3), inject a custom bundle:

wow-patcher -l Wow.exe -o Wow-patched.exe \
  --rsa-file bundle-signing-modulus.bin \
  --cert-bundle data/cert-bundle/bgs-key-fingerprint

Portal Domain

Override the login portal hostname:

wow-patcher -l Wow.exe -o Wow-patched.exe \
  --bgs-portal-domain bgs.corp

macOS Code Signing

The CLI strips macOS code signatures by default (--strip-binary-codesign defaults to true). This is required for patched binaries to run on macOS.

To keep the code signature (not recommended):

wow-patcher -l Wow.exe -s=false

Dry Run

Preview what will change:

wow-patcher --dry-run -l Wow.exe -o Wow-patched.exe

Verbose Output

See details about each patch operation:

wow-patcher -v -l Wow.exe -o Wow-patched.exe

Library API

Cargo.toml

Add to your Cargo.toml:

[dependencies]
wow-patcher = "0.1"

Enable CLI feature if needed:

wow-patcher = { version = "0.1", features = ["cli"] }

Basic Usage

use wow_patcher::Patcher;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    Patcher::new("Wow.exe")
        .patch()?;

    Ok(())
}

Builder API

Input and Output

#![allow(unused)]
fn main() {
Patcher::new("Wow.exe")
    .output("Wow-patched.exe")  // Optional, auto-generated if not set
    .patch()?;
}

Keys

TrinityCore Defaults

#![allow(unused)]
fn main() {
Patcher::new("Wow.exe")
    .trinity_core_keys()
    .patch()?;
}

Custom Keys from Bytes

#![allow(unused)]
fn main() {
let rsa_key: Vec<u8> = /* 256 bytes */;
let ed25519_key: Vec<u8> = /* 32 bytes */;

Patcher::new("Wow.exe")
    .custom_keys(&rsa_key, &ed25519_key)?
    .patch()?;
}

Custom Keys from Hex

#![allow(unused)]
fn main() {
Patcher::new("Wow.exe")
    .custom_keys_from_hex(
        "AA00BB11...",  // RSA (512 hex chars = 256 bytes)
        "CC22DD33...",  // Ed25519 (64 hex chars = 32 bytes)
    )?
    .patch()?;
}

Custom Keys from Files

#![allow(unused)]
fn main() {
Patcher::new("Wow.exe")
    .custom_keys_from_files(
        "/path/to/rsa.bin",
        "/path/to/ed25519.bin",
    )?
    .patch()?;
}

CDN URLs

#![allow(unused)]
fn main() {
Patcher::new("Wow.exe")
    .version_url("https://my-cdn.example.com/versions")
    .cdns_url("https://my-cdn.example.com/cdns")
    .patch()?;
}

Options

#![allow(unused)]
fn main() {
Patcher::new("Wow.exe")
    .dry_run(true)              // Preview changes
    .strip_codesign(true)        // Remove macOS code signature
    .verbose(true)               // Print details
    .patch()?;
}

Error Handling

#![allow(unused)]
fn main() {
use wow_patcher::{Patcher, WowPatcherError};

fn patch() -> Result<(), WowPatcherError> {
    match Patcher::new("Wow.exe").patch() {
        Ok(_) => println!("Success"),
        Err(e) => eprintln!("Error: {}", e),
    }
    Ok(())
}
}

Key Validation

Keys must meet these requirements:

KeySizeRestrictions
RSA256 bytesNot all zeros, not all identical bytes
Ed2551932 bytesNot all zeros, not all identical bytes

Validation occurs when KeyConfig is created or loaded.

Configuration

Keys

What Keys Do

wow-patcher replaces cryptographic keys embedded in the WoW executable. The RSA modulus verifies the cert bundle signature; the bundle itself lists which TLS CAs the client trusts. The Ed25519 key is used for alternative signature verification.

Key Types

RSA Modulus

  • Size: 256 bytes
  • Purpose: Verifies the cert bundle’s PKCS#1 v1.5 signature. The client uses this modulus to confirm the bundle file is authentic before trusting the CA fingerprints inside it.
  • Required: Yes

Ed25519 Public Key

  • Size: 32 bytes
  • Purpose: Alternative signature verification for some protocols
  • Required: For Retail and Classic (optional for Classic Era)

Key Sources

TrinityCore Defaults

The patcher includes default keys for TrinityCore servers. Use these if your server uses standard TrinityCore configuration:

#![allow(unused)]
fn main() {
Patcher::new("Wow.exe")
    .trinity_core_keys()
    .patch()?;
}

Custom Keys

Generate keys for your server:

# RSA private key (TrinityCore uses this)
openssl genrsa -out server.key 2048

# Extract public modulus (256 bytes)
openssl rsa -in server.key -modulus -noout | sed 's/Modulus=//' | xxd -r -p

For Ed25519:

# Generate Ed25519 key pair
openssl genpkey -algorithm ed25519 -out ed25519.key

# Extract public key (32 bytes)
openssl pkey -in ed25519.key -pubout -outform DER | tail -c 32

Key Validation

All keys must pass these checks:

  • Correct size (256 bytes for RSA, 32 bytes for Ed25519)
  • Not all zeros
  • Not all identical bytes (entropy check)

Invalid keys cause a validation error before patching begins.

Key Storage

CLI accepts keys from files:

wow-patcher -l Wow.exe \
  --rsa-file /path/to/rsa.key \
  --ed25519-file /path/to/ed25519.key

Library accepts keys via Patcher builder methods:

  • Bytes: .custom_keys(&rsa, &ed25519)?
  • Hex strings: .custom_keys_from_hex(rsa_hex, ed25519_hex)?
  • Files: .custom_keys_from_files(rsa_path, ed25519_path)?

KeyConfig is also available for direct key management via KeyConfig::new(), KeyConfig::from_hex(), and KeyConfig::from_files().

Portal Domain

What the Portal Domain Does

The patcher rewrites the BGS login portal hostname suffix from .actual.battle.net to .actual.<your-domain>. The default target is wowemu.dev (byte-identical length to battle.net, so no NUL padding is needed). Shorter domains up to 10 bytes work via NUL padding. This is controlled by --bgs-portal-domain (or the WOW_BGS_PORTAL_DOMAIN env var).

Cert Bundle

What the Cert Bundle Does

The cert bundle is a signed JSON file that tells the client which TLS certificate authorities to trust (RootCAPublicKeys). Two patching mechanisms exist depending on client version:

  • Embedded (1.14.x / 2.5.3): The bundle is baked into .rdata. --cert-bundle replaces those bytes directly.
  • Remote (1.13.2): The client downloads the bundle at startup. --cert-bundle-url rewrites the download URL.

Both mechanisms require the RSA modulus to be patched via --rsa-file so the client trusts the bundle’s signature.

Generate a bundle with scripts/gen-cert-bundle.py. See docs/custom-cert-bundle.md for the full guide.

URL Types

Portal URL

The patcher redirects the portal connection by rewriting the .actual.battle.net suffix. The default target domain is wowemu.dev.

Version URL

  • Default: https://us.version.battle.net/v2/products/wow/versions
  • Purpose: Fetches version information
  • Required: No (optional)

CDNs URL

  • Default: https://us.cdn.battle.net/1119/wow/cdns
  • Purpose: Fetches CDN configuration
  • Required: No (optional)

Default URLs

When no custom URLs are provided, the patcher uses Arctium CDN defaults:

#![allow(unused)]
fn main() {
let version_url = "http://cdn.arctium.io/versions";
let cdns_url = "http://cdn.arctium.io/cdns";
}

Custom URLs

Set your own CDN:

wow-patcher -l Wow.exe \
  --version-url "https://my-cdn.example.com/versions" \
  --cdns-url "https://my-cdn.example.com/cdns"

Or via library:

#![allow(unused)]
fn main() {
Patcher::new("Wow.exe")
    .version_url("https://my-cdn.example.com/versions")
    .cdns_url("https://my-cdn.example.com/cdns")
    .patch()?;
}

Unified API (v3)

Newer WoW clients use a unified version API. If detected, the patcher uses the v3 pattern and ignores the separate CDNs URL.

Patches

What Gets Modified

The patcher replaces specific byte patterns in the WoW executable. These patterns represent embedded configuration.

Mandatory Patches

Portal URL

Pattern: .actual.battle.net (18 bytes)

Replacement: .localhost (10 bytes) followed by 8 NUL bytes.

The .localhost hostname resolves to 127.0.0.1 via nss-myhostname (RFC 6761) without /etc/hosts edits. Using all NUL bytes would break the URL assembly (NUL terminates C strings, dropping the path).

Purpose: Redirects the BGS login portal connection to a host you control

Status: Must be found for patching to succeed

Note: Override the target domain with --bgs-portal-domain (default: wowemu.dev).

RSA Modulus

Patterns (tried in order):

  1. ConnectTo pattern (8 bytes signature)
  2. Signature pattern (8 bytes signature)
  3. Crypto pattern (8 bytes signature)

Replacement: Your 256-byte RSA modulus

Purpose: Verifies the cert bundle’s PKCS#1 v1.5 signature. The client uses this modulus to confirm the bundle file is authentic before trusting the CA fingerprints listed inside it.

Status: Must be found for patching to succeed

Note: The patcher searches for 8-byte signatures to locate the full 256-byte RSA modulus in the binary.

Optional Patches

Cert Bundle (embedded)

Pattern: {"Created": (11 bytes)

Replacement: Your signed cert bundle JSON + signature (≤ 32761 bytes)

Purpose: Injects a custom certificate bundle into builds that ship one embedded in .rdata (1.14.x / 2.5.3). The bundle’s RootCAPublicKeys lists which TLS CAs the client trusts.

Status: Optional, skipped on builds without an embedded bundle

Note: For builds that download the bundle at startup (1.13.2), use --cert-bundle-url to redirect the download URL instead. See the certificate bundle guide for generation instructions.

Cert Bundle URL

Pattern: http://nydus.battle.net/Bnet/zxx/client/bgs-key-fingerprint (59 bytes)

Replacement: Your custom download URL (≤ 59 bytes)

Purpose: Redirects the cert bundle download to a host you control (1.13.2, 1.14.x, 2.5.3 builds that fetch the bundle at startup).

Status: Optional, skipped on builds without a bundle URL pattern

Note: See the certificate bundle guide for bundle generation and serving instructions.

Ed25519 Public Key

Pattern: Crypto Ed25519 signature (8 bytes)

Replacement: Your 32-byte Ed25519 public key

Purpose: Alternative signature verification

Status: Optional, warning if not found

Version URL

Patterns (tried in order):

  1. v1: http://%s.patch.battle.net:1119/%s/versions (43 bytes)
  2. v2: https://%s.version.battle.net/v2/products/%s/versions (53 bytes)
  3. v3: https://%s.version.battle.net/v2/products/%s/%s (48 bytes)

Replacement: Your custom version URL (or Arctium default)

Purpose: Changes where the client fetches version information

Status: Optional, warning if not found

CDNs URL

Pattern: http://%s.patch.battle.net:1119/%s/cdns (40 bytes)

Replacement: Your custom CDN URL (or Arctium default)

Purpose: Changes where the client fetches CDN configuration

Status: Optional, warning if not found

Note: Skipped if v3 unified API is detected (the v3 pattern handles both).

Patch Locations

The patcher verifies that all patterns are found in patchable sections of the binary:

Binary FormatPatchable Sections
PE (Windows).rdata, .data
Mach-O (macOS)__DATA, __DATA_CONST, __TEXT.__const
ELF (Linux).data

Patterns found in code sections (.text, __TEXT) are rejected. This prevents accidental code modification.

Dry Run

Preview what will change before patching:

wow-patcher --dry-run -l Wow.exe -o Wow-patched.exe

Dry run shows:

  • Client type and detected version
  • All patterns found and their locations
  • Replacement values that will be written

What Is Not Patched

  • Runtime code patches (cert chain check bypass, integrity guards, anti-tamper): These live in Arxan-encrypted .text and require the deferred launch subcommand. The static patcher only modifies data sections (.rdata / .data).
  • Cosmetic nydus URLs (driver-unsupported, trial-restriction, gametime, checkout, checkoutnav): Not rewritten. --bgs-portal-domain only affects the .actual.battle.net suffix.

Verification

After patching, verify the output file:

  1. Size: Should be identical to input (patcher does not add or remove bytes)
  2. Permissions: Unix executables have 0o755 permissions
  3. Code Signing: macOS binaries preserve signatures unless strip_codesign is enabled

Generating a Custom Certificate Bundle

This guide walks through generating a signed BGS certificate bundle that the WoW Classic client will trust after patching. A single script does everything: CA generation, leaf certificate, bundle-signing key, and the signed bundle JSON.

Prerequisites

  • Python 3.10+ with the cryptography library
pip install cryptography

How the client uses certificates

The WoW client needs to trust two things before it will connect to your server: the server itself, and the file that tells it which servers to trust. It handles these as two separate checks.

Verifying the server (TLS)

When the client connects to the login server, the server presents a certificate – like showing an ID card. The client looks up which certificate authorities it trusts and checks whether the server’s certificate was signed by one of them.

The list of trusted authorities lives in the cert bundle, a small file containing the fingerprint of your TLS CA. The bundle tells the client: “allow connections to servers whose certificate was issued by this CA.”

Verifying the bundle itself (signature)

This creates a chicken-and-egg problem: how does the client know the bundle file is genuine? If anyone could swap in a different CA fingerprint, they could trick the client into trusting a malicious server.

The solution is a second, separate check. The bundle file carries a digital signature at the end – like a tamper-proof seal. The client verifies this signature against a key that is hardcoded in the game binary. By replacing this key during patching, you make the client accept bundles signed by you instead of Blizzard.

How the client gets the bundle

Older builds (1.13.2) download the bundle from a Blizzard URL at startup. Patching rewrites that URL to point at your own server. You serve the bundle file over HTTP.

Newer builds (1.14.x / 2.5.3) have the bundle embedded directly inside the game executable. Patching replaces those bytes in-place. No HTTP serving needed.

In both cases the signature check happens the same way: the client verifies the bundle against the modulus injected into the binary via --rsa-file.

Key architecture: two separate keys

The cert bundle uses two distinct keypairs that serve different purposes and must not be conflated:

KeyPurposeInjected into binary?
TLS CA (ca.pem / ca-key.pem)Signs the leaf server cert presented by your BGS server. The client trusts this CA because its SPKI hash is listed in the bundle’s RootCAPublicKeys.No. Embedded inside the bundle JSON as SigningCertificates[0].RawData and referenced by SPKI hash.
Bundle-signing key (bundle-signing-key.pem)Signs the bundle JSON itself (PKCS#1 v1.5 / SHA-256). The client verifies this signature against the modulus injected into the binary.Yes. The modulus replaces the stock RSA modulus via --rsa-file.

The TLS CA is the root of trust for TLS server authentication (is the BGS server who it claims to be?). The bundle-signing key is the root of trust for bundle authenticity (is this bundle file from a trusted source?). They are independent: you can keep the same TLS CA and rotate the bundle-signing key (re-patching the binary each time), or vice versa.

Overview

The signed JSON bundle has the following wire format:

+------------------------------+
| JSON document (UTF-8)        |  <-- variable length
+------------------------------+
| "NGIS"  (4 bytes, ASCII)     |  <-- magic, marks signature start
+------------------------------+
| Signature (256 bytes, LE)    |  <-- RSA-2048 PKCS#1 v1.5, byte-reversed
+------------------------------+

The signature is RSA-2048 PKCS#1 v1.5 over SHA-256(JSON || "Blizzard Certificate Bundle"), with the signature bytes reversed to little-endian (matching Blizzard’s internal BigNumber representation).

For the full technical specification, see management/src/reverse-engineering/wow-classic/1.13.2/31650/bgs/cert-bundle-signature-format.md.

Step 1: Run the bundle generator

Run the script from the project root. It generates everything — CA, leaf cert, bundle-signing key, and signed bundle — in one pass:

cd "$(git rev-parse --show-toplevel)"

# Default: wowemu.dev domain, leaf CN *.actual.wowemu.dev
python3 scripts/gen-cert-bundle.py

# Custom portal domain (must be <= 10 bytes, same as 'battle.net')
python3 scripts/gen-cert-bundle.py --portal-domain bgs.corp

# Custom leaf CN (set to the IP or hostname the BGS server listens on)
python3 scripts/gen-cert-bundle.py --leaf-cn '127.0.0.1'

# Custom output directory
python3 scripts/gen-cert-bundle.py --output-dir /tmp/my-certs

The script is idempotent: the bundle-signing key is generated once and reused across runs. Only the bundle JSON changes (the Created timestamp is updated). This means wow-patcher --rsa-file stays valid across regenerations.

To regenerate the bundle-signing key (requires re-patching the binary):

python3 scripts/gen-cert-bundle.py --force

Step 2: Patch the WoW client

The script prints a ready-to-use wow-patcher invocation at the end:

>> wow-patcher invocation:
  wow-patcher -l /path/to/WowClassic.exe \
    --rsa-file data/tls/bundle-signing-modulus.bin \
    --cert-bundle data/cert-bundle/bgs-key-fingerprint \
    --bgs-portal-domain wowemu.dev \
    --cert-bundle-url http://wowemu.dev/bnet/bundle \
    -v

Run it. The two relevant binary patches:

  • RSA modulus: the bundle-signing key’s modulus replaces the stock Blizzard modulus in .rdata. The client uses this modulus to verify the PKCS#1 v1.5 signature on the bundle JSON.
  • Cert bundle: the bundle file replaces the embedded Blizzard bundle (for 1.14.x / 2.5.3 builds). The client reads RootCAPublicKeys to determine which CAs to trust for TLS.

Step 3: Serve the cert bundle (1.13.2 and remote builds only)

If your client downloads the bundle at startup (1.13.2 or builds where you used --cert-bundle-url), serve the file over HTTP:

# Quick test with Python
cd data/cert-bundle
python3 -m http.server 80

# Or nginx, or any HTTP server. The file is:
#   data/cert-bundle/bgs-key-fingerprint

Step 4: Install the CA in the OS trust store

The client needs the CA to verify the leaf cert. The script prints platform-specific commands. Example:

# Fedora/RHEL
sudo install -m 0644 data/tls/ca.pem /usr/share/pki/ca-trust-source/anchors/wow-patcher-ca.crt && sudo update-ca-trust

# Debian/Ubuntu
sudo install -m 0644 data/tls/ca.pem /usr/local/share/ca-certificates/wow-patcher-ca.crt && sudo update-ca-certificates

Step 5: DNS resolution

The patched client connects to .actual.wowemu.dev (or your custom portal domain). Add to /etc/hosts for local testing:

echo "127.0.0.1 wowemu.dev" >> /etc/hosts

Or set up a real DNS entry pointing to your BGS server.

Step 6: Start your BGS server

Your BGS server must:

  1. Listen on port 443 with TLS.
  2. Present data/tls/leaf.pem and data/tls/leaf-key.pem.
  3. Accept connections from the patched client.

File layout

data/tls/
  ca.pem                       - TLS CA certificate
  ca-key.pem                   - TLS CA private key (NEVER share)
  leaf.pem                     - Leaf server cert (deploy on BGS server)
  leaf-key.pem                 - Leaf private key (deploy on BGS server)
  bundle-signing-key.pem       - Bundle signing private key (NEVER share)
  bundle-signing-pub.pem       - Bundle signing public key
  bundle-signing-modulus.bin   - LE 256-byte modulus (for --rsa-file)

data/cert-bundle/
  bgs-key-fingerprint          - Signed bundle (serve via HTTP)

Regenerating the bundle

The script is idempotent: the bundle-signing key is generated once and reused. To change the TLS CA (no re-patch needed):

python3 scripts/gen-cert-bundle.py

The bundle JSON is rebuilt with the fresh CA but the same signing key, so --rsa-file stays valid.

To rotate the bundle-signing key (re-patch required):

python3 scripts/gen-cert-bundle.py --force

This generates a new signing keypair and modulus. Re-run wow-patcher with the new modulus.