这是一次真实的应急事件,某个客户的设备被检测出来在持续向外发送大量的流量,疑似被控制作为DDoS肉鸡。经过上机排查进行流量抓包,的确是存在和DDoS攻击现象,也和恶意的CNC在进行通信。
分析mirai样本前,我们首先需要明确分析的目的,此处需要分析得到的内容如下:
- CNC会连地址:类似于获取C2的地址来进行溯源
- 弱口令字典:以便知道设备是如何沦陷的
CNC回连地址
源码解析
通过之前分析mirai源码,可以知道CNC会连以及相关的配置是在函数table_init
->add_entry
中进行添加的,每次添加一条。bot源码如下,包含了CNC会连域名+端口,扫描成功结果反馈域名+端口。
1 | add_entry(TABLE_CNC_DOMAIN, "\x4F\x4B\x50\x43\x4B\x0C\x41\x4C\x41\x22", 11); // mirai.cnc |
但是add_entry
是将加密后的数据硬编码到bot中,如果想从样本中获取到解密后的数据,还需要分析样本作者可能自定义的加解密逻辑,如下是add_entry
函数的逻辑,可以看到仅是简单的内存复制到table
结构体中。
1 | static void add_entry(uint8_t id, char *buf, int buf_len) |
对于table
结构体中的加密内容,会在bot的运行过程中根据需要对table
中的内容进行实时的加解密,例如在解析CNC地址的时候,就会动态对TABLE_CNC_DOMAIN
和TABLE_CNC_PORT
进行解密,使用完毕后,再调用table_lock_val
进行加密。通过使用这样的手段,可以防止硬编码的CNC数据被直接从样本静态分析中得到,也可以在一定程度防止某些动态扫描内存的手法从bot中获取到数据。
1 | static void resolve_cnc_addr(void) |
样本静态分析
讲述完原理,接下来对样本进行静态分析就需要做两个工作:
- 获取样本中硬编码的
table
中的CNC回连地址信息 - 逆向样本作者自定义的
table_unlocal_val
函数。
运气比较好,这个样本应该是出于开发或者实验阶段,作者居然没有去除掉样本的符号表,因此可以直接定位到table_init
函数。其中的add_entry
函数应该是被编译优化了,但是依旧可以从uti_memcpy
函数中得到硬编码的加密数据存放的位置。
接下来就是获取解密函数table_unlock_val
的逻辑,如下。table_unlock_val
根据table_id
获取到待解密的结构体,并获取到其中的数据存放指针v2
。然后从全局变量table_keys
中每轮取出4个字节,并使用这4个字节对待解密数据进行xor计算。解密逻辑还是比较清晰明了的。
接下来就是编写脚本对数据进行解密,我写了一个IDAPython脚本,大致的工作就是在table_init
函数中,分析util_memcpy
的调用传参,获取到硬编码的table
数据所在,然后使用解密逻辑进行解密:执行结果如下,其中CNC地址和Telnet爆破成功数据返回地址就为1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82import idc
import idautils
ea = idc.get_name_ea_simple("util_memcpy")
# table_init范围内
start = 0x0000FA94
end = 0x0000FCC0
# 根据ARM传参规则,分析util_memcpy中的待解密数据地址
def find_args(addr):
each_op = addr
tmp_args = {
"R1" : 0,
"R2" : 0
}
while each_op >= start:
op_disasm = idc.GetDisasm(each_op)
if "LDR" in op_disasm and "R1" in op_disasm:
tmp_args["R1"] = each_op
if "MOV" in op_disasm and "R2" in op_disasm:
tmp_args["R2"] = each_op
if tmp_args["R1"] != 0 and tmp_args["R2"] != 0:
# print(hex(tmp_args["R1"]), hex(tmp_args["R2"]))
return tmp_args
each_op = idc.prev_head(each_op)
return None
# 根据地址获取待解密数据
def get_op_string(addr):
str_addr_list = list(idautils.DataRefsFrom(addr))
data = []
start = str_addr_list[1]
i = 0
while True:
byte = idc.get_wide_byte(start + i)
if byte == 0:
break
data.append(byte)
i += 1
# print("0x%x" % start, data)
return data, i
# 读取硬编码的table_keys
def init_key():
addr = 0x00025C14
key = []
i = 0
while True:
byte = idc.get_wide_byte(addr + i)
if byte == 0:
break
key.append(byte)
i += 1
return key, i
# 真正的解密函数
def encrypt(data, key):
for i in range(0, 80, 4):
key1 = key[i]
key2 = key[i + 1]
key3 = key[i + 2]
key4 = key[i + 3]
for j in range(0, len(data)):
data[j] ^= key1
data[j] ^= key2
data[j] ^= key3
data[j] ^= key4
result = ""
for each in data:
result += chr(each)
print(result)
key, key_len = init_key()
for addr in idautils.CodeRefsTo(ea, 0):
if start <= addr < end:
args = find_args(addr)
arg1, arg_len = get_op_string(args["R1"])
# print(arg1, arg_len)
encrypt(arg1, key)cjfop.xyz
和sdfsd.xyz
,经过验证这和抓包数据中的通信地址吻合。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15cjfop.xyz
sdfsd.xyz
gosh that chinese family at the other table sure ate a lot
TSource Engine Query
/proc/
/exe
/fd
/cmdline
enable
system
shell
sh
/bin/busybox BOTNET
ncorrect
BOTNET
弱口令字典
源码解析
同样的,从mirai源码中,可以知道弱口令字典是在函数scanner_init
->add_auth_entry
进行添加的,如下:
1 | // Set up passwords |
在函数add_auth_entry
中,会调用解密函数deobf
将硬编码的加密数据进行解密,然后保存到auth_table
结构体中。
1 | static void add_auth_entry(char *enc_user, char *enc_pass, uint16_t weight) |
静态分析
类似的,分析完源码之后,需要解决的就是两个问题:
- 定位到硬编码的加密字典
- 根据样本的解密逻辑对字典进行解密
硬编码的解密字典可以在scanner_init
函数中获取,只需要分析add_auth_entry
函数的传参就可以得到。
对于解密逻辑,可以在函数add_auth_entry
中得到,可以看到deobf
函数已经被编译优化了,但是我们依旧可以看到解密逻辑仅仅是使用了简单的xor操作,对enc_user
或enc_pass
逐个字节xor0xEA
。
因此可以对应写出字典提取的IDAPython脚本如下,大致逻辑依旧是从scanner_init
函数地址空间中分析add_auth_entry
的传参,获取到硬编码的字典,然后对其进行解密:解密结果如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54import idautils
import idc
ea = idc.get_name_ea_simple("add_auth_entry")
print("code refers to 0x%x" % ea)
# 分析函数传参
def find_args(addr):
# 在当前addr和当前函数范围之间
start = idc.get_func_attr(addr, idc.FUNCATTR_START)
each_op = addr
tmp_args = {
"R0" : 0,
"R1" : 0,
"R2" : 0
}
while each_op >= start:
op_diasm = idc.GetDisasm(each_op)
if "LDR" in op_diasm:
if "R0" in op_diasm and tmp_args["R0"] == 0:
tmp_args["R0"] = each_op
if "R1" in op_diasm and tmp_args["R1"] == 0:
tmp_args["R1"] = each_op
# if "R2" in op_diasm and tmp_args["R2"] == 0:
# tmp_args["R2"] = hex(each_op)
if tmp_args["R0"] !=0 and tmp_args["R1"] != 0 :# and tmp_args["R2"] != 0:
return tmp_args
each_op = idc.prev_head(each_op)
return None
# 获取硬编码的字典数据并解密
def get_op_string(addr):
str_addr_list = list(idautils.DataRefsFrom(addr))
data = ""
start = str_addr_list[1]
i = 0
while True:
byte = idc.get_wide_byte(start + i)
if byte == 0:
break
data += chr(byte ^ 0xEA)
i += 1
# print("0x%x" % start, data)
return data
# 解密
for addr in idautils.CodeRefsTo(ea, 0):
# print("call func at %s" % hex(addr))
args = find_args(addr)
arg0 = get_op_string(args["R0"])
arg1 = get_op_string(args["R1"])
print("name: %s" % arg0)
print("pass: %s" % arg1)
print("\n")1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98name: root
pass: Fireitup
name: root
pass: Pon521
name: telecomadmin:nE
pass: jA%5m
name: telnetadmin
pass: jA%5m
name: telecomadmin
pass: admintelecom
name: ftp
pass: admintelecom
name: user
pass: admintelecom
name: guest
pass: admintelecom
name: root
pass: icatch99
name: root
pass: vizxv
name: root
pass:
name: guest
pass: 12345
name: root
pass: tsgoingon
name: admin
pass: tsgoingon
name: default
pass: tsgoingon
name: default
pass: OxhlwSG8
name: daemon
pass: OxhlwSG8
name: root
pass: solokey
name: default
pass: lJwpbo6
name: root
pass: lJwpbo6
name: default
pass: tlJwpbo6
name: default
pass:
name: guest
pass: 123456
name: default
pass: S2fGqNFs
name: support
pass: S2fGqNFs
name: admin
pass: 1234
name: root
pass: admin
name: bin
pass: admin
name: default
pass: 1cDuLJ7c
name: root
pass: 123456
name: root
pass: 1001chin
name: bin
pass:
name: root
pass: Zte521
其他的一些有趣地方
在mirai源码中,进程隐藏是向argv[0]
写入随机字符串,以及使用ptctl
修改进程名的方式来进行的。该样本是直接将进程名修改为一个静态的字符串/var/Sofia
。经过调研,/var/Sofia
似乎是某些IoT设备的视频应用程序,但明显不是本次应急设备的,因此猜测该样本还正在处于不断的开发调试中(毕竟连调试信息都没去除)。
从弱口令字典中可以看出,该样本应该是针对国内某些设备的,因为字典中包含的弱口令有几个是中国电信的光猫设备的常见默认密码。