本文转自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 位、反向补丁和补丁实例方法。我写了几个例子并将它们放在自述文件中