2. 典型的恶意程序
早在 2012 年,Symantec(现已被博通收购)就曝光了一个 Go 语言编写的 Windows 平台上的恶意软件: Encriyoko,这是鄙人能查到的最早的 Go 编写的恶意软件。当时,这个恶意软件在业内并没引起多大注意。
到了 2016 年 8 月,Go 编写的两个恶意软件被俄罗斯网络安全公司 Dr.Web 曝光,在业内吸引了很多注意: Linux.Lady和 Linux.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 二进制文件解析工具:基于 GoRE的 redress。 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 二进制文件里打包进去了 runtime和 GC模块,还有独特的 Type Reflection(类型反射) 和 Stack Trace机制,都需要用到这些信息。参考前文 redress报错的配图,redress 本身也是 Go 语言编写,其报错时打出来的栈回溯信息,除了参数以及参数地址,还包含 pkg 路径、函数信息、类型信息、源码文件路径、以及在源码文件中的行数。
至于内置于 Go 二进制文件中的类型信息,主要为 Go 语言中的 Type Reflection 和类型转换服务。Go 语言内置的数据类型如下:
而这些类型的底层实现,其实都基于一个底层的结构定义扩展而来:
再加上 Go 允许为数据类型绑定方法,这样就可以定义更复杂的类型和数据结构。而这些类型在进行类型断言和反射时,都需要对这些底层结构进行解析。
第二个问题,对于 Go 二进制文件中,可以解析并对逆向分析分析有帮助的信息,我做了个列表,详情如下:
后文会以这张图为大纲,以 go_parser为例,详细讲解如何查找、解析并有组织地展示出这些信息,尽最大可能提升 Go 二进制文件逆向分析的效率。