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开始: * 为了找出 gc 如何使用,我们用 go run 执行一个 main package 中的 main 函数(这也是 Golang 中的约定):

$ 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

parseFile

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

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

ssa

function call in low level(calling convention)

关闭 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 <main.Call+85>
   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 <main.Test>
   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 <runtime.morestack_noctxt>
   0x000000000045262a <+90>:    jmp    0x4525d0 <main.Call>
End of assembler dump.

call

Go 函数 Hook 的实现

Monkey Patch

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

   0x48f8a0 <main.a>:   mov    QWORD PTR [rsp+0x8],0x0
   0x48f8a9 <main.a+9>: mov    QWORD PTR [rsp+0x8],0x1
   0x48f8b2 <main.a+18>:    ret

运行Hook后

   0x48f8a0 <main.a>:   movabs rdx,0x4cbeb8
   0x48f8aa <main.a+10>:    jmp    QWORD PTR [rdx]
   0x48f8ac <main.a+12>:    and    al,0x8
   0x48f8ae <main.a+14>:    add    DWORD PTR [rax],eax
   0x48f8b0 <main.a+16>:    add    BYTE PTR [rax],al
   0x48f8b2 <main.a+18>:    ret

跳转的函数

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

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

Gohook

Gohook Gohook 的实现 brahma-adshonor/gohook,通过作者在文章里的描述,“gohook 则通过把目标函数绝对地址压到栈上,再执行 ret 指令实现跳转”,代码生成:链接,这里会有判断,如果|from-to| < 2G 则使用relaive jump, 直接 jmp 到Hook函数。

jmp func_b 5 bytes 会插入到被Hook函数的入口,并且通过Trampoline的方式回到原函数。下图说明了原理:

ah 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 <main.a>:   mov    rcx,QWORD PTR fs:0xfffffffffffffff8
   0x4c79d9 <main.a+9>: cmp    rsp,QWORD PTR [rcx+0x10]
   0x4c79dd <main.a+13>:    jbe    0x4c7a2c <main.a+92>
   0x4c79df <main.a+15>:    sub    rsp,0x18
   0x4c79e3 <main.a+19>:    mov    QWORD PTR [rsp+0x10],rbp
   0x4c79e8 <main.a+24>:    lea    rbp,[rsp+0x10]
   0x4c79ed <main.a+29>:    mov    QWORD PTR [rsp+0x20],0x0
   0x4c79f6 <main.a+38>:    call   0x42b350 <runtime.printlock>
   0x4c79fb <main.a+43>:    lea    rax,[rip+0x42282]        # 0x509c84
   0x4c7a02 <main.a+50>:    mov    QWORD PTR [rsp],rax
   0x4c7a06 <main.a+54>:    mov    QWORD PTR [rsp+0x8],0x2
   0x4c7a0f <main.a+63>:    call   0x42bc90 <runtime.printstring>
   0x4c7a14 <main.a+68>:    call   0x42b3d0 <runtime.printunlock>
   0x4c7a19 <main.a+73>:    mov    QWORD PTR [rsp+0x20],0x1
   0x4c7a22 <main.a+82>:    mov    rbp,QWORD PTR [rsp+0x10]
   0x4c7a27 <main.a+87>:    add    rsp,0x18
   0x4c7a2b <main.a+91>:    ret
   0x4c7a2c <main.a+92>:    call   0x452960 <runtime.morestack_noctxt>
   0x4c7a31 <main.a+97>:    jmp    0x4c79d0 <main.a>
   0x4c7a33:    int3

执行 Hook 后

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

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

gdb-peda$ x/20i main.b
   0x4c7af0 <main.b>:   mov    rcx,QWORD PTR fs:0xfffffffffffffff8
   0x4c7af9 <main.b+9>: cmp    rsp,QWORD PTR [rcx+0x10]
   0x4c7afd <main.b+13>:    jbe    0x4c7b56 <main.b+102>
   0x4c7aff <main.b+15>:    sub    rsp,0x20
   0x4c7b03 <main.b+19>:    mov    QWORD PTR [rsp+0x18],rbp
   0x4c7b08 <main.b+24>:    lea    rbp,[rsp+0x18]
   0x4c7b0d <main.b+29>:    mov    QWORD PTR [rsp+0x28],0x0
   0x4c7b16 <main.b+38>:    call   0x42b350 <runtime.printlock>
   0x4c7b1b <main.b+43>:    lea    rax,[rip+0x42164]        # 0x509c86
   0x4c7b22 <main.b+50>:    mov    QWORD PTR [rsp],rax
   0x4c7b26 <main.b+54>:    mov    QWORD PTR [rsp+0x8],0x2
   0x4c7b2f <main.b+63>:    call   0x42bc90 <runtime.printstring>
   0x4c7b34 <main.b+68>:    call   0x42b3d0 <runtime.printunlock>
   0x4c7b39 <main.b+73>:    call   0x4c7b60 <main.c>
   0x4c7b3e <main.b+78>:    mov    rax,QWORD PTR [rsp]
   0x4c7b42 <main.b+82>:    mov    QWORD PTR [rsp+0x10],rax
   0x4c7b47 <main.b+87>:    mov    QWORD PTR [rsp+0x28],rax
   0x4c7b4c <main.b+92>:    mov    rbp,QWORD PTR [rsp+0x18]
   0x4c7b51 <main.b+97>:    add    rsp,0x20
   0x4c7b55 <main.b+101>:   ret

Reference