├── .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 |
--------------------------------------------------------------------------------