2. 典型的恶意程序

早在 2012 年,Symantec(现已被博通收购)就曝光了一个 Go 语言编写的 Windows 平台上的恶意软件: Encriyoko,这是鄙人能查到的最早的 Go 编写的恶意软件。当时,这个恶意软件在业内并没引起多大注意。

到了 2016 年 8 月,Go 编写的两个恶意软件被俄罗斯网络安全公司 Dr.Web 曝光,在业内吸引了很多注意: Linux.LadyLinux.Rex。前者后来发展成臭名昭著的 DDGMiner,后者则是 史上第一个 Go 编写的 P2P Botnet(基于 DHT )。从公开的信息来看,正是从这时开始,业内的安全研究人员开始对 Go 二进制文件的逆向分析进行初步探索。

2017 年,TrendMicro 曝光了一个大型的黑客团伙 BlackTech ,他们用到的一个核心的数据窃取工具 DRIGO,即是用 Go 语言编写。2019 年 ESET 发布一篇报告,分析了 APT28 组织用到的知名后门工具 Zebrocy,也有了 Go 语言版本。这也说明 Go 语言编写的木马越来越成熟,Go 语言开始被大型黑客组织纳入编程工具箱。

再往后,Go 语言编写的恶意软件就呈泛滥的态势了。2020 年初,鹅厂还曝光过一个功能比较复杂的跨平台恶意挖矿木马 SysupdataMiner,也是由 Go 语言编写。就在最近, Guardicore 刚爆光了一个 Go 编写的功能复杂的 P2P Botnet: FritzFrog

3. 已有研究与工具

前文说过,Go 语言二进制文件有它自己的特殊性,使得它分析起来跟普通的二进制文件不太一样。主要有以下三个方面:

由于恶意软件大都是被 strip 处理过,已经去除了二进制文件里的调试信息和函数符号,所以 Go 二进制文件的逆向分析技术的探索,前期主要围绕着函数符号的恢复来展开。

最早是有人尝试过为函数符号做 Signature ,然后把 Signature 导入到反汇编工具里的做法。这是一个 Hard Way 的笨办法,比较原始,但挺实用。r2con 2016 上 zl0wram 的议题《Reversing Linux Malware》中就演示过这种做法:

进一步,大家发现了隐藏在 Go 二进制文件种 pclntab结构中的函数名信息,并没有被 strip 掉,而且可以通过辅助脚本在反汇编工具里将其恢复。比如 RedNaga 的 @timstrazz写了一篇 Reversing GO binaries like a pro,详细讲述了如何从被 strip 的 Go 二进制文件中恢复函数符号以及解析函数中用到的字符串,让 IDAPro 逆向 Go 二进制文件变得更轻松。@timstrazz 还开源了他写的 IDA 脚本 golang_loader_assist

再进一步,有安全研究员发现除了可以从 pclntab结构中解析、恢复函数符号,Go 二进制文件中还有大量的类型、方法定义的信息,也可以解析出来。这样就可以大大方便对 Go 二进制文件中复杂数据结构的逆向分析。两个代表工具:

前文提到的 Reversing GO binaries like a pro可能是业内最火的介绍 Go 二进制逆向的文章,但最火的工具可能还是 IDAGolangHelper

IDAGolangHelper对 Go 的不同版本做了更精细化处理,而且第一次在 Go 二进制文件解析中引入 moduledata这个数据结构。而且提供一个 GUI 界面给用户提供丰富的操作选项,用户体验更胜一筹。

不过 IDAGolangHelper的缺点也非常明显:

更进一步,2019 年 10 月份,JEB 官方博客发表一篇文章 《 Analyzing Golang Executables》,并发布一个 JEB 专用的 Go 二进制文件解析插件 jeb-golang-analyzer。这是一个功能比前面几个工具更加完善的 Go 二进制文件解析工具,除了解析前面提到的函数名、字符串和数据类型信息,还会解析 Duff’s device 、Source File Path list、GOROOT 以及 Interface Table 等信息。甚至会把每个 pkg 中定义的特定数据类型分门别类地列出来,比如解析某 Go 二进制文件中的部分类型信息:

> PACKAGE: net/http:

> struct http.Request (5 fields):

- string Method (offset:0)

- *url.URL URL (offset:10)

- string Proto (offset:18)

- int ProtoMajor (offset:28)

- int ProtoMinor (offset:30)

> PACKAGE: net/url:

> struct url.URL (9 fields):

- string Scheme (offset:0)

- string Opaque (offset:10)

- *url.Userinfo User (offset:20)

- string Host (offset:28)

- string Path (offset:38)

- string RawPath (offset:48)

- bool ForceQuery (offset:58)

- string RawQuery (offset:60)

- string Fragment (offset:70)

> struct url.Userinfo (3 fields):

- string username (offset:0)

- string password (offset:10)

- bool passwordSet (offset:20)

jeb-golang-analyzer也有一些问题:对 strings 和 string pointers 的解析并不到位,虽然支持多种 CPU 架构类型(x86/ARM/MIPS)的字符串解析,但是 Go 二进制文件中字符串的操作方式有多种,该工具覆盖不全。另外,该工具内部定位 pclntab的功能实现,基于 Section Name 查找和靠 Magic Number 暴力搜索来结合的方式,还是可能存在误判的可能性,一旦发生误判,找不到 pclntab结构,至少会导致无法解析函数名的后果。最后,这个工具只能用于 JEB,而对于用惯了 IDAPro 的人来说,JEB 插件的解析功能虽强大,但在 JEB 中展示出来的效果并不是很好,而且 JEB 略卡顿,操作体验不是很好。

另外,还有一个非典型的 Go 二进制文件解析工具:基于 GoREredress。 GoRE 是一个 Go 语言编写的 Go 二进制文件解析库, redress是基于这个库来实现的 Go 二进制文件解析的 命令行工具。redress 的强大之处,可以 结构化打印 Go 二进制文件中各种详细信息,比如打印 Go 二进制文件中的一些 Interface 定义:

$ redress -interface pplauncher

type error interface {

Error string

}

type interface {} interface{}

type route.Addr interface {

Family int

}

type route.Message interface {

Sys []route.Sys

}

type route.Sys interface {

SysType int

}

type route.binaryByteOrder interface {

PutUint16([]uint8, uint16)

PutUint32([]uint8, uint32)

Uint16([]uint8) uint16

Uint32([]uint8) uint32

Uint64([]uint8) uint64

}

或者打印 Go 二进制文件中的一些 Struct 定义以及绑定的方法定义:

$ redress -struct -method pplauncher

type main.asset struct{

bytes []uint8

info os.FileInfo

}

type main.bindataFileInfo struct{

name string

size int64

mode uint32

modTime time.Time

}

func (main.bindataFileInfo) IsDir bool

func (main.bindataFileInfo) ModTime time.Time

func (main.bindataFileInfo) Mode uint32

func (main.bindataFileInfo) Name string

func (main.bindataFileInfo) Size int64

func (main.bindataFileInfo) Sys interface {}

type main.bintree struct{

Func func (*main.asset, error)

Children map[string]*main.bintree

}

这些能力,都是上面列举的反汇编工具的插件难以实现的。另外,用 Go 语言来实现这种工具有天然的优势:可以复用 Go 语言开源的底层代码中解析各种基础数据结构的能力。比如可以借鉴 src/debug/gosym/pclntab.go 中的代码来解析 pclntab结构,可以借鉴 src/runtime/symtab.go 中的代码来解析 moduledata结构,以及借鉴 src/reflect/type.go 中的代码来解析各种数据类型的信息。

redress是一个接近极致的工具,它把逆向分析需要的信息尽可能地都解析到,并以友好的方式展示出来。但是它只是个命令行工具,跟反汇编工具的插件相比并不是很方便。另外,它目前还有个除 jeb-golang-analyzer之外以上工具都有的缺点:限于内部实现的机制, 无法解析 buildmode=pie 模式编译出来的二进制文件。用 redress 解析一个 PIE(Position Independent Executable)二进制文件,报错如下:

最后,是鄙人开发的一个 IDAPro 插件: go_parser,该工具除了拥有以上各工具的绝大部分功能(strings 解析暂时只支持 x86 架构的二进制文件,这一点不如 jeb-golang-analyzer支持的丰富),还 支持对 PIE 二进制文件的解析。另外会把解析结果以更友好、更方便进一步操作的方式在 IDAPro 中展示。以 DDG 样本中一个复杂的结构体类型为例,解析结果如下:

4. 原理初探

前文盘点了关于 Go 二进制文件解析的已有研究,原理层面都是一句带过。可能很多师傅看了会有两点疑惑:

第一个问题,一句话解释就是,Go 二进制文件里打包进去了 runtimeGC模块,还有独特的 Type Reflection(类型反射) 和 Stack Trace机制,都需要用到这些信息。参考前文 redress报错的配图,redress 本身也是 Go 语言编写,其报错时打出来的栈回溯信息,除了参数以及参数地址,还包含 pkg 路径、函数信息、类型信息、源码文件路径、以及在源码文件中的行数。

至于内置于 Go 二进制文件中的类型信息,主要为 Go 语言中的 Type Reflection 和类型转换服务。Go 语言内置的数据类型如下:

而这些类型的底层实现,其实都基于一个底层的结构定义扩展而来:

再加上 Go 允许为数据类型绑定方法,这样就可以定义更复杂的类型和数据结构。而这些类型在进行类型断言和反射时,都需要对这些底层结构进行解析。

第二个问题,对于 Go 二进制文件中,可以解析并对逆向分析分析有帮助的信息,我做了个列表,详情如下:

后文会以这张图为大纲,以 go_parser为例,详细讲解如何查找、解析并有组织地展示出这些信息,尽最大可能提升 Go 二进制文件逆向分析的效率。