介绍对僵尸网络的节点进行端口与协议识别,有助于提供威胁情报,减缓和预防相关恶意事件的影响。
僵尸网络探测原理与实现
僵尸网络 Botnet 是指采用一种或多种传播手段,将大量主机感染bot程序,从而在控制者和被感染主机之间所形成的一个可一对多控制的网络。攻击者通过各种途径传播僵尸程序感染互联网上的大量主机,而被感染的主机将通过一个控制信道接收攻击者的指令,组成一个僵尸网络。之所以用僵尸网络这个名字,是为了更形象地让人们认识到这类危害的特点:众多的计算设备在不知不觉中被攻击者利用,成为被人利用的一种工具,沦为拒绝服务攻击、发送垃圾邮件、窃取秘密、挖矿的僵尸主机。 因此,对僵尸网络的节点进行识别,有助于提供威胁情报,减缓和预防相关恶意事件的影响。
第一节 概要
1.1 僵尸网络通信方式
僵尸网络从通信组织方式大致分为C2中心式和P2P分布式两种。 C2(命令和控制)服务器,是一个中心计算机,负责对僵尸主机发送命令,及从僵尸主机接收信息。C2的基础架构通常包括多台服务器和其他技术组件。大多数僵尸网络采用“客户端-服务器端”的结构。还有一种常见的结构是僵尸网络采用了P2P结构,这种结构将C&C功能集成到了僵尸网络中。P2P僵尸网络使用了分布式的僵尸主机网络,主要是为了保护僵尸网络、防止网络中断。
1.2 僵尸网络的识别方式
僵尸网络通信协议一般都属于私有协议,对这种私有协议的识别,首先需要做的事对协议的逆向,然后全网探活相关端口,最后模拟协议的通信方式。如果目的节点响应的内容符合协议的格式,则认为识别出开放该协议的节点。例如netbus为c2中心式,netbus常用的端口为tcp:12345,因此,需要全网探活12345端口,从而确定可疑的netbus节点。 但这种方式存在一些问题:
- 1) 端口扫描比较耗费时间
- 2)存在一部分netbus c2节点,没有使用12345默认端口,端口扫描难以对这部分节点进行识别。
对于P2P僵尸网络,如zeroaccess,该家族节点均具备发现其他节点的能力(zeroaccess通信协议中的getL命令,具有返回该节点记录的节点的功能),因此,从一个已知的zeroaccess节点出发,通过模拟zeroaccess的getL协议的节点发现过程,即可获取其相邻节点的地址,之后,对得到的节点,依次通过模拟节点发现过程递归遍历,最终即可遍历整个zeroaccess网络。该方法的优势在于,仅对zeroaccess网络中的节点进行遍历,一是快,二来发现的相对比较完整。
第二节 通信过程
2.1 netbus通信过程
对于netbus c2基于TCP连接,默认端口12345,对其探测过程就是获取端口的banner信息(常见版本有NetBus 1.60、NetBus 1.70)。如果返回结果为NetBus,即有较大概率判定为netbus木马。代码实现如下:
def check(ip, port, timeout=3):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
s.connect((ip, port))
receive = (s.recv(10))
s.close()
if receive.startswith('NetBus'):
return ip,port
至于想得到存活tcp12345端口的ip,除了自己利用masscan或zmap扫描外,我们可以利用已经扫描好的数据如shodan、zoomeye、fofa、censys等,这里就不展开了。
上面关于C2的探测流程简单介绍完毕,netbus相对是比较简单的,大多数的C2节点探测,需要client先请求server特定内容,通过响应的信息与知识库比对来识别判定是否为C2。
2.2 zeroaccess通信过程
若一个P2P僵尸节点,希望加入到僵尸网络,总体上流程分为两步:
1)获取到当前P2P网络中网络状况正常的节点列表(已经预先植入种子节点列表)
2)运行zeroaccess的协议栈,与P2P网络中的其他节点通信
第一个问题就是如何得到zeroaccess的种子节点。思路如下:
- 1 确定你要获取的僵尸网络家族名称
- 2 找到该家族的样本或样本sha256值,如:af3258ecd3c2a70bae8b7a7bb3ecfd22edcbc5a0ee252fb060afa79e76dbe563
- 3 自己沙箱执行抓包或上传在线沙箱执行得到网络行为(virustotal或threatbook)
- 4 找到可疑的(IP,Port)(zeroaccess p2p常见通信端口如下表)
网络 | 32位系统端口 | 64位系统端口 |
---|---|---|
网络1 | Udp 16471 | Udp 16470 |
网络2 | Udp 16464 | Udp 16465 |
如下所示,从沙箱中得到初始化的IP地址:
95.105.117.206:16471
206.254.253.254:16471
69.251.73.140:16471
89.46.141.133:16471
188.27.180.95:16471
24.135.222.216:16471
...
本节将以其中获取的可疑节点为例,对P2P僵尸网络节点之间的通信过程做相应的说明。
在完成上述的步骤后,正常情况下,可以获取到16个僵尸节点的地址,之后即可通过zeroacess协议进行通信,zeroacees节点的通信流程如图所示。
交互逻辑如下所示:
(1)僵尸主机A向B请求B所知悉的IP列表
(2)僵尸主机B向A响应所知悉的IP列表
(3)僵尸主机A更新自己的知悉列表
第三节 通信协议
Zeroaccess P2P网络中,连接均使用UDP的方式。本章将对zeroaccess的通信协议做简单的描述,在本节的基础上,对P2P节点的识别,需要实际上是对通信数据包的过滤和解析。 zeroaccess的协议有过更新,目前掌握的版本信息如下,我们将以V2进行介绍,重点介绍getL和retL两个命令行的构造。
版本 | 命令 | 描述 |
---|---|---|
V1.0/V2.0 | getL | 请求peer列表 |
V1.0/V2.0 | retL | getL的回应报,发送peer列表以及文件元信息 |
V1.0 | getF | 请求file列表 |
V1.0 | setF | retL的回应报,发送请求的文件 |
V1.0 | srv! | 发送隐藏驱动的创建时间 |
V1.0 | yes! | Srv!的响应报文,发送感染时间 |
3.1 getL构造
zeroaccess V2的getL报文格式如下所示:
注意:所有字段的存储都为小端存储
CRC初始值为空, get报文中cmd的值为“getL” flag默认为0 magic字段为随机生成 生成报文的过程如下:
- 构造cmd,flag,magic字段的报文
- 计算得到crc32校验码
- 利用'ftp2'对整个报文xor操作
代码如下:
def getL():
# 构造cmd,flag,magic字段
magic = 0xD9AEA1A8
message = struct.pack('I4cIL',0,'L', 't', 'e', 'g',0,magic)
print("message1:%sr"%message)
# 计算crc32校验码
crc_sum = zlib.crc32(message) & 0xffffffffL
message = struct.pack('I4cIL',crc_sum,'L', 't', 'e', 'g',0,magic)
print("message2:%r"%message)
# xor操作
key = [ord('2'), ord('p'), ord('t'), ord('f')]
final_message = xorMessage(message, key)
print("final_message:%s"%final_message)
return final_message
3.2 retL报文解析
Zeroaccess V2 retL作为getL的响应报文,对retL的解析就是逆操作。
- 1)以“ftp2”对报文进行xor操作
- 2)按下图结构对数据解析
crc为校验码
cmd为retL
flag一般默认为0
Num IPs为ip地址的数量,后面以固定格式存储ip地址
def getretLip(self,decryptstr):
ip_list =[]
count =0
crc32 = decryptstr[count:count+4]
print 'crc32:%r'%self.reverse(crc32)
count +=4
retL = decryptstr[count:count + 4]
print 'retL:%r'%reverse(retL)
count += 4
zero = decryptstr[count:count + 4]
print 'zero:%r'%reverse(zero)
count += 4
ip_count = decryptstr[count:count + 4]
ip_count = struct.unpack('I',ip_count)
print 'ip_count:%d'%ip_count
count += 4
size = ip_count[0]*8+16
while(count <size):
global count
unit=decryptstr[count:count+8]
count=count+8
ip_int,time = struct.unpack('II', unit)
ip = inet_ntoa(struct.pack('I', htonl(ip_int))).split('.')
ip = changeip(ip)
if ip not in ip_list:
ip_list.append(ip+':'+str(udp_port))
return ip_list
通过对getL和retL协议的构造后,我们可以把可疑的IP,port作为自己的种子地址,迭代获取其它zeroaccess感染的IP地址。
第四节 小结
本文主要通过介绍了netbus、zeroaccess的探测思路,与主要的实现代码。后续的对识别的数据进行分析将在后面的公布,敬请期待。
参考资料:
- https://en.wikipedia.org/wiki/ZeroAccess_botnet
- ZeroAccess Indepth