gomonkey 教程_go语言中文网

gomonkey 教程_go语言中文网如果你对monkey补丁是怎么工作的不感兴趣,你只是想使用它的话,你可以在这里找到对应的库文件让我们看看一下代码产生的汇编码:packagema

很多人认为 monkey 补丁只能在动态语言,比如 Ruby 和 Python 中才存在。但是,这并不对。因为计算机只是很笨的机器,我们总能让它做我们想让它做的事儿!让我们看看 Go 中的函数是怎么工作的,并且,我们如何在运行时修改它们。本文会用到大量的 Intel 汇编,所以,我假设你可以读汇编代码,或者在读本文时正拿着参考手册.

如果你对 monkey 补丁是怎么工作的不感兴趣,你只是想使用它的话,你可以在这里找到对应的库文件

让我们看看一下代码产生的汇编码:

package main
func a() int { return 1 }
func main() {
 print(a())
}

example1.go 由 GitHub 托管 查看源文件

上述代码应该用 go build -gcflags=-l 来编译,以避免内联。在本文中我假设你的电脑架构是 64 位,并且你使用的是一个基于unix 的操作系统比如 Mac OSX 或者某个 Linux 系统。

当代码编译后,我们用 Hopper 来查看,可以看到如上代码会产生如下汇编代码:

gomonkey 教程_go语言中文网

我将引用屏幕左侧显示的各种指令的地址。

我们的代码从 main.main 过程开始,从 0x2010 到 0x2026 的指令构建堆栈。你可以在这儿获得更多的相关知识,本文后续的篇幅里,我将忽略这部分代码。

0x202a 行是调用 0x2000 行的 main.a 函数,这个函数只是简单的将 0x1 压入堆栈然后就返回了。0x202f 到 0x2037这几行 将这个值传递给 runtime.printint.

足够简单!现在让我们看看在 Go 语言中 函数的值是怎么实现的。

Go 语言中的函数值是如何工作的

看看下面的代码:

package main
import (
 "fmt"
 "unsafe"
)
func a() int { return 1 }
func main() {
 f := a
 fmt.Printf("0x%x\n", *(*uintptr)(unsafe.Pointer(&f)))
}

funcval.go 由 GitHub 托管 查看源文件

我在第11行 将 a 赋值给 f,这意味者,执行 f() 就会调用 a。然后我使用 Go 中的 unsafe 包来直接读出 f 中存储的值。如果你是有 C 语言的开发背景 ,你可以会觉得 f 就是一个简单的函数指针,并且这段代码会输出 0x2000 (我们在上面看到的 main.a 的地址)。当我在我的机器上运行时,我得到的是 0x102c38, 这个地址甚至与我们的代码都不挨着!当反编译时,这就是上面第11行所对应的:

gomonkey 教程_go语言中文网

这里提到了一个 main.a.f,当我们查它的地址,我们可以看到这个:

gomonkey 教程_go语言中文网

啊哈!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)))
}

funcval2.go 由GitHub托管 查看源文件

现在输出的正是预期中的 0x2000。我们可以在这里找到一点为什么代码要这样写的线索。在 Go 语言中函数值可以包含额外的信息,闭包和绑定实例方法借此实现的。

让我们看看调用一个函数值是怎么工作的。我把上面的代码修改一下,在给 f 赋值后直接调用它。

package main
func a() int { return 1 }
func main() {
 f := a
 f()
}

callfuncval.go 由 GitHub 托管 查看源文件

当我们反编译后我们可以看到:

gomonkey 教程_go语言中文网

main.a.f 的地址被加载到 rdx,然后无论 rdx 指向啥都会被加载到 rbx 中,然后 rbx 会被调用。函数的地址都会被首先加载到 rdx 中,然后被调用的函数可以用来加载一些额外的可能用到的信息。对绑定实例方法和匿名闭包函数来说,额外的信息就是一个指向实例的指针。如果你希望了解更多,我建议你用反编译器自己深入研究下。

让我们用刚学到的知识在 Go 中实现 monkey 补丁。

运行期替换一个函数

我们希望做到的是,让下面的代码输出 2:

package main
func a() int { return 1 }
func b() int { return 2 }
func main() {
 replace(a, b)
 print(a())
}

replace.go 由GitHub托管 查看源文件

现在我们该怎么实现这种替换?我们需要修改函数 a 跳到 b 的代码,而不是执行它自己的函数体。本质上,我们需要这么替换,把 b 的函数值加载到 rdx 然后跳转到 rdx 所指向的地址。

mov rdx, main.b.f ; 48 C7 C2 ?? ?? ?? ??
jmp [rdx] ; FF 22

replacement.asm 由 GitHub 托管 查看源文件

我将上述代码编译后产生的对应的机器码列出来了(用在线编译器,比如这个,你可以随意尝试编译)。很明显,我们需要写一个能产生这样机器码的函数,它应该看起来像这样:

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]
}
}

assemble_jump.go 由 GitHub 托管 查看源文件

现在万事俱备,我们已经准备好将 a 的函数体替换为从 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())
}

patch_attempt.go 由 GitHub 托管 查看源文件

然而,运行上述代码并没有达到我们的目的,实际上,它会产生一个段错误。这是因为默认情况下,已经加载的二进制代码是不可写的。我们可以用 mprotect 系统调用来取消这个保护,并且这个最终版本的代码就像我们期望的一样,把函数 a 替换成了 b,然后 ‘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())
}

patch_success.go 由 GitHub 托管 查看源文件

包装成一个很好的库

我将上述代码包装为一个易用的库。它支持 32 位,取消补丁,以及对实例方法进行补丁,并且我在 README 中写了几个使用示例。

总结

有志者事竟成!让一个程序在运行期修改自身是可能的,这让我们可以实现一些很酷的事儿,比如 monkey 补丁。

我希望你从这边博客中有些收获,而我在写这篇文章时很开心!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/12052.html

(0)

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注