本文转自https://bou.ke/blog/monkey-patching-in-go/
前提
使用go build -gcflags=-l来构建,禁用内联。
对于本文,假设架构是 64 位,并且使用的是基于 Unix 的操作系统,如 Mac OSX 或 Linux
具体实现
让我们看看以下代码在反汇编时会产生什么:
package main
func a() int { return 1 }
func main() {
print(a())
}通过Hopper编译和查看时,上面的代码会产生这样的汇编代码:

代码main.main开始,其中指令0x2010至0x2026设置堆栈
0x202a是函数调用main.a在0x2000简单地移动0x1到堆栈并返回。0x202f至0x2037再传给该值runtime.printint
函数值在 Go 中的工作原理
package main
import (
"fmt"
"unsafe"
)
func a() int { return 1 }
func main() {
f := a
fmt.Printf("0x%x\n", *(*uintptr)(unsafe.Pointer(&f)))
}我在第 11 行所做的是分配a给f,这意味着f()将调用a。 然后我使用unsafe Go 包直接读出f。 如果有学习过C语言,您可能希望f只是一个指向的函数指针a,从而打印出此代码0x2000(main.a我们在上面看到的位置)。
当我在我的机器上运行它时,我得到0x102c38,这是一个甚至不接近我们代码的地址!反汇编后,这就是上面第 11 行发生的情况:

这引用了一个叫做main.a.f的东西,当我们查看那个位置时,我们看到:

啊哈!main.a.f在0x102c38包含了 0x2000,这是main.a的位置。
似乎f不是指向函数的指针,而是指向函数指针的指针。让我们修改代码来弥补这一点。
package main
import (
"fmt"
"unsafe"
)
func a() int { return 1 }
func main() {
f := a
fmt.Printf("0x%x\n", **(**uintptr)(unsafe.Pointer(&f)))
}这将按0x2000预期打印。我们可以找到一个线索,说明为什么要按原样执行此操作。Go 函数值可以包含额外的信息,这就是闭包和绑定实例方法的实现方式。
让我们看看调用函数值是如何工作的。我会f在分配后更改要调用的代码。
package main
func a() int { return 1 }
func main() {
f := a
f()
}当我们拆开它时,我们得到以下信息:
main.a.f被加载到rdx,然后rdx点被加载到rbx,最后被调用。
函数值的地址总是被加载到rdx,被调用的代码可以使用它来加载它可能需要的任何额外信息。
此额外信息是指向绑定实例方法的实例和匿名函数的闭包的指针。
如果你想了解更多,我建议你拿出一个反汇编器并深入研究!
让我们使用我们新获得的知识在 Go 中实现monkey patching
在运行时替换函数
我们想要实现的是用下面的代码打印出2:
package main
func a() int { return 1 }
func b() int { return 2 }
func main() {
replace(a, b)
print(a())
}现在我们如何实现replace?我们需要修改函数a以跳转到b的代码而不是执行它自己的函数体。
本质上,我们需要加载b到rdx,然后跳转到rdx指向的位置
mov rdx, main.b.f ; 48 C7 C2 ?? ?? ?? ??
jmp [rdx] ; FF 22我已经将这些行在组装时生成的相应机器代码放在它旁边(您可以使用像这样的在线汇编器轻松地玩汇编)。编写将生成此代码的函数现在很简单,如下所示:
func assembleJump(f func() int) []byte {
funcVal := *(*uintptr)(unsafe.Pointer(&f))
return []byte{
0x48, 0xC7, 0xC2,
byte(funcval >> 0),
byte(funcval >> 8),
byte(funcval >> 16),
byte(funcval >> 24), // MOV rdx, funcVal
0xFF, 0x22, // JMP [rdx]
}
}我们现在实现了从a跳转到替换函数体b所需的一切!下面的代码试图将机器码直接复制到函数体的位置。
package main
import (
"syscall"
"unsafe"
)
func a() int { return 1 }
func b() int { return 2 }
func rawMemoryAccess(b uintptr) []byte {
return (*(*[0xFF]byte)(unsafe.Pointer(b)))[:]
}
func assembleJump(f func() int) []byte {
funcVal := *(*uintptr)(unsafe.Pointer(&f))
return []byte{
0x48, 0xC7, 0xC2,
byte(funcVal >> 0),
byte(funcVal >> 8),
byte(funcVal >> 16),
byte(funcVal >> 24), // MOV rdx, funcVal
0xFF, 0x22, // JMP [rdx]
}
}
func replace(orig, replacement func() int) {
bytes := assembleJump(replacement)
functionLocation := **(**uintptr)(unsafe.Pointer(&orig))
window := rawMemoryAccess(functionLocation)
copy(window, bytes)
}
func main() {
replace(a, b)
print(a())
}但是,运行此代码不起作用,并且会导致分段错误。这是因为加载的二进制文件默认是不可写的。
我们可以使用系统调用mprotect来禁用这种保护,而这个最终版本的代码正是这样做的,实现 functiona被functionb替换,并打印2。
package main
import (
"syscall"
"unsafe"
)
func a() int { return 1 }
func b() int { return 2 }
func getPage(p uintptr) []byte {
return (*(*[0xFFFFFF]byte)(unsafe.Pointer(p & ^uintptr(syscall.Getpagesize()-1))))[:syscall.Getpagesize()]
}
func rawMemoryAccess(b uintptr) []byte {
return (*(*[0xFF]byte)(unsafe.Pointer(b)))[:]
}
func assembleJump(f func() int) []byte {
funcVal := *(*uintptr)(unsafe.Pointer(&f))
return []byte{
0x48, 0xC7, 0xC2,
byte(funcVal >> 0),
byte(funcVal >> 8),
byte(funcVal >> 16),
byte(funcVal >> 24), // MOV rdx, funcVal
0xFF, 0x22, // JMP rdx
}
}
func replace(orig, replacement func() int) {
bytes := assembleJump(replacement)
functionLocation := **(**uintptr)(unsafe.Pointer(&orig))
window := rawMemoryAccess(functionLocation)
page := getPage(functionLocation)
syscall.Mprotect(page, syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC)
copy(window, bytes)
}
func main() {
replace(a, b)
print(a())
}封装成一个包
我把上面的代码放在一个库中。它支持 32 位、反向补丁和补丁实例方法。我写了几个例子并将它们放在自述文件中
