1. 概述

DDG 是一个 一直活跃的挖矿僵尸网络,其主样本由 Go 语言编写。它最新活跃的版本中用基于 Gossip 协议实现集群管理的第三方库 把整个僵尸网络构建成了一个非典型的 P2P 结构。关于其 P2P 网络结构以及如何基于 P2P 特性追踪该僵尸网络,我在以前的两篇文章中有详细描述:

11.6 日晚上,我的 DDG 挖矿僵尸网络追踪程序检测到 DDG 家族更新到了版本 4005,IoC 如下:

MD5:

  • 64c6692496110c0bdce7be1bc7cffd47 ddgs.i686
  • 638061d2a06ebdfc82a189cf027d8136 ddgs.x86_64

CC:

  • 67.207.95[.]103:8000
  • 103.219.112[.]66:8000

经过简单的分析,新版恶意样本的关键行为与旧版本差异不大,以前部署的追踪程序依然能持续追踪。不过其中一个小的技术点引起了我的注意。

Memberlist.join()Join()ddgs_network__mlUtils_*

对于旧版样本的做法,定位到 Hub List 数据后,在 IDAPro 中逆向分析样本时直接用 IDAPython 脚本将 Hex 形式的 IP 地址转成点分十进制表示即可一目了然把这些 IP 提取出来,但新版样本中这些数据被 gob 序列化编码过,该怎么提取?

 

2. gob 序列化编码

gob(Go Binary 的简称),是 Go 语言自带的序列化编码方案,用法简洁,灵活性和效率很高,也有一定的扩展性。可以类比 Python 的 Pickle,用来对结构化数据进行序列化编解码以方便数据传输。由于 gob 是 Go 独有的,并没有现成的 Python 接口,所以想用 Python 在 IDAPro 中直接解码不太现实,就只好手动把编码过的二进制数据从样本中 Dump 出来,然后写 Go 程序来解码。

使用 gob 对数据编码,一般是发送端针对已定义好结构的数据进行编码后发送;接收端收到二进制数据后,按照与发送端兼容的数据结构进行解码(不一定是完全相同的结构定义,但数据类型以及数量要兼容发送端的数据结构定义,这个 兼容 则体现了 gob 的灵活性)。一个简单的数据结构如下所示:

type S struct {
    X, Y, Z int
    Name    string
    L       []string
}

所以,要逆向分析经 gob 序列化编码过的数据,对数据进行精准解码,最大的难点在于逆向出 Go 语言形式的数据结构定义。

gob 的用法不是本文重点,可以参考官方介绍 与这一篇中文详解 。

 

3. 恶意样本中的数据解码过程

ddgs_network__mlUtils_JoinAll()Memberlist.Join()

ddgs_network__mlUtils_Seeds()

在 IDAPro 中逆向分析样本,无法还原数据的结构定义。我们把这段数据手动 Dump 出来看看:

ddgs_network__mlUtils_Seeds()ddgs_network__mlUtils_Seeds_func1()

ddgs_network__mlUtils_Seeds_func1()ddgs_network__seedNode_Address()

ddgs_network__seedNode_Address()

ddgs_network__seedNode_Address()net.IP.String()net.IPIP:Port

这正好跟前文用 Hexdump 查看 Raw 二进制数据里的两个字段 IPPort 对上了。

至此,我们就可以断定,这些 gob 编码数据的基础结构定义应该如下所示:

import "net"

type SeedNode struct {
    IP   net.IP
    Port int64
}

 

4. 完成数据解码

上面我们分析出了编码数据原始的基础结构定义,之所以说是基础,是因为这个结构定义只代表单个 Seed Node,而这些数据是一批 Seed Node 的列表。要想写程序完成最终的数据解码,还需要用 Go 的数组或切片把上面的数据结构定义封装一下。最终的数据解码代码关键部分示例如下:

type SeedNode struct {
    IP   net.IP
    Port int64
}

// Open the dumped raw data file
fd, fdErr := os.OpenFile("raw_data.dump", os.O_RDONLY, 0644)
br := bufio.NewReader(fd)

dec := gob.NewDecoder(br)

// make Seed Node slice
var d []SeedNode
decErr := dec.Decode(&d)

for _, seedNode := range d {
    fmt.Printf("%s:%dn", seedNode.IP, seedNode.Port)
}

 

5. 辅助工具——degob

DDG v4005 的样本中涉及的 gob 数据编码,原始数据结构简单,逆向难度不高。如果遇到结构更复杂的数据经 gob 序列化编码,逆向难度肯定要增加。如果有一款工具可以自动化把任意 gob 序列化后的数据还原,最好不过了。

Google 一番,我找到了一个还算理想的工具,degob,专为逆向分析 gob 编码数据而生: https://gitlab.com/drosseau/degob

net.IP
type IP []byte
net.IP[]byte[]bytenet.IP.String()[]bytenet.IP
// []Anon65_5e8660ee

// type ID: 65
type Anon65_5e8660ee struct {
        IP []byte
        Port int64
}

然后就是 degob 解出来的部分数据(不完美解析,需要结合样本逆向才能确认 IP 的真实结构类型):

不过,这个 degob 两年没更新了,作者可能也不维护了,在它的 cmds/degob/main.go 文件中还有一个 Bug,命令行参数把 inFile 误写成了 outFile

 

6. 总结

DDG 的恶意样本中还有另外一个序列化数据的解码,即用 msgPack 编码的云端配置数据。如果要用 msgPack 的 Go 语言 SDK 去解码这个配置文件,需要逆向分析出更复杂的配置数据结构定义(在 以P2P的方式追踪 DDG 僵尸网络(下) 一文中有详细阐述)。不过好在 msgPack 是个通用的序列化编码方案,除了 Go,还支持其他语言,比如 Python。更方便的是,用 msgPack for Python 来对序列化数据进行解码并不需要预先知道数据结构定义即可直接解码,这就大大降低了逆向工作的难度。

然而 gob 序列化只属于 Go 语言自有,并没有其他语言的 SDK,要想逆向解码 gob 序列化编码过的二进制数据数据,就必须分析出原始的数据结构定义。这样来看, gob 序列化数据逆向解码并没有万全之策,即使有 degob 这种工具的加持,也得结合样本逆向分析才能精准解析、还原明文数据。

本文用到的 Go 语言程序、从样本中提取的 gob 编码的原始二进制数据以及样本运行时的 debug 日志,都上传到 Github,感兴趣的师傅自取: