├── .github └── workflows │ └── goreleaser.yml ├── .goreleaser.yaml ├── LICENSE.md ├── README.md ├── cmd └── root.go ├── go.mod ├── go.sum ├── internal ├── binary │ └── binary.go ├── patterns │ └── patterns.go ├── platform │ ├── client.go │ ├── platform_darwin.go │ └── platform_other.go └── trinity │ └── trinity.go └── main.go /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | goreleaser: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: '^1.22' 22 | - name: Build 23 | uses: goreleaser/goreleaser-action@v6 24 | if: ${{ !startsWith(github.ref, 'refs/tags/') }} 25 | with: 26 | distribution: goreleaser 27 | version: '~> v2' 28 | args: build --clean --snapshot 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | - name: Release 32 | uses: goreleaser/goreleaser-action@v6 33 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 34 | with: 35 | distribution: goreleaser 36 | version: '~> v2' 37 | args: release --clean 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 2 | # vim: set ts=2 sw=2 tw=0 fo=jcroql 3 | version: 2 4 | 5 | env: 6 | - GO111MODULE=on 7 | 8 | report_sizes: true 9 | 10 | metadata: 11 | mod_timestamp: "{{ .CommitTimestamp }}" 12 | 13 | builds: 14 | - env: 15 | - CGO_ENABLED=0 16 | goos: 17 | - linux 18 | - darwin 19 | - windows 20 | goarch: 21 | - amd64 22 | - arm64 23 | ignore: 24 | - goos: windows 25 | goarch: arm 26 | mod_timestamp: "{{ .CommitTimestamp }}" 27 | flags: 28 | - -trimpath 29 | ldflags: 30 | - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }} -X main.builtBy=goreleaser -X main.treeState={{ .IsGitDirty }} 31 | 32 | checksum: 33 | name_template: "checksums.txt" 34 | 35 | changelog: 36 | sort: asc 37 | use: github 38 | filters: 39 | exclude: 40 | - "^test:" 41 | - "^test\\(" 42 | - "merge conflict" 43 | - Merge pull request 44 | - Merge remote-tracking branch 45 | - Merge branch 46 | - go mod tidy 47 | 48 | archives: 49 | - name_template: >- 50 | {{- .ProjectName }}_ 51 | {{- title .Os }}_ 52 | {{- if eq .Arch "amd64" }}x86_64 53 | {{- else if eq .Arch "386" }}i386 54 | {{- else }}{{ .Arch }}{{ end }} 55 | {{- if .Arm }}v{{ .Arm }}{{ end -}} 56 | format_overrides: 57 | - goos: windows 58 | format: zip 59 | builds_info: 60 | group: root 61 | owner: root 62 | mtime: "{{ .CommitDate }}" 63 | files: 64 | - src: README.md 65 | info: 66 | owner: root 67 | group: root 68 | mtime: "{{ .CommitDate }}" 69 | - src: LICENSE.md 70 | info: 71 | owner: root 72 | group: root 73 | mtime: "{{ .CommitDate }}" 74 | 75 | release: 76 | name_template: "v{{ .Version }}" 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wowpatch 2 | It's yet another WoW client patcher, but this time written in Go in order to maximize cross-platform 3 | compatibility, and without any in-client memory modifications. This means that each you can generate 4 | an executable and redistribute it to others as long as they use the same operating system and processor 5 | architecture. 6 | 7 | ## Who can use this? 8 | This approach will ONLY work if you: 9 | 1. are connecting to a server with a valid TLS certificate that chains to a trusted root CA 10 | in your system trust store. 11 | 1. are using a hostname and not an IP address for your portal cvar setting in `WTF/Config.wtf`. 12 | 1. are connecting to a server that uses the same [gamecrypto key](https://github.com/TrinityCore/TrinityCore/blob/343f637435cc97ddedd725945cbad417c8f14391/src/server/game/Server/Packets/AuthenticationPackets.cpp#L221) as what is hardcoded (so basically, TrinityCore) 13 | 14 | ## Usage 15 | ```bash 16 | ./wowpatch -h 17 | This application takes as input a retail World of Warcraft client and will generate a modified executable 18 | from it by using binary patching. The resulting executable can be run safely and connect to private servers. 19 | 20 | Usage: 21 | wowpatch [flags] 22 | 23 | Examples: 24 | wowpatch -l ./your/wow/exe -o ./patched-exe 25 | 26 | Flags: 27 | -h, --help help for wowpatch 28 | -o, --output-file string where to output a modified client (default "Arctium") 29 | -s, --strip-binary-codesign removes macOS codesigning from resulting binary (default true) 30 | -l, --warcraft-exe string the location of the WoW executable (default "/Applications/World of Warcraft/_retail_/World of Warcraft.app/Contents/MacOS/World of Warcraft") 31 | ``` 32 | 33 | ## FAQ 34 | **Q: Why does this generate an exe with the name `Arctium` by default?** 35 | 36 | **A:** In the event your client crashes, this helps Blizzard filter out the private server noise from 37 | their automated client telemetry. 38 | 39 | ## Thanks 40 | An absolutely **enormous** amount of thanks to [Fabian](https://github.com/Fabi) from [Arctium](https://arctium.io/) for basically 41 | all of the knowledge that went into this. -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/motivewc/wowpatch/internal/binary" 7 | "github.com/motivewc/wowpatch/internal/patterns" 8 | "github.com/motivewc/wowpatch/internal/platform" 9 | "github.com/motivewc/wowpatch/internal/trinity" 10 | "github.com/spf13/cobra" 11 | "os" 12 | "path/filepath" 13 | ) 14 | 15 | type RootOptions struct { 16 | StripCodesignAttributes bool 17 | WarcraftExeLocation string 18 | OutputFile string 19 | } 20 | 21 | var ( 22 | options RootOptions 23 | ) 24 | 25 | // rootCmd represents the base command when called without any subcommands 26 | var rootCmd = &cobra.Command{ 27 | Use: "wowpatch", 28 | Short: "modifies WoW binary to enable connecting to private servers", 29 | Long: `This application takes as input a retail World of Warcraft client and will generate a modified executable 30 | from it by using binary patching. The resulting executable can be run safely and connect to private servers.`, 31 | RunE: func(cmd *cobra.Command, args []string) error { 32 | data, err := os.ReadFile(options.WarcraftExeLocation) 33 | if err != nil { 34 | return fmt.Errorf("unable to read the WoW executable file: %w", err) 35 | } 36 | binary.Patch(&data, patterns.PortalPattern, patterns.PortalPattern.Empty()) 37 | binary.Patch(&data, patterns.ConnectToModulusPattern, trinity.RsaModulus) 38 | binary.Patch(&data, patterns.CryptoEdPublicKeyPattern, trinity.CryptoEd25519PublicKey) 39 | 40 | wd, err := os.Getwd() 41 | if err != nil { 42 | return fmt.Errorf("unable to determine current working directory: %w", err) 43 | } 44 | 45 | file, err := filepath.Abs(options.OutputFile) 46 | if err != nil { 47 | return fmt.Errorf("invalid output file path specified: %w", err) 48 | } 49 | 50 | if err = os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) { 51 | return fmt.Errorf("unable to remove file that already exists at %v: %w", file, err) 52 | } 53 | 54 | err = os.WriteFile(file, data, 0777) 55 | if err != nil { 56 | return fmt.Errorf("unable to write to file %v: %w", file, err) 57 | } 58 | 59 | if options.StripCodesignAttributes { 60 | if err = platform.RemoveCodesigningSignature(file); err != nil { 61 | return fmt.Errorf("unable to remove codesigning signature from %v: %w", file, err) 62 | } 63 | } 64 | relativePath, _ := filepath.Rel(wd, file) 65 | 66 | fmt.Printf("Client has been successfully patched and saved to \"%v\".\n", relativePath) 67 | 68 | return nil 69 | }, 70 | } 71 | 72 | // Execute adds all child commands to the root command and sets flags appropriately. 73 | // This is called by main.main(). It only needs to happen once to the rootCmd. 74 | func Execute() { 75 | err := rootCmd.Execute() 76 | if err != nil { 77 | fmt.Println("An error has occurred, the client has not been patched.") 78 | fmt.Println() 79 | fmt.Println(err.Error()) 80 | os.Exit(1) 81 | } 82 | } 83 | 84 | func init() { 85 | rootCmd.Example = "wowpatch -l ./your/wow/exe -o ./patched-exe" 86 | rootCmd.PersistentFlags().StringVarP(&options.OutputFile, "output-file", "o", "Arctium", "where to output a modified client") 87 | rootCmd.PersistentFlags().StringVarP(&options.WarcraftExeLocation, "warcraft-exe", "l", platform.FindWarcraftClientExecutable(), "the location of the WoW executable") 88 | rootCmd.PersistentFlags().BoolVarP(&options.StripCodesignAttributes, "strip-binary-codesign", "s", true, "removes macOS codesigning from resulting binary") 89 | } 90 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/motivewc/wowpatch 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 7 | github.com/spf13/cobra v1.8.1 // indirect 8 | github.com/spf13/pflag v1.0.5 // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 3 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 4 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 5 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 6 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 7 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 8 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /internal/binary/binary.go: -------------------------------------------------------------------------------- 1 | package binary 2 | 3 | import ( 4 | "log/slog" 5 | "slices" 6 | ) 7 | 8 | /* 9 | Pattern A pattern is a representation of the binary stream representing the client that we will search for. 10 | We use an int16 type so that we can represent -1 as a wildcard search, and a valid pattern should only ever 11 | encompass the values [-1, 255]. 12 | */ 13 | type Pattern []int16 14 | 15 | func (p *Pattern) Empty() []byte { 16 | return make([]byte, len(*p)) 17 | } 18 | 19 | func StringToPattern(s string) Pattern { 20 | p := make(Pattern, len(s)) 21 | for i := 0; i < len(s); i++ { 22 | p[i] = int16(s[i]) 23 | } 24 | return p 25 | } 26 | 27 | func Patch(in *[]byte, find Pattern, replace []byte) { 28 | for i := 0; i < len(*in)-len(find); i++ { 29 | cmp := (*in)[i : i+len(find)] 30 | if slices.EqualFunc(cmp, find, func(b byte, i int16) bool { 31 | if i == -1 { 32 | return true 33 | } 34 | 35 | return int16(b) == i 36 | }) { 37 | 38 | slog.Debug("found pattern", "offset", i) 39 | for j := i; j < i+len(replace); j++ { 40 | (*in)[j] = replace[j-i] 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /internal/patterns/patterns.go: -------------------------------------------------------------------------------- 1 | package patterns 2 | 3 | import "github.com/motivewc/wowpatch/internal/binary" 4 | 5 | var ( 6 | PortalPattern = binary.StringToPattern(".actual.battle.net") 7 | ConnectToModulusPattern = binary.Pattern{0x91, 0xD5, 0x9B, 0xB7, 0xD4, 0xE1, 0x83, 0xA5} 8 | CryptoEdPublicKeyPattern = binary.Pattern{0x15, 0xD6, 0x18, 0xBD, 0x7D, 0xB5, 0x77, 0xBD} 9 | ) 10 | -------------------------------------------------------------------------------- /internal/platform/client.go: -------------------------------------------------------------------------------- 1 | package platform 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | ) 7 | 8 | func FindWarcraftClientExecutable() string { 9 | switch runtime.GOOS { 10 | case "windows": 11 | return "" // todo: idk 12 | case "darwin": 13 | return filepath.Join( 14 | "/", 15 | "Applications", 16 | "World of Warcraft", 17 | "_retail_", 18 | "World of Warcraft.app", 19 | "Contents", 20 | "MacOS", 21 | "World of Warcraft", 22 | ) 23 | } 24 | return "" 25 | } 26 | -------------------------------------------------------------------------------- /internal/platform/platform_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | 3 | package platform 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "os/exec" 9 | ) 10 | 11 | func RemoveCodesigningSignature(path string) error { 12 | cmd := exec.Command("/usr/bin/codesign", "--remove-signature", path) 13 | var out bytes.Buffer 14 | cmd.Stdout = nil 15 | cmd.Stderr = &out 16 | 17 | if err := cmd.Run(); err != nil { 18 | return fmt.Errorf("unable to remove codesigning attributes: %s\n%w", out.String(), err) 19 | } 20 | 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /internal/platform/platform_other.go: -------------------------------------------------------------------------------- 1 | //go:build windows || linux 2 | 3 | package platform 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | func RemoveCodesigningSignature(path string) error { 10 | fmt.Println("Codesigning is a null op on your OS, TBD if this is OK.") 11 | return nil 12 | } 13 | -------------------------------------------------------------------------------- /internal/trinity/trinity.go: -------------------------------------------------------------------------------- 1 | package trinity 2 | 3 | var ( 4 | RsaModulus = []byte{0x5F, 0xD6, 0x80, 0x0B, 0xA7, 0xFF, 0x01, 0x40, 0xC7, 0xBC, 0x8E, 0xF5, 0x6B, 0x27, 0xB0, 0xBF, 5 | 0xF0, 0x1D, 0x1B, 0xFE, 0xDD, 0x0B, 0x1F, 0x3D, 0xB6, 0x6F, 0x1A, 0x48, 0x0D, 0xFB, 0x51, 0x08, 6 | 0x65, 0x58, 0x4F, 0xDB, 0x5C, 0x6E, 0xCF, 0x64, 0xCB, 0xC1, 0x6B, 0x2E, 0xB8, 0x0F, 0x5D, 0x08, 7 | 0x5D, 0x89, 0x06, 0xA9, 0x77, 0x8B, 0x9E, 0xAA, 0x04, 0xB0, 0x83, 0x10, 0xE2, 0x15, 0x4D, 0x08, 8 | 0x77, 0xD4, 0x7A, 0x0E, 0x5A, 0xB0, 0xBB, 0x00, 0x61, 0xD7, 0xA6, 0x75, 0xDF, 0x06, 0x64, 0x88, 9 | 0xBB, 0xB9, 0xCA, 0xB0, 0x18, 0x8B, 0x54, 0x13, 0xE2, 0xCB, 0x33, 0xDF, 0x17, 0xD8, 0xDA, 0xA9, 10 | 0xA5, 0x60, 0xA3, 0x1F, 0x4E, 0x27, 0x05, 0x98, 0x6F, 0xAA, 0xEE, 0x14, 0x3B, 0xF3, 0x97, 0xA8, 11 | 0x12, 0x02, 0x94, 0x0D, 0x84, 0xDC, 0x0E, 0xF1, 0x76, 0x23, 0x95, 0x36, 0x13, 0xF9, 0xA9, 0xC5, 12 | 0x48, 0xDB, 0xDA, 0x86, 0xBE, 0x29, 0x22, 0x54, 0x44, 0x9D, 0x9F, 0x80, 0x7B, 0x07, 0x80, 0x30, 13 | 0xEA, 0xD2, 0x83, 0xCC, 0xCE, 0x37, 0xD1, 0xD1, 0xCF, 0x85, 0xBE, 0x91, 0x25, 0xCE, 0xC0, 0xCC, 14 | 0x55, 0xC8, 0xC0, 0xFB, 0x38, 0xC5, 0x49, 0x03, 0x6A, 0x02, 0xA9, 0x9F, 0x9F, 0x86, 0xFB, 0xC7, 15 | 0xCB, 0xC6, 0xA5, 0x82, 0xA2, 0x30, 0xC2, 0xAC, 0xE6, 0x98, 0xDA, 0x83, 0x64, 0x43, 0x7F, 0x0D, 16 | 0x13, 0x18, 0xEB, 0x90, 0x53, 0x5B, 0x37, 0x6B, 0xE6, 0x0D, 0x80, 0x1E, 0xEF, 0xED, 0xC7, 0xB8, 17 | 0x68, 0x9B, 0x4C, 0x09, 0x7B, 0x60, 0xB2, 0x57, 0xD8, 0x59, 0x8D, 0x7F, 0xEA, 0xCD, 0xEB, 0xC4, 18 | 0x60, 0x9F, 0x45, 0x7A, 0xA9, 0x26, 0x8A, 0x2F, 0x85, 0x0C, 0xF2, 0x19, 0xC6, 0x53, 0x92, 0xF7, 19 | 0xF0, 0xB8, 0x32, 0xCB, 0x5B, 0x66, 0xCE, 0x51, 0x54, 0xB4, 0xC3, 0xD3, 0xD4, 0xDC, 0xB3, 0xEE} 20 | CryptoEd25519PublicKey = []byte{0x02, 0x59, 0x6F, 0x0D, 0x0C, 0x06, 0x1A, 0x8B, 0x30, 0x74, 0x59, 0x88, 0xFD, 0x72, 0xC5, 0x9E, 21 | 0x29, 0xEC, 0x36, 0x7F, 0xB0, 0xF3, 0x41, 0xF2, 0x8E, 0x0F, 0x08, 0xD0, 0x37, 0xBA, 0xFC, 0x69} 22 | ) 23 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/motivewc/wowpatch/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | --------------------------------------------------------------------------------