golang_rev
# golang
写一些关于 <golang逆向> 的东西,主要是逆向的手段和对这个语言特性的一些demo,
<关闭编译优化> 使用go build -gcflags "-N -l" xxx.go
。
主要对于go的逆向,本文不会涉及关于一些go代码的编写。
参考:
Reversing GO binaries like a pro (opens new window)
# 相关结构:
# 字符串:
go语言中的字符串并未采用c风格的以'\x00'
结束,而是将所有的字符串连续的都是存在于一片内存内,且没有'\x00'
分割,相当于是一个巨大的字符串, 然后使用一个结构表示一个字符串:
type stringStruct struct {
str unsafe.Pointer
len int
}
2
3
4
使用指针和长度在连续的字符串中确定到指定的一个字符串,这样做有一定的好处,因为内存的切割复制操作会比较繁琐,使用这样的内存模型的话,字符串的分割不过就是重新设置指针和长度。
当然,ida识别会认为这是一个巨大的字符串,我们想要搜索的话, 要使用<alt+b>搜索字节,然后查找交叉引用,
这里牵扯到一个点:在go中所有和字符串有牵扯的位置,一般来说都会是指针+长度的内存模型。
当然在逆向中我们可以简单看到底层的这些指针,但是在go的正向代码中字符串这些内存模型等都是不可见,不会被暴露的。
# 语言特点:
# 简单io的识别:
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
user_input, _, err := bufio.NewReader(os.Stdin).ReadLine()
if err == nil {
fmt.Println(string(user_input))
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
逆向的话就两个函数的位置:
reader
这个函数会成这个样子,首先newreader,然后将返回值作为参数调用readline函数,最后这个函数接收输入数据,返回三个值,分别为error,输入数据,长度,
printf
这个函数大致是如此, 参数将会有指向字符串的指针和对应长度。
# 函数和参数传递:
go语言可以支持 <多个返回值> ,主要写下这个返回值和参数的传递,我们写一个简单的demo:
package main
import "fmt"
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("hello", "world")
fmt.Println(a, b)
}
2
3
4
5
6
7
8
9
10
11
12
其实一个函数的定义应该是func swap (string, string) (string, string)
, 后两个表示两个返回值,相当于c语言中void swap(string, string, string, string)
, 其中函数调用时首先传入 <参数个数+返回值个数> 个参数,返回值位置一般是0, 然后在函数内返回时会直接将返回值写入到传参时预留的返回值的空位里面,
go的函数调用前:
为ret2保留空位
为ret1保留空位
参数2
参数1 <-SP
2
3
4
我们同样可以看到我们写的retrun arg2, arg1
这个语句,编译为了arg2赋值到ret1, arg1赋值到ret2。 这就是go的函数调用和返回操作。
# 程序流程:
程序入口为应该是_rt0_amd64_linux
, 然后jmp到_rt0_amd64
,
_rt0_amd664
会jmp到runtime_rt0_go
, 这个函数可以反编译,前面经过一系列的初始化操作,然后到倒数第三个函数调用, runtime_newproc
, 这是创建了一个新的goroutine执行runtime_main
, 这次调用的参数为指向runtime_main
的指针,
runtime.newproc
会把runtime.main
的goroutine放到就绪线程队列里面。本线程继续执行, 到runtime.mstart
,runtime.mstart
会调用到调度函数schedule
。这个函数会根据当前线程队列中线程状态挑选一个来运行。由于当前只有这一个goroutine,它会被调度,然后就到了runtime.main
函数中来。
然后在runtime_main内又是一段初始化和检测, 在后段的一个逻辑于的if语句内,调用了main_main函数,并且其参数中还有一个指向自身的一个指针。
runtime.main
会调用用户的main.main
, 从此进入用户代码。
这样可以找到main_main函数的位置。
# 恢复字符串
golang中字符串是通过指针+长度方式确定,我们可以通过ida 对应api设置:
idc.MakeStr(idc.here(), idc.here() + str_len)
注意这个函数有一个小bug,请查看本文档ida-python对应部分。
我简单模仿lazy_ida写了一个ida 插件,简单的封装这个调用,可以不必每次都在下面的python窗口调用这个函数。
# 恢复符号
根据几个脚本的测试,初步完成以下脚本, 解析.gopclntab段,并恢复符号,
其中.gopclntab段的格式: 首先前8字节没有意义, 然后下一个指针为这个段包含的函数的符号个数func_num,下面开始func_num * size * 2 的长度都是函数和对应符号,格式为, 指向函数的指针, 跟一个对应符号的偏移量,这个偏移量指向这个段内的一个数据,这个数据是对应符号相对的偏移,
需要注意的是相关指针和大小,32位,为Dword和size=4, 642位, 为Qword和size=8,
ida相关的函数可以确定程序的位数:
idaapi.get_inf_structure().is_64bit()
GetSegmentAttr(addr, SEGATTR_BITNESS) # 比特段 (0: 16, 1: 32, 2: 64 比特段)
2
详情可以看到相关的脚本。
base = ida_segment.get_segm_by_name('.gopclntab').start_ea
ea = base + 8
len = Qword(ea) * 8 * 2
ptr = base + 8 + 8
end = base + len
while ptr <= end:
MakeQword(ptr)
func_addr = Qword(ptr)
MakeQword(ptr + 8)
name_offset = Qword(ptr + 8)
name_addr = Dword(base + 8 + name_offset) + base
name = GetString(name_addr)
name = name.replace('.','_').replace("<-",'_chan_left_').replace('*','_ptr_').replace('-','_').replace(';','').replace('"','').replace('\\\\','')
name = name.replace('(','').replace(')','').replace('/','_').replace(' ','_').replace(',','comma').replace('{','').replace('}','')
MakeName(func_addr, name)
print(name)
print(ptr)
ptr += 16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# goroutine:
这个是个重头戏,关于go的高并发性能,
关于这个goroutine, 首先写下个go中的比较核心的一个概念runtime库。
# go-关键字:
go语言中的关键字go,会启动一个新的goroutine运行指定的函数,这个可以看做是开启了一个轻量级的小线程,去运行指定的函数,但是相比于普通的一个线程,go的实现更为简洁一些。
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5 ; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("hello");
say("world")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
我们测试下会发现不同,因为是两个goroutine,所以他们的顺序没有固定规律,
而这个go关键字其实
# 工具
最近自己做的一个golangd的小工具。