4

Go二进制文件逆向分析从基础到进阶(1)——综述

 2 years ago
source link: https://jiayu0x.com/2020/08/22/go-binary-reverse-engineering-summary/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Go二进制文件逆向分析从基础到进阶(1)——综述

Posted on 2020-08-22

| In security

| 0 Comments

本文首发于安全客: https://www.anquanke.com/post/id/214940

Go 语言是一个比较新的强类型静态语言,2009 年由 Google 发布,在 2012 年才发布首个稳定版。Go 语言靠 Goroutine 和 channel、wait group、select、context 以及 sync 等辅助机制实现的 CSP 并发模型,以简洁高效编写高并发程序为亮点,很快就在全球范围内吸引大量开发者使用其编写各种程序。现在 Go 已成为云原生领域的首选开发语言。

由于 Go 语言上手快,编码效率高,程序运行效率也高,而且很方便跨平台交叉编译,也吸引了恶意软件开发者的注意。渐渐地,安全厂商开始捕获到越来越多的 Go 语言编写的恶意软件 ,这些恶意软件横跨 Windows、Linux 和 Mac 三大平台。

然而,Go 语言的编译工具链会全静态链接构建二进制文件,把标准库函数和第三方 package 全部做了静态编译,再加上 Go 二进制文件中还打包进去了 runtime 和 GC(Garbage Collection,垃圾回收) 模块代码,所以即使做了 strip 处理( go build -ldflags "-s -w" ),生成的二进制文件体积仍然很大。在反汇编工具中打开 Go 语言二进制文件,可以看到里面包含动辄几千个函数。再加上 Go 语言的独特的函数调用约定、栈结构和多返回值机制,使得对 Go 二进制文件的分析,无论是静态逆向还是动态调式分析,都比分析普通的二进制程序要困难很多。

不过,好消息是安全社区还是找到了针对性的方法,让安全分析人员对 Go 语言二进制文件的逆向变得更加轻松。最开始有人尝试过针对函数库做符号 Signature 来导入反汇编工具中,还原一部分二进制文件中的函数符号。后来有人研究出 Go 语言二进制文件内包含大量的运行时所需的符号和类型信息,以及字符串在 Go 二进制文件中独特的用法,然后开发出了针对性的符号甚至类型信息恢复工具。至此,对 Go 语言二进制文件的逆向分析工作,就变得轻松异常了。

本系列文章将简单介绍 Go 语言二进制文件逆向姿势的发展历史,以及几个典型的恶意程序家族。然后详细介绍基于二进制文件内置的符号、类型信息来逆向分析 Go 语言二进制文件的技术原理和工具,最后以实际的分析案例演示前面介绍的工具和方法。至于还有人研究出基于符号执行和形式化定理证明的高深技术,来恢复 Go 语言二进制文件的符号和类型信息的姿势,不在本文讨论范围之内。因为鄙人也没研究明白

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 语言二进制文件有它自己的特殊性,使得它分析起来跟普通的二进制文件不太一样。主要有以下三个方面:

  • Go 语言内置一些复杂的数据类型,并支持类型的组合与方法绑定,这些复杂数据类型在汇编层面有独特的表示方式和用法。比如 Go 二进制文件中的 string 数据不是传统的以 0x00 结尾的 C-String,而是用 (StartAddress, Length) 两个元素表示一个 string 数据;比如一个 slice 数据要由 (StartAddress, Length, Capacity) 三个元素表示。这样的话,在汇编代码中看,给一个函数传一个 string 类型的参数,其实要传两个值;给一个函数传一个 slice 类型的参数,其实要传 3 个值。返回值同理;

  • 独特的调用约定和栈管理机制,使 C/C++ 二进制文件逆向分析的经验在这里力不从心:Go 语言用的是 continue stack 栈管理机制 ,并且 Go 语言函数中 callee 的栈空间由 caller 来维护,callee 的参数、返回值都由 caller 在栈中预留空间,就难以直观看出哪个是参数、哪个是返回值。详见 The Go low-level calling convention on x86-64

  • 全静态链接构建,里面函数的数量动辄大几千,如果没有调试信息和符号,想静态逆向分析其中的特定功能点,如大海捞针,很容易迷失在函数的海洋中;动态调试难度更大,其独特的 Goroutine 调度机制再加上海量的函数,很容易调飞。

由于恶意软件大都是被 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

值得一提的是, golang_loader_assist 诞生时,旧版的 IDAPro 对 Go 二进制中的函数识别效果并不是很好,很多函数体识别不全,导致 IDAPro 的自动分析能分析出的函数量有限。所以 golang_loader_assist 实现了一种依靠 Go 语言中 连续栈(Continue Stack) 维护的机制来解析、标注函数体的功能。具体做法是靠汇编代码的特征来找出 runtime_morestackruntime_morestack_noctxt 函数,然后在 IDAPro 种遍历对这两个函数交叉引用的位置来找出函数体。这是一个无奈之举,IDAPro v7.x 的版本对 Go 二进制文件中函数体的自动解析功能加强了很多,绝大部分函数都可以被识别出来,无需自己费劲去识别、解析函数体。

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

  1. 用于 radare2 的 r2_go_helper ,由上面提到的 zl0wram 在 r2con 2016 上发布;
  2. 用于 IDAPro 的 GoUtils ,以及基于 GoUtils 2.0 开发的更强的 IDAGolangHelper

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

idagolanghelper.png

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

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

  1. 支持的 Golang 版本略旧。目前最高支持 Go 1.10,而最新的 Go 1.15 已经发布了。 Go 1.2 之后这些版本之间的差异并不大,所以这也不是个太大的问题;

  2. 太久不更新,目前在 IDAPro v7.x 上已经无法顺利执行,这个问题比较严重;

  3. 其内部有个独特的做法:把 Go 语言各种数据类型的底层实现,在 IDAPro 中定义成了相应的 ida_struct。这样一来,即使可以顺利在 IDAPro 中解析出各种数据类型信息,展示出来的效果并不是很直观,需要查看相应的 struct 定义才能理解类型信息中各字段的意义,而且不方便跳转操作。窃以为这种体验并不好:

更进一步,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 二进制文件解析的已有研究,原理层面都是一句带过。可能很多师傅看了会有两点疑惑:

  1. 为什么 Go 二进制文件中会有这么多无法被 strip 掉的符号和类型信息?
  2. 具体有哪些可以解析并辅助逆向分析的信息?

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

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

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

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

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

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

且看下回分解。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK