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
- Reads the WoW executable
- Detects client type and version
- Replaces embedded URLs (portal, version, CDNs, cert bundle download)
- Replaces cryptographic keys (RSA modulus for bundle signature verification, Ed25519 public key)
- Injects a custom cert bundle into builds that embed one
- 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 - Command-line interface
- Library API - Rust crate integration
- Configuration - Keys and CDNs
- Patches - What gets modified
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
| Argument | Description | Required | Default |
|---|---|---|---|
-l, --warcraft-exe | Path to WoW executable | Yes (auto-detected on macOS) | - |
-o, --output-file | Output file path | No | Arctium |
--bgs-portal-domain | Portal hostname suffix (max 10 bytes) | No | wowemu.dev |
--cert-bundle | Cert bundle file to embed (≤ 32761 bytes) | No | - |
--cert-bundle-url | Cert bundle download URL (≤ 59 bytes) | No | - |
Optional Flags
| Flag | Description |
|---|---|
-h, --help | Show help message |
-v, --verbose | Print detailed output |
-n, --dry-run | Preview changes without writing |
-s, --strip-binary-codesign | Remove 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:
| Key | Size | Restrictions |
|---|---|---|
| RSA | 256 bytes | Not all zeros, not all identical bytes |
| Ed25519 | 32 bytes | Not 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-bundlereplaces those bytes directly. - Remote (1.13.2): The client downloads the bundle at startup.
--cert-bundle-urlrewrites 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):
- ConnectTo pattern (8 bytes signature)
- Signature pattern (8 bytes signature)
- 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):
- v1:
http://%s.patch.battle.net:1119/%s/versions(43 bytes) - v2:
https://%s.version.battle.net/v2/products/%s/versions(53 bytes) - 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 Format | Patchable 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
.textand require the deferredlaunchsubcommand. The static patcher only modifies data sections (.rdata/.data). - Cosmetic nydus URLs (driver-unsupported, trial-restriction,
gametime, checkout, checkoutnav): Not rewritten.
--bgs-portal-domainonly affects the.actual.battle.netsuffix.
Verification
After patching, verify the output file:
- Size: Should be identical to input (patcher does not add or remove bytes)
- Permissions: Unix executables have
0o755permissions - Code Signing: macOS binaries preserve signatures unless
strip_codesignis 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
cryptographylibrary
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:
| Key | Purpose | Injected 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
RootCAPublicKeysto 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:
- Listen on port 443 with TLS.
- Present
data/tls/leaf.pemanddata/tls/leaf-key.pem. - 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.