├── .drone.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── go.mod ├── go.sum ├── kobopatch ├── kobopatch.go └── kobopatch_test.go ├── patchfile ├── kobopatch │ ├── kobopatch.go │ ├── patch.go │ └── patch_test.go ├── patch32lsb │ ├── patch32lsb.go │ └── patch32lsb_test.go └── patchfile.go ├── patchlib ├── asm.go ├── asm_test.go ├── css.go ├── css_test.go ├── patcher.go ├── patcher_test.go ├── syms.go ├── syms_test.go └── testdata │ ├── libnickel.so.1.0.0 │ └── nickel └── tools ├── cssextract └── main.go ├── kobopatch-apply └── kobopatch-apply.go └── symdump └── main.go /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | name: kobopatch 3 | 4 | steps: 5 | - name: test 6 | image: golang:1.13-buster 7 | commands: 8 | - go test -cover -v ./... 9 | 10 | --- 11 | 12 | kind: pipeline 13 | name: test-patches 14 | 15 | steps: 16 | - name: build 17 | image: golang:1.13-buster 18 | commands: 19 | - mkdir build 20 | - CGO_ENABLED=1 go build -o build/kobopatch ./kobopatch 21 | - name: get-patches 22 | image: golang:1.13-buster 23 | commands: 24 | - git clone https://github.com/pgaskin/kobopatch-patches 25 | - cd kobopatch-patches 26 | - git checkout "$(git describe --tags --abbrev=0 HEAD)" 27 | - go build -o ./scripts/testscript ./scripts/test 28 | - name: test 29 | image: golang:1.13-buster 30 | commands: 31 | - cd kobopatch-patches 32 | - ./scripts/testscript --kpbin ../build/kobopatch 33 | 34 | --- 35 | 36 | kind: pipeline 37 | name: release 38 | 39 | platform: 40 | os: linux 41 | arch: amd64 42 | 43 | steps: 44 | - name: notes 45 | image: golang:1.13-buster 46 | commands: 47 | - git fetch --tags 48 | - git log "$(git describe --tags --abbrev=0 HEAD~1)..HEAD" --oneline --format='%h %s' | tee -a notes.md 49 | - name: build-linux-amd64 50 | image: docker.elastic.co/beats-dev/golang-crossbuild:1.13.6-main 51 | command: [ 52 | "--platforms", "linux/amd64", 53 | "--build-cmd", " 54 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always)\" -o build/kobopatch-linux-64bit ./kobopatch && 55 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always)\" -o build/kobopatch-apply-linux-64bit ./tools/kobopatch-apply && 56 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always)\" -o build/cssextract-linux-64bit ./tools/cssextract && 57 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always)\" -o build/symdump-linux-64bit ./tools/symdump", 58 | ] 59 | - name: build-linux-386 60 | image: docker.elastic.co/beats-dev/golang-crossbuild:1.13.6-main 61 | command: [ 62 | "--platforms", "linux/amd64", 63 | "--build-cmd", " 64 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always)\" -o build/kobopatch-linux-32bit ./kobopatch && 65 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always)\" -o build/kobopatch-apply-linux-32bit ./tools/kobopatch-apply && 66 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always)\" -o build/cssextract-linux-32bit ./tools/cssextract && 67 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always)\" -o build/symdump-linux-32bit ./tools/symdump", 68 | ] 69 | - name: build-linux-arm 70 | image: docker.elastic.co/beats-dev/golang-crossbuild:1.13.6-arm 71 | command: [ 72 | "--platforms", "linux/armv6", 73 | "--build-cmd", " 74 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always)\" -o build/kobopatch-linux-arm ./kobopatch && 75 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always)\" -o build/kobopatch-apply-linux-arm ./tools/kobopatch-apply && 76 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always)\" -o build/cssextract-linux-arm ./tools/cssextract && 77 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always)\" -o build/symdump-linux-arm ./tools/symdump", 78 | ] 79 | - name: build-windows-386 80 | image: docker.elastic.co/beats-dev/golang-crossbuild:1.13.6-main 81 | command: [ 82 | "--platforms", "windows/386", 83 | "--build-cmd", " 84 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always) -extldflags -static\" -o build/koboptch-windows.exe ./kobopatch && 85 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always) -extldflags -static\" -o build/koboptch-apply-windows.exe ./tools/kobopatch-apply && 86 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always) -extldflags -static\" -o build/cssextract-windows.exe ./tools/cssextract && 87 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always) -extldflags -static\" -o build/symdump-windows.exe ./tools/symdump", 88 | ] 89 | - name: build-darwin-amd64 90 | image: docker.elastic.co/beats-dev/golang-crossbuild:1.13.6-darwin 91 | command: [ 92 | "--platforms", "darwin/amd64", 93 | "--build-cmd", " 94 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always)\" -o build/kobopatch-darwin-64bit ./kobopatch && 95 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always)\" -o build/kobopatch-apply-darwin-64bit ./tools/kobopatch-apply && 96 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always)\" -o build/cssextract-darwin-64bit ./tools/cssextract && 97 | CGO_ENABLED=1 go build -ldflags \"-s -w -linkmode external -X main.version=$(git describe --tags --always)\" -o build/symdump-darwin-64bit ./tools/symdump", 98 | ] 99 | - name: list 100 | image: golang:1.13-buster 101 | commands: 102 | - ls -lah build 103 | depends_on: [build-linux-amd64, build-linux-arm, build-linux-386, build-windows-386, build-darwin-amd64] 104 | - name: release 105 | image: plugins/github-release 106 | settings: 107 | api_key: {from_secret: GITHUB_TOKEN} 108 | title: ${DRONE_TAG} 109 | note: notes.md 110 | files: build/* 111 | draft: true 112 | when: {ref: {include: ["refs/tags/*"]}} 113 | depends_on: [notes, list] 114 | 115 | depends_on: 116 | - kobopatch 117 | - test-patches -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2020 Patrick Gaskin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kobopatch 2 | An improved patching system for Kobo eReaders. See https://www.mobileread.com/forums/showthread.php?t=297338. Download patches for v4.9.11311+ [here](https://github.com/pgaskin/kobopatch-patches/releases/latest). 3 | 4 | ## Features 5 | - Zlib replacement. 6 | - Add additional files. 7 | - Add additional symlinks. 8 | - Translation file support. 9 | - Simplified BLX instruction replacement. 10 | - Multi-version configuration file. 11 | - Extensible patch file. 12 | - Built-in generation of Kobo update files. 13 | - Additional instructions. 14 | - Single executable. 15 | - Automated testing of patches. 16 | - Comprehensive log file and error messages. 17 | - Modular and embeddable. 18 | - Structured patch file format. 19 | - Backwards-compatible with old patch format. 20 | 21 | ## Usage 22 | ``` 23 | Usage: kobopatch [OPTIONS] [CONFIG_FILE] 24 | 25 | Options: 26 | -f, --firmware string firmware file to be used (can also use a testdata tarball from kobopatch-patches) 27 | -h, --help show this help text 28 | -t, --run-tests test all patches (instead of running kobopatch) 29 | 30 | If CONFIG_FILE is not specified, kobopatch will use ./kobopatch.yaml. 31 | ``` 32 | 33 | ``` 34 | cssextract extracts zlib-compressed from a binary file 35 | Usage: cssextract BINARY_FILE 36 | ``` 37 | 38 | ``` 39 | symdump dumps symbol addresses from an ARMv6+ 32-bit ELF executable 40 | Usage: symdump BINARY_FILE 41 | ``` 42 | 43 | ``` 44 | Usage: kobopatch-apply [OPTIONS] 45 | 46 | Options: 47 | -h, --help show this help text 48 | -i, --input string the file to patch (required) 49 | -o, --output string the file to write the patched output to (will be overwritten if exists) (required) 50 | -p, --patch-file string the file containing the patches (required) 51 | -f, --patch-format string the patch format (one of: kobopatch,patch32lsb) (default "kobopatch") 52 | -v, --verbose show verbose output from patchlib 53 | ``` 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pgaskin/kobopatch 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 // indirect 7 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 8 | github.com/pgaskin/czlib v0.0.4 9 | github.com/riking/cssparse v0.0.0-20180325025645-c37ded0aac89 10 | github.com/spf13/pflag v1.0.5 11 | github.com/stretchr/testify v1.5.1 12 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 13 | golang.org/x/text v0.3.2 // indirect 14 | gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22 15 | rsc.io/arm v0.0.0-20150420010332-9c32f2193064 16 | ) 17 | 18 | replace gopkg.in/yaml.v3 => github.com/pgaskin/yaml v0.0.0-20190717135119-db0123c0912e // v3-node-decodestrict 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c= 5 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 6 | github.com/pgaskin/czlib v0.0.4 h1:biwyjtvo6xiXwvgYWyKz0GpmAmDJi4as3zl8Go7Pr9w= 7 | github.com/pgaskin/czlib v0.0.4/go.mod h1:ZRHNrWwa4Jv0HU5r0u64eKRZXcBUicpI6rtaEEbduaU= 8 | github.com/pgaskin/yaml v0.0.0-20190717135119-db0123c0912e h1:jIAOCdmm9VlOD9ezgGGiJOQofvz2mnLIH1sA1wyI8D4= 9 | github.com/pgaskin/yaml v0.0.0-20190717135119-db0123c0912e/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/riking/cssparse v0.0.0-20180325025645-c37ded0aac89 h1:hMsoSMebpfpaDW7+B7gsxNnMBNChjekeqmK8wkzAlc0= 13 | github.com/riking/cssparse v0.0.0-20180325025645-c37ded0aac89/go.mod h1:yc5MYwuNUGggTQ8++IDAbOYq/9PXxsg73+EHYgoG/4w= 14 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 15 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 16 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 19 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 20 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= 21 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= 22 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 23 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 24 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 27 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 28 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 29 | rsc.io/arm v0.0.0-20150420010332-9c32f2193064 h1:bBbas3KhLwE6f59Z9lUipY23xUX9qrvyLBdQzzV2Tko= 30 | rsc.io/arm v0.0.0-20150420010332-9c32f2193064/go.mod h1:MVYPdlFruujBlzEY3x2Q3XBk7XLdYRNZ7zDbrzYFO7w= 31 | -------------------------------------------------------------------------------- /kobopatch/kobopatch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/tar" 5 | "archive/zip" 6 | "bytes" 7 | "compress/gzip" 8 | "crypto/sha1" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "io/ioutil" 14 | "os" 15 | "os/exec" 16 | "path/filepath" 17 | "reflect" 18 | "runtime" 19 | "strings" 20 | "time" 21 | 22 | "github.com/pgaskin/kobopatch/patchfile" 23 | "github.com/pgaskin/kobopatch/patchfile/kobopatch" 24 | _ "github.com/pgaskin/kobopatch/patchfile/patch32lsb" 25 | "github.com/pgaskin/kobopatch/patchlib" 26 | 27 | "github.com/spf13/pflag" 28 | "github.com/xi2/xz" 29 | "gopkg.in/yaml.v3" 30 | ) 31 | 32 | var version = "unknown" 33 | 34 | func main() { 35 | help := pflag.BoolP("help", "h", false, "show this help text") 36 | fw := pflag.StringP("firmware", "f", "", "firmware file to be used (can also use a testdata tarball from kobopatch-patches)") 37 | t := pflag.BoolP("run-tests", "t", false, "test all patches (instead of running kobopatch)") 38 | pflag.Parse() 39 | 40 | if *help || pflag.NArg() > 1 { 41 | fmt.Fprintf(os.Stderr, "Usage: kobopatch [OPTIONS] [CONFIG_FILE]\n") 42 | fmt.Fprintf(os.Stderr, "\nVersion: %s\n\nOptions:\n", version) 43 | pflag.PrintDefaults() 44 | fmt.Fprintf(os.Stderr, "\nIf CONFIG_FILE is not specified, kobopatch will use ./kobopatch.yaml.\n") 45 | os.Exit(1) 46 | } 47 | 48 | var tmp bytes.Buffer 49 | var logfile io.Writer = &tmp 50 | 51 | k := &KoboPatch{ 52 | Logf: func(format string, a ...interface{}) { 53 | fmt.Printf(format+"\n", a...) 54 | }, 55 | Errorf: func(format string, a ...interface{}) { 56 | fmt.Fprintf(os.Stderr, format+"\n", a...) 57 | }, 58 | Debugf: func(format string, a ...interface{}) { 59 | fmt.Fprintf(logfile, format+"\n", a...) 60 | }, 61 | sums: map[string]string{}, 62 | } 63 | 64 | patchfile.Log = func(format string, a ...interface{}) { 65 | k.Debugf(" | %s", strings.ReplaceAll(fmt.Sprintf(strings.TrimRight(format, "\n"), a...), "\n", "\n | ")) 66 | } 67 | 68 | k.Logf("kobopatch %s\nhttps://github.com/pgaskin/kobopatch\n", version) 69 | k.Debugf("kobopatch %s\nhttps://github.com/pgaskin/kobopatch\n", version) 70 | 71 | conf := "kobopatch.yaml" 72 | if pflag.NArg() >= 1 { 73 | conf = pflag.Arg(0) 74 | } 75 | 76 | if *fw != "" { 77 | var err error 78 | *fw, err = filepath.Abs(*fw) 79 | if err != nil { 80 | fmt.Fprintf(os.Stderr, "Error: could not resolve path to firmware file: %v\n", err) 81 | os.Exit(1) 82 | } 83 | } 84 | 85 | k.Logf("Loading configuration from %s", conf) 86 | if conf == "-" { 87 | err := k.LoadConfig(os.Stdin) 88 | if err != nil { 89 | k.Errorf("Error: could not load config file from stdin: %v", err) 90 | os.Exit(1) 91 | return 92 | } 93 | } else { 94 | os.Chdir(filepath.Dir(conf)) 95 | f, err := os.Open(conf) 96 | if err != nil { 97 | k.Errorf("Error: could not load config file: %v", err) 98 | os.Exit(1) 99 | return 100 | } 101 | err = k.LoadConfig(f) 102 | if err != nil { 103 | k.Errorf("Error: could not load config file: %v", err) 104 | os.Exit(1) 105 | return 106 | } 107 | f.Close() 108 | } 109 | 110 | f, err := os.Create(k.Config.Log) 111 | if err != nil { 112 | k.Errorf("Error: could not create log file") 113 | os.Exit(1) 114 | return 115 | } 116 | defer f.Close() 117 | f.Write(tmp.Bytes()) 118 | logfile = f 119 | 120 | if *fw != "" { 121 | k.d("firmware file overridden from command line: %s", *fw) 122 | k.Config.In = *fw 123 | } 124 | 125 | if *t { 126 | if res, err := k.RunPatchTests(); err != nil { 127 | k.Errorf("Error: could not apply patches: %v", err) 128 | os.Exit(1) 129 | return 130 | } else { 131 | errs := []string{} 132 | for pfn, ps := range res { 133 | for pn, err := range ps { 134 | if err != nil { 135 | errs = append(errs, fmt.Sprintf("%s: %s: %v", pfn, pn, err)) 136 | } 137 | } 138 | } 139 | if len(errs) > 0 { 140 | k.l("\nErrors:\n %s", strings.Join(errs, "\n ")) 141 | if runtime.GOOS == "windows" { 142 | fmt.Printf("\n\nWaiting 60 seconds because runnning on Windows\n") 143 | time.Sleep(time.Second * 60) 144 | } 145 | os.Exit(1) 146 | } 147 | } 148 | fmt.Println("\nAll patches applied successfully.") 149 | if runtime.GOOS == "windows" { 150 | fmt.Printf("\n\nWaiting 60 seconds because runnning on Windows\n") 151 | time.Sleep(time.Second * 60) 152 | } 153 | os.Exit(0) 154 | } 155 | 156 | k.OutputInit() 157 | 158 | if err := k.ApplyPatches(); err != nil { 159 | k.Errorf("Error: could not apply patches: %v", err) 160 | os.Exit(1) 161 | return 162 | } 163 | 164 | if err := k.ApplyTranslations(); err != nil { 165 | k.Errorf("Error: could not apply translations: %v", err) 166 | os.Exit(1) 167 | return 168 | } 169 | 170 | if err := k.ApplyFiles(); err != nil { 171 | k.Errorf("Error: could not apply additional files: %v", err) 172 | os.Exit(1) 173 | return 174 | } 175 | 176 | if err := k.ApplySymlinks(); err != nil { 177 | k.Errorf("Error: could not apply additional symlinks: %v", err) 178 | os.Exit(1) 179 | return 180 | } 181 | 182 | if err := k.WriteOutput(); err != nil { 183 | k.Errorf("Error: could not write output: %v", err) 184 | os.Exit(1) 185 | return 186 | } 187 | 188 | fmt.Printf("\nSuccessfully saved patched KoboRoot.tgz to %s. Remember to make sure your kobo is running the target firmware version before patching.\n", k.Config.Out) 189 | 190 | if runtime.GOOS == "windows" { 191 | fmt.Printf("\n\nWaiting 60 seconds because runnning on Windows\n") 192 | time.Sleep(time.Second * 60) 193 | } 194 | } 195 | 196 | type KoboPatch struct { 197 | Config *Config 198 | 199 | outBuf bytes.Buffer 200 | outTar *tar.Writer 201 | outGZ *gzip.Writer 202 | outTarExpectedSize int64 203 | sums map[string]string 204 | 205 | Logf func(format string, a ...interface{}) // displayed to user 206 | Errorf func(format string, a ...interface{}) // displayed to user 207 | Debugf func(format string, a ...interface{}) // for verbose logging 208 | } 209 | 210 | type Config struct { 211 | Version string 212 | In string 213 | Out string 214 | Log string 215 | PatchFormat string `yaml:"patchFormat"` // DEPRECATED: now detected from extension; .patch -> p32lsb, .yaml -> kobopatch 216 | Patches map[string]string 217 | Overrides map[string]map[string]bool 218 | Lrelease string 219 | Translations map[string]string 220 | Symlinks map[string]string 221 | Files map[string]stringSlice 222 | } 223 | 224 | func (k *KoboPatch) OutputInit() { 225 | k.d("\n\nKoboPatch::OutputInit") 226 | k.outBuf.Reset() 227 | k.outGZ = gzip.NewWriter(&k.outBuf) 228 | k.outTar = tar.NewWriter(k.outGZ) 229 | } 230 | 231 | func (k *KoboPatch) WriteOutput() error { 232 | k.d("\n\nKoboPatch::WriteOutput") 233 | 234 | k.d("Removing old output tgz '%s'", k.Config.Out) 235 | os.Remove(k.Config.Out) 236 | 237 | k.d("Closing tar") 238 | if err := k.outTar.Close(); err != nil { 239 | k.d("--> %v", err) 240 | return wrap(err, "could not finalize output tar.gz") 241 | } 242 | k.outTar = nil 243 | 244 | k.d("Closing gz") 245 | if err := k.outGZ.Close(); err != nil { 246 | k.d("--> %v", err) 247 | return wrap(err, "could not finalize output tar.gz") 248 | } 249 | k.outGZ = nil 250 | 251 | k.d("Writing buf to output '%s'", k.Config.Out) 252 | if err := ioutil.WriteFile(k.Config.Out, k.outBuf.Bytes(), 0644); err != nil { 253 | k.d("--> %v", err) 254 | return wrap(err, "could not write output tar.gz") 255 | } 256 | 257 | k.l("\nChecking patched KoboRoot.tgz for consistency") 258 | k.d("Checking patched KoboRoot.tgz for consistency") 259 | 260 | f, err := os.Open(k.Config.Out) 261 | if err != nil { 262 | k.d("--> %v", err) 263 | return wrap(err, "could not open output for reading") 264 | } 265 | 266 | zr, err := gzip.NewReader(f) 267 | if err != nil { 268 | k.d("--> %v", err) 269 | return wrap(err, "could not open output gz") 270 | } 271 | 272 | tr := tar.NewReader(zr) 273 | 274 | var sum int64 275 | for h, err := tr.Next(); err != io.EOF; h, err = tr.Next() { 276 | sum += h.Size 277 | } 278 | 279 | k.d("sum:%d == expected:%d", sum, k.outTarExpectedSize) 280 | if sum != k.outTarExpectedSize { 281 | k.d("--> size mismatch") 282 | return fmt.Errorf("size mismatch: expected %d, got %d (please report this)", k.outTarExpectedSize, sum) 283 | } 284 | 285 | k.d("\nsha1 checksums:") 286 | for f, s := range k.sums { 287 | k.d(" %s %s", s, f) 288 | } 289 | 290 | return nil 291 | } 292 | 293 | func (k *KoboPatch) LoadConfig(r io.Reader) error { 294 | k.d("\n\nKoboPatch::LoadConfig") 295 | 296 | k.d("reading config file from %v", reflect.TypeOf(r)) 297 | buf, err := ioutil.ReadAll(r) 298 | if err != nil { 299 | k.d("--> %v", err) 300 | return wrap(err, "error reading config") 301 | } 302 | 303 | k.d("unmarshaling yaml") 304 | dec := yaml.NewDecoder(bytes.NewReader(buf)) 305 | dec.KnownFields(true) 306 | err = dec.Decode(&k.Config) 307 | if err != nil { 308 | k.d("--> %v", err) 309 | return wrap(err, "error reading kobopatch.yaml") 310 | } 311 | 312 | if k.Config.Version == "" || k.Config.In == "" || k.Config.Out == "" || k.Config.Log == "" { 313 | err = errors.New("invalid kobopatch.yaml: version, in, out, and log are required") 314 | k.d("--> %v", err) 315 | return err 316 | } 317 | 318 | if _, ok := patchfile.GetFormat(k.Config.PatchFormat); !ok { 319 | err = fmt.Errorf("invalid patch format '%s', expected one of %s", k.Config.PatchFormat, strings.Join(patchfile.GetFormats(), ", ")) 320 | k.d("--> %v", err) 321 | return err 322 | } 323 | 324 | k.dp(" | ", "%s", jm(k.Config)) 325 | return nil 326 | } 327 | 328 | func (k *KoboPatch) ApplyPatches() error { 329 | k.d("\n\nKoboPatch::ApplyPatches") 330 | 331 | tr, closeAll, err := k.openIn() 332 | if err != nil { 333 | return err 334 | } 335 | defer closeAll() 336 | 337 | for { 338 | h, err := tr.Next() 339 | if err == io.EOF { 340 | break 341 | } else if err != nil { 342 | k.d("--> could not read entry from tgz: %v", err) 343 | return wrap(err, "could not read input firmware") 344 | } 345 | 346 | patchfiles := []string{} 347 | for n, f := range k.Config.Patches { 348 | if h.Name == "./"+f || h.Name == f || filepath.Base(f) == h.Name { 349 | if filepath.Base(f) == h.Name { // from testdata tarball 350 | h.Name = "./" + f 351 | } 352 | patchfiles = append(patchfiles, n) 353 | } 354 | } 355 | 356 | if len(patchfiles) < 1 { 357 | continue 358 | } 359 | 360 | k.d(" patching entry name:'%s' size:%d mode:'%v' typeflag:'%v' with files: %s", h.Name, h.Size, h.Mode, h.Typeflag, strings.Join(patchfiles, ", ")) 361 | k.l("\nPatching %s", h.Name) 362 | 363 | if h.Typeflag != tar.TypeReg { 364 | k.d(" --> could not patch: not a regular file") 365 | return fmt.Errorf("could not patch file '%s': not a regular file", h.Name) 366 | } 367 | 368 | k.d(" reading entry contents") 369 | buf, err := ioutil.ReadAll(tr) 370 | if err != nil { 371 | k.d(" --> could not patch: could not read contents: %v", err) 372 | return wrap(err, "could not patch file '%s': could not read contents", h.Name) 373 | } 374 | 375 | pt := patchlib.NewPatcher(buf) 376 | 377 | for _, pfn := range patchfiles { 378 | k.d(" loading patch file '%s' (detected format %s)", pfn, getFormat(pfn)) 379 | ps, err := patchfile.ReadFromFile(getFormat(pfn), pfn) 380 | if err != nil { 381 | k.d(" --> %v", err) 382 | return wrap(err, "could not load patch file '%s'", pfn) 383 | } 384 | 385 | for ofn, o := range k.Config.Overrides { 386 | if ofn != pfn || o == nil || len(o) < 1 { 387 | continue 388 | } 389 | k.l(" Applying overrides") 390 | k.d(" applying overrides") 391 | for on, os := range o { 392 | if os { 393 | k.l(" ENABLE `%s`", on) 394 | } else { 395 | k.l(" DISABLE `%s`", on) 396 | } 397 | k.d(" override %s -> enabled:%t", on, os) 398 | if err := ps.SetEnabled(on, os); err != nil { 399 | k.d(" --> %v", err) 400 | return wrap(err, "could not override enabled for patch '%s'", on) 401 | } 402 | } 403 | } 404 | 405 | k.d(" validating patch file") 406 | if err := ps.Validate(); err != nil { 407 | k.d(" --> %v", err) 408 | return wrap(err, "invalid patch file '%s'", pfn) 409 | } 410 | 411 | k.d(" applying patch file") 412 | if err := ps.ApplyTo(pt); err != nil { 413 | k.d(" --> %v", err) 414 | return wrap(err, "error applying patch file '%s'", pfn) 415 | } 416 | } 417 | 418 | fbuf := pt.GetBytes() 419 | k.outTarExpectedSize += h.Size 420 | k.d(" patched file - orig:%d new:%d", h.Size, len(fbuf)) 421 | 422 | k.d(" copying new header to output tar - size:%d mode:'%v'", len(fbuf), h.Mode) 423 | // Preserve attributes (VERY IMPORTANT) 424 | err = k.outTar.WriteHeader(&tar.Header{ 425 | Typeflag: h.Typeflag, 426 | Name: h.Name, 427 | Mode: h.Mode, 428 | Uid: h.Uid, 429 | Gid: h.Gid, 430 | ModTime: time.Now(), 431 | Uname: h.Uname, 432 | Gname: h.Gname, 433 | PAXRecords: h.PAXRecords, 434 | Size: int64(len(fbuf)), 435 | Format: h.Format, 436 | }) 437 | if err != nil { 438 | k.d(" --> %v", err) 439 | return wrap(err, "could not write new file header to patched KoboRoot.tgz") 440 | } 441 | 442 | k.d(" writing patched file to tar writer") 443 | if i, err := k.outTar.Write(fbuf); err != nil { 444 | k.d(" --> %v", err) 445 | return wrap(err, "error writing new file to patched KoboRoot.tgz") 446 | } else if i != len(fbuf) { 447 | k.d(" --> error writing new file to patched KoboRoot.tgz") 448 | return errors.New("error writing new file to patched KoboRoot.tgz") 449 | } 450 | 451 | k.sums[h.Name] = fmt.Sprintf("%x", sha1.Sum(fbuf)) 452 | } 453 | 454 | return nil 455 | } 456 | 457 | func (k *KoboPatch) ApplyTranslations() error { 458 | k.d("\n\nKoboPatch::ApplyTranslations") 459 | if len(k.Config.Translations) >= 1 { 460 | k.l("\nProcessing translations") 461 | k.d("looking for lrelease in config") 462 | lr := k.Config.Lrelease 463 | var err error 464 | if lr == "" { 465 | k.d("looking for lrelease in path") 466 | lr, err = exec.LookPath("lrelease") 467 | if lr == "" { 468 | k.d("looking for lrelease.exe in path") 469 | lr, err = exec.LookPath("lrelease.exe") 470 | if err != nil { 471 | k.d("--> %v", err) 472 | return wrap(err, "could not find lrelease (part of QT Linguist)") 473 | } 474 | } 475 | } else if lr, err = exec.LookPath(lr); err != nil { 476 | k.d("--> %v", err) 477 | return wrap(err, "could not find lrelease (part of QT Linguist)") 478 | } 479 | 480 | for ts, qm := range k.Config.Translations { 481 | k.l(" LRELEASE %s", ts) 482 | k.d(" processing '%s' -> '%s'", ts, qm) 483 | if !strings.HasPrefix(qm, "usr/local/Kobo/translations/") { 484 | err = errors.New("output for translation must start with usr/local/Kobo/translations/") 485 | k.d(" --> %v", err) 486 | return wrap(err, "could not process translation") 487 | } 488 | 489 | k.d(" creating temp dir for lrelease") 490 | td, err := ioutil.TempDir(os.TempDir(), "lrelease-qm") 491 | if err != nil { 492 | k.d(" --> %v", err) 493 | return wrap(err, "could not make temp dir for lrelease") 494 | } 495 | 496 | tf := filepath.Join(td, "out.qm") 497 | 498 | cmd := exec.Command(lr, ts, "-qm", tf) 499 | var outbuf, errbuf bytes.Buffer 500 | cmd.Stdout, cmd.Stderr = &outbuf, &errbuf 501 | 502 | err = cmd.Run() 503 | k.dp(" | ", "lrelease stdout: %s", outbuf.String()) 504 | k.dp(" | ", "lrelease stderr: %s", errbuf.String()) 505 | if err != nil { 506 | k.e(errbuf.String()) 507 | os.RemoveAll(td) 508 | k.d(" --> %v", err) 509 | return wrap(err, "error running lrelease") 510 | } 511 | 512 | k.d(" reading generated qm '%s'", ts) 513 | buf, err := ioutil.ReadFile(tf) 514 | if err != nil { 515 | k.d(" --> %v", err) 516 | return wrap(err, "could not read generated qm file") 517 | } 518 | os.RemoveAll(td) 519 | 520 | k.d(" writing header") 521 | err = k.outTar.WriteHeader(&tar.Header{ 522 | Typeflag: tar.TypeReg, 523 | Name: "./" + qm, 524 | Mode: 0777, 525 | Uid: 0, 526 | Gid: 0, 527 | ModTime: time.Now(), 528 | Size: int64(len(buf)), 529 | }) 530 | if err != nil { 531 | k.d(" --> %v", err) 532 | return wrap(err, "could not write translation file to KoboRoot.tgz") 533 | } 534 | 535 | k.d(" writing file") 536 | if i, err := k.outTar.Write(buf); err != nil { 537 | k.d(" --> %v", err) 538 | return wrap(err, "error writing translation file to KoboRoot.tgz") 539 | } else if i != len(buf) { 540 | k.d(" --> error writing translation file to KoboRoot.tgz") 541 | return errors.New("error writing translation file to KoboRoot.tgz") 542 | } 543 | k.outTarExpectedSize += int64(len(buf)) 544 | } 545 | } 546 | return nil 547 | } 548 | 549 | func (k *KoboPatch) ApplyFiles() error { 550 | k.d("\n\nKoboPatch::ApplyFiles") 551 | if len(k.Config.Files) >= 1 { 552 | k.l("\nAdding additional files") 553 | for src, dests := range k.Config.Files { 554 | for _, dest := range dests { 555 | k.l(" ADD %-35s TO %s", src, dest) 556 | k.d(" %s -> %s", src, dest) 557 | if strings.HasPrefix(dest, "/") { 558 | k.d(" --> destination must not start with a slash") 559 | return errors.New("could not add file: destination must not start with a slash") 560 | } 561 | 562 | k.d(" reading file") 563 | buf, err := ioutil.ReadFile(src) 564 | if err != nil { 565 | k.d(" --> %v", err) 566 | return wrap(err, "could not read additional file '%s'", src) 567 | } 568 | 569 | k.d(" writing header") 570 | err = k.outTar.WriteHeader(&tar.Header{ 571 | Typeflag: tar.TypeReg, 572 | Name: "./" + dest, 573 | Mode: 0777, 574 | Uid: 0, 575 | Gid: 0, 576 | ModTime: time.Now(), 577 | Size: int64(len(buf)), 578 | }) 579 | if err != nil { 580 | k.d(" --> %v", err) 581 | return wrap(err, "could not write additional file to KoboRoot.tgz") 582 | } 583 | 584 | k.d(" writing file") 585 | if i, err := k.outTar.Write(buf); err != nil { 586 | k.d(" --> %v", err) 587 | return wrap(err, "error writing additional file to KoboRoot.tgz") 588 | } else if i != len(buf) { 589 | k.d(" --> error writing additional file to KoboRoot.tgz") 590 | return errors.New("error writing additional file to KoboRoot.tgz") 591 | } 592 | k.outTarExpectedSize += int64(len(buf)) 593 | } 594 | } 595 | } 596 | return nil 597 | } 598 | 599 | func (k *KoboPatch) ApplySymlinks() error { 600 | k.d("\n\nKoboPatch::ApplySymlinks") 601 | 602 | if len(k.Config.Symlinks) >= 1 { 603 | k.l("\nAdding additional symlinks") 604 | 605 | for src, dest := range k.Config.Symlinks { 606 | k.l(" SYMLINK %-35s TO %s", src, dest) 607 | k.d(" %s -> %s", src, dest) 608 | 609 | if strings.HasPrefix(src, "/") || strings.HasPrefix(src, ".") { 610 | k.d(" --> source must not start with a slash/dot") 611 | return errors.New("could not add symlink: source must not start with a slash/dot") 612 | } 613 | if strings.HasPrefix(dest, "/") { 614 | k.d(" --> destination must not start with a slash") 615 | return errors.New("could not add symlink: destination must not start with a slash") 616 | } 617 | 618 | targets := []string{} 619 | for _, fdests := range k.Config.Files { 620 | for _, fdest := range fdests { 621 | if fdest == src { 622 | targets = append(targets, src) 623 | } 624 | } 625 | } 626 | if len(targets) < 1 { 627 | k.d(" --> target is not declared in files") 628 | return errors.New("could not add symlink: target is not declared in files") 629 | } 630 | 631 | k.d(" writing header") 632 | err := k.outTar.WriteHeader(&tar.Header{ 633 | Typeflag: tar.TypeSymlink, 634 | Name: "./" + dest, 635 | Linkname: "/" + src, 636 | Mode: 0777, 637 | Uid: 0, 638 | Gid: 0, 639 | ModTime: time.Now(), 640 | }) 641 | if err != nil { 642 | k.d(" --> %v", err) 643 | return wrap(err, "could not write additional symlink to KoboRoot.tgz") 644 | } 645 | } 646 | } 647 | return nil 648 | } 649 | 650 | func (k *KoboPatch) RunPatchTests() (map[string]map[string]error, error) { 651 | k.d("\n\nKoboPatch::RunPatchTests") 652 | 653 | res := map[string]map[string]error{} 654 | 655 | tr, closeAll, err := k.openIn() 656 | if err != nil { 657 | return nil, err 658 | } 659 | defer closeAll() 660 | 661 | for { 662 | h, err := tr.Next() 663 | if err == io.EOF { 664 | break 665 | } else if err != nil { 666 | k.d("--> could not read entry from tgz: %v", err) 667 | return nil, wrap(err, "could not read input firmware") 668 | } 669 | 670 | patchfiles := []string{} 671 | for n, f := range k.Config.Patches { 672 | if h.Name == "./"+f || h.Name == f || filepath.Base(f) == h.Name { 673 | patchfiles = append(patchfiles, n) 674 | } 675 | } 676 | 677 | if len(patchfiles) < 1 { 678 | continue 679 | } 680 | 681 | k.d(" patching entry name:'%s' size:%d mode:'%v' typeflag:'%v' with files: %s", h.Name, h.Size, h.Mode, h.Typeflag, strings.Join(patchfiles, ", ")) 682 | k.l("\nPatching %s", h.Name) 683 | 684 | if h.Typeflag != tar.TypeReg { 685 | k.d(" --> could not patch: not a regular file") 686 | return nil, fmt.Errorf("could not patch file '%s': not a regular file", h.Name) 687 | } 688 | 689 | k.d(" reading entry contents") 690 | buf, err := ioutil.ReadAll(tr) 691 | if err != nil { 692 | k.d(" --> could not patch: could not read contents: %v", err) 693 | return nil, wrap(err, "could not patch file '%s': could not read contents", h.Name) 694 | } 695 | getBuf := func() []byte { 696 | nbuf := make([]byte, len(buf)) 697 | copy(nbuf, buf) 698 | return nbuf 699 | } 700 | 701 | for _, pfn := range patchfiles { 702 | k.d(" loading patch file '%s' (detected format %s)", pfn, getFormat(pfn)) 703 | if getFormat(pfn) != "kobopatch" { 704 | k.d(" --> format not kobopatch") 705 | return nil, errors.New("patch testing only works with kobopatch format patches") 706 | } 707 | ps, err := patchfile.ReadFromFile(getFormat(pfn), pfn) 708 | if err != nil { 709 | k.d(" --> %v", err) 710 | return nil, wrap(err, "could not load patch file '%s'", pfn) 711 | } 712 | 713 | k.d(" validating patch file") 714 | if err := ps.Validate(); err != nil { 715 | k.d(" --> %v", err) 716 | return nil, wrap(err, "invalid patch file '%s'", pfn) 717 | } 718 | 719 | res[pfn] = map[string]error{} 720 | 721 | sortedNames := reflect.ValueOf(ps).Interface().(*kobopatch.PatchSet).SortedNames() 722 | 723 | errs := map[string]error{} 724 | for _, name := range sortedNames { 725 | fmt.Printf(" - %s", name) 726 | var err error 727 | for _, pname := range sortedNames { 728 | if err = ps.SetEnabled(pname, pname == name); err != nil { 729 | break 730 | } 731 | } 732 | if err != nil { 733 | fmt.Printf("\r ✕ %s\n", name) 734 | errs[name] = err 735 | res[pfn][name] = err 736 | continue 737 | } 738 | out := os.Stdout 739 | os.Stdout = nil 740 | if err := ps.ApplyTo(patchlib.NewPatcher(getBuf())); err != nil { 741 | os.Stdout = out 742 | fmt.Printf("\r ✕ %s\n", name) 743 | errs[name] = err 744 | res[pfn][name] = err 745 | continue 746 | } 747 | os.Stdout = out 748 | if err := ps.SetEnabled(name, false); err != nil { 749 | fmt.Printf("\r ✕ %s\n", name) 750 | errs[name] = err 751 | res[pfn][name] = err 752 | continue 753 | } 754 | fmt.Printf("\r ✔ %s\n", name) 755 | res[pfn][name] = nil 756 | } 757 | } 758 | } 759 | 760 | k.dp(" | ", "%s", jm(res)) 761 | 762 | return res, nil 763 | } 764 | 765 | func (k *KoboPatch) openIn() (*tar.Reader, func(), error) { 766 | k.d(" KoboPatch::openIn") 767 | closeReaders := func() {} 768 | var tbr io.Reader 769 | if strings.HasSuffix(k.Config.In, ".tar.xz") { 770 | k.l("Reading input firmware testdata tarball") 771 | k.d(" Opening testdata tarball '%s'", k.Config.In) 772 | 773 | f, err := os.Open(k.Config.In) 774 | if err != nil { 775 | k.d(" --> %v", err) 776 | return nil, closeReaders, wrap(err, "could not open firmware tarball") 777 | } 778 | 779 | xzr, err := xz.NewReader(f, 0) 780 | if err != nil { 781 | k.d(" --> %v", err) 782 | f.Close() 783 | return nil, closeReaders, wrap(err, "could not open firmware tarball as xz") 784 | } 785 | tbr = xzr 786 | closeReaders = func() { f.Close() } 787 | } else { 788 | k.l("Reading input firmware zip") 789 | k.d(" Opening firmware zip '%s'", k.Config.In) 790 | 791 | zr, err := zip.OpenReader(k.Config.In) 792 | if err != nil { 793 | k.d(" --> %v", err) 794 | return nil, closeReaders, wrap(err, "could not open firmware zip") 795 | } 796 | 797 | k.d(" Looking for KoboRoot.tgz in zip") 798 | var kr io.ReadCloser 799 | for _, f := range zr.File { 800 | k.d(" --> found %s", f.Name) 801 | if f.Name == "KoboRoot.tgz" { 802 | k.d(" --> opening KoboRoot.tgz") 803 | kr, err = f.Open() 804 | if err != nil { 805 | k.d(" --> --> %v", err) 806 | return nil, closeReaders, wrap(err, "could not open KoboRoot.tgz in firmware zip") 807 | } 808 | break 809 | } 810 | } 811 | if kr == nil { 812 | k.d(" --> could not find KoboRoot.tgz") 813 | return nil, closeReaders, errors.New("could not find KoboRoot.tgz") 814 | } 815 | 816 | k.d(" Opening gzip reader") 817 | gzr, err := gzip.NewReader(kr) 818 | if err != nil { 819 | k.d(" --> %v", err) 820 | kr.Close() 821 | return nil, closeReaders, wrap(err, "could not decompress KoboRoot.tgz") 822 | } 823 | tbr = gzr 824 | closeReaders = func() { 825 | gzr.Close() 826 | kr.Close() 827 | } 828 | } 829 | 830 | k.d(" Creating tar reader") 831 | return tar.NewReader(tbr), closeReaders, nil 832 | } 833 | 834 | func (k *KoboPatch) l(format string, a ...interface{}) { 835 | if k.Logf != nil { 836 | k.Logf(format, a...) 837 | } 838 | } 839 | 840 | func (k *KoboPatch) e(format string, a ...interface{}) { 841 | if k.Errorf != nil { 842 | k.Errorf(format, a...) 843 | } 844 | } 845 | 846 | func (k *KoboPatch) d(format string, a ...interface{}) { 847 | if k.Debugf != nil { 848 | k.Debugf(format, a...) 849 | } 850 | } 851 | 852 | func (k *KoboPatch) dp(prefix string, format string, a ...interface{}) { 853 | k.d("%s%s", prefix, strings.ReplaceAll(fmt.Sprintf(format, a...), "\n", "\n"+prefix)) 854 | } 855 | 856 | func wrap(err error, format string, a ...interface{}) error { 857 | return fmt.Errorf("%s: %v", fmt.Sprintf(format, a...), err) 858 | } 859 | 860 | func jm(v interface{}) string { 861 | if buf, err := json.MarshalIndent(v, "", " "); err == nil { 862 | return string(buf) 863 | } 864 | return "" 865 | } 866 | 867 | func getFormat(filename string) string { 868 | f := strings.TrimLeft(filepath.Ext(filename), ".") 869 | f = strings.ReplaceAll(f, "patch", "patch32lsb") 870 | f = strings.ReplaceAll(f, "yaml", "kobopatch") 871 | return f 872 | } 873 | 874 | // stringArray forces strings to become arrays during yaml decoding. 875 | type stringSlice []string 876 | 877 | // UnmarshalYAML unmarshals a stringArray. 878 | func (a *stringSlice) UnmarshalYAML(unmarshal func(interface{}) error) error { 879 | var strings []string 880 | if err := unmarshal(&strings); err != nil { 881 | var str string 882 | if err := unmarshal(&str); err != nil { 883 | return err 884 | } 885 | *a = []string{str} 886 | } else { 887 | *a = strings 888 | } 889 | return nil 890 | } 891 | -------------------------------------------------------------------------------- /kobopatch/kobopatch_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | func TestStringSlice(t *testing.T) { 11 | for _, c := range []struct { 12 | In string 13 | Out []string 14 | }{ 15 | {`asd`, []string{"asd"}}, 16 | {`asd sdf`, []string{"asd sdf"}}, 17 | {`[asd, sdf]`, []string{"asd", "sdf"}}, 18 | {`["asd", "sdf"]`, []string{"asd", "sdf"}}, 19 | {` 20 | - asd 21 | - sdf`, []string{"asd", "sdf"}}, 22 | {` 23 | - "asd" 24 | - "sdf"`, []string{"asd", "sdf"}}, 25 | } { 26 | t.Run(c.In, func(t *testing.T) { 27 | t.Parallel() 28 | var obj struct { 29 | Test stringSlice `yaml:"Test"` 30 | } 31 | if err := yaml.Unmarshal([]byte(fmt.Sprintf("Test: %s", c.In)), &obj); err != nil { 32 | t.Fatalf("unexpected unmarshal error: %v", err) 33 | } 34 | t.Logf("out: %#v", obj) 35 | for _, ev := range c.Out { 36 | var found bool 37 | for _, av := range obj.Test { 38 | found = found || av == ev 39 | } 40 | if !found { 41 | t.Errorf("expected to find '%s' in output", ev) 42 | } 43 | } 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /patchfile/kobopatch/kobopatch.go: -------------------------------------------------------------------------------- 1 | package kobopatch 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "reflect" 7 | "sort" 8 | 9 | "github.com/pgaskin/kobopatch/patchfile" 10 | "github.com/pgaskin/kobopatch/patchlib" 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | type PatchSet struct { 15 | parsed map[string]*parsedPatch 16 | } 17 | 18 | // parsedPatch holds a representation of a PatchNode for use internally. It 19 | // cannot be re-marshaled directly (use the PatchNode and InstructionNode for 20 | // that). 21 | type parsedPatch struct { 22 | Enabled bool 23 | Description string 24 | PatchGroups []string 25 | Instructions []*parsedInstruction 26 | } 27 | 28 | // parsedInstruction holds a representation of a InstructionNode for use internally. 29 | type parsedInstruction struct { 30 | Index int 31 | Line int 32 | Instruction PatchableInstruction 33 | } 34 | 35 | func init() { 36 | patchfile.RegisterFormat("kobopatch", Parse) 37 | } 38 | 39 | // Parse parses a PatchSet from a buf. 40 | func Parse(buf []byte) (patchfile.PatchSet, error) { 41 | patchfile.Log("parsing patch file: unmarshaling to map[string]yaml.Node\n") 42 | var psn map[string]yaml.Node 43 | if err := yaml.Unmarshal(buf, &psn); err != nil { 44 | if bytes.Contains(buf, []byte{'\t'}) { 45 | return nil, fmt.Errorf("patch file contains tabs (it should be indented with spaces, not tabs): %w", err) 46 | } 47 | return nil, err 48 | } 49 | 50 | patchfile.Log("parsing patch file: converting to map[string]*parsedPatch\n") 51 | ps := PatchSet{map[string]*parsedPatch{}} 52 | for name, node := range psn { 53 | patchfile.Log(" unmarshaling patch %#v to PatchNode ([]yaml.Node)\n", name) 54 | var pn PatchNode 55 | if err := node.DecodeStrict(&pn); err != nil { 56 | return nil, fmt.Errorf("line %d: patch %#v: %w", node.Line, name, err) 57 | } 58 | 59 | patchfile.Log(" converting to []InstructionNode (map[string]yaml.Node)\n") 60 | ns, err := pn.ToInstructionNodes() 61 | if err != nil { 62 | return nil, fmt.Errorf("line %d: patch %#v: %w", node.Line, name, err) 63 | } 64 | 65 | patchfile.Log(" converting to *parsedPatch\n") 66 | ps.parsed[name] = &parsedPatch{} 67 | for i, instNode := range ns { 68 | patchfile.Log(" unmarshaling instruction %d to Instruction\n", i+1) 69 | inst, err := instNode.ToInstruction() 70 | if err != nil { 71 | return nil, fmt.Errorf("line %d: patch %#v: instruction %d: %w", node.Line, name, i+1, err) 72 | } 73 | 74 | patchfile.Log(" converting to SingleInstruction...") 75 | sinst := inst.ToSingleInstruction() 76 | patchfile.Log(" type=%s\n", reflect.TypeOf(sinst)) 77 | switch sinst.(type) { 78 | case Enabled: 79 | ps.parsed[name].Enabled = bool(sinst.(Enabled)) 80 | case Description: 81 | if ps.parsed[name].Description != "" { 82 | return nil, fmt.Errorf("patch %#v: line %d: instruction %d: duplicate Description instruction", name, instNode.Line(node.Line), i+1) 83 | } 84 | ps.parsed[name].Description = string(sinst.(Description)) 85 | case PatchGroup: 86 | g := string(sinst.(PatchGroup)) 87 | ps.parsed[name].PatchGroups = append(ps.parsed[name].PatchGroups, g) 88 | default: 89 | patchfile.Log(" converting to PatchableInstruction\n") 90 | if psinst, ok := sinst.(PatchableInstruction); ok { 91 | ps.parsed[name].Instructions = append(ps.parsed[name].Instructions, &parsedInstruction{i + 1, instNode.Line(node.Line), psinst}) 92 | break 93 | } 94 | panic(fmt.Errorf("incomplete implementation (missing implementation of PatchableInstruction) for type %s", reflect.TypeOf(sinst))) 95 | } 96 | } 97 | } 98 | return &ps, nil 99 | } 100 | 101 | // ApplyTo applies a PatchSet to a Patcher. 102 | func (ps *PatchSet) ApplyTo(pt *patchlib.Patcher) error { 103 | patchfile.Log("validating patch file\n") 104 | if err := ps.Validate(); err != nil { 105 | err = fmt.Errorf("invalid patch file: %w", err) 106 | fmt.Printf(" Error: %v\n", err) 107 | return err 108 | } 109 | 110 | patchfile.Log("looping over patches\n") 111 | for _, name := range ps.SortedNames() { 112 | patch := ps.parsed[name] 113 | patchfile.Log(" Patch(%#v) enabled=%t\n", name, patch.Enabled) 114 | 115 | patchfile.Log(" ResetBaseAddress()\n") 116 | pt.ResetBaseAddress() 117 | 118 | if !patch.Enabled { 119 | patchfile.Log(" skipping\n") 120 | fmt.Printf(" SKIP `%s`\n", name) 121 | continue 122 | } 123 | 124 | patchfile.Log(" applying\n") 125 | fmt.Printf(" APPLY `%s`\n", name) 126 | 127 | patchfile.Log(" looping over instructions\n") 128 | for _, inst := range patch.Instructions { 129 | patchfile.Log(" %s index=%d line=%d\n", reflect.TypeOf(inst.Instruction), inst.Index, inst.Line) 130 | if err := inst.Instruction.ApplyTo(pt, func(format string, a ...interface{}) { 131 | patchfile.Log(" %s\n", fmt.Sprintf(format, a...)) 132 | }); err != nil { 133 | err = fmt.Errorf("could not apply patch %#v: line %d: inst %d: %w", name, inst.Line, inst.Index, err) 134 | patchfile.Log(" %v", err) 135 | fmt.Printf(" Error: %v\n", err) 136 | return err 137 | } 138 | } 139 | } 140 | 141 | return nil 142 | } 143 | 144 | // SetEnabled sets the Enabled state of a Patch in a PatchSet. 145 | func (ps *PatchSet) SetEnabled(patch string, enabled bool) error { 146 | if patch, ok := ps.parsed[patch]; ok { 147 | patch.Enabled = enabled 148 | return nil 149 | } 150 | return fmt.Errorf("no such patch %#v", patch) 151 | } 152 | 153 | // SortedNames gets the names of patches sorted alphabetically. 154 | func (ps *PatchSet) SortedNames() []string { 155 | names := make([]string, len(ps.parsed)) 156 | var i int 157 | for name := range ps.parsed { 158 | names[i] = name 159 | i++ 160 | } 161 | sort.Strings(names) 162 | return names 163 | } 164 | 165 | // Validate validates the PatchSet. 166 | func (ps *PatchSet) Validate() error { 167 | usedPatchGroups := map[string]string{} 168 | for _, name := range ps.SortedNames() { 169 | patch := ps.parsed[name] 170 | 171 | seenPatchGroups := map[string]bool{} 172 | for _, g := range patch.PatchGroups { 173 | if seenPatchGroups[g] { 174 | return fmt.Errorf("patch %#v: duplicate PatchGroup instruction for PatchGroup %#v", name, g) 175 | } 176 | seenPatchGroups[g] = true 177 | if patch.Enabled { 178 | if r, ok := usedPatchGroups[g]; ok { 179 | return fmt.Errorf("patch %#v: more than one patch enabled in PatchGroup %#v (other patch is %#v)", name, g, r) 180 | } 181 | usedPatchGroups[g] = name 182 | } 183 | } 184 | 185 | if len(patch.Instructions) == 0 { 186 | return fmt.Errorf("patch %#v: no instructions which modify anything", name) 187 | } 188 | 189 | for _, inst := range patch.Instructions { 190 | pfx := fmt.Sprintf("patch %#v: line %d: inst %d", name, inst.Line, inst.Index) 191 | switch inst.Instruction.(type) { 192 | case ReplaceBytesNOP: 193 | if len(inst.Instruction.(ReplaceBytesNOP).Find)%2 != 0 { 194 | return fmt.Errorf("%s: ReplaceBytesNOP: find must be a multiple of 2 to be replaced with 00 46 (MOV r0, r0)", pfx) 195 | } 196 | case ReplaceString: 197 | if inst.Instruction.(ReplaceString).MustMatchLength { 198 | if d := len(inst.Instruction.(ReplaceString).Replace) - len(inst.Instruction.(ReplaceString).Find); d < 0 { 199 | return fmt.Errorf("%s: ReplaceString: replacement string %d chars too short", pfx, -d) 200 | } else if d > 0 { 201 | return fmt.Errorf("%s: ReplaceString: replacement string %d chars too long", pfx, d) 202 | } 203 | } 204 | case FindReplaceString: 205 | if inst.Instruction.(FindReplaceString).MustMatchLength { 206 | if d := len(inst.Instruction.(FindReplaceString).Replace) - len(inst.Instruction.(FindReplaceString).Find); d < 0 { 207 | return fmt.Errorf("%s: FindReplaceString: replacement string %d chars too short", pfx, -d) 208 | } else if d > 0 { 209 | return fmt.Errorf("%s: FindReplaceString: replacement string %d chars too long", pfx, d) 210 | } 211 | } 212 | case FindZlibHash: 213 | if len(inst.Instruction.(FindZlibHash)) != 40 { 214 | return fmt.Errorf("%s: FindZlibHash: hash must be 40 chars long", pfx) 215 | } 216 | case ReplaceZlibGroup: 217 | r := inst.Instruction.(ReplaceZlibGroup) 218 | if len(r.Replacements) == 0 { 219 | return fmt.Errorf("%s: ReplaceZlibGroup: no replacements specified", pfx) 220 | } 221 | for i, repl := range r.Replacements { 222 | if repl.Find == "" || repl.Replace == "" { 223 | return fmt.Errorf("%s: ReplaceZlibGroup: replacement %d: Find and Replace must be set", pfx, i+1) 224 | } 225 | } 226 | } 227 | } 228 | } 229 | return nil 230 | } 231 | -------------------------------------------------------------------------------- /patchfile/kobopatch/patch.go: -------------------------------------------------------------------------------- 1 | package kobopatch 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | 9 | "github.com/pgaskin/kobopatch/patchlib" 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | type Patch []*Instruction 14 | 15 | type PatchNode []yaml.Node 16 | 17 | func (p *PatchNode) ToInstructionNodes() ([]InstructionNode, error) { 18 | n := make([]InstructionNode, len(*p)) 19 | for i, t := range *p { 20 | if err := t.DecodeStrict(&n[i]); err != nil { 21 | return n, err 22 | } 23 | } 24 | return n, nil 25 | } 26 | 27 | func (p *PatchNode) ToPatch() (Patch, error) { 28 | n := make(Patch, len(*p)) 29 | for i, t := range *p { 30 | if err := t.DecodeStrict(&n[i]); err != nil { 31 | return n, err 32 | } 33 | } 34 | return n, nil 35 | } 36 | 37 | type Instruction struct { 38 | Enabled *Enabled `yaml:"Enabled,omitempty"` 39 | Description *Description `yaml:"Description,omitempty"` 40 | PatchGroup *PatchGroup `yaml:"PatchGroup,omitempty"` 41 | BaseAddress *BaseAddress `yaml:"BaseAddress,omitempty,flow"` 42 | FindBaseAddressHex *FindBaseAddressHex `yaml:"FindBaseAddressHex,omitempty"` 43 | FindBaseAddressString *FindBaseAddressString `yaml:"FindBaseAddressString,omitempty"` 44 | FindZlib *FindZlib `yaml:"FindZlib,omitempty"` 45 | FindZlibHash *FindZlibHash `yaml:"FindZlibHash,omitempty"` 46 | FindReplaceString *FindReplaceString `yaml:"FindReplaceString,omitempty"` 47 | ReplaceString *ReplaceString `yaml:"ReplaceString,omitempty"` 48 | ReplaceInt *ReplaceInt `yaml:"ReplaceInt,omitempty,flow"` 49 | ReplaceFloat *ReplaceFloat `yaml:"ReplaceFloat,omitempty,flow"` 50 | ReplaceBytes *ReplaceBytes `yaml:"ReplaceBytes,omitempty"` 51 | ReplaceZlib *ReplaceZlib `yaml:"ReplaceZlib,omitempty"` 52 | ReplaceZlibGroup *ReplaceZlibGroup `yaml:"ReplaceZlibGroup,omitempty"` 53 | FindBaseAddressSymbol *FindBaseAddressSymbol `yaml:"FindBaseAddressSymbol,omitempty"` // Deprecated: Use BaseAddress instead. 54 | ReplaceBytesAtSymbol *ReplaceBytesAtSymbol `yaml:"ReplaceBytesAtSymbol,omitempty"` // Deprecated: Use ReplaceBytes.Base instead. 55 | ReplaceBytesNOP *ReplaceBytesNOP `yaml:"ReplaceBytesNOP,omitempty"` // Deprecated: Use ReplaceBytes.ReplaceNOP instead. 56 | ReplaceBLX *ReplaceBLX `yaml:"ReplaceBLX,omitempty"` // Deprecated: Use ReplaceBytes.FindInstBLX and ReplaceBytes.ReplaceInstBLX instead. 57 | } 58 | 59 | type InstructionNode map[string]yaml.Node 60 | 61 | func (i InstructionNode) ToInstruction() (*Instruction, error) { 62 | if len(i) == 0 { 63 | return nil, fmt.Errorf("expected instruction, got nothing") 64 | } 65 | var found bool 66 | var n Instruction 67 | for name, node := range i { 68 | if found { 69 | return nil, fmt.Errorf("line %d: multiple types found in instruction, maybe you forgot a '-'", node.Line) 70 | } else if field := reflect.ValueOf(&n).Elem().FieldByName(name); !field.IsValid() { 71 | return nil, fmt.Errorf("line %d: unknown instruction type %#v", node.Line, name) 72 | } else if err := node.DecodeStrict(field.Addr().Interface()); err != nil { 73 | return nil, fmt.Errorf("line %d: error decoding instruction: %w", node.Line, err) 74 | } else { 75 | found = true 76 | } 77 | } 78 | return &n, nil 79 | } 80 | 81 | func (i InstructionNode) Line(def int) int { 82 | for _, node := range i { 83 | return node.Line 84 | } 85 | return def 86 | } 87 | 88 | func (i Instruction) ToSingleInstruction() interface{} { 89 | iv := reflect.ValueOf(i) 90 | for i := 0; i < iv.NumField(); i++ { 91 | if !iv.Field(i).IsNil() { 92 | return iv.Field(i).Elem().Interface() 93 | } 94 | } 95 | return nil 96 | } 97 | 98 | // FlexAbsOffset allows specifying an absolute offset with either a direct 99 | // integer (absolute offset - Offset), string (normal symbol - Sym), or a field. 100 | type FlexAbsOffset struct { 101 | Offset *int32 `yaml:"Offset,omitempty"` // can be specified in place of this object 102 | Sym *string `yaml:"Sym,omitempty"` 103 | SymPLT *string `yaml:"SymPLT,omitempty"` 104 | SymPLTTail *string `yaml:"SymPLTTail,omitempty"` 105 | Inline bool `yaml:"-"` // whether the Offset/Sym was inline 106 | Rel *int32 `yaml:"Rel,omitempty"` // optional, gets added to the absolute offset found 107 | } 108 | 109 | func (f *FlexAbsOffset) UnmarshalYAML(n *yaml.Node) error { 110 | *f = FlexAbsOffset{} // reset 111 | 112 | var offset int32 113 | if err := n.DecodeStrict(&offset); err == nil { 114 | f.Offset = &offset 115 | f.Inline = true 116 | return nil 117 | } 118 | 119 | var sym string 120 | if err := n.DecodeStrict(&sym); err == nil { 121 | f.Sym = &sym 122 | f.Inline = true 123 | return nil 124 | } 125 | 126 | type FlexAbsOffsetData FlexAbsOffset // this works because the MarshalYAML won't be inherited, so it won't result in an infinite loop, but struct tags will remain 127 | var obj FlexAbsOffsetData 128 | if err := n.DecodeStrict(&obj); err != nil { 129 | return fmt.Errorf("line %d: %w", n.Line, err) 130 | } 131 | *f = FlexAbsOffset(obj) 132 | return nil 133 | } 134 | 135 | func (f FlexAbsOffset) MarshalYAML() (interface{}, error) { 136 | if err := f.validate(); err != nil { 137 | return nil, err 138 | } 139 | if f.Inline { 140 | if f.Offset != nil { 141 | return f.Offset, nil 142 | } 143 | if f.Sym != nil { 144 | return f.Sym, nil 145 | } 146 | } 147 | type FlexAbsOffsetData FlexAbsOffset // this works because the MarshalYAML won't be inherited, so it won't result in an infinite loop, but struct tags will remain 148 | return FlexAbsOffsetData(f), nil 149 | } 150 | 151 | func (f FlexAbsOffset) Resolve(p *patchlib.Patcher) (int32, error) { 152 | if err := f.validate(); err != nil { 153 | return 0, err 154 | } 155 | var rel int32 156 | if f.Rel != nil { 157 | rel = *f.Rel 158 | } 159 | off, err := func() (int32, error) { 160 | switch { 161 | case f.Offset != nil: 162 | return *f.Offset + rel, nil 163 | case f.Sym != nil: 164 | return p.ResolveSym(*f.Sym) 165 | case f.SymPLT != nil: 166 | return p.ResolveSymPLT(*f.SymPLT) 167 | case f.SymPLTTail != nil: 168 | return p.ResolveSymPLTTail(*f.SymPLTTail) 169 | default: 170 | panic("this should have been caught by FlexAbsOffset.validate") 171 | } 172 | }() 173 | return off + rel, err 174 | } 175 | 176 | func (f FlexAbsOffset) validate() error { 177 | if f.Offset != nil && *f.Offset < 0 { 178 | return fmt.Errorf("offset must be positive, got %d", *f.Offset) 179 | } 180 | var c int 181 | for _, v := range []bool{f.Offset != nil, f.Sym != nil, f.SymPLT != nil, f.SymPLTTail != nil} { 182 | if v { 183 | c++ 184 | } 185 | } 186 | if c == 0 { 187 | return fmt.Errorf("no offset method specified (%#v)", f) 188 | } 189 | if c > 1 { 190 | return fmt.Errorf("multiple offset methods specified (%#v)", f) 191 | } 192 | return nil 193 | } 194 | 195 | type Enabled bool 196 | type Description string 197 | type PatchGroup string 198 | 199 | type PatchableInstruction interface { 200 | ApplyTo(*patchlib.Patcher, func(string, ...interface{})) error 201 | } 202 | 203 | type BaseAddress FlexAbsOffset 204 | type FindBaseAddressHex string 205 | type FindBaseAddressString string 206 | type FindZlib string 207 | type FindZlibHash string 208 | 209 | func (b BaseAddress) ApplyTo(pt *patchlib.Patcher, log func(string, ...interface{})) error { 210 | log("BaseAddress(%#v)", b) 211 | offset, err := FlexAbsOffset(b).Resolve(pt) 212 | if err != nil { 213 | return fmt.Errorf("BaseAddress: resolve address (%#v): %w", b, err) 214 | } 215 | log(" BaseAddress(%#v)", offset) 216 | return pt.BaseAddress(int32(offset)) 217 | } 218 | 219 | func (b *BaseAddress) UnmarshalYAML(n *yaml.Node) error { 220 | return (*FlexAbsOffset)(b).UnmarshalYAML(n) 221 | } 222 | 223 | func (b BaseAddress) MarshalYAML() (interface{}, error) { 224 | return (FlexAbsOffset)(b).MarshalYAML() 225 | } 226 | 227 | func (b FindBaseAddressHex) ApplyTo(pt *patchlib.Patcher, log func(string, ...interface{})) error { 228 | log("FindBaseAddressHex(%#v)", b) 229 | var buf []byte 230 | _, err := fmt.Sscanf(strings.ReplaceAll(string(b), " ", ""), "%x\n", &buf) 231 | if err != nil { 232 | return fmt.Errorf("FindBaseAddressHex: error parsing hex: %w", err) 233 | } 234 | if err := pt.FindBaseAddress(buf); err != nil { 235 | return fmt.Errorf("FindBaseAddressHex: %w", err) 236 | } 237 | return nil 238 | } 239 | 240 | func (b FindBaseAddressString) ApplyTo(pt *patchlib.Patcher, log func(string, ...interface{})) error { 241 | log("FindBaseAddressString(%#v) | hex:%x", b, []byte(b)) 242 | return pt.FindBaseAddressString(string(b)) 243 | } 244 | 245 | func (b FindZlib) ApplyTo(pt *patchlib.Patcher, log func(string, ...interface{})) error { 246 | log("FindZlib(%#v) | hex:%x", b, []byte(b)) 247 | return pt.FindZlib(string(b)) 248 | } 249 | 250 | func (b FindZlibHash) ApplyTo(pt *patchlib.Patcher, log func(string, ...interface{})) error { 251 | log("FindZlibHash(%#v) | hex:%x", b, []byte(b)) 252 | return pt.FindZlibHash(string(b)) 253 | } 254 | 255 | type FindReplaceString struct { 256 | Find string `yaml:"Find"` 257 | Replace string `yaml:"Replace"` 258 | MustMatchLength bool `yaml:"MustMatchLength,omitempty"` 259 | } 260 | 261 | type ReplaceString struct { 262 | Offset int32 `yaml:"Offset,omitempty"` 263 | Find string `yaml:"Find"` 264 | Replace string `yaml:"Replace"` 265 | MustMatchLength bool `yaml:"MustMatchLength,omitempty"` 266 | } 267 | 268 | type ReplaceInt struct { 269 | Offset int32 `yaml:"Offset,omitempty"` 270 | Find uint8 `yaml:"Find"` 271 | Replace uint8 `yaml:"Replace"` 272 | } 273 | 274 | type ReplaceFloat struct { 275 | Offset int32 `yaml:"Offset,omitempty"` 276 | Find float64 `yaml:"Find"` 277 | Replace float64 `yaml:"Replace"` 278 | } 279 | 280 | type ReplaceBytes struct { 281 | Base *FlexAbsOffset `yaml:"Base,omitempty,flow"` // if specified, Offset is based on this rather than the current offset 282 | Offset int32 `yaml:"Offset,omitempty"` 283 | Find []byte `yaml:"Find,omitempty"` 284 | Replace []byte `yaml:"Replace,omitempty"` 285 | // generators 286 | FindH *string `yaml:"FindH,omitempty"` 287 | ReplaceH *string `yaml:"ReplaceH,omitempty"` 288 | FindInstBLX *FlexAbsOffset `yaml:"FindInstBLX,omitempty,flow"` 289 | ReplaceInstBLX *FlexAbsOffset `yaml:"ReplaceInstBLX,omitempty,flow"` 290 | FindInstBW *FlexAbsOffset `yaml:"FindInstBW,omitempty,flow"` 291 | ReplaceInstBW *FlexAbsOffset `yaml:"ReplaceInstBW,omitempty,flow"` 292 | ReplaceInstNOP *bool `yaml:"ReplaceInstNOP,omitempty,flow"` // if specified, must be true 293 | FindBLX *uint32 `yaml:"FindBLX,omitempty"` // Deprecated: Use FindInstBLX instead. 294 | // special 295 | CheckOnly *bool `yaml:"CheckOnly,omitempty"` // if specified and true, it will only ensure the presence of the find string 296 | } 297 | 298 | type ReplaceZlib struct { 299 | Offset int32 `yaml:"Offset,omitempty"` 300 | Find string `yaml:"Find"` 301 | Replace string `yaml:"Replace"` 302 | } 303 | 304 | type ReplaceZlibGroup struct { 305 | Offset int32 `yaml:"Offset,omitempty"` 306 | Replacements []struct { 307 | Find string `yaml:"Find"` 308 | Replace string `yaml:"Replace"` 309 | } `yaml:"Replacements"` 310 | } 311 | 312 | func (r FindReplaceString) ApplyTo(pt *patchlib.Patcher, log func(string, ...interface{})) error { 313 | log("FindReplaceString(%#v, %#v)", r.Find, r.Replace) 314 | log(" FindBaseAddressString(%#v)", r.Find) 315 | if err := pt.FindBaseAddressString(r.Find); err != nil { 316 | return fmt.Errorf("FindReplaceString: %w", err) 317 | } 318 | log(" ReplaceString(0, %#v, %#v)", r.Find, r.Replace) 319 | if err := pt.ReplaceString(0, r.Find, r.Replace); err != nil { 320 | return fmt.Errorf("FindReplaceString: %w", err) 321 | } 322 | return nil 323 | } 324 | 325 | func (r ReplaceString) ApplyTo(pt *patchlib.Patcher, log func(string, ...interface{})) error { 326 | log("ReplaceString(%#v, %#v, %#v)", r.Offset, r.Find, r.Replace) 327 | return pt.ReplaceString(r.Offset, r.Find, r.Replace) 328 | } 329 | 330 | func (r ReplaceInt) ApplyTo(pt *patchlib.Patcher, log func(string, ...interface{})) error { 331 | log("ReplaceInt(%#v, %#v, %#v)", r.Offset, r.Find, r.Replace) 332 | return pt.ReplaceInt(r.Offset, r.Find, r.Replace) 333 | } 334 | 335 | func (r ReplaceFloat) ApplyTo(pt *patchlib.Patcher, log func(string, ...interface{})) error { 336 | log("ReplaceFloat(%#v, %#v, %#v)", r.Offset, r.Find, r.Replace) 337 | return pt.ReplaceFloat(r.Offset, r.Find, r.Replace) 338 | } 339 | 340 | func (r ReplaceBytes) ApplyTo(pt *patchlib.Patcher, log func(string, ...interface{})) (perr error) { 341 | log("ReplaceBytes(%#v)", r) 342 | 343 | // TODO: add tests for new expansions 344 | 345 | curOrig := pt.GetCur() 346 | cur := curOrig 347 | if r.Base != nil { 348 | log(" Base.Resolve(%#v)", *r.Base) 349 | off, err := r.Base.Resolve(pt) 350 | if err != nil { 351 | err = fmt.Errorf("ReplaceBytes: expand Base=%#v: %v", *r.Base, err) 352 | log(" -> Error: %v", err) 353 | return err 354 | } 355 | cur = off 356 | log(" -> Offset: 0x%X", off) 357 | 358 | log(" BaseAddress(0x%X) [temp]", cur) 359 | if err := pt.BaseAddress(cur); err != nil { 360 | err = fmt.Errorf("ReplaceBytes: update cur to 0x%X: %v", cur, err) 361 | log(" -> Error: %v", err) 362 | return err 363 | } 364 | 365 | defer func() { 366 | log(" BaseAddress(0x%X) [restore]", curOrig) 367 | if err := pt.BaseAddress(curOrig); err != nil { 368 | err = fmt.Errorf("ReplaceBytes: restore overridden cur to 0x%X", curOrig) 369 | log(" -> Error: %v", err) 370 | if perr == nil { 371 | perr = err 372 | } 373 | return 374 | } 375 | }() 376 | } 377 | 378 | if r.FindH != nil { 379 | log("FindH.Expand(%#v)", *r.FindH) 380 | buf, err := hex.DecodeString(strings.ReplaceAll(*r.FindH, " ", "")) 381 | if err != nil { 382 | err = fmt.Errorf("ReplaceBytes: expand FindH=%#v: %v", *r.FindH, err) 383 | log(" -> Error: %v", err) 384 | return err 385 | } 386 | r.Find = buf 387 | log(" -> Find = %#v", buf) 388 | } 389 | 390 | if r.ReplaceH != nil { 391 | log("ReplaceH.Expand(%#v)", *r.ReplaceH) 392 | buf, err := hex.DecodeString(strings.ReplaceAll(*r.ReplaceH, " ", "")) 393 | if err != nil { 394 | err = fmt.Errorf("ReplaceBytes: expand ReplaceH=%#v: %v", *r.ReplaceH, err) 395 | log(" -> Error: %v", err) 396 | return err 397 | } 398 | r.Replace = buf 399 | log(" -> Replace = %#v", buf) 400 | } 401 | 402 | if r.FindBLX != nil { 403 | log("FindBLX(0x%X)", *r.FindBLX) 404 | o := int32(*r.FindBLX) 405 | r.FindInstBLX = &FlexAbsOffset{Offset: &o} 406 | log(" -> FindInstBLX = FindBLX = 0x%X", *r.FindBLX) 407 | } 408 | 409 | if r.FindInstBLX != nil { 410 | log("FindInstBLX.Expand(%#v)", *r.FindInstBLX) 411 | 412 | log(" FindInstBLX.Resolve(%#v)", *r.FindInstBLX) 413 | tgt, err := r.FindInstBLX.Resolve(pt) 414 | if err != nil { 415 | err = fmt.Errorf("ReplaceBytes: expand FindInstBLX=%#v: %v", *r.FindInstBLX, err) 416 | log(" -> Error: %v", err) 417 | return err 418 | } 419 | log(" -> Target: 0x%X", tgt) 420 | 421 | pc := cur + r.Offset 422 | log(" AsmBLX(0x%X, 0x%X)", pc, tgt) 423 | buf := patchlib.AsmBLX(uint32(pc), uint32(tgt)) 424 | r.Find = buf 425 | log(" -> Find = %#v", buf) 426 | } 427 | 428 | if r.ReplaceInstBLX != nil { 429 | log("ReplaceInstBLX.Expand(%#v)", *r.ReplaceInstBLX) 430 | 431 | log(" ReplaceInstBLX.Resolve(%#v)", *r.ReplaceInstBLX) 432 | tgt, err := r.ReplaceInstBLX.Resolve(pt) 433 | if err != nil { 434 | err = fmt.Errorf("ReplaceBytes: expand ReplaceInstBLX=%#v: %v", *r.ReplaceInstBLX, err) 435 | log(" -> Error: %v", err) 436 | return err 437 | } 438 | log(" -> Target: 0x%X", tgt) 439 | 440 | pc := cur + r.Offset 441 | log(" AsmBLX(0x%X, 0x%X)", pc, tgt) 442 | buf := patchlib.AsmBLX(uint32(pc), uint32(tgt)) 443 | r.Replace = buf 444 | log(" -> Replace = %#v", buf) 445 | } 446 | 447 | if r.FindInstBW != nil { 448 | log("FindInstBW.Expand(%#v)", *r.FindInstBW) 449 | 450 | log(" FindInstBW.Resolve(%#v)", *r.FindInstBW) 451 | tgt, err := r.FindInstBW.Resolve(pt) 452 | if err != nil { 453 | err = fmt.Errorf("ReplaceBytes: expand FindInstB=%#v: %v", *r.FindInstBW, err) 454 | log(" -> Error: %v", err) 455 | return err 456 | } 457 | log(" -> Target: 0x%X", tgt) 458 | 459 | pc := cur + r.Offset 460 | log(" AsmBW(0x%X, 0x%X)", pc, tgt) 461 | buf := patchlib.AsmBW(uint32(pc), uint32(tgt)) 462 | r.Find = buf 463 | log(" -> Find = %#v", buf) 464 | } 465 | 466 | if r.ReplaceInstBW != nil { 467 | log("ReplaceInstBW.Expand(%#v)", *r.ReplaceInstBW) 468 | 469 | log(" ReplaceInstBW.Resolve(%#v)", *r.ReplaceInstBW) 470 | tgt, err := r.ReplaceInstBW.Resolve(pt) 471 | if err != nil { 472 | err = fmt.Errorf("ReplaceBytes: expand ReplaceInstB=%#v: %v", *r.ReplaceInstBW, err) 473 | log(" -> Error: %v", err) 474 | return err 475 | } 476 | log(" -> Target: 0x%X", tgt) 477 | 478 | pc := cur + r.Offset 479 | log(" AsmBW(0x%X, 0x%X)", pc, tgt) 480 | buf := patchlib.AsmBW(uint32(pc), uint32(tgt)) 481 | r.Replace = buf 482 | log(" -> Replace = %#v", buf) 483 | } 484 | 485 | if r.ReplaceInstNOP != nil { 486 | if !*r.ReplaceInstNOP { 487 | return fmt.Errorf("ReplaceBytes: ReplaceInstNOP must either be true or unspecified") 488 | } 489 | // note: must be after all Find expansions, as it depends on checking the length 490 | log("ReplaceInstNOP.Expand(%#v)", *r.ReplaceInstNOP) 491 | if len(r.Find)%2 != 0 { 492 | return fmt.Errorf("ReplaceBytes: find not a multiple of 2 (len=%d)", len(r.Find)) 493 | } 494 | buf := make([]byte, len(r.Find)) 495 | for i := 0; i < len(buf); i += 2 { 496 | buf[i], buf[i+1] = 0x00, 0x46 497 | } 498 | r.Replace = buf 499 | log(" -> Replace = %#v", buf) 500 | } 501 | 502 | if pt.GetCur() != cur { 503 | panic("why is cur different?") 504 | } 505 | 506 | if r.CheckOnly != nil && *r.CheckOnly { 507 | if len(r.Replace) != 0 { 508 | return fmt.Errorf("ReplaceBytes: CheckOnly is true, but Replace is not empty") 509 | } 510 | log("CheckBytes(%#v, %#v)", r.Offset, r.Find) 511 | log(" ReplaceBytes(%#v, %#v, %#v) [cur:0x%X + off:%d -> abs:0x%X]", r.Offset, r.Find, r.Find, cur, r.Offset, r.Offset+cur) 512 | return pt.ReplaceBytes(r.Offset, r.Find, r.Find) 513 | } 514 | 515 | if r.FindInstBLX != nil && r.ReplaceInstBLX != nil { 516 | if (r.FindInstBLX.SymPLT != nil) != (r.ReplaceInstBLX.SymPLT != nil) { 517 | return fmt.Errorf("ReplaceBytes: for safety, you cannot replace a BLX to a PLT entry with one to a non-PLT entry or vice-versa (to suppress this warning, split the ReplaceBytes into two steps using placeholder bytes)") 518 | } 519 | } 520 | 521 | if r.FindInstBW != nil && r.ReplaceInstBW != nil { 522 | if (r.FindInstBW.SymPLTTail != nil) != (r.ReplaceInstBW.SymPLTTail != nil) { 523 | return fmt.Errorf("ReplaceBytes: for safety, you cannot replace a B.W to a PLT entry's tail stub with one to a non-PLT entry's tail stub or vice-versa (to suppress this warning, split the ReplaceBytes into two steps using placeholder bytes)") 524 | } 525 | } 526 | 527 | log("ReplaceBytes(%#v, %#v, %#v) [cur:0x%X + off:%d -> abs:0x%X]", r.Offset, r.Find, r.Replace, cur, r.Offset, r.Offset+cur) 528 | return pt.ReplaceBytes(r.Offset, r.Find, r.Replace) 529 | } 530 | 531 | func (r ReplaceZlib) ApplyTo(pt *patchlib.Patcher, log func(string, ...interface{})) error { 532 | log("ReplaceZlib(%#v, %#v, %#v)", r.Offset, r.Find, r.Replace) 533 | return pt.ReplaceZlib(r.Offset, r.Find, r.Replace) 534 | } 535 | 536 | func (r ReplaceZlibGroup) ApplyTo(pt *patchlib.Patcher, log func(string, ...interface{})) error { 537 | log("ReplaceZlibGroup(%#v, %#v)", r.Offset, r.Replacements) 538 | rs := []patchlib.Replacement{} 539 | for _, rr := range r.Replacements { 540 | rs = append(rs, patchlib.Replacement{Find: rr.Find, Replace: rr.Replace}) 541 | } 542 | return pt.ReplaceZlibGroup(r.Offset, rs) 543 | } 544 | 545 | func expandHex(in *string, out *[]byte) (bool, error) { 546 | if in == nil { 547 | return false, nil 548 | } 549 | buf, err := hex.DecodeString(strings.ReplaceAll(*in, " ", "")) 550 | if err != nil { 551 | return true, fmt.Errorf("error expanding shorthand hex `%s`: %w", *in, err) 552 | } 553 | *out = buf 554 | return true, nil 555 | } 556 | 557 | // Deprecated 558 | func (r ReplaceBytesAtSymbol) ApplyTo(pt *patchlib.Patcher, log func(string, ...interface{})) error { 559 | if replaced, err := expandHex(r.FindH, &r.Find); err != nil { 560 | log("ReplaceBytesAtSymbol.FindH -> %v", err) 561 | return err 562 | } else if replaced { 563 | log("ReplaceBytesAtSymbol.FindH -> Expand <%s> to set ReplaceBytesAtSymbol.Find to <%x>", *r.FindH, r.Find) 564 | } 565 | if replaced, err := expandHex(r.ReplaceH, &r.Replace); err != nil { 566 | log("ReplaceBytesAtSymbol.ReplaceH -> %v", err) 567 | return err 568 | } else if replaced { 569 | log("ReplaceBytesAtSymbol.ReplaceH -> Expand <%s> to set ReplaceBytesAtSymbol.Replace to <%x>", *r.ReplaceH, r.Replace) 570 | } 571 | 572 | log(" ReplaceBytesAtSymbol(%#v, %#v, %#v, %#v)", r.Symbol, r.Offset, r.Find, r.Replace) 573 | log(" FindBaseAddressSymbol(%#v) -> ", r.Symbol) 574 | if err := pt.FindBaseAddressSymbol(r.Symbol); err != nil { 575 | return fmt.Errorf("ReplaceBytesAtSymbol: %w", err) 576 | } 577 | log(" 0x%06x", pt.GetCur()) 578 | if r.FindBLX != nil { 579 | r.Find = patchlib.AsmBLX(uint32(pt.GetCur()+r.Offset), *r.FindBLX) 580 | log(" ReplaceBytesAtSymbol.FindBLX -> Set ReplaceBytesAtSymbol.Find to BLX(0x%X, 0x%X) -> %X", pt.GetCur()+r.Offset, *r.FindBLX, r.Find) 581 | } 582 | log(" ReplaceBytes(%#v, %#v, %#v)", r.Offset, r.Find, r.Replace) 583 | if err := pt.ReplaceBytes(r.Offset, r.Find, r.Replace); err != nil { 584 | return fmt.Errorf("ReplaceBytesAtSymbol: %w", err) 585 | } 586 | return nil 587 | } 588 | 589 | // Deprecated: Use BaseAddress instead. 590 | type FindBaseAddressSymbol string 591 | 592 | // Deprecated 593 | func (b FindBaseAddressSymbol) ApplyTo(pt *patchlib.Patcher, log func(string, ...interface{})) error { 594 | log("FindBaseAddressSymbol(%#v)", b) 595 | return pt.FindBaseAddressSymbol(string(b)) 596 | } 597 | 598 | // Deprecated: Use Base on ReplaceBytes instead. 599 | type ReplaceBytesAtSymbol struct { 600 | Symbol string `yaml:"Symbol,omitempty"` 601 | Offset int32 `yaml:"Offset,omitempty"` 602 | FindH *string `yaml:"FindH,omitempty"` 603 | ReplaceH *string `yaml:"ReplaceH,omitempty"` 604 | FindBLX *uint32 `yaml:"FindBLX,omitempty"` 605 | Find []byte `yaml:"Find,omitempty"` 606 | Replace []byte `yaml:"Replace,omitempty"` 607 | } 608 | 609 | // Deprecated: Use ReplaceInstNOP on ReplaceBytes instead. 610 | type ReplaceBytesNOP struct { 611 | Offset int32 `yaml:"Offset,omitempty"` 612 | FindH *string `yaml:"FindH,omitempty"` 613 | FindBLX *uint32 `yaml:"FindBLX,omitempty"` 614 | Find []byte `yaml:"Find,omitempty"` 615 | } 616 | 617 | // Deprecated: Use FindInstBLX and ReplaceInstBLX on ReplaceBytes instead. 618 | type ReplaceBLX struct { 619 | Offset int32 `yaml:"Offset,omitempty"` 620 | Find uint32 `yaml:"Find"` 621 | Replace uint32 `yaml:"Replace"` 622 | } 623 | 624 | // Deprecated 625 | func (r ReplaceBytesNOP) ApplyTo(pt *patchlib.Patcher, log func(string, ...interface{})) error { 626 | if replaced, err := expandHex(r.FindH, &r.Find); err != nil { 627 | log("ReplaceBytesNOP.FindH -> %v", err) 628 | return err 629 | } else if replaced { 630 | log("ReplaceBytesNOP.FindH -> Expand <%s> to set ReplaceBytesNOP.Find to <%x>", *r.FindH, r.Find) 631 | } 632 | if r.FindBLX != nil { 633 | r.Find = patchlib.AsmBLX(uint32(pt.GetCur()+r.Offset), *r.FindBLX) 634 | log("ReplaceBytesNOP.FindBLX -> Set ReplaceBytesNOP.Find to BLX(0x%X, 0x%X) -> %X", pt.GetCur()+r.Offset, *r.FindBLX, r.Find) 635 | } 636 | log("ReplaceBytesNOP(%#v, %#v)", r.Offset, r.Find) 637 | return pt.ReplaceBytesNOP(r.Offset, r.Find) 638 | } 639 | 640 | // Deprecated 641 | func (r ReplaceBLX) ApplyTo(pt *patchlib.Patcher, log func(string, ...interface{})) error { 642 | log("ReplaceBLX(%#v, %#v, %#v)", r.Offset, r.Find, r.Replace) 643 | return pt.ReplaceBLX(r.Offset, r.Find, r.Replace) 644 | } 645 | -------------------------------------------------------------------------------- /patchfile/kobopatch/patch_test.go: -------------------------------------------------------------------------------- 1 | package kobopatch 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "reflect" 9 | "testing" 10 | 11 | "gopkg.in/yaml.v3" 12 | ) 13 | 14 | func TestInstructionNodeToInstruction(t *testing.T) { 15 | tc := func(msg, y string, i *Instruction, equality bool, eerr error, remarshal bool) { 16 | var n InstructionNode 17 | if err := yaml.Unmarshal([]byte(y), &n); err != nil { 18 | panic(err) 19 | } 20 | t.Run(msg, func(t *testing.T) { 21 | ai, err := n.ToInstruction() 22 | if a, b := fmt.Sprint(eerr), fmt.Sprint(err); a != b { 23 | t.Errorf("unexpected error in ToInstruction: expected %#v, got %#v", a, b) 24 | } 25 | if eerr != nil { 26 | return 27 | } 28 | bufa, bufb := bytes.NewBuffer(nil), bytes.NewBuffer(nil) 29 | json.NewEncoder(bufa).Encode(*i) 30 | json.NewEncoder(bufb).Encode(ai) 31 | if equality != reflect.DeepEqual(bufa.String(), bufb.String()) { 32 | t.Errorf("wrong result of ToInstruction: expected %s, got %s", bufa.String(), bufb.String()) 33 | } 34 | if remarshal { 35 | t.Run("Marshal", func(t *testing.T) { 36 | buf, err := yaml.Marshal(ai) 37 | if err != nil { 38 | t.Errorf("unexpected error re-marshaling node: %v", err) 39 | } 40 | buf = bytes.TrimRight(buf, "\n") 41 | if !bytes.Equal(buf, []byte(y)) { 42 | t.Errorf("wrong re-marshaled bytes: expected orig %#v, got %#v", y, string(buf)) 43 | } 44 | }) 45 | } 46 | }) 47 | } 48 | tc("None", ``, nil, true, errors.New("expected instruction, got nothing"), false) 49 | tc("Unknown", `Unknown: true`, nil, true, errors.New("line 1: unknown instruction type \"Unknown\""), false) 50 | d := int32(1) 51 | a := BaseAddress(FlexAbsOffset{Offset: &d, Inline: true}) 52 | tc("TooMany", `{BaseAddress: 1, FindBaseAddressString: "test"}`, &Instruction{BaseAddress: &a}, false, errors.New("line 1: multiple types found in instruction, maybe you forgot a '-'"), false) 53 | tc("ValueEqual", `BaseAddress: 1`, &Instruction{BaseAddress: &a}, true, nil, false) 54 | tc("ValueNotEqual", `BaseAddress: 0`, &Instruction{}, false, nil, false) 55 | tc("StructEqual", `FindReplaceString: {Find: "test", Replace: "test"}`, &Instruction{FindReplaceString: &FindReplaceString{Find: "test", Replace: "test"}}, true, nil, false) 56 | tc("StructNotEqual", `FindReplaceString: {Find: "test", Replace: "test"}`, &Instruction{FindReplaceString: &FindReplaceString{Find: "test", Replace: ""}}, false, nil, false) 57 | tc("StructExtra", `FindReplaceString: {Find: "test", Replace: "test", Extra: "asd"}`, &Instruction{FindReplaceString: &FindReplaceString{Find: "test", Replace: "text"}}, false, errors.New("line 1: error decoding instruction: yaml: unmarshal errors:\n line 1: field Extra not found in type kobopatch.FindReplaceString"), false) 58 | 59 | e := "Test" 60 | tc("FlexAbsOffset/Inline/BaseAddress", `BaseAddress: 1`, &Instruction{BaseAddress: &BaseAddress{Offset: &d, Inline: true}}, true, nil, true) 61 | tc("FlexAbsOffset/Offset/BaseAddress", `BaseAddress: {Offset: 1}`, &Instruction{BaseAddress: &BaseAddress{Offset: &d}}, true, nil, true) 62 | tc("FlexAbsOffset/Sym/BaseAddress", `BaseAddress: {Sym: Test}`, &Instruction{BaseAddress: &BaseAddress{Sym: &e}}, true, nil, true) 63 | tc("FlexAbsOffset/SymPLT/BaseAddress", `BaseAddress: {SymPLT: Test}`, &Instruction{BaseAddress: &BaseAddress{SymPLT: &e}}, true, nil, true) 64 | tc("FlexAbsOffset/SymPLTTail/BaseAddress", `BaseAddress: {SymPLTTail: Test}`, &Instruction{BaseAddress: &BaseAddress{SymPLTTail: &e}}, true, nil, true) 65 | tc("FlexAbsOffset/Inline/ReplaceBytesBase", `ReplaceBytes: {Base: 1}`, &Instruction{ReplaceBytes: &ReplaceBytes{Base: &FlexAbsOffset{Offset: &d, Inline: true}}}, true, nil, false) 66 | tc("FlexAbsOffset/Offset/ReplaceBytesBase", `ReplaceBytes: {Base: {Offset: 1}}`, &Instruction{ReplaceBytes: &ReplaceBytes{Base: &FlexAbsOffset{Offset: &d}}}, true, nil, false) 67 | tc("FlexAbsOffset/Sym/ReplaceBytesBase", `ReplaceBytes: {Base: {Sym: Test}}`, &Instruction{ReplaceBytes: &ReplaceBytes{Base: &FlexAbsOffset{Sym: &e}}}, true, nil, false) 68 | tc("FlexAbsOffset/SymPLT/ReplaceBytesBase", `ReplaceBytes: {Base: {SymPLT: Test}}`, &Instruction{ReplaceBytes: &ReplaceBytes{Base: &FlexAbsOffset{SymPLT: &e}}}, true, nil, false) 69 | tc("FlexAbsOffset/SymPLTTail/ReplaceBytesBase", `ReplaceBytes: {Base: {SymPLTTail: Test}}`, &Instruction{ReplaceBytes: &ReplaceBytes{Base: &FlexAbsOffset{SymPLTTail: &e}}}, true, nil, false) 70 | // TODO: more FlexAbsOffset tests? 71 | } 72 | -------------------------------------------------------------------------------- /patchfile/patch32lsb/patch32lsb.go: -------------------------------------------------------------------------------- 1 | // Package patch32lsb reads patch32lsb style patches. 2 | package patch32lsb 3 | 4 | import ( 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/pgaskin/kobopatch/patchfile" 12 | "github.com/pgaskin/kobopatch/patchlib" 13 | ) 14 | 15 | // PatchSet represents a series of patches. 16 | type PatchSet map[string]patch 17 | 18 | type patch []instruction 19 | type instruction struct { 20 | Enabled *bool 21 | PatchGroup *string 22 | BaseAddress *int32 23 | Comment *string 24 | FindBaseAddress *string 25 | FindZlib *string 26 | FindZlibHash *string 27 | ReplaceBytes *struct { 28 | Offset int32 29 | Find []byte 30 | Replace []byte 31 | } 32 | ReplaceFloat *struct { 33 | Offset int32 34 | Find float64 35 | Replace float64 36 | } 37 | ReplaceInt *struct { 38 | Offset int32 39 | Find uint8 40 | Replace uint8 41 | } 42 | ReplaceString *struct { 43 | Offset int32 44 | Find string 45 | Replace string 46 | } 47 | ReplaceZlib *struct { 48 | Offset int32 49 | Find string 50 | Replace string 51 | } 52 | FindReplaceString *struct { 53 | Find string 54 | Replace string 55 | } 56 | } 57 | 58 | // Parse parses a PatchSet from a buf. 59 | func Parse(buf []byte) (patchfile.PatchSet, error) { 60 | // TODO: make less hacky, make cleaner, add logs 61 | 62 | ps := PatchSet{} 63 | var patchName string 64 | var inPatch bool 65 | curPatch := patch{} 66 | eqRegexp := regexp.MustCompile(" +?= +?") 67 | for i, l := range strings.Split(strings.ReplaceAll(string(buf), "\r\n", "\n"), "\n") { 68 | l = strings.TrimSpace(l) 69 | switch { 70 | case strings.HasPrefix(l, "#"), l == "": 71 | c := strings.TrimLeft(l, "# ") 72 | if strings.HasPrefix(c, "patch_group") { 73 | return nil, fmt.Errorf("line %d: patch_group should not be a comment", i+1) 74 | } 75 | curPatch = append(curPatch, instruction{Comment: &c}) 76 | break 77 | case strings.ToLower(l) == "": 78 | if inPatch { 79 | return nil, fmt.Errorf("line %d: unexpected (already in patch)", i+1) 80 | } 81 | curPatch = patch{} 82 | inPatch = true 83 | break 84 | case strings.ToLower(l) == "": 85 | if !inPatch { 86 | return nil, fmt.Errorf("line %d: unexpected (not in patch)", i+1) 87 | } 88 | if patchName == "" { 89 | return nil, fmt.Errorf("line %d: no patch_name for patch", i+1) 90 | } 91 | if _, ok := ps[patchName]; ok { 92 | return nil, fmt.Errorf("line %d: duplicate patch with name '%s'", i+1, patchName) 93 | } 94 | ps[patchName] = curPatch[:] 95 | inPatch = false 96 | break 97 | case !eqRegexp.MatchString(l): 98 | return nil, fmt.Errorf("line %d: bad instruction: no equals sign", i+1) 99 | case eqRegexp.MatchString(l): 100 | spl := eqRegexp.Split(l, 2) 101 | switch strings.ToLower(spl[0]) { 102 | case "patch_name": 103 | var err error 104 | patchName, err = unescape(spl[1]) 105 | if err != nil { 106 | return nil, fmt.Errorf("line %d: error unescaping patch_name: %w", i+1, err) 107 | } 108 | case "patch_group": 109 | g, err := unescape(spl[1]) 110 | if err != nil { 111 | return nil, fmt.Errorf("line %d: error unescaping patch_group: %w", i+1, err) 112 | } 113 | curPatch = append(curPatch, instruction{PatchGroup: &g}) 114 | case "patch_enable": 115 | if patchName == "" { 116 | return nil, fmt.Errorf("line %d: patch_enable set before patch_name", i+1) 117 | } 118 | switch spl[1] { 119 | case "`yes`": 120 | e := true 121 | curPatch = append(curPatch, instruction{Enabled: &e}) 122 | case "`no`": 123 | e := false 124 | curPatch = append(curPatch, instruction{Enabled: &e}) 125 | default: 126 | return nil, fmt.Errorf("line %d: unexpected patch_enable value '%s' (should be yes or no)", i+1, spl[1]) 127 | } 128 | case "replace_bytes": 129 | args := strings.ReplaceAll(spl[1], " ", "") 130 | var offset int32 131 | var find, replace []byte 132 | _, err := fmt.Sscanf(args, "%x,%x,%x", &offset, &find, &replace) 133 | if err != nil { 134 | return nil, fmt.Errorf("line %d: replace_bytes malformed: %w", i+1, err) 135 | } 136 | curPatch = append(curPatch, instruction{ReplaceBytes: &struct { 137 | Offset int32 138 | Find []byte 139 | Replace []byte 140 | }{Offset: offset, Find: find, Replace: replace}}) 141 | case "base_address": 142 | var addr int32 143 | _, err := fmt.Sscanf(spl[1], "%x", &addr) 144 | if err != nil { 145 | return nil, fmt.Errorf("line %d: base_address malformed: %w", i+1, err) 146 | } 147 | curPatch = append(curPatch, instruction{BaseAddress: &addr}) 148 | case "replace_float": 149 | args := strings.ReplaceAll(spl[1], " ", "") 150 | var offset int32 151 | var find, replace float64 152 | _, err := fmt.Sscanf(args, "%x,%f,%f", &offset, &find, &replace) 153 | if err != nil { 154 | return nil, fmt.Errorf("line %d: replace_float malformed: %w", i+1, err) 155 | } 156 | curPatch = append(curPatch, instruction{ReplaceFloat: &struct { 157 | Offset int32 158 | Find float64 159 | Replace float64 160 | }{Offset: offset, Find: find, Replace: replace}}) 161 | case "replace_int": 162 | args := strings.ReplaceAll(spl[1], " ", "") 163 | var offset int32 164 | var find, replace uint8 165 | _, err := fmt.Sscanf(args, "%x,%d,%d", &offset, &find, &replace) 166 | if err != nil { 167 | return nil, fmt.Errorf("line %d: replace_int malformed: %w", i+1, err) 168 | } 169 | curPatch = append(curPatch, instruction{ReplaceInt: &struct { 170 | Offset int32 171 | Find uint8 172 | Replace uint8 173 | }{Offset: offset, Find: find, Replace: replace}}) 174 | case "find_base_address": 175 | str, err := unescape(spl[1]) 176 | if err != nil { 177 | return nil, fmt.Errorf("line %d: find_base_address malformed: %w", i+1, err) 178 | } 179 | curPatch = append(curPatch, instruction{FindBaseAddress: &str}) 180 | case "replace_string": 181 | ab := strings.SplitN(spl[1], ", ", 2) 182 | if len(ab) != 2 { 183 | return nil, fmt.Errorf("line %d: replace_string malformed", i+1) 184 | } 185 | var offset int32 186 | if len(ab[0]) == 8 { 187 | // ugly hack to fix negative offsets 188 | ab[0] = strings.Replace(ab[0], "FFFFFF", "-", 1) 189 | } 190 | _, err := fmt.Sscanf(ab[0], "%x", &offset) 191 | if err != nil { 192 | return nil, fmt.Errorf("line %d: replace_string offset malformed: %w", i+1, err) 193 | } 194 | var find, replace, leftover string 195 | leftover = ab[1] 196 | find, leftover, err = unescapeFirst(leftover) 197 | if err != nil { 198 | return nil, fmt.Errorf("line %d: replace_string find malformed: %w", i+1, err) 199 | } 200 | leftover = strings.TrimLeft(leftover, ", ") 201 | replace, leftover, err = unescapeFirst(leftover) 202 | if err != nil { 203 | return nil, fmt.Errorf("line %d: replace_string replace malformed: %w", i+1, err) 204 | } 205 | if leftover != "" { 206 | return nil, fmt.Errorf("line %d: replace_string malformed: extraneous characters after last argument", i+1) 207 | } 208 | curPatch = append(curPatch, instruction{ReplaceString: &struct { 209 | Offset int32 210 | Find string 211 | Replace string 212 | }{Offset: offset, Find: find, Replace: replace}}) 213 | case "find_zlib": 214 | str, err := unescape(spl[1]) 215 | if err != nil { 216 | return nil, fmt.Errorf("line %d: find_zlib malformed: %w", i+1, err) 217 | } 218 | curPatch = append(curPatch, instruction{FindZlib: &str}) 219 | case "find_zlib_hash": 220 | str, err := unescape(spl[1]) 221 | if err != nil { 222 | return nil, fmt.Errorf("line %d: find_zlib_hash malformed: %w", i+1, err) 223 | } 224 | curPatch = append(curPatch, instruction{FindZlibHash: &str}) 225 | case "replace_zlib": 226 | ab := strings.SplitN(spl[1], ", ", 2) 227 | if len(ab) != 2 { 228 | return nil, fmt.Errorf("line %d: replace_zlib malformed", i+1) 229 | } 230 | var offset int32 231 | if len(ab[0]) == 8 { 232 | // ugly hack to fix negative offsets 233 | ab[0] = strings.Replace(ab[0], "FFFFFF", "-", 1) 234 | } 235 | _, err := fmt.Sscanf(ab[0], "%x", &offset) 236 | if err != nil { 237 | return nil, fmt.Errorf("line %d: replace_zlib offset malformed: %w", i+1, err) 238 | } 239 | var find, replace, leftover string 240 | leftover = ab[1] 241 | find, leftover, err = unescapeFirst(leftover) 242 | if err != nil { 243 | return nil, fmt.Errorf("line %d: replace_zlib find malformed: %w", i+1, err) 244 | } 245 | leftover = strings.TrimLeft(leftover, ", ") 246 | replace, leftover, err = unescapeFirst(leftover) 247 | if err != nil { 248 | return nil, fmt.Errorf("line %d: replace_zlib replace malformed: %w", i+1, err) 249 | } 250 | if leftover != "" { 251 | return nil, fmt.Errorf("line %d: replace_zlib malformed: extraneous characters after last argument", i+1) 252 | } 253 | curPatch = append(curPatch, instruction{ReplaceZlib: &struct { 254 | Offset int32 255 | Find string 256 | Replace string 257 | }{Offset: offset, Find: find, Replace: replace}}) 258 | case "find_replace_string": 259 | var find, replace, leftover string 260 | leftover = spl[1] 261 | find, leftover, err := unescapeFirst(leftover) 262 | if err != nil { 263 | return nil, fmt.Errorf("line %d: find_replace_string find malformed: %w", i+1, err) 264 | } 265 | leftover = strings.TrimLeft(leftover, ", ") 266 | replace, leftover, err = unescapeFirst(leftover) 267 | if err != nil { 268 | return nil, fmt.Errorf("line %d: find_replace_string replace malformed: %w", i+1, err) 269 | } 270 | if leftover != "" { 271 | return nil, fmt.Errorf("line %d: find_replace_string malformed: extraneous characters after last argument", i+1) 272 | } 273 | curPatch = append(curPatch, instruction{FindReplaceString: &struct { 274 | Find string 275 | Replace string 276 | }{Find: find, Replace: replace}}) 277 | default: 278 | return nil, fmt.Errorf("line %d: unexpected instruction: %s", i+1, spl[0]) 279 | } 280 | default: 281 | return nil, fmt.Errorf("line %d: unexpected statement: %s", i+1, l) 282 | } 283 | } 284 | return &ps, nil 285 | } 286 | 287 | // Validate validates the PatchSet. 288 | func (ps *PatchSet) Validate() error { 289 | enabledPatchGroups := map[string]bool{} 290 | for n, p := range *ps { 291 | pgc := 0 292 | ec := 0 293 | e := false 294 | pg := "" 295 | 296 | for _, i := range p { 297 | ic := 0 298 | if i.Enabled != nil { 299 | ec++ 300 | e = *i.Enabled 301 | ic++ 302 | } 303 | if i.PatchGroup != nil { 304 | pgc++ 305 | pg = *i.PatchGroup 306 | ic++ 307 | } 308 | if i.BaseAddress != nil { 309 | ic++ 310 | } 311 | if i.Comment != nil { 312 | ic++ 313 | } 314 | if i.FindBaseAddress != nil { 315 | ic++ 316 | } 317 | if i.ReplaceBytes != nil { 318 | ic++ 319 | } 320 | if i.ReplaceFloat != nil { 321 | ic++ 322 | } 323 | if i.ReplaceInt != nil { 324 | ic++ 325 | } 326 | if i.ReplaceString != nil { 327 | ic++ 328 | } 329 | if i.FindZlib != nil { 330 | ic++ 331 | } 332 | if i.FindZlibHash != nil { 333 | ic++ 334 | if len(*i.FindZlibHash) != 40 { 335 | return fmt.Errorf("hash must be 40 chars in FindZlibHash in `%s`", n) 336 | } 337 | } 338 | if i.ReplaceZlib != nil { 339 | ic++ 340 | } 341 | if i.FindReplaceString != nil { 342 | ic++ 343 | } 344 | if ic != 1 { 345 | return fmt.Errorf("internal error (you should report this): ic > 1, '%#v'", i) 346 | } 347 | } 348 | if ec != 1 { 349 | return fmt.Errorf("you must have exactly 1 patch_enable option in each patch (%s)", n) 350 | } 351 | if pgc > 1 { 352 | return fmt.Errorf("you must have at most 1 patch_group option in each patch (%s)", n) 353 | } 354 | if pg != "" && e { 355 | if _, ok := enabledPatchGroups[pg]; ok { 356 | return fmt.Errorf("more than one patch enabled in patch_group '%s'", pg) 357 | } 358 | enabledPatchGroups[pg] = true 359 | } 360 | } 361 | return nil 362 | } 363 | 364 | // ApplyTo applies a PatchSet to a Patcher. 365 | func (ps *PatchSet) ApplyTo(pt *patchlib.Patcher) error { 366 | patchfile.Log("validating patch file\n") 367 | err := ps.Validate() 368 | if err != nil { 369 | err = fmt.Errorf("invalid patch file: %w", err) 370 | fmt.Printf(" Error: %v\n", err) 371 | return err 372 | } 373 | 374 | patchfile.Log("looping over patches\n") 375 | num, total := 0, len(*ps) 376 | for n, p := range *ps { 377 | var err error 378 | num++ 379 | patchfile.Log(" ResetBaseAddress()\n") 380 | pt.ResetBaseAddress() 381 | 382 | enabled := false 383 | for _, i := range p { 384 | if i.Enabled != nil && *i.Enabled { 385 | enabled = *i.Enabled 386 | break 387 | } 388 | } 389 | patchfile.Log(" Enabled: %t\n", enabled) 390 | 391 | if !enabled { 392 | patchfile.Log(" skipping patch `%s`\n", n) 393 | fmt.Printf(" [%d/%d] Skipping disabled patch `%s`\n", num, total, n) 394 | continue 395 | } 396 | 397 | patchfile.Log(" applying patch `%s`\n", n) 398 | fmt.Printf(" [%d/%d] Applying patch `%s`\n", num, total, n) 399 | 400 | patchfile.Log("looping over instructions\n") 401 | for _, i := range p { 402 | switch { 403 | case i.Enabled != nil || i.PatchGroup != nil || i.Comment != nil: 404 | patchfile.Log(" skipping non-instruction Enabled(), PatchGroup() or Comment()\n") 405 | // Skip non-instructions 406 | err = nil 407 | case i.BaseAddress != nil: 408 | patchfile.Log(" BaseAddress(%#v)\n", *i.BaseAddress) 409 | err = pt.BaseAddress(*i.BaseAddress) 410 | case i.FindBaseAddress != nil: 411 | patchfile.Log(" FindBaseAddressString(%#v) | hex:%x\n", *i.FindBaseAddress, []byte(*i.FindBaseAddress)) 412 | err = pt.FindBaseAddressString(*i.FindBaseAddress) 413 | case i.ReplaceBytes != nil: 414 | r := *i.ReplaceBytes 415 | patchfile.Log(" ReplaceBytes(%#v, %#v, %#v)\n", r.Offset, r.Find, r.Replace) 416 | err = pt.ReplaceBytes(r.Offset, r.Find, r.Replace) 417 | case i.ReplaceFloat != nil: 418 | r := *i.ReplaceFloat 419 | patchfile.Log(" ReplaceFloat(%#v, %#v, %#v)\n", r.Offset, r.Find, r.Replace) 420 | err = pt.ReplaceFloat(r.Offset, r.Find, r.Replace) 421 | case i.ReplaceInt != nil: 422 | r := *i.ReplaceInt 423 | patchfile.Log(" ReplaceInt(%#v, %#v, %#v)\n", r.Offset, r.Find, r.Replace) 424 | err = pt.ReplaceInt(r.Offset, r.Find, r.Replace) 425 | case i.ReplaceString != nil: 426 | r := *i.ReplaceString 427 | patchfile.Log(" ReplaceString(%#v, %#v, %#v)\n", r.Offset, r.Find, r.Replace) 428 | err = pt.ReplaceString(r.Offset, r.Find, r.Replace) 429 | case i.FindZlib != nil: 430 | patchfile.Log(" FindZlib(%#v) | hex:%x\n", *i.FindZlib, []byte(*i.FindZlib)) 431 | err = pt.FindZlib(*i.FindZlib) 432 | case i.FindZlibHash != nil: 433 | patchfile.Log(" FindZlibHash(%#v) | hex:%x\n", *i.FindZlibHash, []byte(*i.FindZlibHash)) 434 | err = pt.FindZlibHash(*i.FindZlibHash) 435 | case i.ReplaceZlib != nil: 436 | r := *i.ReplaceZlib 437 | patchfile.Log(" ReplaceZlib(%#v, %#v, %#v)\n", r.Offset, r.Find, r.Replace) 438 | err = pt.ReplaceZlib(r.Offset, r.Find, r.Replace) 439 | case i.FindReplaceString != nil: 440 | r := *i.FindReplaceString 441 | patchfile.Log(" FindReplaceString(%#v, %#v)\n", r.Find, r.Replace) 442 | patchfile.Log(" FindBaseAddressString(%#v)\n", r.Find) 443 | err = pt.FindBaseAddressString(r.Find) 444 | if err != nil { 445 | err = fmt.Errorf("FindReplaceString: %w", err) 446 | break 447 | } 448 | patchfile.Log(" ReplaceString(0, %#v, %#v)\n", r.Find, r.Replace) 449 | err = pt.ReplaceString(0, r.Find, r.Replace) 450 | if err != nil { 451 | err = fmt.Errorf("FindReplaceString: %w", err) 452 | break 453 | } 454 | default: 455 | patchfile.Log(" invalid instruction: %#v\n", i) 456 | err = fmt.Errorf("invalid instruction: %#v", i) 457 | } 458 | 459 | if err != nil { 460 | patchfile.Log("could not apply patch: %v\n", err) 461 | fmt.Printf(" Error: could not apply patch: %v\n", err) 462 | return err 463 | } 464 | } 465 | } 466 | 467 | return nil 468 | } 469 | 470 | // SetEnabled sets the Enabled state of a Patch in a PatchSet. 471 | func (ps *PatchSet) SetEnabled(patch string, enabled bool) error { 472 | for n := range *ps { 473 | if n != patch { 474 | continue 475 | } 476 | for i := range (*ps)[n] { 477 | if (*ps)[n][i].Enabled != nil { 478 | *(*ps)[n][i].Enabled = enabled 479 | return nil 480 | } 481 | } 482 | return fmt.Errorf("could not set enabled state of '%s' to %t: no Enabled instruction in patch", patch, enabled) 483 | } 484 | if enabled { 485 | return fmt.Errorf("could not set enabled state of '%s' to %t: no such patch", patch, enabled) 486 | } 487 | return nil 488 | } 489 | 490 | func unescape(str string) (string, error) { 491 | if !(strings.HasPrefix(str, "`") && strings.HasSuffix(str, "`")) || (string(str[len(str)-2]) == `\` && string(str[len(str)-3]) != `\`) { 492 | return str, errors.New("string not wrapped in backticks") 493 | } 494 | str = str[1 : len(str)-1] 495 | 496 | var buf bytes.Buffer 497 | for { 498 | if len(str) == 0 { 499 | break 500 | } 501 | switch str[0] { 502 | case '\\': 503 | switch str[1] { 504 | case 'n': 505 | buf.Write([]byte("\n")) 506 | str = str[2:] 507 | case 'r': 508 | buf.Write([]byte("\r")) 509 | str = str[2:] 510 | case 't': 511 | buf.Write([]byte("\t")) 512 | str = str[2:] 513 | case 'v': 514 | buf.Write([]byte("\v")) 515 | str = str[2:] 516 | case '"': 517 | buf.Write([]byte("\"")) 518 | str = str[2:] 519 | case '\'': 520 | buf.Write([]byte("'")) 521 | str = str[2:] 522 | case '`': 523 | buf.Write([]byte("`")) 524 | str = str[2:] 525 | case '0': 526 | buf.Write([]byte("\x00")) 527 | str = str[2:] 528 | case '\\': 529 | buf.Write([]byte("\\")) 530 | str = str[2:] 531 | case 'x': 532 | var b []byte 533 | _, err := fmt.Sscanf(str[2:4], "%x", &b) 534 | if err != nil { 535 | return "", err 536 | } 537 | buf.Write(b) 538 | str = str[4:] 539 | default: 540 | return "", errors.New("unknown escape " + string(str[1])) 541 | } 542 | default: 543 | buf.Write([]byte{str[0]}) 544 | str = str[1:] 545 | } 546 | } 547 | return string(buf.Bytes()), nil 548 | } 549 | 550 | func unescapeFirst(str string) (string, string, error) { 551 | // TODO: make more efficient 552 | for i := 2; i <= len(str); i++ { 553 | nstr, err := unescape(str[:i]) 554 | if err == nil { 555 | return nstr, str[i:], nil 556 | } 557 | } 558 | return "", "", errors.New("could not find valid string") 559 | } 560 | 561 | func init() { 562 | patchfile.RegisterFormat("patch32lsb", Parse) 563 | } 564 | -------------------------------------------------------------------------------- /patchfile/patch32lsb/patch32lsb_test.go: -------------------------------------------------------------------------------- 1 | package patch32lsb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestUnescape(t *testing.T) { 10 | for _, c := range [][]string{ 11 | {`test`, "test"}, 12 | {`test\0`, "test\x00"}, 13 | {`test\0\xcc`, "test\x00\xcc"}, 14 | {`test\0\n\t\v\r\xcc`, "test\x00\n\t\v\r\xcc"}, 15 | {`test\0\n\t\v\r\xcc\"\'\` + "`", "test\x00\n\t\v\r\xcc\"'`"}, 16 | {`ÉÀÇ`, "ÉÀÇ"}, 17 | } { 18 | u, err := unescape("`" + c[0] + "`") 19 | assert.NoError(t, err) 20 | assert.Equal(t, c[1], u) 21 | } 22 | } 23 | 24 | func TestUnescapeFirst(t *testing.T) { 25 | for _, c := range [][]string{ 26 | {`test`, "test"}, 27 | {`test\0`, "test\x00"}, 28 | {`test\0"\xcc`, "test\x00\"\xcc"}, 29 | {`test\0\n\t\v\r\xcc`, "test\x00\n\t\v\r\xcc"}, 30 | {`test\0\n\t\v\r\xcc\"\'\` + "`", "test\x00\n\t\v\r\xcc\"'`"}, 31 | } { 32 | u, r, err := unescapeFirst("`" + c[0] + "`" + "dfgdfg dfgdfgd fgdf dfg `dfg`") 33 | assert.NoError(t, err) 34 | assert.Equal(t, c[1], u) 35 | assert.Equal(t, "dfgdfg dfgdfgd fgdf dfg `dfg`", r) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /patchfile/patchfile.go: -------------------------------------------------------------------------------- 1 | // Package patchfile provides a standard interface to read patchsets from files. 2 | package patchfile 3 | 4 | import ( 5 | "fmt" 6 | "io/ioutil" 7 | 8 | "github.com/pgaskin/kobopatch/patchlib" 9 | ) 10 | 11 | // Log is used to log debugging messages. 12 | var Log = func(format string, a ...interface{}) {} 13 | 14 | // PatchSet represents a set of patches which can be applied to a Patcher. 15 | type PatchSet interface { 16 | // Validate validates the PatchSet. 17 | Validate() error 18 | // ApplyTo applies a PatchSet to a Patcher. 19 | ApplyTo(*patchlib.Patcher) error 20 | // SetEnabled sets the Enabled state of a Patch in a PatchSet. 21 | SetEnabled(string, bool) error 22 | } 23 | 24 | var formats = map[string]func([]byte) (PatchSet, error){} 25 | 26 | // RegisterFormat registers a format. 27 | func RegisterFormat(name string, f func([]byte) (PatchSet, error)) { 28 | if _, ok := formats[name]; ok { 29 | panic("attempt to register duplicate format " + name) 30 | } 31 | formats[name] = f 32 | } 33 | 34 | // GetFormat gets a format. 35 | func GetFormat(name string) (func([]byte) (PatchSet, error), bool) { 36 | f, ok := formats[name] 37 | return f, ok 38 | } 39 | 40 | // GetFormats gets all registered formats. 41 | func GetFormats() []string { 42 | f := []string{} 43 | for n := range formats { 44 | f = append(f, n) 45 | } 46 | return f 47 | } 48 | 49 | // ReadFromFile reads a patchset from a file (but does not validate it). 50 | func ReadFromFile(format, filename string) (PatchSet, error) { 51 | f, ok := GetFormat(format) 52 | if !ok { 53 | return nil, fmt.Errorf("no format called '%s'", format) 54 | } 55 | 56 | buf, err := ioutil.ReadFile(filename) 57 | if err != nil { 58 | return nil, fmt.Errorf("could not open patch file: %w", err) 59 | } 60 | 61 | ps, err := f(buf) 62 | if err != nil { 63 | return nil, fmt.Errorf("could not parse patch file: %w", err) 64 | } 65 | 66 | return ps, nil 67 | } 68 | -------------------------------------------------------------------------------- /patchlib/asm.go: -------------------------------------------------------------------------------- 1 | package patchlib 2 | 3 | import "encoding/binary" 4 | 5 | // note: this is the 32-bit thumb-2 instruction encoding, not thumb-1 6 | // see the thumb-2 reference manual, section 4.6.18 7 | 8 | // AsmBW assembles a B.W instruction and returns a byte slice which can be patched 9 | // directly into a binary. 10 | func AsmBW(pc, target uint32) []byte { 11 | return mustBytes(toBEBin(bw(pc, target))) 12 | } 13 | 14 | // AsmBLX assembles a BLX instruction and returns a byte slice which can be patched 15 | // directly into a binary. 16 | func AsmBLX(pc, target uint32) []byte { 17 | return mustBytes(toBEBin(blx(pc, target))) 18 | } 19 | 20 | // Thumb-2 reference manual, 4.6.12 21 | // B.W (encoding T4) (no cond) (thumb to thumb) 22 | // 1 1 1 1 0 s imm10 1 0 J1 1 J2 imm11 23 | // 24 | // I1 = NOT(J1 EOR S) thus... J1 = NOT(I1) EOR S 25 | // I2 = NOT(J2 EOR S) thus... J2 = NOT(I2) EOR S 26 | // imm32 = SignExtend(S:I1:I2:imm10:imm11:'0', 32) thus... S = SIGN(imm32) 27 | // I1 = imm32[24] 28 | // I2 = imm32[23] 29 | // imm10 = imm32[12:22] 30 | // imm11 = imm32[1:12] 31 | // 32 | // BranchWritePC(PC + imm32) thus... imm32 = target - PC - 4 33 | // 34 | // imm32 must be between -16777216 and 16777214 35 | func bw(PC, target uint32) uint32 { 36 | var imm32 int32 37 | var imm11, imm10, S, I2, I1, J2, J1 uint32 38 | imm32 = int32(target) - int32(PC) - 4 39 | imm11 = uint32(imm32>>1) & 0b11111111111 // imm32[1:12] 40 | imm10 = uint32(imm32>>12) & 0b1111111111 // imm32[12:22] 41 | I2 = bi((imm32>>22)&1 != 0) // imm32[22] 42 | I1 = bi((imm32>>23)&1 != 0) // imm32[23] 43 | S = bi(imm32 < 0) // SIGN(imm32) 44 | J2 = (^I2 ^ S) & 1 45 | J1 = (^I1 ^ S) & 1 46 | 47 | var inst uint32 48 | inst |= uint32(1) << 31 49 | inst |= uint32(1) << 30 50 | inst |= uint32(1) << 29 51 | inst |= uint32(1) << 28 52 | inst |= uint32(0) << 27 53 | inst |= uint32(S) << 26 54 | inst |= uint32(imm10) << 16 // 17 18 19 20 21 22 23 24 25 55 | inst |= uint32(1) << 15 56 | inst |= uint32(0) << 14 57 | inst |= uint32(J1) << 13 58 | inst |= uint32(1) << 12 59 | inst |= uint32(J2) << 11 60 | inst |= uint32(imm11) << 0 // 1 2 3 4 5 6 7 8 9 10 61 | 62 | lebuf := make([]byte, 4) 63 | lebuf[0] = uint8(inst >> 8 & 0xFF) 64 | lebuf[1] = uint8(inst >> 0 & 0xFF) 65 | lebuf[2] = uint8(inst >> 24 & 0xFF) 66 | lebuf[3] = uint8(inst >> 16 & 0xFF) 67 | 68 | return binary.LittleEndian.Uint32(lebuf) // le to sys endianness 69 | } 70 | 71 | // Thumb-2 reference manual, 4.6.18 72 | // BLX (encoding T2) (no cond) (thumb to arm) 73 | // 1 1 1 1 0 S imm10H 1 1 J1 0 J2 imm10L 0 74 | // 75 | // I1 = NOT(J1 EOR S) thus... J1 = NOT(I1) EOR S 76 | // I2 = NOT(J2 EOR S) thus... J2 = NOT(I2) EOR S 77 | // imm32 = SignExtend(S:I1:I2:imm10H:imm10L:'00', 32) thus... S = SIGN(imm32) 78 | // I1 = imm32[24] 79 | // I2 = imm32[23] 80 | // imm10H = imm32[12:22] 81 | // imm10L = imm32[2:12] 82 | // 83 | // next_instr_addr = PC n/a (for the return address) 84 | // LR = next_instr_addr<31:1>:'1' n/a (for the return address) 85 | // SelectInstrSet(InstrSet_ARM) n/a (for the target) 86 | // BranchWritePC(Align(PC, 4) + imm32) thus... imm32 = target - (pc & ~3) - 4 87 | // 88 | // imm32 must be multiples of 4 between -16777216 and 16777212 89 | func blx(PC, target uint32) uint32 { 90 | var imm32 int32 91 | var imm10L, imm10H, S, I2, I1, J2, J1 uint32 92 | imm32 = int32(target) - int32(PC&^3) - 4 93 | imm10L = uint32(imm32>>2) & 0b1111111111 // imm32[2:12] 94 | imm10H = uint32(imm32>>12) & 0b1111111111 // imm32[12:22] 95 | I2 = bi((imm32>>22)&1 != 0) // imm32[22] 96 | I1 = bi((imm32>>23)&1 != 0) // imm32[23] 97 | S = bi(imm32 < 0) // SIGN(imm32) 98 | J2 = (^I2 ^ S) & 1 99 | J1 = (^I1 ^ S) & 1 100 | 101 | var inst uint32 102 | inst |= uint32(1) << 31 103 | inst |= uint32(1) << 30 104 | inst |= uint32(1) << 29 105 | inst |= uint32(1) << 28 106 | inst |= uint32(0) << 27 107 | inst |= uint32(S) << 26 108 | inst |= uint32(imm10H) << 16 // 17 18 19 20 21 22 23 24 25 109 | inst |= uint32(1) << 15 110 | inst |= uint32(1) << 14 111 | inst |= uint32(J1) << 13 112 | inst |= uint32(0) << 12 113 | inst |= uint32(J2) << 11 114 | inst |= uint32(imm10L) << 1 // 2 3 4 5 6 7 8 9 10 115 | inst |= uint32(0) << 0 116 | 117 | lebuf := make([]byte, 4) 118 | lebuf[0] = uint8(inst >> 8 & 0xFF) 119 | lebuf[1] = uint8(inst >> 0 & 0xFF) 120 | lebuf[2] = uint8(inst >> 24 & 0xFF) 121 | lebuf[3] = uint8(inst >> 16 & 0xFF) 122 | 123 | return binary.LittleEndian.Uint32(lebuf) // le to sys endianness 124 | } 125 | 126 | func bi(b bool) uint32 { 127 | if b { 128 | return 1 129 | } 130 | return 0 131 | } 132 | -------------------------------------------------------------------------------- /patchlib/asm_test.go: -------------------------------------------------------------------------------- 1 | package patchlib 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | // Note: kstool thumb "B..." 0xOFFSET 9 | 10 | func TestAsmBW(t *testing.T) { 11 | for _, tc := range []struct{ pc, target, inst uint32 }{ 12 | {0x83EDE8, 0x40EF40, 0xD0F7AAB0}, 13 | {0x83EDE8, 0x41A4A0, 0xDBF75AB3}, 14 | {0x83D426, 0x40EF40, 0xD1F78BB5}, 15 | {0x83D426, 0x41A4A0, 0xDDF73BB0}, 16 | {0x02EFE6, 0x0189C8, 0xE9F7EFBC}, 17 | } { 18 | t.Run(fmt.Sprintf("%X_%X", tc.pc, tc.target), func(t *testing.T) { 19 | if inst := bw(tc.pc, tc.target); inst != tc.inst { 20 | t.Errorf("%X: B.W #0x%X - expected %X, got %X", tc.pc, tc.target, tc.inst, inst) 21 | } else if fmt.Sprintf("%X", inst) != fmt.Sprintf("%X", AsmBW(tc.pc, tc.target)) { 22 | t.Errorf("mismatch between []byte and uint32 versions") 23 | } 24 | }) 25 | } 26 | } 27 | 28 | func TestAsmBLX(t *testing.T) { 29 | for _, tc := range []struct{ pc, target, inst uint32 }{ 30 | {0x83EDE8, 0x40EF40, 0xD0F7AAE0}, 31 | {0x83EDE8, 0x41A4A0, 0xDBF75AE3}, 32 | {0x83D426, 0x40EF40, 0xD1F78CE5}, 33 | {0x83D426, 0x41A4A0, 0xDDF73CE0}, 34 | } { 35 | t.Run(fmt.Sprintf("%X_%X", tc.pc, tc.target), func(t *testing.T) { 36 | if inst := blx(tc.pc, tc.target); inst != tc.inst { 37 | t.Errorf("%X: BLX #0x%X - expected %X, got %X", tc.pc, tc.target, tc.inst, inst) 38 | } else if fmt.Sprintf("%X", inst) != fmt.Sprintf("%X", AsmBLX(tc.pc, tc.target)) { 39 | t.Errorf("mismatch between []byte and uint32 versions") 40 | } 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /patchlib/css.go: -------------------------------------------------------------------------------- 1 | package patchlib 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/riking/cssparse/tokenizer" 7 | ) 8 | 9 | // IsCSS uses a heuristic to check if the input is likely to be CSS. There may 10 | // be false negatives for invalid CSS or for CSS documents with more rules than 11 | // properties, and false positives for CSS-like languages. An error is returned 12 | // if and only if the io.Reader returns an error. 13 | func IsCSS(r io.Reader) (bool, error) { 14 | tk := tokenizer.NewTokenizer(r) 15 | var seenNonWhitespace bool 16 | var openBraceCount, closeBraceCount, propCount int 17 | var prev, cur tokenizer.Token 18 | for { 19 | t := tk.Next() 20 | if err := tk.Err(); err != nil { 21 | if err == io.EOF { 22 | break 23 | } 24 | return false, tk.Err() 25 | } 26 | prev, cur = cur, t 27 | if !seenNonWhitespace { 28 | if cur.Type != tokenizer.TokenS { 29 | if cur.Type != tokenizer.TokenIdent && cur.Type != tokenizer.TokenColon && cur.Type != tokenizer.TokenAtKeyword && !(cur.Type == tokenizer.TokenDelim && (cur.Value == "*" || cur.Value == ".")) && cur.Type != tokenizer.TokenHash && cur.Type != tokenizer.TokenOpenBracket { 30 | return false, nil // doesn't start with an identifier (for a selector) 31 | } 32 | seenNonWhitespace = true 33 | } 34 | continue 35 | } 36 | switch cur.Type { 37 | case tokenizer.TokenOpenBrace: 38 | openBraceCount++ 39 | case tokenizer.TokenCloseBrace: 40 | closeBraceCount++ 41 | case tokenizer.TokenColon: 42 | if prev.Type == tokenizer.TokenIdent && openBraceCount > closeBraceCount { 43 | propCount++ // is likely a property if it has an identifier before and is inside an unclosed block (i.e. not a selector) 44 | } 45 | } 46 | } 47 | if openBraceCount != closeBraceCount { 48 | return false, nil // different number of open/close braces 49 | } 50 | if openBraceCount == 0 { 51 | return false, nil // no rules 52 | } 53 | if openBraceCount > propCount { 54 | return false, nil // more blocks than properties 55 | } 56 | return true, nil 57 | } 58 | -------------------------------------------------------------------------------- /patchlib/css_test.go: -------------------------------------------------------------------------------- 1 | package patchlib 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestIsCSS(t *testing.T) { 9 | for _, tc := range []struct { 10 | in string 11 | out bool 12 | why string 13 | }{ 14 | {"", false, "no rules are present"}, 15 | {"asd", false, "no rules are present"}, 16 | {" asd", false, "no rules are present"}, 17 | {"asd {", false, "there are incomplete rules"}, 18 | {"asd {}", false, "there aren't any properties (technically it is CSS, but for all intents and purposes it isn't)"}, 19 | {"asd {asd:fgh}", true, "it obviously is"}, 20 | {" asd {asd: fgh}", true, "it obviously is"}, 21 | {" asd {asd: fgh}/*}*/", true, "it obviously is"}, 22 | {" * {asd: fgh}/*}*/", true, "it obviously is"}, 23 | {" asd {asd: url(:sdf{}dfg)}", true, "it obviously is"}, 24 | {" asd {asd: fgh; dfg: asd}", true, "it obviously is"}, 25 | {" @media print { asd {asd: fgh; dfg: asd} }", true, "it obviously is"}, 26 | {"[prop]{dfg: asd}", true, "it obviously is"}, 27 | {".class{dfg: asd}", true, "it obviously is"}, 28 | {"#id{dfg: asd}", true, "it obviously is"}, 29 | {":hover{dfg: asd}", true, "it obviously is"}, 30 | {":hover{dfg: asd} fgh {content: 'dfg}'}", true, "it obviously is"}, 31 | {":hover{dfg: asd; fgh: asd;} fgh {dfgdfgdfg}", true, "it's syntactically valid"}, 32 | {"function asd() { fgh(); }", false, "there's more blocks than rules (i.e. it's JavaScript)"}, 33 | {"dfgdfg", false, "it obviously isn't (i.e. it's HTML)"}, 34 | {"dfgdfg", false, "it obviously isn't (i.e. it's HTML, even though it might contain CSS in it), as it doesn't start with a selector"}, 35 | } { 36 | ic, err := IsCSS(strings.NewReader(tc.in)) 37 | if err != nil { 38 | panic(err) 39 | } 40 | var st string 41 | if !tc.out { 42 | st = "not " 43 | } 44 | if ic != tc.out { 45 | t.Errorf("incorrect: %#v should %sbe CSS because %s, but IsCSS()=%t", tc.in, st, tc.why, ic) 46 | } else { 47 | t.Logf("correct: %#v is %sCSS because %s, so IsCSS()=%t", tc.in, st, tc.why, ic) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /patchlib/patcher.go: -------------------------------------------------------------------------------- 1 | // Package patchlib provides common functions related to patching binaries. 2 | package patchlib 3 | 4 | import ( 5 | "bytes" 6 | "compress/zlib" 7 | "crypto/sha1" 8 | "debug/elf" 9 | "encoding/binary" 10 | "errors" 11 | "fmt" 12 | "io/ioutil" 13 | "strings" 14 | "unicode/utf8" 15 | 16 | "github.com/ianlancetaylor/demangle" 17 | "github.com/pgaskin/czlib" 18 | ) 19 | 20 | // Patcher applies patches to a byte array. All operations are done starting from cur. 21 | type Patcher struct { 22 | buf []byte 23 | cur int32 24 | hook func(offset int32, find, replace []byte) error 25 | 26 | dynsymsLoaded bool // for lazy-loading on first use 27 | dynsymsLoadedPLTGOT bool // for only decoding PLT if needed (on first use) 28 | dynsyms []*dynsym 29 | } 30 | 31 | // NewPatcher creates a new Patcher. 32 | func NewPatcher(in []byte) *Patcher { 33 | return &Patcher{in, 0, nil, false, false, nil} 34 | } 35 | 36 | // GetBytes returns the current content of the Patcher. 37 | func (p *Patcher) GetBytes() []byte { 38 | return p.buf 39 | } 40 | 41 | // ResetBaseAddress moves cur to 0. 42 | func (p *Patcher) ResetBaseAddress() { 43 | p.cur = 0 44 | } 45 | 46 | // Hook sets a hook to be called right before every change. If it returns an 47 | // error, it will be passed on. If nil (the default), the hook will be removed. 48 | // The find and replace arguments MUST NOT be modified by the hook. 49 | func (p *Patcher) Hook(fn func(offset int32, find, replace []byte) error) { 50 | p.hook = fn 51 | } 52 | 53 | // BaseAddress moves cur to an offset. The offset starts at 0. 54 | func (p *Patcher) BaseAddress(offset int32) error { 55 | if offset < 0 { 56 | return errors.New("BaseAddress: offset less than 0") 57 | } 58 | if offset >= int32(len(p.buf)) { 59 | return errors.New("BaseAddress: offset greater than length of buf") 60 | } 61 | p.cur = offset 62 | return nil 63 | } 64 | 65 | // FindBaseAddress moves cur to the offset of a sequence of bytes. 66 | func (p *Patcher) FindBaseAddress(find []byte) error { 67 | if len(find) > len(p.buf) { 68 | return errors.New("FindBaseAddress: length of bytes to find greater than buf") 69 | } 70 | i := bytes.Index(p.buf, find) 71 | if i < 0 { 72 | return errors.New("FindBaseAddress: could not find bytes") 73 | } 74 | p.cur = int32(i) 75 | return nil 76 | } 77 | 78 | // FindBaseAddressString moves cur to the offset of a string. 79 | func (p *Patcher) FindBaseAddressString(find string) error { 80 | if err := p.FindBaseAddress([]byte(find)); err != nil { 81 | return fmt.Errorf("FindBaseAddressString: %w", err) 82 | } 83 | return nil 84 | } 85 | 86 | // ReplaceBytes replaces the first occurrence of a sequence of bytes with another of the same length. 87 | func (p *Patcher) ReplaceBytes(offset int32, find, replace []byte) error { 88 | if err := p.replaceValue(offset, find, replace, true); err != nil { 89 | return fmt.Errorf("ReplaceBytes: %w", err) 90 | } 91 | return nil 92 | } 93 | 94 | // ReplaceString replaces the first occurrence of a string with another of the same length. 95 | func (p *Patcher) ReplaceString(offset int32, find, replace string) error { 96 | if len(replace) < len(find) { 97 | // If replacement shorter than find, append a null to the replacement string to be consistent with the original patch32lsb. 98 | replace += "\x00" 99 | replace = replace + find[len(replace):] 100 | } 101 | if err := p.replaceValue(offset, find, replace, false); err != nil { 102 | return fmt.Errorf("ReplaceString: %w", err) 103 | } 104 | return nil 105 | } 106 | 107 | // ReplaceInt replaces the first occurrence of an integer between 0 and 255 inclusively. 108 | func (p *Patcher) ReplaceInt(offset int32, find, replace uint8) error { 109 | if err := p.replaceValue(offset, find, replace, true); err != nil { 110 | return fmt.Errorf("ReplaceInt: %w", err) 111 | } 112 | return nil 113 | } 114 | 115 | // ReplaceFloat replaces the first occurrence of a float. 116 | func (p *Patcher) ReplaceFloat(offset int32, find, replace float64) error { 117 | if err := p.replaceValue(offset, find, replace, true); err != nil { 118 | return fmt.Errorf("ReplaceFloat: %w", err) 119 | } 120 | return nil 121 | } 122 | 123 | // FindZlib finds the base address of a zlib css stream based on a substring (not sensitive to whitespace). 124 | func (p *Patcher) FindZlib(find string) error { 125 | if len(find) > len(p.buf) { 126 | return errors.New("FindZlib: length of string to find greater than buf") 127 | } 128 | z, err := p.ExtractZlib() 129 | if err != nil { 130 | return fmt.Errorf("FindZlib: could not extract zlib streams: %w", err) 131 | } 132 | var i int32 133 | for _, zi := range z { 134 | if strings.Contains(zi.CSS, find) || strings.Contains(stripWhitespace(zi.CSS), stripWhitespace(find)) { 135 | if i != 0 { 136 | return errors.New("FindZlib: substring to find is not unique") 137 | } 138 | i = zi.Offset 139 | continue 140 | } 141 | // Handle minification from below 142 | zi.CSS = strings.ReplaceAll(zi.CSS, "\n ", "\n") 143 | zi.CSS = strings.ReplaceAll(zi.CSS, "\n ", "\n") 144 | zi.CSS = strings.ReplaceAll(zi.CSS, "\n ", "\n") 145 | findm := strings.ReplaceAll(find, "\n ", "\n") 146 | findm = strings.ReplaceAll(find, "\n ", "\n") 147 | findm = strings.ReplaceAll(findm, "\n ", "\n") 148 | if strings.Contains(zi.CSS, findm) || strings.Contains(stripWhitespace(zi.CSS), stripWhitespace(findm)) { 149 | if i != 0 { 150 | return errors.New("FindZlib: substring to find is not unique") 151 | } 152 | i = zi.Offset 153 | continue 154 | } 155 | zi.CSS = strings.ReplaceAll(zi.CSS, ": ", ":") 156 | zi.CSS = strings.ReplaceAll(zi.CSS, " {", "{") 157 | findm = strings.ReplaceAll(findm, ": ", ":") 158 | findm = strings.ReplaceAll(findm, " {", "{") 159 | if strings.Contains(zi.CSS, findm) || strings.Contains(stripWhitespace(zi.CSS), stripWhitespace(findm)) { 160 | if i != 0 { 161 | return errors.New("FindZlib: substring to find is not unique") 162 | } 163 | i = zi.Offset 164 | continue 165 | } 166 | zi.CSS = strings.ReplaceAll(zi.CSS, "\n", "") 167 | zi.CSS = strings.ReplaceAll(zi.CSS, "{ ", "") 168 | zi.CSS = strings.ReplaceAll(zi.CSS, "; ", "") 169 | findm = strings.ReplaceAll(findm, "{ ", "{") 170 | findm = strings.ReplaceAll(findm, "; ", ";") 171 | if strings.Contains(zi.CSS, findm) || strings.Contains(stripWhitespace(zi.CSS), stripWhitespace(findm)) { 172 | if i != 0 { 173 | return errors.New("FindZlib: substring to find is not unique") 174 | } 175 | i = zi.Offset 176 | continue 177 | } 178 | } 179 | if i == 0 { 180 | return errors.New("FindZlib: could not find string") 181 | } 182 | p.cur = i 183 | return nil 184 | } 185 | 186 | // FindZlibHash finds the base address of a zlib css stream based on it's SHA1 hash (can be found using the cssextract tool). 187 | func (p *Patcher) FindZlibHash(hash string) error { 188 | if len(hash) != 40 { 189 | return errors.New("FindZlibHash: invalid hash") 190 | } 191 | z, err := p.ExtractZlib() 192 | if err != nil { 193 | return fmt.Errorf("FindZlibHash: could not extract zlib streams: %w", err) 194 | } 195 | f := false 196 | for _, zi := range z { 197 | if fmt.Sprintf("%x", sha1.Sum([]byte(zi.CSS))) == stripWhitespace(hash) { 198 | p.cur = zi.Offset 199 | f = true 200 | break 201 | } 202 | } 203 | if !f { 204 | return errors.New("FindZlibHash: could not find hash") 205 | } 206 | return nil 207 | } 208 | 209 | // ReplaceZlib replaces a part of a zlib css stream at the current offset. 210 | func (p *Patcher) ReplaceZlib(offset int32, find, replace string) error { 211 | return p.ReplaceZlibGroup(offset, []Replacement{{find, replace}}) 212 | } 213 | 214 | // Replacement is a replacement for ReplaceZlibGroup. 215 | type Replacement struct { 216 | Find, Replace string 217 | } 218 | 219 | // ReplaceZlibGroup is the same as ReplaceZlib, but it replaces all at once. 220 | func (p *Patcher) ReplaceZlibGroup(offset int32, repl []Replacement) error { 221 | if !bytes.HasPrefix(p.buf[p.cur+offset:p.cur+offset+2], []byte{0x78, 0x9c}) { 222 | return errors.New("ReplaceZlib: not a zlib stream") 223 | } 224 | r, err := zlib.NewReader(bytes.NewReader(p.buf[p.cur+offset:])) // Need to use go zlib lib because it is more lenient about corrupt data after end of zlib stream 225 | if err != nil { 226 | return fmt.Errorf("ReplaceZlib: could not initialize zlib reader: %w", err) 227 | } 228 | dbuf, err := ioutil.ReadAll(r) 229 | r.Close() 230 | if err != nil && !strings.Contains(err.Error(), "corrupt input") && !strings.Contains(err.Error(), "invalid checksum") { 231 | return fmt.Errorf("ReplaceZlib: could not decompress stream: %w", err) 232 | } 233 | if len(dbuf) == 0 || !utf8.Valid(dbuf) { 234 | return errors.New("ReplaceZlib: not a valid zlib stream") 235 | } 236 | tbuf := compress(dbuf) 237 | if !bytes.HasPrefix(p.buf[p.cur+offset:], tbuf) || len(tbuf) < 4 { 238 | return errors.New("ReplaceZlib: sanity check failed: recompressed original data does not match original (this is a bug, so please report it)") 239 | } 240 | for _, r := range repl { 241 | find, replace := r.Find, r.Replace 242 | if !bytes.Contains(dbuf, []byte(find)) { 243 | find = strings.ReplaceAll(find, "\n ", "\n") 244 | find = strings.ReplaceAll(find, "\n ", "\n") 245 | find = strings.ReplaceAll(find, "\n ", "\n") 246 | if !bytes.Contains(dbuf, []byte(find)) { 247 | find = strings.ReplaceAll(find, ": ", ":") 248 | find = strings.ReplaceAll(find, " {", "{") 249 | if !bytes.Contains(dbuf, []byte(find)) { 250 | find = strings.ReplaceAll(find, "\n", "") 251 | find = strings.ReplaceAll(find, "; ", ";") 252 | find = strings.ReplaceAll(find, "{ ", "{") 253 | if !bytes.Contains(dbuf, []byte(find)) { 254 | return fmt.Errorf("ReplaceZlib: find string not found in stream (%s)", strings.ReplaceAll(find, "\n", "\\n")) 255 | } 256 | } 257 | } 258 | } 259 | dbuf = bytes.Replace(dbuf, []byte(find), []byte(replace), -1) 260 | } 261 | nbuf := compress(dbuf) 262 | if len(nbuf) == 0 { 263 | return errors.New("ReplaceZlib: error compressing new data (this is a bug, so please report it)") 264 | } 265 | if len(nbuf) > len(tbuf) { 266 | // Attempt to remove indentation to save space 267 | dbuf = bytes.Replace(dbuf, []byte("\n "), []byte("\n"), -1) 268 | dbuf = bytes.Replace(dbuf, []byte("\n "), []byte("\n"), -1) 269 | dbuf = bytes.Replace(dbuf, []byte("\n "), []byte("\n"), -1) 270 | nbuf = compress(dbuf) 271 | } 272 | if len(nbuf) > len(tbuf) { 273 | // Attempt to remove spaces after colons to save space 274 | dbuf = bytes.Replace(dbuf, []byte(": "), []byte(":"), -1) 275 | dbuf = bytes.Replace(dbuf, []byte(" {"), []byte("{"), -1) 276 | nbuf = compress(dbuf) 277 | } 278 | if len(nbuf) > len(tbuf) { 279 | // Attempt to remove newlines to save space 280 | dbuf = bytes.Replace(dbuf, []byte("\n"), []byte(""), -1) 281 | dbuf = bytes.Replace(dbuf, []byte("; "), []byte(";"), -1) 282 | dbuf = bytes.Replace(dbuf, []byte("{ "), []byte("{"), -1) 283 | nbuf = compress(dbuf) 284 | } 285 | if len(nbuf) > len(tbuf) { 286 | return fmt.Errorf("ReplaceZlib: new compressed data is %d bytes longer than old data (try removing whitespace or unnecessary css)", len(nbuf)-len(tbuf)) 287 | } 288 | if p.hook != nil { 289 | if err := p.hook(p.cur+offset, tbuf, nbuf); err != nil { 290 | return fmt.Errorf("hook returned error: %v", err) 291 | } 292 | } 293 | copy(p.buf[p.cur+offset:p.cur+offset+int32(len(tbuf))], nbuf) 294 | r, err = zlib.NewReader(bytes.NewReader(p.buf[p.cur+offset:])) // Need to use go zlib lib because it is more lenient about corrupt data after end of zlib stream 295 | if err != nil { 296 | return fmt.Errorf("ReplaceZlib: could not initialize zlib reader: %w", err) 297 | } 298 | ndbuf, err := ioutil.ReadAll(r) 299 | r.Close() 300 | if !bytes.Equal(dbuf, ndbuf) { 301 | return errors.New("ReplaceZlib: decompressed new data does not match new data (this is a bug, so please report it)") 302 | } 303 | return nil 304 | } 305 | 306 | // ZlibItem is a CSS zlib stream. 307 | type ZlibItem struct { 308 | Offset int32 309 | CSS string 310 | } 311 | 312 | // ExtractZlib extracts all CSS zlib streams. It returns it as a map of offsets and strings. 313 | func (p *Patcher) ExtractZlib() ([]ZlibItem, error) { 314 | zlibs := []ZlibItem{} 315 | for i := 0; i < len(p.buf)-2; i++ { 316 | if bytes.HasPrefix(p.buf[i:i+2], []byte{0x78, 0x9c}) { 317 | r, err := zlib.NewReader(bytes.NewReader(p.buf[i:])) // Need to use go zlib lib because it is more lenient about corrupt data after end of zlib stream 318 | if err != nil { 319 | return zlibs, fmt.Errorf("could not initialize zlib reader: %w", err) 320 | } 321 | dbuf, err := ioutil.ReadAll(r) 322 | r.Close() 323 | if err != nil && !strings.Contains(err.Error(), "corrupt input") && !strings.Contains(err.Error(), "invalid checksum") { 324 | return zlibs, fmt.Errorf("could not decompress stream: %w", err) 325 | } 326 | if len(dbuf) == 0 || !utf8.Valid(dbuf) { 327 | continue 328 | } 329 | ic, err := IsCSS(bytes.NewReader(dbuf)) 330 | if err != nil { 331 | panic(err) // bytes.Reader should never error 332 | } 333 | if !ic { 334 | continue 335 | } 336 | tbuf := compress(dbuf) 337 | if !bytes.HasPrefix(p.buf[i:], tbuf) || len(tbuf) < 4 { 338 | return zlibs, errors.New("sanity check failed: recompressed data does not match original (this is a bug, so please report it)") 339 | } 340 | zlibs = append(zlibs, ZlibItem{int32(i), string(dbuf)}) 341 | } 342 | } 343 | return zlibs, nil 344 | } 345 | 346 | // GetCur gets the current base address. 347 | func (p *Patcher) GetCur() int32 { 348 | return p.cur 349 | } 350 | 351 | // ResolveSym resolves a mangled (fallback to unmangled) symbol name and returns 352 | // its base address (error if not found). The symbol table will be loaded if not 353 | // already done. 354 | func (p *Patcher) ResolveSym(name string) (int32, error) { 355 | s, err := p.getDynsym(name, false) 356 | if err != nil { 357 | return 0, fmt.Errorf("ResolveSym(%#v): %w", name, err) 358 | } 359 | return int32(s.Offset), nil 360 | } 361 | 362 | // ResolveSymPLT resolves a mangled (fallback to unmangled) symbol name and 363 | // returns its PLT address (error if it doesn't have one). The symbol table will 364 | // be loaded if not already done. 365 | func (p *Patcher) ResolveSymPLT(name string) (int32, error) { 366 | s, err := p.getDynsym(name, true) 367 | if err != nil { 368 | return 0, fmt.Errorf("ResolveSymPLT(%#v): %w", name, err) 369 | } 370 | if s.OffsetPLT == 0 { 371 | return 0, fmt.Errorf("ResolveSymPLT(%#v) = %#v: no PLT entry found", name, s) 372 | } 373 | return int32(s.OffsetPLT), nil 374 | } 375 | 376 | // ResolveSymPLTTail resolves a mangled (fallback to unmangled) symbol name and 377 | // returns its PLT tail call address (error if it doesn't have one). The symbol 378 | // table will be loaded if not already done. 379 | func (p *Patcher) ResolveSymPLTTail(name string) (int32, error) { 380 | s, err := p.getDynsym(name, true) 381 | if err != nil { 382 | return 0, fmt.Errorf("ResolveSymPLTTail(%#v): %w", name, err) 383 | } 384 | if s.OffsetPLT == 0 { 385 | return 0, fmt.Errorf("ResolveSymPLTTail(%#v) = %#v: no PLT entry found", name, s) 386 | } 387 | if s.OffsetPLTTail == 0 { 388 | return 0, fmt.Errorf("ResolveSymPLTTail(%#v) = %#v: no tail stub before PLT entry", name, s) 389 | } 390 | return int32(s.OffsetPLTTail), nil 391 | } 392 | 393 | func (p *Patcher) getDynsym(name string, needPLTGOT bool) (*dynsym, error) { 394 | ds, err := p.ExtractDynsyms(needPLTGOT) 395 | if err != nil { 396 | return nil, fmt.Errorf("get dynsyms: %w", err) 397 | } 398 | for _, s := range ds { 399 | if s.Name == name { 400 | return s, nil 401 | } 402 | } 403 | for _, s := range ds { 404 | if s.Demangled == name { 405 | return s, nil 406 | } 407 | } 408 | return nil, fmt.Errorf("no such symbol %#v", name) 409 | } 410 | 411 | func (p *Patcher) ExtractDynsyms(needPLTGOT bool) ([]*dynsym, error) { 412 | if !p.dynsymsLoaded || (needPLTGOT && !p.dynsymsLoadedPLTGOT) { 413 | e, err := elf.NewFile(bytes.NewReader(p.buf)) 414 | if err != nil { 415 | return nil, fmt.Errorf("load elf: %w", err) 416 | } 417 | defer e.Close() 418 | 419 | ds, err := decdynsym(e, !needPLTGOT) 420 | if err != nil { 421 | return nil, fmt.Errorf("load syms (pltgot: %t): %w", needPLTGOT, err) 422 | } 423 | p.dynsyms = ds 424 | p.dynsymsLoaded = true 425 | p.dynsymsLoadedPLTGOT = needPLTGOT 426 | } 427 | return p.dynsyms, nil 428 | } 429 | 430 | // replaceValue encodes find and replace as little-endian binary and replaces 431 | // the first occurrence starting at cur. The lengths of the encoded find and 432 | // replace must be the same, or an error will be returned. 433 | func (p *Patcher) replaceValue(offset int32, find, replace interface{}, strictOffset bool) error { 434 | if int32(len(p.buf)) < p.cur+offset { 435 | return errors.New("offset past end of buf") 436 | } 437 | 438 | var err error 439 | var fbuf, rbuf []byte 440 | 441 | if fstr, ok := find.(string); ok { 442 | fbuf = []byte(fstr) 443 | } else { 444 | fbuf, err = toLEBin(find) 445 | if err != nil { 446 | return fmt.Errorf("could not encode find: %v", err) 447 | } 448 | } 449 | 450 | if rstr, ok := replace.(string); ok { 451 | rbuf = []byte(rstr) 452 | } else { 453 | rbuf, err = toLEBin(replace) 454 | if err != nil { 455 | return fmt.Errorf("could not encode replace: %v", err) 456 | } 457 | } 458 | 459 | if len(fbuf) != len(rbuf) { 460 | return errors.New("length mismatch in byte replacement") 461 | } 462 | if int32(len(p.buf)) < p.cur+offset+int32(len(fbuf)) { 463 | return errors.New("replaced value past end of buf") 464 | } 465 | 466 | if !bytes.Contains(p.buf[p.cur+offset:], fbuf) { 467 | return errors.New("could not find specified bytes") 468 | } 469 | 470 | if strictOffset && !bytes.HasPrefix(p.buf[p.cur+offset:], fbuf) { 471 | return errors.New("could not find specified bytes at offset") 472 | } 473 | 474 | if p.hook != nil { 475 | if err := p.hook(p.cur+offset, fbuf, rbuf); err != nil { 476 | return fmt.Errorf("hook returned error: %v", err) 477 | } 478 | } 479 | copy(p.buf[p.cur+offset:], bytes.Replace(p.buf[p.cur+offset:], fbuf, rbuf, 1)) 480 | return nil 481 | } 482 | 483 | func toLEBin(v interface{}) ([]byte, error) { 484 | buf := new(bytes.Buffer) 485 | err := binary.Write(buf, binary.LittleEndian, v) 486 | return buf.Bytes(), err 487 | } 488 | 489 | func toBEBin(v interface{}) ([]byte, error) { 490 | buf := new(bytes.Buffer) 491 | err := binary.Write(buf, binary.BigEndian, v) 492 | return buf.Bytes(), err 493 | } 494 | 495 | func mustBytes(b []byte, err error) []byte { 496 | if err != nil { 497 | panic(err) 498 | } 499 | return b 500 | } 501 | 502 | // compress compresses data in a way compatible with python's zlib. 503 | // This uses czlib internally, as the std zlib produces different results. 504 | func compress(src []byte) []byte { 505 | b, err := czlib.Compress(src) // Need to use czlib to keep header correct 506 | if err != nil { 507 | panic(err) 508 | } 509 | d, err := decompress(b) 510 | if err != nil { 511 | panic(err) 512 | } 513 | if !bytes.Equal(d, src) { 514 | panic("compressed and decompressed data not equal") 515 | } 516 | return b 517 | } 518 | 519 | func decompress(src []byte) ([]byte, error) { 520 | return czlib.Decompress(src) 521 | } 522 | 523 | func stripWhitespace(src string) string { 524 | src = strings.ReplaceAll(src, " ", "") 525 | src = strings.ReplaceAll(src, "\t", "") 526 | src = strings.ReplaceAll(src, "\n", "") 527 | src = strings.ReplaceAll(src, "\r", "") 528 | return src 529 | } 530 | 531 | // FindBaseAddressSymbol moves cur to the offset of a symbol by it's demangled c++ name. 532 | // Warning: All symbols are off by one for historical reasons. 533 | // 534 | // Deprecated: Use ResolveSym instead. 535 | func (p *Patcher) FindBaseAddressSymbol(find string) error { 536 | e, err := elf.NewFile(bytes.NewReader(p.buf)) 537 | if err != nil { 538 | return fmt.Errorf("FindBaseAddressSymbol: could not open file as elf binary: %w", err) 539 | } 540 | syms, err := e.DynamicSymbols() 541 | if err != nil { 542 | return fmt.Errorf("FindBaseAddressSymbol: could not read dynsyms: %w", err) 543 | } 544 | for _, sym := range syms { 545 | name, err := demangle.ToString(sym.Name) 546 | if err != nil { 547 | name = sym.Name 548 | } 549 | if find != "" && find == name { 550 | p.cur = int32(sym.Value) 551 | return nil 552 | } 553 | } 554 | return errors.New("FindBaseAddressSymbol: could not find symbol") 555 | } 556 | 557 | // ReplaceBLX replaces a BLX instruction at PC (offset). Find and Replace are the target offsets. 558 | // 559 | // Deprecated: Assemble the instruction with AsmBLX and use ReplaceBytes instead. 560 | func (p *Patcher) ReplaceBLX(offset int32, find, replace uint32) error { 561 | if int32(len(p.buf)) < p.cur+offset { 562 | return errors.New("ReplaceBLX: offset past end of buf") 563 | } 564 | fi, ri := AsmBLX(uint32(p.cur+offset), find), AsmBLX(uint32(p.cur+offset), replace) 565 | f, r := mustBytes(toBEBin(fi)), mustBytes(toBEBin(ri)) 566 | if len(f) != len(r) { 567 | return errors.New("ReplaceBLX: internal error: wrong blx length") 568 | } 569 | if !bytes.HasPrefix(p.buf[p.cur+offset:], f) { 570 | return errors.New("ReplaceBLX: could not find bytes") 571 | } 572 | if p.hook != nil { 573 | if err := p.hook(p.cur+offset, f, r); err != nil { 574 | return fmt.Errorf("hook returned error: %v", err) 575 | } 576 | } 577 | copy(p.buf[p.cur+offset:], r) 578 | return nil 579 | } 580 | 581 | // ReplaceBytesNOP replaces an instruction with 0046 (MOV r0, r0) as many times as needed. 582 | // 583 | // Deprecated: Generate the NOP externally and use ReplaceBytes instead. 584 | func (p *Patcher) ReplaceBytesNOP(offset int32, find []byte) error { 585 | if int32(len(p.buf)) < offset { 586 | return errors.New("ReplaceBytesNOP: offset past end of buf") 587 | } 588 | if len(find)%2 != 0 { 589 | return errors.New("ReplaceBytesNOP: find not a multiple of 2") 590 | } 591 | r := make([]byte, len(find)) 592 | for i := 0; i < len(r); i += 2 { 593 | r[i], r[i+1] = 0x00, 0x46 594 | } 595 | if !bytes.HasPrefix(p.buf[offset:], find) { 596 | return errors.New("ReplaceBytesNOP: could not find bytes") 597 | } 598 | if p.hook != nil { 599 | if err := p.hook(offset, find, r); err != nil { 600 | return fmt.Errorf("hook returned error: %v", err) 601 | } 602 | } 603 | copy(p.buf[offset:], r) 604 | return nil 605 | } 606 | -------------------------------------------------------------------------------- /patchlib/patcher_test.go: -------------------------------------------------------------------------------- 1 | package patchlib 2 | 3 | import ( 4 | "crypto/sha1" 5 | "crypto/sha256" 6 | "fmt" 7 | "io/ioutil" 8 | "reflect" 9 | "runtime/debug" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func TestGetBytes(t *testing.T) { 15 | p := NewPatcher([]byte(`this is a test`)) 16 | eq(t, p.GetBytes(), []byte(`this is a test`), "unexpected output") 17 | } 18 | 19 | func TestResetBaseAddress(t *testing.T) { 20 | p := NewPatcher([]byte(`this is a test`)) 21 | p.cur = 5 22 | p.ResetBaseAddress() 23 | eq(t, p.cur, int32(0), "unexpected base address") 24 | } 25 | 26 | func TestBaseAddress(t *testing.T) { 27 | p := NewPatcher([]byte(`this is a test`)) 28 | err(t, p.BaseAddress(14)) // past buf len 29 | err(t, p.BaseAddress(-1)) // negative 30 | nerr(t, p.BaseAddress(4)) 31 | eq(t, p.cur, int32(4), "unexpected base address") 32 | } 33 | 34 | func TestFindBaseAddress(t *testing.T) { 35 | p := NewPatcher([]byte(`this is a test`)) 36 | err(t, p.FindBaseAddress([]byte(`thiss`))) 37 | err(t, p.FindBaseAddress([]byte(`this is a test sdfsdf`))) 38 | nerr(t, p.FindBaseAddress([]byte(`a test`))) 39 | eq(t, p.cur, int32(8), "unexpected base address") 40 | } 41 | 42 | func TestFindBaseAddressString(t *testing.T) { 43 | p := NewPatcher([]byte(`this is a test`)) 44 | err(t, p.FindBaseAddressString(`thiss`)) 45 | err(t, p.FindBaseAddressString(`this is a test sdfsdf`)) 46 | nerr(t, p.FindBaseAddressString(`a test`)) 47 | eq(t, p.cur, int32(8), "unexpected base address") 48 | } 49 | 50 | func TestReplaceString(t *testing.T) { 51 | p := NewPatcher([]byte(`this is a test`)) 52 | nerr(t, p.ReplaceString(10, `test`, `n`)) 53 | nerr(t, p.ReplaceString(0, `this `, `that `)) 54 | err(t, p.ReplaceString(0, `this `, `that `)) 55 | nerr(t, p.ReplaceString(0, `s`, `5`)) 56 | eq(t, p.GetBytes(), []byte("that i5 a n\x00st"), "unexpected output") 57 | } 58 | 59 | func TestReplaceBytes(t *testing.T) { 60 | p := NewPatcher([]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05}) 61 | err(t, p.ReplaceBytes(0, []byte{0x00}, []byte{0x00, 0x01})) 62 | err(t, p.ReplaceBytes(3, []byte{0x02, 0x03}, []byte{0x03, 0x02})) 63 | nerr(t, p.ReplaceBytes(2, []byte{0x02, 0x03}, []byte{0x03, 0x02})) 64 | err(t, p.ReplaceBytes(0, []byte{0x02, 0x03}, []byte{0x03, 0x02})) 65 | eq(t, p.GetBytes(), []byte{0x00, 0x01, 0x03, 0x02, 0x04, 0x05}, "unexpected output") 66 | } 67 | 68 | func TestReplaceInt(t *testing.T) { 69 | p := NewPatcher([]byte{0x00, 0x01, 0x02, 0xff, 0x04, 0x05}) 70 | err(t, p.ReplaceInt(4, 255, 195)) 71 | nerr(t, p.ReplaceInt(3, 255, 195)) 72 | err(t, p.ReplaceInt(2, 255, 195)) 73 | eq(t, p.GetBytes(), []byte{0x00, 0x01, 0x02, 0xc3, 0x04, 0x05}, "unexpected output") 74 | } 75 | 76 | func TestReplaceFloat(t *testing.T) { 77 | p := NewPatcher([]byte{0x00, 0xcd, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xf0, 0x3f, 0x05}) 78 | nerr(t, p.ReplaceFloat(1, 1.05, 3.25)) 79 | err(t, p.ReplaceFloat(0, 1.05, 3.25)) 80 | eq(t, p.GetBytes(), []byte{0x00, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x40, 0x05}, "unexpected output") 81 | } 82 | 83 | func TestZlib(t *testing.T) { 84 | nickel, errr := ioutil.ReadFile("./testdata/nickel") 85 | nerr(t, errr) 86 | p := NewPatcher(nickel) 87 | 88 | err(t, p.FindZlib("height")) 89 | 90 | z, errr := p.ExtractZlib() 91 | nerr(t, errr) 92 | for _, zi := range z { 93 | if strings.Contains(zi.CSS, "#boggleContainer[qApp_deviceIsPika=true]") { 94 | eq(t, fmt.Sprintf("%x", sha1.Sum([]byte(zi.CSS))), "1b80e45ffa47d77642b053205452a528d7b37c76", "should start with correct data hash") 95 | } 96 | } 97 | 98 | nerr(t, p.FindZlib("#boggleContainer[qApp_deviceIsPika=true]")) 99 | eq(t, p.cur, int32(4563746), "FindZlib should return correct offset") 100 | nerr(t, p.ReplaceZlib(0, "qproperty-visible: false;", "qproperty-visible:true;")) 101 | err(t, p.ReplaceZlib(0, "dfgdfgdfg", "dfgdfgdfg")) 102 | err(t, p.ReplaceZlib(0, "false", "a really long string which is way too long to possibly fit asndkjas aksjndkajsnd kajsnjkwjnw akjnwr kejnskr fjsdndf ksjdndkf jsd")) 103 | 104 | z, errr = p.ExtractZlib() 105 | nerr(t, errr) 106 | for _, zi := range z { 107 | if strings.Contains(zi.CSS, "#boggleContainer[qApp_deviceIsPika=true]") { 108 | eq(t, fmt.Sprintf("%x", sha1.Sum([]byte(zi.CSS))), "cc9d7ce57f8746517ea692b7c65e9ed74c1d765b", "data should be intact and correctly changed") 109 | } 110 | } 111 | 112 | p.ResetBaseAddress() 113 | nerr(t, p.FindZlibHash("cc9d7ce57f8746517ea692b7c65e9ed74c1d765b")) 114 | eq(t, p.cur, int32(4563746), "FindZlibHash should return correct offset") 115 | 116 | nerr(t, p.ReplaceZlib(0, "qproperty-visible: true;", "qproperty-visible: ALongString;")) // should also find string due to accomodations for minification. 117 | nerr(t, p.ReplaceZlib(0, "qproperty-visible: ALongString;", "qproperty-visible: true;")) 118 | 119 | err(t, p.ReplaceZlibGroup(0, []Replacement{ 120 | {"qproperty-visible: true;", "qproperty-visible:true; a really long string which is way too long to possibly fit asndkjas aksjndkajsnd kajsnjkwjnw akjnwr kejnskr fjsdndf ksjdndkf jsd"}, 121 | })) 122 | 123 | nerr(t, p.ReplaceZlibGroup(0, []Replacement{ 124 | {"qproperty-visible: true;", "qproperty-visible:true; a really long string which is way too long to possibly fit asndkjas aksjndkajsnd kajsnjkwjnw akjnwr kejnskr fjsdndf ksjdndkf jsd"}, 125 | {"qproperty-visible:true; a really long string which is way too long to possibly fit asndkjas aksjndkajsnd kajsnjkwjnw akjnwr kejnskr fjsdndf ksjdndkf jsd", "qproperty-visible: true;"}, 126 | })) // should correctly replace groups, and only return an error if the final result is too long 127 | 128 | nerr(t, p.ReplaceZlibGroup(0, []Replacement{ 129 | {"qproperty-visible: true;", "qproperty-visible:true; a really long string which is way too long to possibly fit asndkjas aksjndkajsnd kajsnjkwjnw akjnwr kejnskr fjsdndf ksjdndkf jsd"}, 130 | {"qproperty-visible:true; a really long string which is way too long to possibly fit asndkjas aksjndkajsnd kajsnjkwjnw akjnwr kejnskr fjsdndf ksjdndkf jsd", "qproperty-visible: true;"}, 131 | })) // making sure the previous one was processed correctly (it does then undoes the change) 132 | 133 | p.ResetBaseAddress() 134 | err(t, p.ReplaceZlib(0, " ", " ")) // not a zlib stream 135 | } 136 | 137 | func TestAll(t *testing.T) { 138 | in := append( 139 | []byte{0x00, 0x01, 0xff, 0xcd, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xf0, 0x3f, 0x01, 0x02, 0x03, 0x04}, 140 | []byte(`this is a test`)..., 141 | ) 142 | eout := append( 143 | []byte{0x33, 0x33, 0x00, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x10, 0x40, 0x01, 0x02, 0x03, 0x04}, 144 | []byte("taht\x00is z test")..., 145 | ) 146 | 147 | p := NewPatcher(in) 148 | eq(t, p.cur, int32(0), "base address should be correct") 149 | 150 | err(t, p.ReplaceInt(0, 255, 0)) // strict matching 151 | nerr(t, p.ReplaceInt(2, 255, 0)) 152 | 153 | nerr(t, p.ReplaceString(0, `this `, `that`)) 154 | err(t, p.ReplaceString(0, `this `, `that`)) // no match 155 | 156 | nerr(t, p.ReplaceFloat(3, 1.05, 4.05)) 157 | 158 | err(t, p.FindBaseAddress([]byte("a testdfgdfg"))) // no match 159 | eq(t, p.cur, int32(0), "base address should be correct") 160 | nerr(t, p.FindBaseAddress([]byte("a test"))) 161 | eq(t, p.cur, int32(23), "base address should be correct") 162 | 163 | err(t, p.ReplaceString(0, `is`, `si`)) // before current base address 164 | nerr(t, p.ReplaceString(0, `a`, `t`)) 165 | 166 | p.ResetBaseAddress() 167 | eq(t, p.cur, int32(0), "base address should be correct") 168 | nerr(t, p.ReplaceString(0, `that`, `taht`)) 169 | 170 | nerr(t, p.FindBaseAddress([]byte("taht"))) 171 | eq(t, p.cur, int32(15), "base address should be correct") 172 | nerr(t, p.ReplaceString(8, `t`, `z`)) 173 | 174 | err(t, p.ReplaceBytes(0, []byte{0x00, 0x01}, []byte{0x33, 0x33})) 175 | p.ResetBaseAddress() 176 | err(t, p.ReplaceBytes(3, []byte{0x00, 0x01}, []byte{0x33, 0x33})) 177 | nerr(t, p.ReplaceBytes(0, []byte{0x00, 0x01}, []byte{0x33, 0x33})) 178 | 179 | eq(t, p.GetBytes(), eout, "unexpected output: "+string(p.GetBytes())) 180 | } 181 | 182 | func TestReal(t *testing.T) { 183 | libNickel, err := ioutil.ReadFile("./testdata/libnickel.so.1.0.0") 184 | nerr(t, err) 185 | p := NewPatcher(libNickel) 186 | 187 | cs := func(exp string) { 188 | sum := fmt.Sprintf("%x", sha256.Sum256(p.GetBytes())) 189 | eq(t, sum, exp, "unexpected checksum: "+sum) 190 | } 191 | 192 | cs("6603e718eb01947c7face497dd766e3447dce95dbcbabb7d31f46e9d09fbb1e5") 193 | 194 | // Test with select patches to try and find edge cases. 195 | 196 | // Brightness fine control 197 | p.ResetBaseAddress() 198 | p.ReplaceInt(0x95DD02, 1, 2) 199 | cs("7a5c27729c5a2ac20246ad1c44c789410a4ac344a344dab2432b04d0093f186e") 200 | 201 | // Ignore .otf fonts 202 | p.ResetBaseAddress() 203 | p.FindBaseAddressString(`*.otf`) 204 | p.ReplaceString(0, `*`, `_`) 205 | cs("ed3de9e305b883642d4ecc1e5ace77c15e2d4f6ceacafa1816727724429e34cb") 206 | 207 | // Clock display duration 208 | p.ResetBaseAddress() 209 | p.ReplaceBytes(0x9F6252, []byte{0x40, 0xF6, 0xB8, 0x31}, []byte{0x03, 0x21, 0x89, 0x02}) 210 | p.ReplaceInt(0x9F6252, 3, 5) 211 | cs("61c9d966ef53018c9de9c674dc2946cbb42701fa5a458704bc115023e193c55f") 212 | 213 | // Allow searches on Extra dictionaries 214 | p.ResetBaseAddress() 215 | p.FindBaseAddressString("\x00Extra:\x20") 216 | p.ReplaceString(0007, "\x20", "_") 217 | cs("8f92a0c8b7041b89d331d5dd0e4579e30caa757272bbc0f47578765f49fe4076") 218 | 219 | // Change dicthtml strings to micthtml 220 | p.ResetBaseAddress() 221 | p.ReplaceString(0xC9717C, `%1/dicthtml%2`, `%1/micthtml%2`) 222 | p.ReplaceString(0xC9718C, `dicthtml`, `micthtml`) 223 | p.ReplaceString(0xC971D4, `/mnt/onboard/.kobo/dict/dicthtml%1`, `/mnt/onboard/.kobo/dict/micthtml%1`) 224 | cs("8a36efb87ccfd3fe0bad3a9913be12273fec089cbb7537e6a4cff69edcae1520") 225 | 226 | // Un-force link decoration in KePubs 227 | p.ResetBaseAddress() 228 | p.FindBaseAddressString(`a:link, a:visited, a:hover, a:active {`) 229 | p.ReplaceString(0x0027, "b", "_") 230 | p.ReplaceString(0x0053, "c", "_") 231 | cs("a64e265813aaf58d5fb227d681a9049ebd2daf1270a1cb3f9d21564d1f260842") 232 | 233 | // KePub stylesheet additions 234 | p.ResetBaseAddress() 235 | p.FindBaseAddressString(".KBHighlighting, .KBSearchResult {") 236 | p.ReplaceString(0x0000, ".KBHighlighting, .KBSearchResult { background-color: #C6C6C6 !important; } \t", ".KBHighlighting,.KBSearchResult{background-color:#C6C6C6!important}.KBSearch") 237 | p.ReplaceString(0x004C, ".KBSearchResult, .KBAnnotation, .KBHighlighting { font-size: 100% !important; -webkit-text-combine: inherit !important; } \t", "Result,.KBAnnotation,.KBHighlighting{font-size:100%!important;-webkit-text-combine:inherit!important}.KBAnnotation[writingM") 238 | p.ReplaceString(0x00C7, ".KBAnnotation[writingMode=\"horizontal-tb\"], .KBAnnotationContinued[writingMode=\"horizontal-tb\"] { border-bottom: 2px solid black !important; } \t", "ode=\"horizontal-tb\"],.KBAnnotationContinued[writingMode=\"horizontal-tb\"]{border-bottom:2px solid black!important}.KBAnnotation[writingMode=\"vert") 239 | p.ReplaceString(0x0157, ".KBAnnotation[writingMode=\"vertical-rl\"], .KBAnnotationContinued[writingMode=\"vertical-rl\"] { border-right: 2px solid black !important; } \t", "ical-rl\"],.KBAnnotationContinued[writingMode=\"vertical-rl\"]{border-right:2px solid black!important}.KBAnnotation[writingMode=\"vertical-lr\"]") 240 | p.ReplaceString(0x01E2, ".KBAnnotation[writingMode=\"vertical-lr\"], .KBAnnotationContinued[writingMode=\"vertical-lr\"] { border-left: 2px solid black !important; }", ",.KBAnnotationContinued[writingMode=\"vertical-lr\"]{border-left:2px solid black!important}/*********************************************/") 241 | cs("07509cc3ed09e60558b37f4f71245af8893b52f221b84810260be53c0f163f6f") 242 | 243 | // My 10 line spacing values 244 | p.ResetBaseAddress() 245 | p.ReplaceBytes(0x659DA4, []byte{0xBE, 0xF5, 0xAE, 0xE9}, []byte{0x00, 0x46, 0x00, 0x46}) 246 | p.ReplaceBytes(0x659DFA, []byte{0xBE, 0xF5, 0x84, 0xE9}, []byte{0x00, 0x46, 0x00, 0x46}) 247 | p.ReplaceBytes(0x659E24, []byte{0xBE, 0xF5, 0x6E, 0xE9}, []byte{0x00, 0x46, 0x00, 0x46}) 248 | p.ReplaceBytes(0x659E60, []byte{0xBE, 0xF5, 0x50, 0xE9}, []byte{0x00, 0x46, 0x00, 0x46}) 249 | p.ReplaceBytes(0x659EC6, []byte{0xBE, 0xF5, 0x1E, 0xE9}, []byte{0x00, 0x46, 0x00, 0x46}) 250 | p.BaseAddress(0x659F60) 251 | p.ReplaceFloat(0x0000, 1.05, 0.8) 252 | p.ReplaceFloat(0x0008, 1.07, 0.85) 253 | p.ReplaceFloat(0x0010, 1.1, 0.875) 254 | p.ReplaceFloat(0x0018, 1.35, 0.9) 255 | p.ReplaceFloat(0x0020, 1.7, 0.925) 256 | p.ReplaceFloat(0x0028, 1.8, 0.95) 257 | p.ReplaceFloat(0x0030, 2.2, 0.975) 258 | p.ReplaceFloat(0x0038, 2.4, 1.0) 259 | p.ReplaceFloat(0x0040, 2.6, 1.05) 260 | p.ReplaceFloat(0x0048, 2.8, 1.1) 261 | cs("d07f0d59517bee75043505da790adfe4875b18eea2f7d65cfd6bb7e61068ecc9") 262 | } 263 | 264 | // TODO: test symbol stuff? 265 | 266 | func nerr(t *testing.T, err error) { 267 | if err != nil { 268 | debug.PrintStack() 269 | t.Fatalf("err should be nil: %v", err) 270 | } 271 | } 272 | 273 | func err(t *testing.T, err error) { 274 | if err == nil { 275 | debug.PrintStack() 276 | t.Fatalf("err should not be nil") 277 | } 278 | } 279 | 280 | func eq(t *testing.T, a, b interface{}, msg string) { 281 | if !reflect.DeepEqual(a, b) { 282 | t.Error(msg) 283 | debug.PrintStack() 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /patchlib/syms.go: -------------------------------------------------------------------------------- 1 | package patchlib 2 | 3 | import ( 4 | "debug/elf" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/ianlancetaylor/demangle" 10 | "rsc.io/arm/armasm" 11 | ) 12 | 13 | // manually tested with libnickel from 12777, 14622, and 5761 and libadobe from 14622 14 | // should work on any ARM binary targeting the ARMv6 ABI or newer which uses the recommended PLT format (e.g. GCC or Clang) 15 | 16 | type dynsym struct { 17 | // decoded from the dynamic symbol table 18 | Name string 19 | Offset uint32 20 | Index uint32 21 | Type elf.SymType 22 | // decoded from the R_ARM_JUMP_SLOT relocs 23 | OffsetGOT uint32 // optional 24 | // decoded from the PLT 25 | OffsetPLT uint32 // optional 26 | OffsetPLTTail uint32 // optional 27 | // generated 28 | Demangled string // optional 29 | } 30 | 31 | func decdynsym(e *elf.File, skipPLTGOT bool) ([]*dynsym, error) { 32 | if e.Class != elf.ELFCLASS32 && e.Machine != elf.EM_ARM { 33 | return nil, fmt.Errorf("not a 32-bit arm elf") 34 | } 35 | 36 | var dynsyms []*dynsym 37 | 38 | // include all dynamic symbols (including the ones without PLT entries) 39 | edynsyms, err := e.DynamicSymbols() 40 | if err != nil { 41 | return nil, fmt.Errorf("get dynamic symbols: %w", err) 42 | } 43 | for i, edynsym := range edynsyms { 44 | if edynsym.Name == "" { 45 | // discard unnamed symbols (usually just _init, etc stuff) 46 | continue 47 | } 48 | v, err := demangle.ToString(edynsym.Name) 49 | if err != nil { 50 | v = "" 51 | } 52 | dynsyms = append(dynsyms, &dynsym{ 53 | Name: edynsym.Name, 54 | Offset: uint32(edynsym.Value) &^ 1, // https://static.docs.arm.com/ihi0044/g/aaelf32.pdf: For the purposes of relocation the value used shall be the address of the instruction (st_value &~1). 55 | Index: uint32(i + 1), // Go's DynamicSymbols() preserves the order (thus making the indexes match), but removes the first (null) dynsyn, 56 | Type: elf.ST_TYPE(edynsym.Info), 57 | Demangled: v, 58 | }) 59 | } 60 | 61 | if skipPLTGOT { 62 | return dynsyms, nil 63 | } 64 | 65 | // for each of the dynamic symbols with R_ARM_JUMP_SLOT relocations, add the 66 | // info from the decoded PLT 67 | pltrels, err := decpltrel(e) 68 | if err != nil { 69 | return nil, fmt.Errorf("read plt relocs: %w", err) 70 | } 71 | pltrelidx := map[uint32]elf.Rel32{} // map the symbol index to the reloc for faster access 72 | for _, pltrel := range pltrels { 73 | pltrelidx[elf.R_SYM32(pltrel.Info)] = pltrel 74 | } 75 | pltents, err := decplt(e) 76 | if err != nil { 77 | return nil, fmt.Errorf("decode plt: %w", err) 78 | } 79 | pltentidx := map[uint32]pltent{} // map the GOT offset to the PLT entry for faster access 80 | for _, pltent := range pltents { 81 | pltentidx[pltent.GOTOffset] = pltent 82 | } 83 | for _, s := range dynsyms { 84 | if s.Type != elf.STT_FUNC { 85 | // only functions will be in the PLT/GOT 86 | continue 87 | } 88 | 89 | // find the GOT offset 90 | pltrel, ok := pltrelidx[s.Index] 91 | if !ok { 92 | // the dynamic symbol may not have a PLT entry 93 | continue 94 | } 95 | if elf.R_ARM(elf.R_TYPE32(pltrel.Info)) != elf.R_ARM_JUMP_SLOT { 96 | // https://static.docs.arm.com/ihi0044/g/aaelf32.pdf: R_ARM_JUMP_SLOT is 97 | // used to mark code targets that will be executed. On platforms that 98 | // support dynamic binding the relocations may be performed lazily on 99 | // demand. The unresolved address stored in the place will initially 100 | // point to the entry sequence stub for the dynamic linker and must be 101 | // adjusted during initial loading by the offset of the load address of 102 | // the segment from its link address. Addresses stored in the place of 103 | // these relocations may not be used for pointer comparison until the 104 | // relocation has been resolved. In a REL form of this relocation the 105 | // addend, A, is always 0. single entry in the GOT. 106 | // 107 | // non-jump slot entries won't be in the decoded PLT/GOT mapping 108 | continue 109 | } 110 | s.OffsetGOT = pltrel.Off 111 | 112 | // find the PLT entry referencing the GOT entry. 113 | pltent, ok := pltentidx[pltrel.Off] 114 | if !ok { 115 | // the GOT entry may not have a PLT one referencing it (this shouldn't happen, but it's not an error if it does) 116 | continue 117 | } 118 | s.OffsetPLT = pltent.PLTOffset 119 | s.OffsetPLTTail = pltent.PLTOffsetTail 120 | } 121 | return dynsyms, nil 122 | } 123 | 124 | func decpltrel(e *elf.File) ([]elf.Rel32, error) { 125 | if e.Class != elf.ELFCLASS32 && e.Machine != elf.EM_ARM { 126 | return nil, fmt.Errorf("not a 32-bit arm elf") 127 | } 128 | relplt := e.Section(".rel.plt") 129 | if relplt == nil { 130 | return nil, fmt.Errorf("read .rel.plt: no such section") 131 | } 132 | r := relplt.Open() 133 | var pltrels []elf.Rel32 134 | for { 135 | var rel elf.Rel32 136 | err := binary.Read(r, e.ByteOrder, &rel) 137 | pltrels = append(pltrels, rel) 138 | if err != nil { 139 | if err == io.EOF { 140 | break 141 | } 142 | return nil, fmt.Errorf("read .rel.plt: %w", err) 143 | } 144 | } 145 | return pltrels, nil 146 | } 147 | 148 | type pltent struct { 149 | PLTOffset uint32 150 | PLTOffsetTail uint32 151 | GOTOffset uint32 152 | } 153 | 154 | func decplt(e *elf.File) ([]pltent, error) { 155 | if e.Class != elf.ELFCLASS32 && e.Machine != elf.EM_ARM { 156 | return nil, fmt.Errorf("not a 32-bit arm elf") 157 | } 158 | // https://stackoverflow.com/a/32808179 159 | plt := e.Section(".plt") 160 | if plt == nil { 161 | return nil, fmt.Errorf("read .plt: no such section") 162 | } 163 | buf, err := plt.Data() 164 | if err != nil { 165 | return nil, fmt.Errorf("read .plt: %w", err) 166 | } 167 | got := e.Section(".got") 168 | if got == nil { 169 | return nil, fmt.Errorf("read .got: no such section") 170 | } 171 | var asmbufo []uint32 172 | var asmbufi []armasm.Inst 173 | var asmbuftail uint32 // non-zero offset of tail call in PLT if it exists for the current entry in asmbuf* 174 | var pltents []pltent 175 | pc := uint32(plt.Offset) 176 | for len(buf) != 0 { 177 | if len(asmbufi) != len(asmbufo) { 178 | panic("len(asmbufi) != len(asmbufo)") 179 | } 180 | 181 | // read the next inst 182 | t, err := armasm.Decode(buf, armasm.ModeARM) 183 | if err != nil { 184 | if len(buf) >= 4 && buf[0] == 0x78 && buf[1] == 0x47 && buf[2] == 0xC0 && buf[3] == 0x46 { 185 | // Thumb: bx pc mov r8, r8 186 | asmbuftail = pc 187 | buf = buf[4:] 188 | pc += 4 189 | continue 190 | } 191 | // probably a different thumb instruction, so skip it (note: even if 192 | // there happens to be another thumb inst (from part of a tail call 193 | // stub or something similar) which is valid arm, it won't cause 194 | // issues due to the error checking later) 195 | buf = buf[2:] 196 | pc += 2 // some thumb insts are 4 long, but that's fine 197 | continue 198 | } 199 | asmbufo, asmbufi = append(asmbufo, pc), append(asmbufi, t) 200 | buf = buf[t.Len:] 201 | pc += uint32(t.Len) 202 | 203 | // we only do the processing at each LDR 204 | if t.Op != armasm.LDR { 205 | // if there's more than 8 instructions (just an arbitrary number) in 206 | // the buffer, something's very wrong (there really should be one at 207 | // most every 3rd instruction) 208 | if len(asmbufi) > 8 { 209 | return nil, fmt.Errorf("parse .plt: at 0x%X: expected LDR instruction somewhere, cur %+q", pc, asmbufi) 210 | } 211 | // decode the next inst 212 | continue 213 | } 214 | 215 | // each PLT entry should look like 2 ADD instructions then a LDR 216 | // (technically, it could be different, but this is what the ARM arch 217 | // guide suggests, and everything I've seen follows it; we also don't 218 | // want to have to implement a full emulator) 219 | // https://static.docs.arm.com/ihi0044/g/aaelf32.pdf 220 | if len(asmbufi) != 3 || asmbufi[0].Op != armasm.ADD || asmbufi[1].Op != armasm.ADD || asmbufi[2].Op != armasm.LDR { 221 | // discard the junk at the start of the PLT 222 | if len(pltents) == 0 { 223 | // if we're more than 32 bytes (just an arbitrary number) into 224 | // the plt, we have more junk than expected 225 | if pc-uint32(plt.Offset) > 32 { 226 | return nil, fmt.Errorf("parse .plt: at 0x%X: more than 32 bytes of junk at start of PLT, cur %+q", pc, asmbufi) 227 | } 228 | // reset the buffer 229 | asmbufo, asmbufi, asmbuftail = nil, nil, 0 230 | continue 231 | } 232 | return nil, fmt.Errorf("parse .plt: at 0x%X: expected 2 ADD instructions before each LDR, got %+q", pc, asmbufi) 233 | } 234 | 235 | // calculate the got offset by doing a basic emulation of the insts 236 | reg := map[armasm.Reg]uint32{ 237 | armasm.PC: 0, 238 | armasm.R12: 0, 239 | // other registers shouldn't be used 240 | } 241 | for n, inst := range asmbufi { 242 | instpc := asmbufo[n] 243 | reg[armasm.PC] = instpc + 8 // https://static.docs.arm.com/ddi0406/c/DDI0406C_C_arm_architecture_reference_manual.pdf: In ARM state, the value of the PC is the address of the current instruction plus 8 bytes 244 | switch inst.Op { 245 | case armasm.ADD: 246 | var dst armasm.Reg 247 | var new uint32 248 | for i, arg := range inst.Args { 249 | switch i { 250 | case 0: 251 | if arg != armasm.R12 { 252 | return nil, fmt.Errorf("parse .plt: at 0x%X: parse entry %+q: emulate inst %s at 0x%X: arg %d: expected dest register for add to be ip (r12)", pc, asmbufi, inst, instpc, i+1) 253 | } 254 | dst = armasm.R12 255 | case 1, 2: 256 | switch v := arg.(type) { 257 | case armasm.Reg: 258 | if rv, ok := reg[v]; !ok { 259 | return nil, fmt.Errorf("parse .plt: at 0x%X: parse entry %+q: emulate inst %s at 0x%X: arg %d: unsupported register %s", pc, asmbufi, inst, instpc, i+1, v) 260 | } else { 261 | new += rv 262 | } 263 | case armasm.Imm: 264 | new += uint32(v) 265 | case armasm.ImmAlt: 266 | new += uint32(v.Imm()) 267 | default: 268 | return nil, fmt.Errorf("parse .plt: at 0x%X: parse entry %+q: emulate inst %s at 0x%X: arg %d: unsupported arg type for %#v", pc, asmbufi, inst, instpc, i+1, v) 269 | } 270 | case 3: 271 | if arg != nil { 272 | return nil, fmt.Errorf("parse .plt: at 0x%X: parse entry %+q: emulate inst %s at 0x%X: arg %d: expected nothing, got %#v", pc, asmbufi, inst, instpc, i+1, arg) 273 | } 274 | default: 275 | panic("armasm should have returned 4 args...") 276 | } 277 | } 278 | reg[dst] = new 279 | case armasm.LDR: 280 | var dst armasm.Reg 281 | var base armasm.Reg 282 | var sub bool 283 | var new uint32 284 | var fn func() 285 | for i, arg := range inst.Args { 286 | switch i { 287 | case 0: 288 | if arg != armasm.PC { 289 | return nil, fmt.Errorf("parse .plt: at 0x%X: parse entry %+q: emulate inst %s at 0x%X: arg %d: expected dest register for ldr to be pc", pc, asmbufi, inst, instpc, i+1) 290 | } 291 | dst = armasm.PC 292 | case 1: 293 | switch v := arg.(type) { 294 | case armasm.Mem: 295 | if _, ok := reg[v.Base]; !ok { 296 | return nil, fmt.Errorf("parse .plt: at 0x%X: parse entry %+q: emulate inst %s at 0x%X: arg %d: unsupported base register %s", pc, asmbufi, inst, instpc, i+1, v.Base) 297 | } else { 298 | base = v.Base 299 | } 300 | switch v.Mode { 301 | case armasm.AddrPostIndex: 302 | fn = func() { 303 | reg[dst] = reg[base] 304 | if sub { 305 | reg[base] = reg[base] - new 306 | } else { 307 | reg[base] = reg[base] + new 308 | } 309 | } 310 | case armasm.AddrPreIndex: 311 | fn = func() { 312 | if sub { 313 | reg[dst] = reg[base] - new 314 | reg[base] = reg[base] - new 315 | } else { 316 | reg[dst] = reg[base] + new 317 | reg[base] = reg[base] + new 318 | } 319 | } 320 | case armasm.AddrOffset: 321 | fn = func() { 322 | if sub { 323 | reg[dst] = reg[base] - new 324 | } else { 325 | reg[dst] = reg[base] + new 326 | } 327 | } 328 | default: 329 | return nil, fmt.Errorf("parse .plt: at 0x%X: parse entry %+q: emulate inst %s at 0x%X: arg %d: unsupported addressing mode for ldr arg", pc, asmbufi, inst, instpc, i+1) 330 | } 331 | if v.Sign != 0 { 332 | sub = false 333 | if v.Sign < 0 { 334 | sub = true 335 | } 336 | if rv, ok := reg[v.Index]; !ok { 337 | return nil, fmt.Errorf("parse .plt: at 0x%X: parse entry %+q: emulate inst %s at 0x%X: arg %d: unsupported index register %s", pc, asmbufi, inst, instpc, i+1, v.Index) 338 | } else { 339 | new = rv 340 | } 341 | switch v.Shift { 342 | case armasm.ShiftLeft: 343 | new = new << v.Count 344 | case armasm.ShiftRight: 345 | new = new >> v.Count 346 | default: 347 | return nil, fmt.Errorf("parse .plt: at 0x%X: parse entry %+q: emulate inst %s at 0x%X: arg %d: unsupported shift mode %s for ldr arg", pc, asmbufi, inst, instpc, i+1, v.Shift) 348 | } 349 | } else { 350 | if v.Offset < 0 { 351 | sub = true 352 | new = uint32(v.Offset * -1) 353 | } else { 354 | sub = false 355 | new = uint32(v.Offset) 356 | } 357 | } 358 | default: 359 | return nil, fmt.Errorf("parse .plt: at 0x%X: parse entry %+q: emulate inst %s at 0x%X: arg %d: unsupported arg type for %#v", pc, asmbufi, inst, instpc, i+1, v) 360 | } 361 | case 2, 3: 362 | if arg != nil { 363 | return nil, fmt.Errorf("parse .plt: at 0x%X: parse entry %+q: emulate inst %s at 0x%X: arg %d: expected nothing, got %#v", pc, asmbufi, inst, instpc, i+1, arg) 364 | } 365 | default: 366 | panic("armasm should have returned 4 args...") 367 | } 368 | } 369 | fn() 370 | default: 371 | panic("the opcode should have already been checked...") 372 | } 373 | } 374 | 375 | if reg[armasm.R12] < uint32(got.Offset) { 376 | return nil, fmt.Errorf("parse .plt: entry at 0x%X: emulated GOT offset (ip/r12) 0x%X before GOT at 0x%X - 0x%X (size: 0x%X)", asmbufo[0], reg[armasm.R12], got.Offset, got.Offset+got.Size, got.Size) 377 | } 378 | 379 | pltents = append(pltents, pltent{ 380 | PLTOffset: asmbufo[0], 381 | PLTOffsetTail: asmbuftail, // will be 0 if doesn't exist 382 | GOTOffset: reg[armasm.R12], 383 | }) 384 | 385 | // reset the buffer for the next entry 386 | asmbufo, asmbufi, asmbuftail = nil, nil, 0 387 | } 388 | seen := map[uint32]int{} 389 | for i, pltent := range pltents { 390 | if j, ok := seen[pltent.GOTOffset]; ok { 391 | return nil, fmt.Errorf("parse .plt: internal error: duplicate emulated got offsets (this is likely a bug) in entry %#v (prev: %#v)", pltent, pltents[j]) 392 | } 393 | seen[pltent.GOTOffset] = i 394 | } 395 | return pltents, nil 396 | } 397 | -------------------------------------------------------------------------------- /patchlib/syms_test.go: -------------------------------------------------------------------------------- 1 | package patchlib 2 | 3 | import ( 4 | "debug/elf" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestSyms(t *testing.T) { 10 | f, err := os.Open("./testdata/libnickel.so.1.0.0") 11 | if err != nil { 12 | panic(err) 13 | } 14 | defer f.Close() 15 | 16 | e, err := elf.NewFile(f) 17 | if err != nil { 18 | panic(err) 19 | } 20 | defer e.Close() 21 | 22 | dynsyms, err := decdynsym(e, false) 23 | if err != nil { 24 | t.Errorf("unexpected error: %v", err) 25 | } 26 | 27 | for i, s := range e.Sections { 28 | if s.Name == ".plt" { 29 | e.Sections = append(e.Sections[:i], e.Sections[i+1:]...) 30 | break 31 | } 32 | } 33 | if _, err := decdynsym(e, true); err != nil { 34 | t.Errorf("unexpected error decoding elf with corrupt plt with pltgot skipped: %v", err) 35 | } 36 | 37 | _ = dynsyms 38 | // TODO: test some random symbols and ensure they are correct 39 | } 40 | -------------------------------------------------------------------------------- /patchlib/testdata/libnickel.so.1.0.0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgaskin/kobopatch/f1a9de045e41752ae99702f05588843c45199bd8/patchlib/testdata/libnickel.so.1.0.0 -------------------------------------------------------------------------------- /patchlib/testdata/nickel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgaskin/kobopatch/f1a9de045e41752ae99702f05588843c45199bd8/patchlib/testdata/nickel -------------------------------------------------------------------------------- /tools/cssextract/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | 9 | "github.com/pgaskin/kobopatch/patchlib" 10 | ) 11 | 12 | func main() { 13 | if len(os.Args) != 2 { 14 | fmt.Fprintln(os.Stderr, "cssextract extracts zlib-compressed from a binary file") 15 | fmt.Fprintln(os.Stderr, "Usage: cssextract BINARY_FILE") 16 | os.Exit(1) 17 | } 18 | 19 | buf, err := ioutil.ReadFile(os.Args[1]) 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | pt := patchlib.NewPatcher(buf) 25 | 26 | z, err := pt.ExtractZlib() 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | f, err := os.Create("cssextract.out.css") 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | for _, zi := range z { 37 | fmt.Fprintf(f, "/* zlib stream: offset_hex(0x%X) offset_int32(%d) len_int32(%d) sha1(%x) */\n%s\n\n", zi.Offset, zi.Offset, len(zi.CSS), sha1.Sum([]byte(zi.CSS)), zi.CSS) 38 | } 39 | 40 | f.Close() 41 | os.Exit(0) 42 | } 43 | -------------------------------------------------------------------------------- /tools/kobopatch-apply/kobopatch-apply.go: -------------------------------------------------------------------------------- 1 | // Command kobopatch-apply applies a single patch file to a binary. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "strings" 9 | 10 | "github.com/pgaskin/kobopatch/patchfile" 11 | _ "github.com/pgaskin/kobopatch/patchfile/kobopatch" 12 | _ "github.com/pgaskin/kobopatch/patchfile/patch32lsb" 13 | "github.com/pgaskin/kobopatch/patchlib" 14 | "github.com/spf13/pflag" 15 | ) 16 | 17 | var version = "unknown" 18 | 19 | func errexit(format string, a ...interface{}) { 20 | fmt.Fprintf(os.Stderr, format, a...) 21 | os.Exit(1) 22 | } 23 | 24 | func main() { 25 | input := pflag.StringP("input", "i", "", "the file to patch (required)") 26 | patchFile := pflag.StringP("patch-file", "p", "", "the file containing the patches (required)") 27 | output := pflag.StringP("output", "o", "", "the file to write the patched output to (will be overwritten if exists) (required)") 28 | patchFormat := pflag.StringP("patch-format", "f", "kobopatch", fmt.Sprintf("the patch format (one of: %s)", strings.Join(patchfile.GetFormats(), ","))) 29 | verbose := pflag.BoolP("verbose", "v", false, "show verbose output from patchlib") 30 | help := pflag.BoolP("help", "h", false, "show this help text") 31 | pflag.Parse() 32 | 33 | if *help || pflag.NArg() != 0 { 34 | fmt.Fprintf(os.Stderr, "Usage: kobopatch-apply [OPTIONS]\n") 35 | fmt.Fprintf(os.Stderr, "\nVersion: %s\n\nOptions:\n", version) 36 | pflag.PrintDefaults() 37 | os.Exit(1) 38 | } 39 | 40 | if *input == "" || *patchFile == "" || *output == "" { 41 | errexit("Error: input, patch-file, and output flags are required. See --help for more info.\n") 42 | } 43 | 44 | if !sliceContains(patchfile.GetFormats(), *patchFormat) { 45 | errexit("Error: invalid format %s. See --help for more info.\n", *patchFormat) 46 | } 47 | 48 | if *verbose { 49 | patchfile.Log = func(format string, a ...interface{}) { 50 | fmt.Printf(format, a...) 51 | } 52 | } else { 53 | patchfile.Log = func(format string, a ...interface{}) {} 54 | } 55 | 56 | ps, err := patchfile.ReadFromFile(*patchFormat, *patchFile) 57 | if err != nil { 58 | errexit("Error: could not read patch file: %v\n", err) 59 | } 60 | 61 | err = ps.Validate() 62 | if err != nil { 63 | errexit("Error: could not validate patch file: %v\n", err) 64 | } 65 | 66 | buf, err := ioutil.ReadFile(*input) 67 | if err != nil { 68 | errexit("Error: could not read input file: %v\n", err) 69 | } 70 | 71 | pt := patchlib.NewPatcher(buf) 72 | 73 | err = ps.ApplyTo(pt) 74 | if err != nil { 75 | errexit("Error: could not apply patch file: %v\n", err) 76 | } 77 | 78 | f, err := os.Create(*output) 79 | if err != nil { 80 | errexit("Error: could not create output file: %v\n", err) 81 | } 82 | 83 | obuf := pt.GetBytes() 84 | n, err := f.Write(obuf) 85 | if err != nil { 86 | errexit("Error: could not write output file: %v\n", err) 87 | } else if n != len(obuf) { 88 | errexit("Error: could not create output file: could not finish writing all bytes to file\n") 89 | } 90 | 91 | fmt.Printf("Successfully patched '%s' using '%s' to '%s'\n", *input, *patchFile, *output) 92 | os.Exit(0) 93 | } 94 | 95 | func sliceContains(arr []string, v string) bool { 96 | for _, i := range arr { 97 | if i == v { 98 | return true 99 | } 100 | } 101 | return false 102 | } 103 | -------------------------------------------------------------------------------- /tools/symdump/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | 9 | "github.com/pgaskin/kobopatch/patchlib" 10 | ) 11 | 12 | func main() { 13 | if len(os.Args) != 2 { 14 | fmt.Fprintln(os.Stderr, "symdump dumps symbol addresses from an ARMv6+ 32-bit ELF executable") 15 | fmt.Fprintln(os.Stderr, "Usage: symdump BINARY_FILE") 16 | os.Exit(1) 17 | } 18 | 19 | buf, err := ioutil.ReadFile(os.Args[1]) 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | pt := patchlib.NewPatcher(buf) 25 | 26 | ds, err := pt.ExtractDynsyms(true) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | f, err := os.Create("symdump.out.json") 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | fmt.Fprintf(f, "[\n") 37 | for i, s := range ds { 38 | if i != 0 { 39 | fmt.Fprintf(f, ",\n") 40 | } 41 | buf, _ := json.Marshal(s) 42 | f.Write(buf) 43 | } 44 | fmt.Fprintf(f, "]\n") 45 | 46 | f.Close() 47 | os.Exit(0) 48 | } 49 | --------------------------------------------------------------------------------