介绍对僵尸网络的节点进行端口与协议识别,有助于提供威胁情报,减缓和预防相关恶意事件的影响。

僵尸网络探测原理与实现

僵尸网络 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等,这里就不展开了。

shodan

上面关于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地址: 1

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节点的通信流程如图所示。 zeroaccess 交互逻辑如下所示: (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字段为随机生成 生成报文的过程如下:

    1. 构造cmd,flag,magic字段的报文
    1. 计算得到crc32校验码
    1. 利用'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的探测思路,与主要的实现代码。后续的对识别的数据进行分析将在后面的公布,敬请期待。

参考资料: