近日,我们蜜罐捕捉到一列挖矿样本,经分析确认为DDG.Mining.Botnet样本的3016版本。与其上一版本相比,3016版本采用了新的分布式框架Memberlist来构建僵尸网络。基于该框架,DDG僵尸网络可以更稳定的实现C2去中心化,以及更灵活的管理和扩展集群。
DDG 是一个利用SSH 、 Redis相关漏洞入侵服务器并构建僵尸网络,利用服务器资源挖取虚拟货币(门罗币)的Linux恶意程序。经过分析,梳理出样本行为整体流程如下:
通过和上一版本v3014的DDG样本对比后,发现样本针对ssh,redis的攻击利用模块并没有太大变化,两个版本基本一致。通过bindiff可以看到更直观看到(ssh攻击模块):
而V3016版本的更新点Memberlist是一个基于Gossip协议实现,golang开发的分布式框架。Gossip是一种去中心化、容错并保证最终一致性的协议。它的基本思想和疫情传播类似,通过不断的和集群中的节点交换信息,经过多次交互之后,Gossip协议即可将信息传递到所有的节点,从而快速收敛达到一致。
DDG采用golang开发,且在编译成可执行文件时去除了符号表,但是其中包含了一个叫.gopclntab的section,里面存着程序所有导入包的符号信息:
利用网上已有的自动化工具实现大部分函数名的还原:
go程序有个特点,.gopclntab中存的函数名=包名_包下的函数名,所以直接搜索main关键字就可以找出main包中所有的函数,我们从main_main函数开始入手分析。可以看到前面跟v3014版本一致,首先是检测运行的环境参数,再利用golang的VividCortex/godaemon包将自己设为守护进程:
接着流程还是跟v3014一致, 先是调用main_singleInstance函数通过判断是否存在3016.lock文件来确定ddg主程序是否已经在受害者机器上运行:
初次运行的话则调用main_NewGuard函数实现一些特殊文件的变化的监控。具体实现采用的golang的fsnotify包,监控的文件对象有三个:/root/.ssh/authorized_keys,/var/spool/cron/crontabs/root,/var/spool/cron/root。
接着调用main_NewBackdoor,通过其中的main__ptr_backdoor_injectSSHKey函数写入内置的公钥到受害者机器的authorized_keys中,完成ssh后门的植入。
创建守护进程,文件监控和后门植入完成后,两个版本的接下来的流程就不再完全一致了:
从汇编代码可以清楚的看到v3016版本采用main_MustXList函数替换了v3014版本的main_NewXHosts函数,且代码的执行流顺序也有所改变,v3016没有再先从内置的IP列表中下载并运行挖矿程序,而是选择先构建分布式节点,加入集群网络。节点构建整体流程如下:
我们跟进这个新的函数,看看其具体实现。首先获取受害者机器当前用户家目录,再将路径与3016.bs拼接后打开:
打开文件后,则利用golang的bufio包循环读取文件中的每行内容,并将不在内置列表中的内容存入内置的ip列表中,完成后关闭文件:
接着通过ddgs_xlist_New函数构建分布式节点。节点名字是以ddg版本号+主机名md5组成,并将刚刚更新后的内置ip列表与节点名字一起传入该函数中。
若样本初次到达受害者机器,3016.bs文件肯定不存在,那么传入ddgs_xlist_New的ip列表就采用最原始内置的:
跟进ddgs_xlist_New函数,就会发现该样本采用的是hashicorp的go开源库memberlist构建分布式网络,而这在v3014版本中是没有的。
借助Memberlist的文档,继续跟进。可以看到首先创建transport和对应的udp和tcp端口的Listener,用于节点之间底层的网络通信,其中udp用于传输ping,alive等消息,tcp传输PUSH-PULL消息来同步节点之间的数据。
PUSH-PULL消息的含义:
接着创建配置启动项,可以看到DDG采用的是默认广域网配置:
由于memberlist是开源的,所以可以在config.go中看到其缺省配置,默认端口号为7946:
而其中DefaultWANConfig是建立在DefaultLANConfig之上的:
与数据包中的通讯端口一致:
配置项创建完成后,通过此配置项创建memberlist,并启动对应的listener:
创建memberlist完成后,再获取本地节点状态等信息以及地址,然后尝试连接ip列表中的远端节点从而加入远端的集群,让远端节点知道本地节点的存在,并尝试通过pushPull消息发送和接收节点信息和数据,最后返回成功连接上的远端节点数量或错误信息。
当返回错误信息时,广播leave消息,但是listener仍然启用,继续参与gossip和状态更新,直到超时:
接着调用ddgs_xlist__ptr_bootstraps_Bootstrap函数,通过Memberlist包的Members函数获取到所有已知存活节点的信息并返回。
返回后将这些新获取到的远端节点信息与内置ip列表合并:
DDG会按照以上的流程循环100次,不断尝试加入远端集群中来获取这些远端节点所拥有的所有存活节点列表。
最终当本地存活节点列表与远端节点的列表达到一致后就跳出循环返回:
前面说到传输节点数据用到的是tcp,在数据包中可以看到确实采用的是tcp传输push-pull消息来同步节点之间的数据:
同步完远端节点的数据信息到本地节点后,接下来流程就跟v3014基本一致了,先是通过main_NewMinerd下载挖矿程序,再通过main_pingpong函数访问内置ip列表中的ip获取配置文件,接着调用main__ptr_minerd_Update函数更新挖矿程序的配置,最后通过ddgs_cmd__ptr_Table_Do函数执行配置文件中返回的控制命令,完成攻击模块的调用、下载最新的bash传播脚本等。
其中获取的配置文件如下(msgpack编码):
以上几个函数在基础功能和流程上,与V3014相比没有太大的变化。下载下来的挖矿程序也一致,依然是v3014版本使用的qW3xT.4,采用开源挖矿程序XMRig/2.8.1编译而成。这里就不一一展开做详细分析了:
最终样本会在受害者机器当前用户家目录下的.ddg目录写入3016.bs文件来存放本地所有的节点列表,可以看到总计1872个:
完整列表:3016.bs
笔者认为DDG采用基于Gossip协议的memberlist,主要优点有以下两方面:
分析完3016版本,在访问其中一个c2:http://132.148.241.138:8000/static/时发现样本版本在短短十几天已经又迭代了多个版本了,已经到了3019,可以看出作者很活跃。
每个版本大致看了下,在3017,3018版本采用upx 3.95压缩了下,减小了elf文件的体积;最新的3019版本没有压缩,只是样本名字变成了fmt.*。几个版本与v3016相比,程序总体流程没有太多改变,应该是在小部分函数上有所变化,感兴趣的朋友可以自行下载跟进一下。
https://github.com/sibears/IDAGolangHelper
https://github.com/hashicorp/memberlist
https://www.colabug.com/1010287.html
https://blog.netlab.360.com/https-blog-netlab-360-com-a-fast-ddg-3014-analyze/