workminer是通过SSH爆破传播的挖矿木马,感染后会释放xmrig挖矿程序利用主机的CPU挖取北方门罗币。该样本能够执行特定的指令,指令保存在一个配置文件config中,config文件类似于xml文件,里面有要执行的指令和参数,样本中内嵌了一个config,无攻击指令。该样本通过使用自己扩展的DHT协议来更新config,有两种方式,一是访问8个引导节点(样本内嵌,公开的)加入p2p网络,通过来寻找具有特定前缀id的同伙,来更新config,二是会随机扫描其它IP的UDP端口(IP是随机生成的,端口是一个范围内的随机端口),若扫中其它受害主机,受害主机会返回config,用这种方式来更新config。
workminer属于Mozi僵尸网络组织,主要攻击Iot设备,作者已于2021年被中国警方逮捕,该僵尸网络目前在互联网上还持续活跃。本文主要介绍workminer的dht通信部分,workminer使用go和C语言混合编译,C语言主要用于与同伙节点通信,拉取和更新config。
基础知识一:Linux系统调用
为了识别样本中的socket相关函数,我们需要了解Linux系统调用,尤其是socket相关函数调用的基础知识。
linux系统中使用int 80来调用linux系统api,linux中调用socket相关函数,是通用系统调用sys_socketcall的实现的,该接口的系统调用号为102,函数定义为
#include <sys/socket.h>
int socketcall(int subcall, unsigned long *args);
调用的时候,寄存器eax保存调用号,ebx为第1个参数,ecx为第2个参数,通过eax和ebx就能知道是哪个api
ebx int subcall
ecx unsigned long *
eax 102
int 0x80
第一个参数subcall要调用的api号,在 /usr/include/linux/net.h
中定义,使用find命令找到这个文件
# cat /usr/include/linux/net.h | grep SYS
#define SYS_SOCKET 1 /* sys_socket(2) */
#define SYS_BIND 2 /* sys_bind(2) */
#define SYS_CONNECT 3 /* sys_connect(2) */
#define SYS_LISTEN 4 /* sys_listen(2) */
#define SYS_ACCEPT 5 /* sys_accept(2) */
#define SYS_GETSOCKNAME 6 /* sys_getsockname(2) */
#define SYS_GETPEERNAME 7 /* sys_getpeername(2) */
#define SYS_SOCKETPAIR 8 /* sys_socketpair(2) */
#define SYS_SEND 9 /* sys_send(2) */
#define SYS_RECV 10 /* sys_recv(2) */
#define SYS_SENDTO 11 /* sys_sendto(2) */
#define SYS_RECVFROM 12 /* sys_recvfrom(2) */
#define SYS_SHUTDOWN 13 /* sys_shutdown(2) */
#define SYS_SETSOCKOPT 14 /* sys_setsockopt(2) */
#define SYS_GETSOCKOPT 15 /* sys_getsockopt(2) */
#define SYS_SENDMSG 16 /* sys_sendmsg(2) */
#define SYS_RECVMSG 17 /* sys_recvmsg(2) */
#define SYS_ACCEPT4 18 /* sys_accept4(2) */
#define SYS_RECVMMSG 19 /* sys_recvmmsg(2) */
#define SYS_SENDMMSG 20 /* sys_sendmmsg(2) */
DHT协议基础
DHT协议是一种BT协议,使用udp来通信,通信内容使用bencode编码。可参考bt协议详解 DHT篇(下) - 蓝猫163 - 博客园 (cnblogs.com)。
DHT协议有以下几种报文
ping
最基础的请求就是ping。这时KPRC协议中的“q”=“ping”。Ping请求包含一个参数id,它是一个20字节的字符串包含了发送者网络字节序的nodeID。对应的ping回复也包含一个参数id,包含了回复者的nodeID。
示例
ping请求={"t":"aa", "y":"q","q":"ping", "a":{"id":"abcdefghij0123456789"}}
B编码=d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:aa1:y1:qe
回复={"t":"aa", "y":"r", "r":{"id":"mnopqrstuvwxyz123456"}}
B编码=d1:rd2:id20:mnopqrstuvwxyz123456e1:t2:aa1:y1:re
find_node
Findnode被用来查找给定ID的node的联系信息。这时KPRC协议中的q=“find_node”。find_node请求包含2个参数,第一个参数是id,包含了请求node的nodeID。第二个参数是target,包含了请求者正在查找的node的nodeID。当一个node接收到了find_node的请求,他应该给出对应的回复,回复中包含2个关键字id和nodes,nodes是一个字符串类型,包含了被请求节点的路由表中最接近目标node的K(8)个最接近的nodes的联系信息
示例
find_node请求={"t":"aa", "y":"q","q":"find_node", "a":{"id":"abcdefghij0123456789","target":"mnopqrstuvwxyz123456"}}
B编码=d1:ad2:id20:abcdefghij01234567896:target20:mnopqrstuvwxyz123456e1:q9:find_node1:t2:aa1:y1:qe
回复={"t":"aa", "y":"r", "r":{"id":"0123456789abcdefghij", "nodes":"def456..."}}
B编码=d1:rd2:id20:0123456789abcdefghij5:nodes9:def456...e1:t2:aa1:y1:re
get_peers
这个请求用来表明发出announce_peer请求的node,正在某个端口下载torrent文件。announce_peer包含4个参数。第一个参数是id,包含了请求node的nodeID;第二个参数是info_hash,包含了torrent文件的infohash;第三个参数是port包含了整型的端口号,表明peer在哪个端口下载;第四个参数数是token,这是在之前的get_peers请求中收到的回复中包含的。收到announce_peer请求的node必须检查这个token与之前我们回复给这个节点get_peers的token是否相同。如果相同,那么被请求的节点将记录发送announce_peer节点的IP和请求中包含的port端口号在peer联系信息中对应的infohash下。
这个请求报文本样本没有
其响应报文的处理同find_node报文
announce_peer
这个请求用来表明发出announce_peer请求的node,正在某个端口下载torrent文件。announce_peer包含4个参数。第一个参数是id,包含了请求node的nodeID;第二个参数是info_hash,包含了torrent文件的infohash;第三个参数是port包含了整型的端口号,表明peer在哪个端口下载;第四个参数数是token,这是在之前的get_peers请求中收到的回复中包含的。收到announce_peer请求的node必须检查这个token与之前我们回复给这个节点get_peers的token是否相同。如果相同,那么被请求的节点将记录发送announce_peer节点的IP和请求中包含的port端口号在peer联系信息中对应的infohash下。
这个请求报文本样本没有
不支持这个报文
workminer的DHT通信部分
workminer通过自己改良过的DHT协议来请求和config,下面来介绍workminer的dht通信过程。
首先会生成一个特殊的version,则workminer节点发出的dht报文中都会有这个version字段。version字段共9个字节,前5个字节为1:v43:
(31 3A 76 34 3A),后面4个字节中根据下面的规则生成的。
第1个字节是随机产生的,第2个字节是硬编码的0x42或由config文件中[ver]
字段指定,第3个字节和第4个字节是通过自定义的校验算法生成的。
下面是抓取的ping包,通过这个特殊的标记,wokerminer在处理接收到的包时就能够识别出哪些包是正常的dht包,哪些包中由同伙发出来的,对于正常的dht协议的请求和响应包,走正常的处理流程,对于同伙的包(ping和find_node),就会交换config。
上面这个计算校验和的算法是识别同伙的关键,如下代码,若结果为0表示是同伙的包,若不是0则是正常的dht数据包。
// 根据version字段来判断是同伙发来的包
int GenCheckSum_82AAB68(unsigned __int16 a1, char *buf_a2, unsigned int buflen_a3)
{
char j; // [esp+5h] [ebp-Bh]
unsigned int i; // [esp+Ch] [ebp-4h]
for ( i = 0; i < buflen_a3; ++i )
{
a1 ^= (unsigned __int8)buf_a2[i] << 8;
for ( j = 8; j; --j )
{
if ( (a1 & 0x8000) != 0 )
a1 = (2 * a1) ^ 0x8005;
else
a1 *= 2;
}
}
return a1;
}
int main(){
char version[] = { 0x31,0x3a ,0x76 ,0x34 ,0x3a ,0x26 ,0x42 ,0x53 ,0x77};
int checksum = GenCheckSum_82AAB68(0,version,9);
if(checksum == 0){
puts("Mozi pkt");
}
else{
puts("Normal dht pkt");
}
return 0;
}
从下列中随机选择一个端口,若端口为0,则取random()%64510+1024
,监听这个udp端口。
port_list[28] = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,8080,8000,1900,1434,1027,6881,4000
30301,5353,11211,8082,8081,8083,5060]
为这UDP端口添加iptables规则。
使用异或的方式解密了两个公钥,异或使用密钥为
4E 66 5A 8F 80 C8 AC 23 8D AC 47 06 D5 4F 6F 7E
将内嵌在样本的config提取出来,共624字节,前524字节为加密的config内容,后96字节为文件的签名。使用第1个公钥来对加密的config验证签名。若验证签名通过,则使用go语言来解析解密后的config,执行config中的指令。若config中含有[ver]
标签,使用ver标签的值来重新计算数据包version字段。
公钥为
第一个公钥 用于验证config前528字节的完整性
02 d5 d5 e7 41 ee dc c8 10 6d 2f 48 0d 04 12 21
27 39 c7 45 0d 2a d1 40 72 01 d1 8b cd c4 16 65
76 57 c1 9d e9 bb 05 0d 3b cf 6e 70 79 60 f1 ea
ef
第二个公钥 用于验证config前428字节的完整性
02 c0 a1 43 78 53 be 3c c4 c8 0a 29 e9 58 bf c6
a7 1b 7e ab 72 15 1d 64 64 98 95 c4 6a 48 c3 2d
6c 39 82 1d 7e 25 f3 80 44 f7 2d 10 6b cb 2f 09
c6
解密后的config
[ss]ssh[/ss][cpu].x[/cpu][hp]88888888[/hp]
构造自身nodeid,nodeid有20个字节,有以下几种可能
一 1/1000的可能 使用字符串888888d1:ad2:id20:
作为前缀,即38 38 38 38 38 38 64 31 3A 61 64 32 3A 69 64 32 30 3A xx xx
,后两个字节为随机值。
二 49/1000的可能,使用888888
作为前缀,即38 38 38 38 38 38 xx xx xx xx xx xx xx xx xx xx xx xx xx xx
,后面12个字节为随机值
三 950/1000的可能,使用随机生成的nodeid.
接着使用上面的构造的selfNodeId,向内置的8个引导节点发送ping请求。TransactionId使用6E 70 00 00
。
router.bittorrent.com:6881
bttracker.debian.org:6881
router.utorrent.com:6881
dht.transmissionbt.com:6881
212.129.33.59:6881
82.221.103.244:6881
130.239.18.159:6881
87.98.162.88:6881
使用wireshark抓包可以看到包的内容
这个包的含义如下,
请求参数(request arguments)
a:{id:38383838383864313a6164323a696432303a9550}
请求类型(request type)
q:ping
会话Id(Transaction Id)
t:706e0000
版本(Version)
v:1942d177
消息类型(Message Type)q表示请求
y:q
设置一个全局的队列,用来存储的节点信息,这个队列结构如下所示。
struct NODE
{
char node_id[20];//节点的node_id
char addr[128];//地址信息 可存储Ipv4和ipv6的sockaddr结构
int addrlen;//sockaddr结构的长度
};
struct NODE_LIST
{
int head;//队列头
int end;//队列尾
NODE nodelist[64];//队列,队列大小为64
};
下面进入一个死循环,不断的向外发包和处理响应包。
每隔899秒,向内嵌的8个公共节点发送ping请求。
从config中取出[nd]
标签的值,这是一个以,
分割的列表,如url1,ip,url2,...
,向其中的每个地址发送ping请求,使用端口6881,会话id使用6E 70 00 00
。(内嵌的config中没有nd标签,不会执行)
每隔299秒,若node_list不为空的话,从其中随机取一个node,发送find_node请求。
find_node消息中的target_node_id使用以下的规则构造,有4种可能。
一是 targetid完全使用随机值,即20个随机的字节
二是 将 123888d1:ad2:id20:
作为target_nodeid的前18个字节,后2个字节随机
三是 以888888
作为target_nodeid的前6个字节,后14个字节随机
四是 以888888d1:ad2:id20:
作为target_nodeid的前18个字节,后2个字节随机
会话id为6E 66 00 00
。
构造一段随机长度的buf,长度为从[3,97]
中取一个随机数,将第二个字节为0x2e,使用前面的算法计算校验值,将2个字节的校验值添加到buf末尾。
从['8080','8000', '1900', '1434', '1027', '6881', '4000', '30301', '5353', '11211', '8082', '8081', '8083', '5060']
中随机取一个端口,构造一个随机的IP地址,将上面构造好的数据发出去。执行100次。
当同伙收到这个报文后,会将加密的config发回来,这里将这个报文定义为GET_CONF报文。
每隔599秒,向node_list中所有节点发送一次find_node请求,target_nodeid有两种情况,会话id为6E660000
.
一是 以888888d1:ad2:id20:
为前缀,后面为随机值
二是 以888888
前缀后面为随机值
下面是处理接收到的数据包的逻辑
首先使用select和recvfrom接收udp的包,将dht相关的包中关键字段解析出来,如下图所示。对dht数据包进行解析,对其中的关键的字段提取,并返回数据包的类型,根据这个类型值进入不同的处理逻辑,提取出来的字段dht字段有
version: 可判断是否为同伙的包
transctionid :可判断ping和find_node的响应
nodeid :对方的nodeid
targetid: find_node请求的目标id
nodes :find_node响应的nodes列表
数据的类型有以下几种
enum DHTPktType : int
{
DHTPktType_OTHER = -1,//其它
DHTPktType_ERROR = 0,//报错信息
DHTPktType_RESPONSE,//响应包 包括ping、find_node的响应
DHTPktType_PING,// ping请求
DHTPktType_FIND_NODE,// find请求
DHTPktType_GET_PEERS,//get_peers请求
DHTPktType_ANNOUNCE_PEER,//annouce_peer 请求
DHTPktType_MOZI_CNF_REQ,//get_conf请求 即上面的GET_CONF报文
DHTPktType_MOZI_ENCODING_CNF //接收到对方发来加密的config
};
下面介绍这几种报文的处理逻辑
ping、find_node的响应
若会话id为6E 70 00 00
,说明是本机发出去的ping请求的响应包,将对方的nodeid、ip、port添加进入全局队列。
若会话id为6E 66 00 00
,说明是本机发出来的find_node请求的响应,将对方的信息加入队列,检查version字段。若不是Mozi节点发来的包,将其中的nodes字段中8个节点加入队列。若是Mozi节点的包,且nodes大小为624,这其实是加密的config,处理这个config。
ping请求
若收到别的节点发来的ping请求,将对方信息加入队列,进行正常的回复。
find_node、get_peers的请求
这两类请求的处理逻辑相同。若是非同伙的包,按照正常的dht协议来处理,从队列中取8个节点信息发给对方。
若是同伙发来的请求,有4/5的概率,会将自身加密的config填充进nodes字段发送给对方。
announce_peer请求
这类请求不支撑,发送一个error信息。
GET_CONF请求
这不是DHT请求,就是前面随机构造的数据包,数据包长度<=99字节,最后两个字段是校验值,当节点收到此请求,会将自身的config发送给对方,大小为624字节。
收到对方的config
若不是DHT报文,且大于99字节,这是别的节点发来的加密的config,处理这个config.
config的处理
对同伙获取的config大小为624字段,其中前528字段是config1,后面96字段是签名信息,使用第1个公钥验证其完整性,
对于config1,取其前428为config2,428到524的96字段为签名信息,使用第2个公钥签证其完整性。
然后对config2进行解密,向对方节点发送一个ping请求,将config文件中的[cpu]、[ss]、[sv]
三个标签的值,提取hp字段,这是的nodeid的前缀,提取ver字段,重新计算verison。
cpu 不知道含义
ss 不知道含义
sv 不知道含义
hp nodeid的前缀
ver 这是一个byte值,作为version字段的第二个字节
最后调用go函数main_deal_conf来处理config,提取出下列标签,进而执行不同的操作。
slan 这是个开关变量,作用未知
swan 这是个开关变量,未知
spl 这是一个url,用于下载更新的bash脚本
sdf 格式为"url|filename",会从url中下载文件保存为/tmp/+filename
sud 其中含有一个url,用于更新本地的病毒母体
ssh 其中含有一个url,用于下载bash脚本来执行
sdr 用于下载文件执行
srn 用于执行命令
scount 指向用于回连的url,默认为`http://ia.51.la/go1`,受害者会周期性的访问这个地址,用于便于攻击者知道受害者的信息
IOC
如何快速识别workminer的流量,可以根据下面的特征
正常的dht报文的version字段符合文中的校验算法
nodeid以88888、88888888、888888d1:ad2:id20:开头
会话id中为6E 70 00 00或6E 66 00 00
udp的载荷小于99,符合文中的校验算法
upd的载荷大小为624
有对外随机的udp扫描行为,目标端口为['8080','8000', '1900', '1434', '1027', '6881', '4000', '30301', '5353', '11211', '8082', '8081', '8083', '5060']
访问url
http://ia.51.la/go1
连接下列地址udp
router.bittorrent.com:6881
bttracker.debian.org:6881
router.utorrent.com:6881
dht.transmissionbt.com:6881
212.129.33.59:6881
82.221.103.244:6881
130.239.18.159:6881
87.98.162.88:6881
访问门罗币矿池
xmr.crypto-pool.fr:6666
扫描TCP以下端口
2222
3389
22222
443
55554
9000
2223
9090
8888
8022
6000
9999
2323
2002
7777
2022
666
444
5555
222
26
2382
830
4118
23
50000
流量中含有
SSH-2.0-Go
总结
workminer的作者使用了一种变形的dht协议来更新config,确认非常聪明,具有很好的隐蔽性。config 文件中有些标签的含义还是没有分析清楚。
参考资料
- Linux系统调用 int 80h int 0x80_怎么寻找int80指令-CSDN博客
- Linux system call table 정리(32bit, 64bit) (tistory.com)
- 深度追踪Mozi僵尸网络:360安全大脑精准溯源,揪出幕后黑手_360社区
- BitTorrent 分布式散列表(DHT)协议详解 | 寂静花园 (addesp.com)
- bt协议详解 DHT篇(下) - 蓝猫163 - 博客园 (cnblogs.com)
- P2P Botnet: Mozi分析报告 (360.com)
- P2P僵尸网络深度追踪——Mozi(二)二叉树吃瓜记-安全客 - 安全资讯平台 (anquanke.com)
- 『P2P僵尸网络漏洞研究——mozi』 netgear路由器漏洞复现-安全客 - 安全资讯平台 (anquanke.com)
- 【转】ECDSA 签名验证原理及C语言实现_ecdsa 嵌入式c语言-CSDN博客