├── .gitignore ├── LICENSE ├── README.md ├── disasm.go ├── disasm_386.go ├── disasm_amd64.go ├── disasm_arm.go ├── disasm_arm64.go ├── disasm_ppc64.go ├── disasm_ppc64le.go ├── go.mod ├── go.sum ├── hook.go ├── hook_386.go ├── hook_amd64.go ├── hook_test.go ├── memory.go ├── memory_unix.go ├── memory_windows.go ├── replace.go ├── replace_x86.go └── unsafe.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | *.exe 4 | *.dll 5 | *.so 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-2019 The hookingo Project 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hookingo # 2 | 3 | The name of this project reads like **hook in go** or **hooking go**, it is just an implementation of the "function hook" technology in golang. 4 | 5 | ### Usage 6 | ```shell script 7 | $ go get github.com/fengyoulin/hookingo 8 | ``` 9 | Import the hooking package: 10 | ```go 11 | import "github.com/fengyoulin/hookingo" 12 | ``` 13 | The document: 14 | ```go 15 | var ( 16 | // ErrDoubleHook means the function already hooked, you cannot hook it again. 17 | ErrDoubleHook = errors.New("double hook") 18 | // ErrHookNotFound means the hook not found in the applied hooks, maybe you 19 | // are trying to restore a corrupted hook. 20 | ErrHookNotFound = errors.New("hook not found") 21 | // ErrDifferentType means "from" and "to" are of different types, you should 22 | // only replace a function with one of the same type. 23 | ErrDifferentType = errors.New("inputs are of different type") 24 | // ErrInputType means either "from" or "to" are not func type, cannot apply. 25 | ErrInputType = errors.New("inputs are not func type") 26 | // ErrRelativeAddr means you cannot call the origin function with the hook 27 | // applied, try disable the hook, pay special attention to the concurrency. 28 | ErrRelativeAddr = errors.New("relative address in instruction") 29 | ) 30 | ``` 31 | 32 | #### type Enabler 33 | 34 | ```go 35 | type Enabler interface { 36 | Enable() 37 | } 38 | ``` 39 | 40 | Enabler is the interface the wraps the Enable method. 41 | 42 | Enable enables a hook which disabled by the Disable method of the Hook 43 | interface. This method will change the code in the text segment, so is 44 | not concurrent safe, need special attention. 45 | 46 | #### type Hook 47 | 48 | ```go 49 | type Hook interface { 50 | // Origin returns the original function, or an error if the original function 51 | // is not usable after the hook applied. 52 | Origin() interface{} 53 | // Disable temporarily disables the hook and restores the original function, the 54 | // hook can be enabled later using the returned Enabler. 55 | Disable() Enabler 56 | // Restore restores the original function permanently, if you want to enable the 57 | // hook again, you should use the Apply function later. 58 | Restore() error 59 | } 60 | ``` 61 | 62 | Hook represents an applied hook, it implements Origin, Disable and Restore. The 63 | Disable and Restore methods will change the code in the text segment, so are not 64 | concurrent safe, need special attention. 65 | 66 | #### func Apply 67 | 68 | ```go 69 | func Apply(from, to interface{}) (Hook, error) 70 | ``` 71 | Apply the hook, replace "from" with "to". This function will change the code in 72 | the text segment, so is not concurrent safe, need special attention. Some other 73 | goroutines may executing the code when you replacing it. 74 | 75 | ### Example 76 | ```go 77 | package main 78 | 79 | import ( 80 | "fmt" 81 | "github.com/fengyoulin/hookingo" 82 | ) 83 | 84 | func say1(n string) { 85 | fmt.Printf("Hello, %s!\n", n) 86 | } 87 | 88 | func say2(n string) { 89 | fmt.Printf("%s,你好!\n", n) 90 | } 91 | 92 | func disable(s string, h hookingo.Hook) { 93 | defer h.Disable().Enable() 94 | say1(s) 95 | } 96 | 97 | func main() { 98 | s := "Golang" 99 | // replace say1 with say2 100 | h, e := hookingo.Apply(say1, say2) 101 | if e != nil { 102 | panic(e) 103 | } 104 | say1(s) // 1st, hooked 105 | if f, ok := h.Origin().(func(string)); ok { 106 | f(s) // 2nd, try to call original say1 107 | } else if e, ok := h.Origin().(error); ok { 108 | panic(e) 109 | } 110 | disable(s, h) // 3rd, temporary disable hook 111 | say1(s) // 4th, enabled again 112 | // restore say1 113 | e = h.Restore() 114 | if e != nil { 115 | panic(e) 116 | } 117 | say1(s) // 5th, restored 118 | } 119 | ``` 120 | Build the example with gcflags to prevent inline optimization: 121 | ```shell script 122 | go build -gcflags '-l' 123 | ``` 124 | The example should output: 125 | ```shell script 126 | Golang,你好! 127 | Hello, Golang! 128 | Hello, Golang! 129 | Golang,你好! 130 | Hello, Golang! 131 | ``` -------------------------------------------------------------------------------- /disasm.go: -------------------------------------------------------------------------------- 1 | package hookingo 2 | 3 | // info of one instruction 4 | type info struct { 5 | length int 6 | relocatable bool 7 | } 8 | 9 | func ensureLength(src []byte, size int) (info, error) { 10 | var inf info 11 | inf.relocatable = true 12 | for inf.length < size { 13 | i, err := analysis(src) 14 | if err != nil { 15 | return inf, err 16 | } 17 | inf.relocatable = inf.relocatable && i.relocatable 18 | inf.length += i.length 19 | src = src[i.length:] 20 | } 21 | return inf, nil 22 | } 23 | -------------------------------------------------------------------------------- /disasm_386.go: -------------------------------------------------------------------------------- 1 | package hookingo 2 | 3 | import ( 4 | "golang.org/x/arch/x86/x86asm" 5 | ) 6 | 7 | func analysis(src []byte) (inf info, err error) { 8 | inst, err := x86asm.Decode(src, 32) 9 | if err != nil { 10 | return 11 | } 12 | inf.length = inst.Len 13 | inf.relocatable = true 14 | for _, a := range inst.Args { 15 | if mem, ok := a.(x86asm.Mem); ok { 16 | if mem.Base == x86asm.EIP { 17 | inf.relocatable = false 18 | return 19 | } 20 | } else if _, ok := a.(x86asm.Rel); ok { 21 | inf.relocatable = false 22 | return 23 | } 24 | } 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /disasm_amd64.go: -------------------------------------------------------------------------------- 1 | package hookingo 2 | 3 | import ( 4 | "golang.org/x/arch/x86/x86asm" 5 | ) 6 | 7 | func analysis(src []byte) (inf info, err error) { 8 | inst, err := x86asm.Decode(src, 64) 9 | if err != nil { 10 | return 11 | } 12 | inf.length = inst.Len 13 | inf.relocatable = true 14 | for _, a := range inst.Args { 15 | if mem, ok := a.(x86asm.Mem); ok { 16 | if mem.Base == x86asm.RIP { 17 | inf.relocatable = false 18 | return 19 | } 20 | } else if _, ok := a.(x86asm.Rel); ok { 21 | inf.relocatable = false 22 | return 23 | } 24 | } 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /disasm_arm.go: -------------------------------------------------------------------------------- 1 | package hookingo 2 | 3 | import ( 4 | "golang.org/x/arch/arm/armasm" 5 | ) 6 | 7 | func analysis(src []byte) (inf info, err error) { 8 | inst, err := armasm.Decode(src, armasm.ModeARM) 9 | if err != nil { 10 | return 11 | } 12 | inf.length = inst.Len 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /disasm_arm64.go: -------------------------------------------------------------------------------- 1 | package hookingo 2 | 3 | import ( 4 | "golang.org/x/arch/arm64/arm64asm" 5 | ) 6 | 7 | func analysis(src []byte) (inf info, err error) { 8 | _, err = arm64asm.Decode(src) 9 | if err != nil { 10 | return 11 | } 12 | inf.length = 4 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /disasm_ppc64.go: -------------------------------------------------------------------------------- 1 | package hookingo 2 | 3 | import ( 4 | "encoding/binary" 5 | "golang.org/x/arch/ppc64/ppc64asm" 6 | ) 7 | 8 | func analysis(src []byte) (inf info, err error) { 9 | inst, err := ppc64asm.Decode(src, binary.BigEndian) 10 | if err != nil { 11 | return 12 | } 13 | inf.length = inst.Len 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /disasm_ppc64le.go: -------------------------------------------------------------------------------- 1 | package hookingo 2 | 3 | import ( 4 | "encoding/binary" 5 | "golang.org/x/arch/ppc64/ppc64asm" 6 | ) 7 | 8 | func analysis(src []byte) (inf info, err error) { 9 | inst, err := ppc64asm.Decode(src, binary.LittleEndian) 10 | if err != nil { 11 | return 12 | } 13 | inf.length = inst.Len 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fengyoulin/hookingo 2 | 3 | go 1.12 4 | 5 | require golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4 6 | 7 | require golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 8 | 9 | replace golang.org/x/arch => github.com/golang/arch v0.0.0-20190927153633-4e8777c89be4 10 | 11 | replace golang.org/x/sys => github.com/golang/sys v0.0.0-20191026070338-33540a1f6037 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/golang/arch v0.0.0-20190927153633-4e8777c89be4 h1:MPm7H94GO7GK8xBHLK/pKpuDZcO6DxPd9WOa9zNMX78= 2 | github.com/golang/arch v0.0.0-20190927153633-4e8777c89be4/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= 3 | github.com/golang/sys v0.0.0-20191026070338-33540a1f6037 h1:Rp0Q8jDgLqfiM7P4gKhr+B+YJoMZb/5NcfaasR3CGe4= 4 | github.com/golang/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 5 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 6 | -------------------------------------------------------------------------------- /hook.go: -------------------------------------------------------------------------------- 1 | package hookingo 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "sync" 7 | "unsafe" 8 | ) 9 | 10 | // Enabler is the interface the wraps the Enable method. 11 | // 12 | // Enable enables a hook which disabled by the Disable method of the Hook interface. 13 | // This method will change the code in the text segment, so is not concurrent safe, 14 | // need special attention. 15 | type Enabler interface { 16 | Enable() 17 | } 18 | 19 | // Hook represents an applied hook, it implements Origin, Disable and Restore. The 20 | // Disable and Restore methods will change the code in the text segment, so are not 21 | // concurrent safe, need special attention. 22 | type Hook interface { 23 | // Origin returns the original function, or an error if the original function 24 | // is not usable after the hook applied. 25 | Origin() interface{} 26 | // Disable temporarily disables the hook and restores the original function, the 27 | // hook can be enabled later using the returned Enabler. 28 | Disable() Enabler 29 | // Restore restores the original function permanently, if you want to enable the 30 | // hook again, you should use the Apply function later. 31 | Restore() error 32 | } 33 | 34 | type hook struct { 35 | // the modified instructions 36 | target []byte 37 | // the moved and jump back instructions 38 | jumper []byte 39 | // use to call the origin function 40 | origin interface{} 41 | // backup the jump to instructions when disabled 42 | backup []byte 43 | } 44 | 45 | func (h *hook) Origin() interface{} { 46 | return h.origin 47 | } 48 | 49 | func (h *hook) Restore() error { 50 | return remove(slicePtr(h.target)) 51 | } 52 | 53 | func (h *hook) Disable() Enabler { 54 | disable(h) 55 | return &enabler{h: h} 56 | } 57 | 58 | type enabler struct { 59 | h *hook 60 | } 61 | 62 | func (e *enabler) Enable() { 63 | enable(e.h) 64 | } 65 | 66 | var ( 67 | // hooks applied with target addresses as keys 68 | hooks map[uintptr]*hook 69 | // protect the hooks map 70 | lock sync.Mutex 71 | ) 72 | 73 | var ( 74 | // ErrDoubleHook means the function already hooked, you cannot hook it again. 75 | ErrDoubleHook = errors.New("double hook") 76 | // ErrHookNotFound means the hook not found in the applied hooks, maybe you 77 | // are trying to restore a corrupted hook. 78 | ErrHookNotFound = errors.New("hook not found") 79 | // ErrDifferentType means "from" and "to" are of different types, you should 80 | // only replace a function with one of the same type. 81 | ErrDifferentType = errors.New("inputs are of different type") 82 | // ErrInputType means either "from" or "to" are not func type, cannot apply. 83 | ErrInputType = errors.New("inputs are not func type") 84 | // ErrRelativeAddr means you cannot call the origin function with the hook 85 | // applied, try disable the hook, pay special attention to the concurrency. 86 | ErrRelativeAddr = errors.New("relative address in instruction") 87 | ) 88 | 89 | func init() { 90 | hooks = make(map[uintptr]*hook) 91 | } 92 | 93 | // Apply the hook, replace "from" with "to". This function will change the code in 94 | // the text segment, so is not concurrent safe, need special attention. Some other 95 | // goroutines may executing the code when you replacing it. 96 | func Apply(from, to interface{}) (Hook, error) { 97 | vf := reflect.ValueOf(from) 98 | vt := reflect.ValueOf(to) 99 | if vf.Type() != vt.Type() { 100 | return nil, ErrDifferentType 101 | } 102 | if vf.Kind() != reflect.Func { 103 | return nil, ErrInputType 104 | } 105 | e := (*eface)(unsafe.Pointer(&from)) 106 | return apply(vf.Pointer(), vt.Pointer(), e.typ) 107 | } 108 | 109 | func apply(from, to uintptr, typ unsafe.Pointer) (*hook, error) { 110 | lock.Lock() 111 | defer lock.Unlock() 112 | _, ok := hooks[from] 113 | if ok { 114 | return nil, ErrDoubleHook 115 | } 116 | // early object allocation 117 | // we may hooking runtime.mallocgc 118 | // or may be runtime.newobject 119 | f := &funcval{} 120 | // early bucket allocation 121 | hooks[from] = nil 122 | h, err := applyHook(from, to) 123 | if err != nil { 124 | delete(hooks, from) // delete on failure 125 | return nil, err 126 | } 127 | if h.origin == nil { 128 | f.fn = slicePtr(h.jumper) 129 | e := (*eface)(unsafe.Pointer(&h.origin)) 130 | e.data = unsafe.Pointer(f) 131 | e.typ = typ 132 | } 133 | // just set value here, should not alloc memory 134 | hooks[from] = h 135 | return h, nil 136 | } 137 | 138 | func remove(from uintptr) error { 139 | lock.Lock() 140 | defer lock.Unlock() 141 | h, ok := hooks[from] 142 | if ok { 143 | copy(h.target, h.jumper) 144 | freeJumper(h.jumper) 145 | h.jumper = nil 146 | h.target = nil 147 | h.origin = nil 148 | h.backup = nil 149 | delete(hooks, from) 150 | return nil 151 | } 152 | return ErrHookNotFound 153 | } 154 | 155 | func disable(h *hook) { 156 | if h.backup == nil { 157 | b := make([]byte, len(h.target)) 158 | copy(b, h.target) 159 | h.backup = b 160 | } 161 | copy(h.target, h.jumper) 162 | } 163 | 164 | func enable(h *hook) { 165 | copy(h.target, h.backup) 166 | } 167 | -------------------------------------------------------------------------------- /hook_386.go: -------------------------------------------------------------------------------- 1 | package hookingo 2 | 3 | const ( 4 | jumperSize = 32 5 | ) 6 | 7 | func applyHook(from, to uintptr) (*hook, error) { 8 | src := makeSlice(from, 32) 9 | inf, err := ensureLength(src, 7) 10 | if err != nil { 11 | return nil, err 12 | } 13 | err = protectPages(from, uintptr(inf.length)) 14 | if err != nil { 15 | return nil, err 16 | } 17 | dst, err := allocJumper() 18 | if err != nil { 19 | return nil, err 20 | } 21 | // early object allocation 22 | hk := &hook{} 23 | src = makeSlice(from, uintptr(inf.length)) 24 | copy(dst, src) 25 | addr := from + uintptr(inf.length) 26 | inst := []byte{ 27 | 0x50, // PUSH EAX 28 | 0x50, // PUSH EAX 29 | 0xb8, // MOV EAX, addr 30 | byte(addr), byte(addr >> 8), // . 31 | byte(addr >> 16), byte(addr >> 24), // . 32 | 0x89, 0x44, 0x24, 0x04, // MOV [ESP+4], EAX 33 | 0x58, // POP EAX 34 | 0xc3, // RET 35 | } 36 | ret := makeSlice(slicePtr(dst)+uintptr(inf.length), uintptr(len(dst)-inf.length)) 37 | copy(ret, inst) 38 | for i := inf.length + len(inst); i < len(dst); i++ { 39 | dst[i] = 0xcc 40 | } 41 | addr = to 42 | inst = []byte{ 43 | 0xb8, // MOV EAX, addr 44 | byte(addr), byte(addr >> 8), // . 45 | byte(addr >> 16), byte(addr >> 24), // . 46 | 0xff, 0xe0, // JMP EAX 47 | } 48 | copy(src, inst) 49 | for i := len(inst); i < len(src); i++ { 50 | src[i] = 0xcc 51 | } 52 | hk.target = src 53 | hk.jumper = dst 54 | if !inf.relocatable { 55 | hk.origin = ErrRelativeAddr 56 | } 57 | return hk, nil 58 | } 59 | -------------------------------------------------------------------------------- /hook_amd64.go: -------------------------------------------------------------------------------- 1 | package hookingo 2 | 3 | const ( 4 | jumperSize = 64 5 | ) 6 | 7 | func applyHook(from, to uintptr) (*hook, error) { 8 | src := makeSlice(from, 32) 9 | inf, err := ensureLength(src, 19) 10 | if err != nil { 11 | return nil, err 12 | } 13 | err = protectPages(from, uintptr(inf.length)) 14 | if err != nil { 15 | return nil, err 16 | } 17 | dst, err := allocJumper() 18 | if err != nil { 19 | return nil, err 20 | } 21 | // early object allocation 22 | hk := &hook{} 23 | src = makeSlice(from, uintptr(inf.length)) 24 | copy(dst, src) 25 | addr := from + uintptr(inf.length) 26 | inst := []byte{ 27 | 0x50, // PUSH RAX 28 | 0x50, // PUSH RAX 29 | 0x48, 0xb8, // MOV RAX, addr 30 | byte(addr), byte(addr >> 8), // . 31 | byte(addr >> 16), byte(addr >> 24), // . 32 | byte(addr >> 32), byte(addr >> 40), // . 33 | byte(addr >> 48), byte(addr >> 56), // . 34 | 0x48, 0x89, 0x44, 0x24, 0x08, // MOV [RSP+8], RAX 35 | 0x58, // POP RAX 36 | 0xc3, // RET 37 | } 38 | ret := makeSlice(slicePtr(dst)+uintptr(inf.length), uintptr(len(dst)-inf.length)) 39 | copy(ret, inst) 40 | for i := inf.length + len(inst); i < len(dst); i++ { 41 | dst[i] = 0xcc 42 | } 43 | addr = to 44 | inst = []byte{ 45 | 0x50, // PUSH RAX 46 | 0x50, // PUSH RAX 47 | 0x48, 0xb8, // MOV RAX, addr 48 | byte(addr), byte(addr >> 8), // . 49 | byte(addr >> 16), byte(addr >> 24), // . 50 | byte(addr >> 32), byte(addr >> 40), // . 51 | byte(addr >> 48), byte(addr >> 56), // . 52 | 0x48, 0x89, 0x44, 0x24, 0x08, // MOV [RSP+8], RAX 53 | 0x58, // POP RAX 54 | 0xc3, // RET 55 | } 56 | copy(src, inst) 57 | for i := len(inst); i < len(src); i++ { 58 | src[i] = 0xcc 59 | } 60 | hk.target = src 61 | hk.jumper = dst 62 | if !inf.relocatable { 63 | hk.origin = ErrRelativeAddr 64 | } 65 | return hk, nil 66 | } 67 | -------------------------------------------------------------------------------- /hook_test.go: -------------------------------------------------------------------------------- 1 | package hookingo_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fengyoulin/hookingo" 6 | "testing" 7 | ) 8 | 9 | func TestApply(t *testing.T) { 10 | s, err := func() (string, error) { 11 | h, err := hookingo.Apply(f1, f3) 12 | if err != nil { 13 | return "", err 14 | } 15 | defer func() { 16 | if h != nil { 17 | _ = h.Restore() 18 | } 19 | }() 20 | s := f2() 21 | o := h.Origin() 22 | if f, ok := o.(func() string); ok { 23 | s += f() 24 | } else if e, ok := o.(error); ok { 25 | return "", e 26 | } 27 | e := h.Disable() 28 | s += f2() 29 | e.Enable() 30 | s += f2() 31 | err = h.Restore() 32 | if err != nil { 33 | return "", err 34 | } 35 | h = nil 36 | s += f2() 37 | return s, nil 38 | }() 39 | if err != nil { 40 | t.Error(err) 41 | } else if s != "f2f3f1f2f1f2f3f2f1" { 42 | t.Error(s) 43 | } 44 | } 45 | 46 | func TestReplace(t *testing.T) { 47 | s, err := func() (string, error) { 48 | h, err := hookingo.Replace(f2, f1, f3, -1) 49 | if err != nil { 50 | return "", err 51 | } 52 | defer func() { 53 | if h != nil { 54 | h.Disable() 55 | } 56 | }() 57 | s := f2() 58 | s += f1() 59 | e := h.Disable() 60 | s += f2() 61 | e.Enable() 62 | s += f2() 63 | e = h.Disable() 64 | h = nil 65 | s += f2() 66 | return s, nil 67 | }() 68 | if err != nil { 69 | t.Error(err) 70 | } else if s != "f2f3f1f2f1f2f3f2f1" { 71 | t.Error(s) 72 | } 73 | } 74 | 75 | func f1() string { 76 | s := "f1" 77 | fmt.Print(s) 78 | return s 79 | } 80 | 81 | func f2() string { 82 | s := "f2" 83 | fmt.Print(s) 84 | return s + f1() 85 | } 86 | 87 | func f3() string { 88 | s := "f3" 89 | fmt.Print(s) 90 | return s 91 | } 92 | -------------------------------------------------------------------------------- /memory.go: -------------------------------------------------------------------------------- 1 | package hookingo 2 | 3 | var ( 4 | pageSize uintptr 5 | freePool [][]byte 6 | ) 7 | 8 | func allocJumper() ([]byte, error) { 9 | if len(freePool) == 0 { 10 | page, err := allocPage() 11 | if err != nil { 12 | return nil, err 13 | } 14 | for addr := page; addr < page+pageSize; addr += jumperSize { 15 | freePool = append(freePool, makeSlice(addr, jumperSize)) 16 | } 17 | } 18 | l := len(freePool) 19 | j := freePool[l-1] 20 | freePool = freePool[:l-1] 21 | return j, nil 22 | } 23 | 24 | func freeJumper(j []byte) { 25 | freePool = append(freePool, j) 26 | } 27 | -------------------------------------------------------------------------------- /memory_unix.go: -------------------------------------------------------------------------------- 1 | // +build aix darwin dragonfly freebsd linux netbsd openbsd solaris 2 | 3 | package hookingo 4 | 5 | import ( 6 | "golang.org/x/sys/unix" 7 | ) 8 | 9 | func init() { 10 | pageSize = uintptr(unix.Getpagesize()) 11 | } 12 | 13 | func allocPage() (uintptr, error) { 14 | data, err := unix.Mmap(-1, 0, int(pageSize), unix.PROT_EXEC|unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_ANONYMOUS) 15 | if err != nil { 16 | return 0, err 17 | } 18 | return slicePtr(data), nil 19 | } 20 | 21 | func protectPages(addr, size uintptr) error { 22 | start := pageSize * (addr / pageSize) 23 | length := pageSize * ((addr + size + pageSize - 1 - start) / pageSize) 24 | for i := uintptr(0); i < length; i += pageSize { 25 | data := makeSlice(start+i, pageSize) 26 | err := unix.Mprotect(data, unix.PROT_EXEC|unix.PROT_READ|unix.PROT_WRITE) 27 | if err != nil { 28 | return err 29 | } 30 | } 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /memory_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package hookingo 4 | 5 | import ( 6 | "golang.org/x/sys/windows" 7 | ) 8 | 9 | func init() { 10 | pageSize = uintptr(windows.Getpagesize()) 11 | } 12 | 13 | // allocate a new readable, writable and executable page 14 | func allocPage() (uintptr, error) { 15 | return windows.VirtualAlloc(0, pageSize, windows.MEM_COMMIT, windows.PAGE_EXECUTE_READWRITE) 16 | } 17 | 18 | // make the pages readable, writable and executable 19 | func protectPages(addr, size uintptr) error { 20 | start := pageSize * (addr / pageSize) 21 | length := pageSize * ((addr + size + pageSize - 1 - start) / pageSize) 22 | var old uint32 23 | for i := uintptr(0); i < length; i += pageSize { 24 | err := windows.VirtualProtect(start+i, pageSize, windows.PAGE_EXECUTE_READWRITE, &old) 25 | if err != nil { 26 | return err 27 | } 28 | } 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /replace.go: -------------------------------------------------------------------------------- 1 | package hookingo 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // HookCaller applies a group of hooks in the caller, by changing the relative 8 | // address in some call instructions. It is not concurrent safe, need special 9 | // attention. 10 | type HookCaller interface { 11 | // Disable disables the hooks and restores the calls to the original function, 12 | // the hook can be enabled later using the returned Enabler. 13 | Disable() Enabler 14 | // Count returns the total number of modified call instructions. If the hooks 15 | // are disabled, it returns 0. 16 | Count() int 17 | } 18 | 19 | type hookCaller struct { 20 | old uintptr 21 | new uintptr 22 | pos []uintptr 23 | dis bool 24 | } 25 | 26 | func (h *hookCaller) Disable() Enabler { 27 | setCall(h.pos, h.old) 28 | h.dis = true 29 | return &enableCaller{h: h} 30 | } 31 | 32 | func (h *hookCaller) Count() int { 33 | if h.dis { 34 | return 0 35 | } 36 | return len(h.pos) 37 | } 38 | 39 | type enableCaller struct { 40 | h *hookCaller 41 | } 42 | 43 | func (e *enableCaller) Enable() { 44 | setCall(e.h.pos, e.h.new) 45 | e.h.dis = false 46 | } 47 | 48 | // Replace the calls to "old" with "new" in caller, without modify any instructions in "old" 49 | func Replace(caller, old, new interface{}, limit int) (h HookCaller, err error) { 50 | vf := reflect.ValueOf(old) 51 | vt := reflect.ValueOf(new) 52 | if vf.Type() != vt.Type() { 53 | return nil, ErrDifferentType 54 | } 55 | if vf.Kind() != reflect.Func { 56 | return nil, ErrInputType 57 | } 58 | vc := reflect.ValueOf(caller) 59 | if vc.Kind() != reflect.Func { 60 | return nil, ErrInputType 61 | } 62 | as, err := findCall(vc.Pointer(), vf.Pointer(), limit) 63 | if err != nil { 64 | return nil, err 65 | } 66 | if err = batchProtect(as); err != nil { 67 | return nil, err 68 | } 69 | hk := &hookCaller{} 70 | setCall(as, vt.Pointer()) 71 | hk.pos = as 72 | hk.old = vf.Pointer() 73 | hk.new = vt.Pointer() 74 | return hk, nil 75 | } 76 | -------------------------------------------------------------------------------- /replace_x86.go: -------------------------------------------------------------------------------- 1 | //go:build 386 || amd64 2 | // +build 386 amd64 3 | 4 | package hookingo 5 | 6 | import ( 7 | "golang.org/x/arch/x86/x86asm" 8 | "unsafe" 9 | ) 10 | 11 | func findCall(from, target uintptr, limit int) (addrs []uintptr, err error) { 12 | lmt := limit 13 | var off uintptr 14 | for { 15 | src := makeSlice(from+off, 32) 16 | var inst x86asm.Inst 17 | inst, err = x86asm.Decode(src, 64) 18 | if err != nil { 19 | return 20 | } 21 | if inst.Op == x86asm.CALL && inst.Len == 5 { 22 | if 0xe8 == *(*byte)(unsafe.Pointer(from + off)) { 23 | ta := from + off + 5 + uintptr(*(*int32)(unsafe.Pointer(from + off + 1))) 24 | if ta == target { 25 | addrs = append(addrs, from+off+1) 26 | lmt-- 27 | } 28 | } 29 | } else if inst.Op == x86asm.RET { 30 | break 31 | } 32 | off += uintptr(inst.Len) 33 | if lmt == 0 { 34 | break 35 | } 36 | } 37 | return 38 | } 39 | 40 | func batchProtect(addrs []uintptr) (err error) { 41 | for i := 0; i < len(addrs); i++ { 42 | if err = protectPages(addrs[i], 4); err != nil { 43 | return 44 | } 45 | } 46 | return nil 47 | } 48 | 49 | func setCall(addrs []uintptr, target uintptr) { 50 | for i := 0; i < len(addrs); i++ { 51 | addr := addrs[i] 52 | *(*int32)(unsafe.Pointer(addr)) = int32(target - (addr + 4)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /unsafe.go: -------------------------------------------------------------------------------- 1 | package hookingo 2 | 3 | import ( 4 | "reflect" 5 | "unsafe" 6 | ) 7 | 8 | type eface struct { 9 | typ unsafe.Pointer 10 | data unsafe.Pointer 11 | } 12 | 13 | type funcval struct { 14 | fn uintptr 15 | } 16 | 17 | func makeSlice(addr, size uintptr) (bs []byte) { 18 | sh := (*reflect.SliceHeader)(unsafe.Pointer(&bs)) 19 | sh.Data = addr 20 | sh.Len = int(size) 21 | sh.Cap = int(size) 22 | return 23 | } 24 | 25 | func slicePtr(bs []byte) uintptr { 26 | sh := (*reflect.SliceHeader)(unsafe.Pointer(&bs)) 27 | return sh.Data 28 | } 29 | --------------------------------------------------------------------------------