Golang Function in Low level and Hook

本文关注 Golang 中的函数在可执行文件中的构成形式,并分析两种 Go 函数 Hook 的实现。

How Go Compiled Source Code

Golang 提供了多个 Go 编译器:gccgo 和 gc( "Go compiler", 不是垃圾回收GC),gc 也就是默认提供的编译器,gccgo 使用 CPP 编写了 GCC 的前端(当然也会存在 GoLLVM)。

gc 的一些历史:Go 1.5 Release Notes

本文中将会使用 gc,gc 的源码在 src/cmd/compile/internal/gc 。这里要注意的是在 src/go/* 下同时存在诸如:go/parsergo/types 等与编译器没有关联的 package, 而 gofmt 等处理 Go 源码的工具会使用这些pacakge。

通过 src/cmd/compile/README.md 中的介绍,gc 在编译源码时的过程中共有4个步骤:

  1. Parsing
  2. Type-checking and AST transformations
  3. Generic SSA
  4. Generating machine code

Debug Compiler

整个编译过程在 src/cmd/compile/internal/gc/main.go 中一览无余(当然也非常复杂),我们从使用 gc开始:

$ go run -x main.go

...
.../go/1.13.4/libexec/pkg/tool/darwin_amd64/compile -o $WORK/b001/_pkg_.a ... ...  -pack -c=4 ./main.go
...
$WORK/b001/exe/main
Hello World
go tool compile main.go
dlv --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec "/usr/local/Cellar/go/1.13.4/libexec/pkg/tool/darwin_amd64/compile" -- main.go

GOSSAFUNC=main GOOS=linux GOARCH=amd64 go build -gcflags "-S" simple.go

生成的ssa.html中可以清楚的看到GO优化代码的过程。

function call in low level(calling convention)

golang.org 上的 Quick Guide: A Quick Guide to Go's Assembler 但是我觉得这个 slide 写的更详细: Go Functions In Assembly

关闭 inline 和优化可以使用 -l -N 参数

$ cat main.go
package test

func Test() { }
$go tool compile -l main.go
$go tool objdump main.o
TEXT %22%22.Test(SB) gofile../Users/alkene/go/src/call/main.go
  main.go:3		0x204			c3			RET
$cat main.go
package test

func Test() int { return 1337 }
$go tool compile -l main.go
$go tool objdump main.o
TEXT %22%22.Test(SB) gofile../Users/alkene/go/src/call/main.go
  main.go:3		0x228			48c744240839050000	MOVQ $0x539, 0x8(SP)
  main.go:3		0x231			c3			RET
$cat main.go
package test

func Test(x,y int ) int { return x - y }

TEXT %22%22.Test(SB) gofile../Users/alkene/go/src/call/main.go
  main.go:3		0x23c			488b442408		MOVQ 0x8(SP), AX
  main.go:3		0x241			488b4c2410		MOVQ 0x10(SP), CX
  main.go:3		0x246			4829c8			SUBQ CX, AX
  main.go:3		0x249			4889442418		MOVQ AX, 0x18(SP)
  main.go:3		0x24e			c3			RET
package test

func Test(x,y int) int { return x - y }

func Call() int { x := Test(1, 2); return x }

$go tool objdump main.o
TEXT %22%22.Test(SB) gofile../Users/alkene/go/src/call/main.go
  main.go:3		0x2c3			48c744241800000000	MOVQ $0x0, 0x18(SP)
  main.go:3		0x2cc			488b442408		MOVQ 0x8(SP), AX
  main.go:3		0x2d1			482b442410		SUBQ 0x10(SP), AX
  main.go:3		0x2d6			4889442418		MOVQ AX, 0x18(SP)
  main.go:3		0x2db			c3			RET

TEXT %22%22.Call(SB) gofile../Users/alkene/go/src/call/main.go
  main.go:5		0x2ee			65488b0c2500000000	MOVQ GS:0, CX		[5:9]R_TLS_LE
  main.go:5		0x2f7			483b6110		CMPQ 0x10(CX), SP
  main.go:5		0x2fb			7646			JBE 0x343
  main.go:5		0x2fd			4883ec28		SUBQ $0x28, SP
  main.go:5		0x301			48896c2420		MOVQ BP, 0x20(SP)
  main.go:5		0x306			488d6c2420		LEAQ 0x20(SP), BP
  main.go:5		0x30b			48c744243000000000	MOVQ $0x0, 0x30(SP)
  main.go:5		0x314			48c7042401000000	MOVQ $0x1, 0(SP)
  main.go:5		0x31c			48c744240802000000	MOVQ $0x2, 0x8(SP)
  main.go:5		0x325			e800000000		CALL 0x32a		[1:5]R_CALL:%22%22.Test
  main.go:5		0x32a			488b442410		MOVQ 0x10(SP), AX
  main.go:5		0x32f			4889442418		MOVQ AX, 0x18(SP)
  main.go:5		0x334			4889442430		MOVQ AX, 0x30(SP)
  main.go:5		0x339			488b6c2420		MOVQ 0x20(SP), BP
  main.go:5		0x33e			4883c428		ADDQ $0x28, SP
  main.go:5		0x342			c3			RET
  main.go:5		0x343			e800000000		CALL 0x348		[1:5]R_CALL:runtime.morestack_noctxt
  main.go:5		0x348			eba4			JMP %22%22.Call(SB)
$cat main.go

package main

func Test(x,y int) int { return x - y }

func Call() int { x := Test(1, 2); return x }

func main() {
    println(Call())
}

$go build -gcflags "-l -N" main.go
gdb-peda$ b main.main
Breakpoint 1 at 0x452630: file /root/go/src/test/main.go, line 7.
gdb-peda$ info functions main
All functions matching regular expression "main":

File /root/go/src/test/main.go:
void main.Call(int);
void main.Test(int, int, int);
void main.main();

File /usr/local/src/go/src/runtime/proc.go:
void runtime.main();
void runtime.main.func1();
void runtime.main.func2(bool *);
gdb-peda$
gdb-peda$ pdisass main.Call
Dump of assembler code for function main.Call:
   0x00000000004525d0 <+0>:	mov    rcx,QWORD PTR fs:0xfffffffffffffff8
   0x00000000004525d9 <+9>:	cmp    rsp,QWORD PTR [rcx+0x10]
   0x00000000004525dd <+13>:	jbe    0x452625 
   0x00000000004525df <+15>:	sub    rsp,0x28
   0x00000000004525e3 <+19>:	mov    QWORD PTR [rsp+0x20],rbp
   0x00000000004525e8 <+24>:	lea    rbp,[rsp+0x20]
   0x00000000004525ed <+29>:	mov    QWORD PTR [rsp+0x30],0x0
=> 0x00000000004525f6 <+38>:	mov    QWORD PTR [rsp],0x1
   0x00000000004525fe <+46>:	mov    QWORD PTR [rsp+0x8],0x2
   0x0000000000452607 <+55>:	call   0x4525b0 
   0x000000000045260c <+60>:	mov    rax,QWORD PTR [rsp+0x10]
   0x0000000000452611 <+65>:	mov    QWORD PTR [rsp+0x18],rax
   0x0000000000452616 <+70>:	mov    QWORD PTR [rsp+0x30],rax
   0x000000000045261b <+75>:	mov    rbp,QWORD PTR [rsp+0x20]
   0x0000000000452620 <+80>:	add    rsp,0x28
   0x0000000000452624 <+84>:	ret
   0x0000000000452625 <+85>:	call   0x44a100 
   0x000000000045262a <+90>:	jmp    0x4525d0 
End of assembler dump.

Go 函数 Hook 的实现

Monkey Patch

Monkey Patch 的实现 bouk/monkey, 作者通过分析 Go 编译后的汇编指令发现,在运行时可以通过计算原函数地址后,将要替代的函数的函数体指令覆写为跳转到 Hook 函数的地址,以达到 Hook 的效果。 patch的内容在:链接 覆写的代码在:链接 通过实现monkey.PatchGuard 链接, 可以做到在 Hook 函数内使用原函数。 运行 Hook 前:

   0x48f8a0 :	mov    QWORD PTR [rsp+0x8],0x0
   0x48f8a9 :	mov    QWORD PTR [rsp+0x8],0x1
   0x48f8b2 :	ret

运行Hook后

   0x48f8a0 :	movabs rdx,0x4cbeb8
   0x48f8aa :	jmp    QWORD PTR [rdx]
   0x48f8ac :	and    al,0x8
   0x48f8ae :	add    DWORD PTR [rax],eax
   0x48f8b0 :	add    BYTE PTR [rax],al
   0x48f8b2 :	ret

跳转的函数

gdb-peda$ x 0x4cbeb8
0x4cbeb8:	0x000000000048f8c0
gdb-peda$ x/10i 0x048f8c0
   0x48f8c0 :	mov    QWORD PTR [rsp+0x8],0x0
   0x48f8c9 :	mov    QWORD PTR [rsp+0x8],0x2
   0x48f8d2 :	ret
   0x48f8d3:	int3
...

作者很详细的将分析过程记录在:链接,但是由于需要对只读内存进行写入,需要使用 syscall mprotect 修改内存区域的保护属性。

Gohook

Gohook 的实现 brahma-adshonor/gohook,通过作者在文章里的描述,“gohook 则通过把目标函数绝对地址压到栈上,再执行 ret 指令实现跳转”,代码生成:链接,这里会有判断,如果|from-to| < 2G 则使用relaive jump, 直接 jmp 到Hook函数。 jmp func_b 5 bytes 会插入到被Hook函数的入口,并且通过Trampoline的方式回到原函数。下图说明了原理:

测试代码:

package main

import (
	"github.com/brahma-adshonor/gohook"
)

func a() int { println("a"); return 1 }
func b() int { println("b"); return c() }
func c() int {
	// long enough
	println("c")
	println("c")
	println("c")
	println("c")
	return 3
}

func main() {
	// src, dst,
	err := gohook.Hook(a, b, c)
	if err != nil {
		println(err)
		return
	}
	println(a())
}

执行结果:

b
a
1
gdb-peda$ x/20i a
   0x4c79d0 :	mov    rcx,QWORD PTR fs:0xfffffffffffffff8
   0x4c79d9 :	cmp    rsp,QWORD PTR [rcx+0x10]
   0x4c79dd :	jbe    0x4c7a2c 
   0x4c79df :	sub    rsp,0x18
   0x4c79e3 :	mov    QWORD PTR [rsp+0x10],rbp
   0x4c79e8 :	lea    rbp,[rsp+0x10]
   0x4c79ed :	mov    QWORD PTR [rsp+0x20],0x0
   0x4c79f6 :	call   0x42b350 
   0x4c79fb :	lea    rax,[rip+0x42282]        # 0x509c84
   0x4c7a02 :	mov    QWORD PTR [rsp],rax
   0x4c7a06 :	mov    QWORD PTR [rsp+0x8],0x2
   0x4c7a0f :	call   0x42bc90 
   0x4c7a14 :	call   0x42b3d0 
   0x4c7a19 :	mov    QWORD PTR [rsp+0x20],0x1
   0x4c7a22 :	mov    rbp,QWORD PTR [rsp+0x10]
   0x4c7a27 :	add    rsp,0x18
   0x4c7a2b :	ret
   0x4c7a2c :	call   0x452960 
   0x4c7a31 :	jmp    0x4c79d0 
   0x4c7a33:	int3

执行 Hook 后

gdb-peda$ x/20i a
   0x4c79d0 :	jmp    0x4c7a40 
   0x4c79d5 :	clc
   0x4c79d6 :	(bad)
   0x4c79d7 :	(bad)
gdb-peda$ x/20i main.c
   0x4c7b60 :	mov    rcx,QWORD PTR fs:0xfffffffffffffff8
   0x4c7b69 :	jmp    0x4c7a89 

由于在 main.b 中直接 call main.c 所以可以利用 main.cjmp 继续执行被Hook的 main.a

gdb-peda$ x/20i main.b
   0x4c7af0 :	mov    rcx,QWORD PTR fs:0xfffffffffffffff8
   0x4c7af9 :	cmp    rsp,QWORD PTR [rcx+0x10]
   0x4c7afd :	jbe    0x4c7b56 
   0x4c7aff :	sub    rsp,0x20
   0x4c7b03 :	mov    QWORD PTR [rsp+0x18],rbp
   0x4c7b08 :	lea    rbp,[rsp+0x18]
   0x4c7b0d :	mov    QWORD PTR [rsp+0x28],0x0
   0x4c7b16 :	call   0x42b350 
   0x4c7b1b :	lea    rax,[rip+0x42164]        # 0x509c86
   0x4c7b22 :	mov    QWORD PTR [rsp],rax
   0x4c7b26 :	mov    QWORD PTR [rsp+0x8],0x2
   0x4c7b2f :	call   0x42bc90 
   0x4c7b34 :	call   0x42b3d0 
   0x4c7b39 :	call   0x4c7b60 
   0x4c7b3e :	mov    rax,QWORD PTR [rsp]
   0x4c7b42 :	mov    QWORD PTR [rsp+0x10],rax
   0x4c7b47 :	mov    QWORD PTR [rsp+0x28],rax
   0x4c7b4c :	mov    rbp,QWORD PTR [rsp+0x18]
   0x4c7b51 :	add    rsp,0x20
   0x4c7b55 :	ret

Reference