├── .gitignore ├── LICENSE ├── README.md ├── main.go ├── patch.go ├── patcher └── patch.go └── test.bash /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # patch 2 | Hot-patching for Go! 3 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "debug/elf" 5 | "debug/gosym" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "path" 12 | "path/filepath" 13 | 14 | "github.com/EricLagergren/proc" 15 | ) 16 | 17 | const debug = false 18 | 19 | var ( 20 | obj = flag.String("obj", "", "object file to link against") 21 | pid = flag.Int("pid", -1, "pid of process to modify") 22 | oldSym = flag.String("old", "", "symbol to replace") 23 | newSym = flag.String("new", "", "replacement symbol") 24 | version = flag.Bool("version", false, "print the version of this program") 25 | ) 26 | 27 | func main() { 28 | log.SetFlags(log.Lshortfile) 29 | flag.Parse() 30 | 31 | if *pid == -1 { 32 | log.Fatalln("must set pid of process to edit") 33 | } 34 | 35 | if !path.IsAbs(*obj) { 36 | gopath := os.Getenv("GOPATH") 37 | *obj = filepath.Join(gopath, "pkg", "linux_amd64_dynlink", *obj) 38 | } 39 | 40 | if debug { 41 | fmt.Println(*pid) 42 | } 43 | 44 | ps := proc.NewProcess(*pid) 45 | 46 | exePath, err := ps.ExePath() 47 | if err != nil { 48 | log.Fatalln(err) 49 | } 50 | 51 | exe := open(exePath) 52 | oldAddr, err := findAddr(exe, *oldSym) 53 | if err != nil { 54 | log.Fatalln(err) 55 | } 56 | if err := exe.Close(); err != nil { 57 | log.Fatalln(err) 58 | } 59 | 60 | objFile := open(*obj) 61 | newAddr, err := findAddr(objFile, *newSym) 62 | if err != nil { 63 | log.Fatalln(err) 64 | } 65 | defer objFile.Close() 66 | 67 | pc := newPatch(oldAddr, newAddr, *pid, mmap(objFile)) 68 | if err := pc.attach(); err != nil { 69 | log.Fatalln(err) 70 | } 71 | if err := pc.patch(); err != nil { 72 | log.Fatalln(err) 73 | } 74 | if err := pc.detach(); err != nil { 75 | log.Fatalln(err) 76 | } 77 | } 78 | 79 | func findAddr(r io.ReaderAt, sym string) (uintptr, error) { 80 | file, err := elf.NewFile(r) 81 | if err != nil { 82 | return 0, err 83 | } 84 | defer file.Close() 85 | 86 | var ( 87 | textStart uint64 88 | symtab, pclntab []byte 89 | ) 90 | 91 | if sect := file.Section(".text"); sect != nil { 92 | textStart = sect.Addr 93 | } 94 | if sect := file.Section(".gosymtab"); sect != nil { 95 | if symtab, err = sect.Data(); err != nil { 96 | return 0, err 97 | } 98 | } 99 | if sect := file.Section(".gopclntab"); sect != nil { 100 | if pclntab, err = sect.Data(); err != nil { 101 | return 0, err 102 | } 103 | } 104 | 105 | tab, err := gosym.NewTable(symtab, gosym.NewLineTable(pclntab, textStart)) 106 | if err != nil { 107 | return 0, err 108 | } 109 | 110 | fn := tab.LookupFunc(sym) 111 | if fn == nil { 112 | return 0, fmt.Errorf("could not find symbol %q", sym) 113 | } 114 | return uintptr(fn.Entry), nil 115 | } 116 | 117 | func open(path string) *os.File { 118 | file, err := os.Open(path) 119 | if err != nil { 120 | log.Fatalln(err) 121 | } 122 | return file 123 | } 124 | -------------------------------------------------------------------------------- /patch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "unsafe" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | type patch struct { 13 | pid int // pid of process we want to patch 14 | addr uintptr // address of symbol to replace 15 | jmp [12]byte // MOVABS + JMP code 16 | old [12]byte // code from last patch 17 | obj []byte // mmaped, executable .so file 18 | } 19 | 20 | func newPatch(old, new uintptr, pid int, obj []byte) *patch { 21 | 22 | // MOVABSQ $addr, *%rax 23 | b := [12]byte{0x48, 0xb8} 24 | 25 | // Endian-specific 64-bit address. 26 | *(*uintptr)(unsafe.Pointer(&b[2])) = new 27 | 28 | // JMPQ *%rax 29 | b[10] = 0xff 30 | b[11] = 0xe0 31 | 32 | return &patch{ 33 | pid: pid, 34 | addr: old, 35 | jmp: b, 36 | obj: obj, 37 | } 38 | } 39 | 40 | func (p *patch) wait() error { 41 | var ws unix.WaitStatus 42 | _, err := unix.Wait4(p.pid, &ws, 0, nil) 43 | fmt.Println("ws", ws) 44 | fmt.Println(ws.Stopped()) 45 | return err 46 | } 47 | 48 | func (p *patch) attach() error { 49 | if err := unix.PtraceAttach(p.pid); err != nil { 50 | return err 51 | } 52 | return p.wait() 53 | } 54 | 55 | func (p *patch) detach() error { 56 | return unix.PtraceDetach(p.pid) 57 | } 58 | 59 | func (p *patch) patch() error { 60 | _, err := unix.PtracePeekData(p.pid, p.addr, p.old[:]) 61 | if err != nil { 62 | return err 63 | } 64 | _, err = unix.PtracePokeData(p.pid, p.addr, p.jmp[:]) 65 | return err 66 | } 67 | 68 | // take a file, save the current position, rewind to the beginning, 69 | // mmap the entire thing, then reset the position. 70 | func mmap(file *os.File) []byte { 71 | cur, err := file.Seek(0, os.SEEK_CUR) 72 | if err != nil { 73 | log.Fatalln(err) 74 | } 75 | if _, err := file.Seek(0, os.SEEK_SET); err != nil { 76 | log.Fatalln(err) 77 | } 78 | stat, err := file.Stat() 79 | if err != nil { 80 | log.Fatalln(err) 81 | } 82 | buf, err := unix.Mmap(int(file.Fd()), 0, int(stat.Size()), unix.PROT_EXEC|unix.PROT_WRITE|unix.PROT_READ, unix.MAP_PRIVATE) 83 | if err != nil { 84 | log.Fatalln(err) 85 | } 86 | _, err = file.Seek(cur, os.SEEK_SET) 87 | if err != nil { 88 | log.Fatalln(err) 89 | } 90 | return buf 91 | } 92 | -------------------------------------------------------------------------------- /patcher/patch.go: -------------------------------------------------------------------------------- 1 | // +build linux,amd64 2 | 3 | package patch 4 | 5 | import ( 6 | "errors" 7 | "log" 8 | "os" 9 | "reflect" 10 | "runtime" 11 | "unsafe" 12 | 13 | "golang.org/x/sys/unix" 14 | 15 | "github.com/EricLagergren/proc" 16 | ) 17 | 18 | // Patch is used to modify functions at runtime. 19 | type Patch struct { 20 | m proc.Map // Region we're writing to. 21 | jmp [12]byte // Buffer for our MOVABS + JMP code. 22 | old [12]byte // Code from last patch. 23 | fnAddr unsafe.Pointer // Address of first func we're patching. 24 | obj []byte // shared object file. 25 | } 26 | 27 | // NewPatch patches the fn using the provided .so file. 28 | // soFile must be the path to a .so file and addr must be the index of the 29 | // byte in the file where the function you want to patch with starts. 30 | // fn *must* be a function, *not* a method. 31 | func NewPatch(fn interface{}, soFile string, addr int) (*Patch, error) { 32 | 33 | val := reflect.ValueOf(fn) 34 | if val.Type().Kind() != reflect.Func { 35 | return nil, errors.New("fn must be a function") 36 | } 37 | 38 | // Address of function we want to hot patch. 39 | fnAddr := val.Pointer() 40 | 41 | m, ok := proc.Find(fnAddr) 42 | if !ok { 43 | return nil, errors.New("could not find proper mmapped region") 44 | } 45 | 46 | buf, err := mmap(soFile) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | // MOVABSQ $addr, *%rax 52 | b := [12]byte{0x48, 0xb8} 53 | 54 | // Endian-specific 64-bit address. 55 | *(*uintptr)(unsafe.Pointer(&b[2])) = 56 | uintptr(unsafe.Pointer(&buf[addr])) 57 | 58 | // JMPQ *%rax 59 | b[10] = 0xff 60 | b[11] = 0xe0 61 | 62 | p := &Patch{ 63 | jmp: b, 64 | m: m, 65 | // fnAddr should never go away because it's 66 | // a function and the GC shouldn't collect 67 | // functions :-) 68 | fnAddr: unsafe.Pointer(fnAddr), 69 | obj: buf, 70 | } 71 | runtime.SetFinalizer(p, (*Patch).Close) 72 | return p, nil 73 | } 74 | 75 | // Patch patches the function and returns any errors that occur. 76 | func (p *Patch) Patch() error { 77 | // Change the protections so we can write without segfaulting. 78 | err := p.m.Mprotect(unix.PROT_EXEC | unix.PROT_WRITE | unix.PROT_READ) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | // Reset the protections on the mapping. 84 | defer p.m.Mprotect(p.m.Perms) 85 | 86 | old := (*[12]byte)(p.fnAddr) 87 | 88 | // Swap out the 12 bytes at the beginning of 89 | // addr with our hand-crafted MOVABS and JMPQ. 90 | p.old, *old = *old, p.jmp 91 | return nil 92 | } 93 | 94 | // Unatch undos the previous patch and returns any errors that occur. 95 | func (p *Patch) Unpatch() error { 96 | err := p.m.Mprotect(unix.PROT_EXEC | unix.PROT_WRITE | unix.PROT_READ) 97 | if err != nil { 98 | return err 99 | } 100 | defer p.m.Mprotect(p.m.Perms) 101 | old := (*[12]byte)(p.fnAddr) 102 | p.jmp, *old = *old, p.old 103 | return nil 104 | } 105 | 106 | // Close releases the shared object file and invalidates the patch. 107 | func (p *Patch) Close() error { 108 | return unix.Munmap(p.obj) 109 | } 110 | 111 | func mmap(name string) ([]byte, error) { 112 | file, err := os.Open(name) 113 | if err != nil { 114 | return nil, err 115 | } 116 | defer file.Close() 117 | stat, err := file.Stat() 118 | if err != nil { 119 | return nil, err 120 | } 121 | return unix.Mmap(int(file.Fd()), 0, int(stat.Size()), unix.PROT_EXEC|unix.PROT_WRITE|unix.PROT_READ, unix.MAP_PRIVATE) 122 | } 123 | -------------------------------------------------------------------------------- /test.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | pid=7626 6 | 7 | ps -up $pid 8 | echo "" 9 | go build 10 | sudo -E ./patch -pid $pid -obj lib.so -new lib.Sum -old main.Sub 11 | echo "" 12 | ps -up $pid 13 | --------------------------------------------------------------------------------