本系列文章从 Botnet(僵尸网络)的基础概念说起,围绕实现了 P2P 特性的 DDG.Mining.Botnet,一步一步设计一个基于 P2P 的僵尸网络追踪程序,来追踪 DDG。DDG 是一个目前仍十分活跃的 Botnet,读懂本文,再加上一些辅助分析工作,就可以自行实现一套针对 DDG 的 P2P 僵尸网络跟踪程序。内容分 上、下 两部分:
- 上 半部分写本人理解的 Botnet 相关概念,然后介绍 DDG Botnet,着重介绍其涉及的 P2P 特性;
- 下 半部分写如何根据 DDG.Mining.Botnet 的 P2P 特性,来设计一个僵尸网络跟踪程序 DDG.P2P.Tracker,用以遍历 Botnet 中的节点、及时获取最新的云端配置文件、及时下载到 Botnet 中最新的恶意样本、及时获知 Botnet 中最新启用的 C&C 服务器。
上半部分传送门: 以P2P的方式追踪 DDG 僵尸网络(上)
3. 追踪程序设计
3.1 追踪程序的执行流程
前文说过,设计追踪程序的最终目标,有 4 个,其中涉及到 Peer 信息的获取和保存、样本与配置数据的解析和保存、记录最新启用的 C&C Server ……这样一来,就不可避免地将相关数据和文件保存到本地或数据库中。
我们可以把最新一次探测到的 P2P 节点信息存储到数据库中,把样本文件、配置数据、最新的 C&C Server 列表保存到本地文件中。根据 Memberlist 框架的实现,程序要调用 memberlist.Join()
函数来加入一个已存在的 P2P 网络,而这个函数需要一个 IP List( Go 变量 [] string
,下文简称 init_peers) 来作为加入 P2P 网络的“介绍人”。当然,这个 IP List 中的 IP,应该是当前已加入 P2P 网络的 IP (按照这个概念,这些 IP 应该是对应常规 P2P 网络中的 Node,P2P 网络中的 Node 和 Peer 的概念可以自行了解,为了简化描述,本文把 P2P 网络中的节点统称为 Peer)。
前文还说过,ddg 主样本中有一份内置硬编码的 HUB IP List。其实,这一份 HUB IP List 就可以拿来当做 memberlist.Join()
函数的参数,即 init_peers。为了方便程序运行,我们可以把这一份 IP List 提前保存到数据库中,追踪程序每次运行,都要先从数据库中读取最新的 init_peers,通过 init_peers 加入 ddg 的 P2P 网络。
这里先说一下追踪程序的概要执行流程,后面分步骤详细说明:
- 从数据库中读取 init_peers IP List ,并调用 memberlist.Join() 加入 ddg 的 P2P 网络;
- 成功加入 P2P 网络后,调用
memberlist.Members()
获取当前网络中的最新 Peers List; - 解析获取到的 Peers List 中的 Peers 信息,将每个 Peer 信息拆解成 IP:Port:Versioin:Hash:DateTime 5 元组,存到数据库中;
- 将每个 Peer IP ,拼接 URL 串
http://<peer_ip>:8000/slave
,并向该 URL 发送 Post 请求,以获取经过 msgPack 编码的配置数据; - 如果成功从某个 Peer 上获取到了配置数据,则:
- 将该 Peer IP 暂存到一个非重复的、并发安全的 IP List 结构中;
- 保存 RAW 格式的配置数据到本地;
- 用该 Peer IP 拼接 URL 串
http://<peer_ip>:8000/i.sh
,并用 HTTP GET 请求的方式尝试获取最新的恶意 Shell 脚本; - 对比上述 i.sh 下载链接与刚获取到的最新配置数据中执行的 i.sh 下载链接是否相同,不同则对最新配置数据中指定的 i.sh 脚本也做下载&解析操作。
- 如果成功获取到 i.sh 脚本,则解析其中的样本 Download URL,下载样本,同本地已下载到的其他样本 MD5 和下载 URL 作对比,MD5 和 下载 URL 其中之一是新的,就保留样本,否则删除刚下载到的样本。对于新样本,通过 Slack 的 Message 接口 Push 相关消息到自己的 Slack Channel 中;
- 最后,将非重复的最新活跃的 C&C Server 列表保存到本地文件中。
3.2 加入 P2P 网络
前文提到,调用 memberlist.Join()
来加入 ddg 的 P2P 网络,需要一个 init_peers 的 IP List。这个 IP List 最初来自 ddg 主样本中硬编码的 HUB IP List,而以后追踪程序每次执行,都要先从数据库中获取这个 IP List。这里先给出一个可用的数据表结构,用来存储 Peer 信息:
+---------+----------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------+----------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | <null> | auto_increment |
| ip | char(16) | NO | | <null> | |
| port | smallint(5) unsigned | NO | | <null> | |
| version | smallint(5) unsigned | NO | | <null> | |
| hash | char(32) | YES | | <null> | |
| tdate | datetime | NO | | <null> | |
+---------+----------------------+------+-----+---------+----------------+
最新的 Peers 信息在我们加入 ddg 的 P2P 网络后可以调用 memberlist.Members()
来获取。在 Memberlist 框架的源码中,这个函数返回的是一个 Node 信息指针列表 (Go 语言变量 []*Node
)。Memberlist 框架中的 Node 结构体的定义如下:
// Node represents a node in the cluster.
type Node struct {
Name string
Addr net.IP
Port uint16
Meta []byte // Metadata from the delegate for this node.
PMin uint8 // Minimum protocol version this understands
PMax uint8 // Maximum protocol version this understands
PCur uint8 // Current version node is speaking
DMin uint8 // Min protocol version for the delegate to understand
DMax uint8 // Max protocol version for the delegate to understand
DCur uint8 // Current version delegate is speaking
}
其中第一项 Name 是形如 VerNumber.HashValue 的一个字符串,如:3020.b1634b9e0c747a6ae728e07c40883e2d 。这里的 Hash 值在 Memberlist 框架中被定义为 UID ,每一个 Peer 都不同,其值是通过对当前 Peer 主机的网络配置用 MD5 算法计算得出。
Memberlist 的开源项目主页上,有一个简单的 Usage Demo,演示加入一个集群(本文就指 ddg 的 P2P 网络了)并获取节点信息的最简方法:
/* Create the initial memberlist from a safe configuration.
Please reference the godoc for other default config types.
http://godoc.org/github.com/hashicorp/memberlist#Config
*/
list, err := memberlist.Create(memberlist.DefaultLocalConfig())
if err != nil {
panic("Failed to create memberlist: " + err.Error())
}
// Join an existing cluster by specifying at least one known member.
n, err := list.Join([]string{"1.2.3.4"})
if err != nil {
panic("Failed to join cluster: " + err.Error())
}
// Ask for members of the cluster
for _, member := range list.Members() {
fmt.Printf("Member: %s %sn", member.Name, member.Addr)
}
// Continue doing whatever you need, memberlist will maintain membership
// information in the background. Delegates can be used for receiving
// events when members join or leave.
可以看到在执行 Join() 函数加入集群之前,还要调用 memberlist.Create() 函数生成一个 Peer 对象(代表当前 Peer),然后用当前对象执行 Join 以及后续操作。这里有一个关键点是当前 Peer 的配置。这份配置的底层结构体定义,在 Memberlist 的 Godoc 文档中有详细说明,此处不赘述。这份配置结构中的两个关键配置项(网络配置和密钥),关乎到追踪程序能否成功加入到 ddg 的 P2P 网络中,以及加入之后能否正常与其他 Peers 通信,这两个关键点要逆向 ddg 主样本和熟知 Memberlist 的原理和实现才能搞定,这里也不赘述。想要自行实现这么一套追踪程序,需要自行完成这两个工作。
需要一提的是,配置项中有一个关于日志输出的配置项:
// Logger is a custom logger which you provide. If Logger is set, it will use
// this for the internal logger. If Logger is not set, it will fall back to the
// behavior for using LogOutput. You cannot specify both LogOutput and Logger
// at the same time.
Logger *log.Logger
我们要用到日志功能,把全局的日志句柄配置在这里,这样 Memberlist 整个框架的运行日志都会打到我们指定的日志文件中。
3.3 获取并解析最新的 Peers List
前文提到,获取最新的 Peers List,只需在加入 ddg 的 P2P 网络后调用 memberlist.Members()
即可。
其实只说了一半,因为这里还有个偶然发现的小 Trick:这个函数获取到的 Peers List 数量并不大,反倒是从 Memberlist 框架的运行日志中可以抽取更多 Peer 信息。
根据 Memberlist 的框架特性,当前节点加入 P2P 网络之后,会随机与其他 Peers 以 Gossip 的形式通信,这种通信具有节点探测的功能。通信的结果会记录在日志中,尤其是通信失败的日志,记录的比较详细。一条失败的 Gossip 通信日志如下:
2019/01/23 08:44:53 [ERR] memberlist: Failed to send gossip to 58.144.150.24:7946: write udp 127.0.0.1:7946->58.144.150.24:7946: sendto: invalid argument
打出这段日志的代码,在 memberlist/stat.go 中实现:
不过,这段错误信息还不足以提供我们想要的 Peer Info 5 元组。那就动手 Patch 一下这段代码,让它打出我们想要的信息。Patch 后的代码如下:
然后,打出来的日志内容就会是如下形式:
2019/02/24 18:01:06 [ERR] memberlist: Failed to send gossip to (114.118.18.70:7946:3020:91f7f67194e0d31d9b58d9e6bef4f711)
这样,既缩减了日志文件的体积,也能精准捕获到我们需要的信息。然后,就可以把 memberlist.Members()
函数获取到的 Peers 信息和日志文件中打出来的 Peers 信息汇总起来,保存到一个变量中,以待后用。
3.4 保存 Peers 信息
将上述步骤获取到的 Peers 信息保存到数据库中,最新的 20 条 Peers 信息示例如下:
3.5 探测最新活跃的 C&C,拉取最新的配置数据
对上面获取到的 Peers Info 中的每一个 Peer IP,拼接成 URL 串 http://<peer_ip>:8000/slave
,向该 URL 发 Post 请求。能获取符合格式的配置数据的,即为当前存活的 C&C IP。把存活的 C&C Host 信息保存到一个非重复的、并发安全的 List 结构的变量中,最后把这份 C&C Host 列表保存到本地文件中。本地 C&C Host 文件列表部分内容如下:
➜ tail cc_server.list
20190503060101 132.148.241.138:8000
20190503060101 109.237.25.145:8000
20190503060101 104.128.230.16:8000
20190503060101 117.141.5.87:8000
20190506060101 132.148.241.138:8000
20190506060101 104.128.230.16:8000
20190506060101 117.141.5.87:8000
20190506120102 104.128.230.16:8000
20190506120102 132.148.241.138:8000
20190506120102 117.141.5.87:8000
前面提到过 ddg 配置数据是经过 msgPack 编码的,前文也列出了解码后的配置数据示例。受限于 Memberlist 的框架实现,我们的追踪程序也只能用 Go 语言来实现。要解码这份配置数据,直接调用 msgPack 的 Go 语言 API 是不够的,还需要逆向分析出配置数据的正确结构,并用 Go 语言的语法来定义这个配置数据的结构。下面是掉了两把头发才逆向出来的配置数据结构,以 Go 语言来定义的结构体:
import msgpack
/*
Salve conf struct
*/
type Conf struct {
Data []byte
Signature []byte
}
type ConfData struct {
CfgVer int
Config MainConf
Miner []MinerConf
Cmd CmdConf
}
type MainConf struct {
Interval string
}
type MinerConf struct {
Exe string
Md5 string
Url string
}
type CmdConf struct {
AAredis CmdConfDetail
AAssh CmdConfDetail
Sh []ShConf
Killer []ProcConf
LKProc []ProcConf
}
type CmdConfDetail struct {
Id int
Version int
ShellUrl string
Duration string
NThreads int
IPDuration string
GenLan bool
GenAAA bool
Timeout string
Ports []int
}
type ShConf struct {
Id int
Version int
Line string
Timeout string
}
type ProcConf struct {
_msgpack struct{} `msgpack:",omitempty"`
Id int
Version int
Expr string
Timeout string
}
将解码成功的配置数据打到日志文件中,只把未解码的 RAW 配置数据保存到本地。最新获取到的配置数据如下:
➜ ll -t slave_conf | head
total 4.6M
2.0K May 6 12:34 117_141_5_87__20190506123410.raw
2.0K May 6 12:34 132_148_241_138__20190506123410.raw
2.0K May 6 12:33 104_128_230_16__20190506123309.raw
2.0K May 6 06:33 104_128_230_16__20190506063312.raw
2.0K May 6 06:31 117_141_5_87__20190506063142.raw
2.0K May 6 06:30 132_148_241_138__20190506063042.raw
2.0K May 6 00:39 104_128_230_16__20190506003932.raw
2.0K May 6 00:37 117_141_5_87__20190506003723.raw
2.0K May 6 00:34 132_148_241_138__20190506003442.raw
3.6 下载最新样本
对于上面步骤中,每一个可以获取合格配置数据的 C&C IP,拼接 URL 串 http://<cc_ip>:8000/i.sh
,这是 ddg 目前用到的最新恶意 Shell 脚本的下载链接。通过 HTTP GET 请求下载这个 i.sh 文件,跟本地已有的、相同 URL 下载到的 i.sh 文件对比 MD5 值,如果 MD5 跟旧的 i.sh 相同,则丢弃刚下载 i.sh 文件。
如果最新的 i.sh 文件跟旧 i.sh 文件 MD5 不同,则进行以下两步操作:
- 对比上述 i.sh 下载链接与刚获取到的最新配置数据中执行的 i.sh 下载链接是否相同,不同则对最新配置数据中指定的 i.sh 脚本也做下载&解析操作;
- 成功获取到 i.sh 脚本,则解析其中的样本 Download URL,下载样本,同本地相同 URL 下载到的样本对比 MD5 和 FileName(其实是 Download URL),如果 MD5 或者 FileName 不同,则保留样本,否则删除刚下载到的样本。样本 MD5 和 FileName 的对比结果,有三种情况:
- 仅仅 MD5 不同而 FileName 相同,说明同一个 URL 中下到了不同 MD5 的样本,即样本有更新;
- 仅仅 FileName 不同而 MD5 相同,则不同的 URL 想到了相同 MD5 的样本,通常意味着 C&C 有变动;
- 两者都不同则说明 C&C 有变动并且样本有更新。
截至目前,我通过 ddg 追踪程序监控到的一部分 ddg 样本如下:
➜ ll sample
total 115M
1.7K 104_236_156_211__8000__i_sh+8801aff2ec7c44bed9750f0659e4c533
8.8M 104_236_156_211__8000__static__3019__fmt_i686+8c2e1719192caa4025ed978b132988d6
11M 104_236_156_211__8000__static__3019__fmt_x86_64+d6187a44abacfb8f167584668e02c918
1.8K 104_248_181_42__8000__i_sh+dc477d4810a8d3620d42a6c9f2e40b40
3.6M 104_248_181_42__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
3.9M 104_248_181_42__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
1.9K 104_248_251_227__8000__i_sh+55ea97d94c6d74ceefea2ab9e1de4d9f
3.6M 104_248_251_227__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
3.9M 104_248_251_227__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
1.1K 117_141_5_87__8000__i_sh+100d1048ee202ff6d5f3300e3e3c77cc
1.7K 117_141_5_87__8000__i_sh+5760d5571fb745e7d9361870bc44f7a3
8.8M 117_141_5_87__8000__static__3019__fmt_i686+8c2e1719192caa4025ed978b132988d6
11M 117_141_5_87__8000__static__3019__fmt_x86_64+d6187a44abacfb8f167584668e02c918
3.6M 117_141_5_87__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
3.9M 117_141_5_87__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
1.3K 119_9_106_27__8000__i_sh+09a3a0f662738279e344b2a38dc93ecb
1.2K 119_9_106_27__8000__i_sh+9dc32a4a87d2b579d03b6adb27e3f604
1.6K 119_9_106_27__8000__i_sh+b8a64e8bfe4a69c36760505cc757c38d
3.6M 119_9_106_27__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
3.9M 119_9_106_27__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
9.4M 119_9_106_27__8000__static__3022__ddgs_i686+c32bd921a71d82696517c22021173480
11M 119_9_106_27__8000__static__3022__ddgs_x86_64+79d762d1ff16142ea3bdae560558e718
1.7K 132_148_241_138__8000__i_sh+44feb3cd31b957e24b18f97c46b57431
1.1K 132_148_241_138__8000__i_sh+fcc003280d8e9060e00fb7273d8edee7
8.8M 132_148_241_138__8000__static__3019__fmt_i686+8c2e1719192caa4025ed978b132988d6
11M 132_148_241_138__8000__static__3019__fmt_x86_64+d6187a44abacfb8f167584668e02c918
3.6M 132_148_241_138__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
3.9M 132_148_241_138__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
➜
➜ md5sum sample/*
8801aff2ec7c44bed9750f0659e4c533 104_236_156_211__8000__i_sh+8801aff2ec7c44bed9750f0659e4c533
8c2e1719192caa4025ed978b132988d6 104_236_156_211__8000__static__3019__fmt_i686+8c2e1719192caa4025ed978b132988d6
d6187a44abacfb8f167584668e02c918 104_236_156_211__8000__static__3019__fmt_x86_64+d6187a44abacfb8f167584668e02c918
dc477d4810a8d3620d42a6c9f2e40b40 104_248_181_42__8000__i_sh+dc477d4810a8d3620d42a6c9f2e40b40
3ebe43220041fe7da8be63d7c758e1a8 104_248_181_42__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
d894bb2504943399f57657472e46c07d 104_248_181_42__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
55ea97d94c6d74ceefea2ab9e1de4d9f 104_248_251_227__8000__i_sh+55ea97d94c6d74ceefea2ab9e1de4d9f
3ebe43220041fe7da8be63d7c758e1a8 104_248_251_227__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
d894bb2504943399f57657472e46c07d 104_248_251_227__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
100d1048ee202ff6d5f3300e3e3c77cc 117_141_5_87__8000__i_sh+100d1048ee202ff6d5f3300e3e3c77cc
5760d5571fb745e7d9361870bc44f7a3 117_141_5_87__8000__i_sh+5760d5571fb745e7d9361870bc44f7a3
8c2e1719192caa4025ed978b132988d6 117_141_5_87__8000__static__3019__fmt_i686+8c2e1719192caa4025ed978b132988d6
d6187a44abacfb8f167584668e02c918 117_141_5_87__8000__static__3019__fmt_x86_64+d6187a44abacfb8f167584668e02c918
3ebe43220041fe7da8be63d7c758e1a8 117_141_5_87__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
d894bb2504943399f57657472e46c07d 117_141_5_87__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
09a3a0f662738279e344b2a38dc93ecb 119_9_106_27__8000__i_sh+09a3a0f662738279e344b2a38dc93ecb
9dc32a4a87d2b579d03b6adb27e3f604 119_9_106_27__8000__i_sh+9dc32a4a87d2b579d03b6adb27e3f604
b8a64e8bfe4a69c36760505cc757c38d 119_9_106_27__8000__i_sh+b8a64e8bfe4a69c36760505cc757c38d
3ebe43220041fe7da8be63d7c758e1a8 119_9_106_27__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
d894bb2504943399f57657472e46c07d 119_9_106_27__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
c32bd921a71d82696517c22021173480 119_9_106_27__8000__static__3022__ddgs_i686+c32bd921a71d82696517c22021173480
79d762d1ff16142ea3bdae560558e718 119_9_106_27__8000__static__3022__ddgs_x86_64+79d762d1ff16142ea3bdae560558e718
44feb3cd31b957e24b18f97c46b57431 132_148_241_138__8000__i_sh+44feb3cd31b957e24b18f97c46b57431
fcc003280d8e9060e00fb7273d8edee7 132_148_241_138__8000__i_sh+fcc003280d8e9060e00fb7273d8edee7
8c2e1719192caa4025ed978b132988d6 132_148_241_138__8000__static__3019__fmt_i686+8c2e1719192caa4025ed978b132988d6
d6187a44abacfb8f167584668e02c918 132_148_241_138__8000__static__3019__fmt_x86_64+d6187a44abacfb8f167584668e02c918
3ebe43220041fe7da8be63d7c758e1a8 132_148_241_138__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
d894bb2504943399f57657472e46c07d 132_148_241_138__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
综合以上描述,本地文件目录及文件示例如下:
- ddg_tracker/
|-- cc_server.list
|-- log/
| |-- 20190123164450.log
| |-- 20190123180232.log
| |-- 20190123211837.log
| |-- 20190124000101.log
| |-- 20190124060101.log
| `-- ......
|-- sample/
| |-- 104_236_156_211__8000__i_sh+8801aff2ec7c44bed9750f0659e4c533
| |-- 104_236_156_211__8000__static__3019__fmt_i686+8c2e1719192caa4025ed978b132988d6
| |-- 104_236_156_211__8000__static__3019__fmt_x86_64+d6187a44abacfb8f167584668e02c918
| |-- 104_248_181_42__8000__i_sh+dc477d4810a8d3620d42a6c9f2e40b40
| |-- 104_248_181_42__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
| |-- 104_248_181_42__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
| |-- 104_248_251_227__8000__i_sh+55ea97d94c6d74ceefea2ab9e1de4d9f
| |-- 104_248_251_227__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
| |-- 104_248_251_227__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
| |-- 117_141_5_87__8000__i_sh+100d1048ee202ff6d5f3300e3e3c77cc
| |-- 117_141_5_87__8000__i_sh+5760d5571fb745e7d9361870bc44f7a3
| |-- 117_141_5_87__8000__static__3019__fmt_i686+8c2e1719192caa4025ed978b132988d6
| `-- ......
`-- slave_conf/
|-- 104_236_156_211__20190123165004.raw
|-- 104_236_156_211__20190123185208.raw
|-- 104_236_156_211__20190123223044.raw
|-- 104_236_156_211__20190124012600.raw
|-- 132_148_241_138__20190224191449.raw
`-- ......
至此,我们就完成了 ddg 追踪程序的设计,为这个程序设置一个计划任务,定时运行一次即可。我个人的源码暂时不会放出来,有兴趣的朋友可以自己动手实现一下。目前的追踪成果(uniq peer ip):
一次探测到的活跃节点数:
部分 DDG 更新的 Slack 消息推送:
发表评论
您还未登录,请先登录。
登录