OneShell

I fight for a brighter tomorrow

0%

mirai源码分析:实战真实mirai样本1-有符号

这是一次真实的应急事件,某个客户的设备被检测出来在持续向外发送大量的流量,疑似被控制作为DDoS肉鸡。经过上机排查进行流量抓包,的确是存在和DDoS攻击现象,也和恶意的CNC在进行通信。
分析mirai样本前,我们首先需要明确分析的目的,此处需要分析得到的内容如下:

  1. CNC会连地址:类似于获取C2的地址来进行溯源
  2. 弱口令字典:以便知道设备是如何沦陷的

CNC回连地址

源码解析

通过之前分析mirai源码,可以知道CNC会连以及相关的配置是在函数table_init->add_entry中进行添加的,每次添加一条。bot源码如下,包含了CNC会连域名+端口,扫描成功结果反馈域名+端口。

1
2
3
4
5
add_entry(TABLE_CNC_DOMAIN, "\x4F\x4B\x50\x43\x4B\x0C\x41\x4C\x41\x22", 11); // mirai.cnc
add_entry(TABLE_CNC_PORT, "\x22\x35", 2); // 23

add_entry(TABLE_SCAN_CB_DOMAIN, "\x4F\x4B\x50\x43\x4B\x0C\x41\x4C\x41\x22", 11); // mirai.cnc
add_entry(TABLE_SCAN_CB_PORT, "\x99\xC7", 2); // 48101

但是add_entry是将加密后的数据硬编码到bot中,如果想从样本中获取到解密后的数据,还需要分析样本作者可能自定义的加解密逻辑,如下是add_entry函数的逻辑,可以看到仅是简单的内存复制到table结构体中。

1
2
3
4
5
6
7
8
9
10
11
12
static void add_entry(uint8_t id, char *buf, int buf_len)
{
char *cpy = malloc(buf_len);

util_memcpy(cpy, buf, buf_len);

table[id].val = cpy;
table[id].val_len = (uint16_t)buf_len;
#ifdef DEBUG
table[id].locked = TRUE;
#endif
}

对于table结构体中的加密内容,会在bot的运行过程中根据需要对table中的内容进行实时的加解密,例如在解析CNC地址的时候,就会动态对TABLE_CNC_DOMAINTABLE_CNC_PORT进行解密,使用完毕后,再调用table_lock_val进行加密。通过使用这样的手段,可以防止硬编码的CNC数据被直接从样本静态分析中得到,也可以在一定程度防止某些动态扫描内存的手法从bot中获取到数据。

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
static void resolve_cnc_addr(void)
{
struct resolv_entries *entries;

table_unlock_val(TABLE_CNC_DOMAIN);
entries = resolv_lookup(table_retrieve_val(TABLE_CNC_DOMAIN, NULL));
table_lock_val(TABLE_CNC_DOMAIN);
if (entries == NULL)
{
#ifdef DEBUG
printf("[main] Failed to resolve CNC address\n");
#endif
return;
}
srv_addr.sin_addr.s_addr = entries->addrs[rand_next() % entries->addrs_len];
resolv_entries_free(entries);

table_unlock_val(TABLE_CNC_PORT);
srv_addr.sin_port = *((port_t *)table_retrieve_val(TABLE_CNC_PORT, NULL));
table_lock_val(TABLE_CNC_PORT);

#ifdef DEBUG
printf("[main] Resolved domain\n");
#endif
}

样本静态分析

讲述完原理,接下来对样本进行静态分析就需要做两个工作:

  1. 获取样本中硬编码的table中的CNC回连地址信息
  2. 逆向样本作者自定义的table_unlocal_val函数。
    运气比较好,这个样本应该是出于开发或者实验阶段,作者居然没有去除掉样本的符号表,因此可以直接定位到table_init函数。其中的add_entry函数应该是被编译优化了,但是依旧可以从uti_memcpy函数中得到硬编码的加密数据存放的位置。
    undifined
    接下来就是获取解密函数table_unlock_val的逻辑,如下。
    table_unlock_val根据table_id获取到待解密的结构体,并获取到其中的数据存放指针v2。然后从全局变量table_keys中每轮取出4个字节,并使用这4个字节对待解密数据进行xor计算。解密逻辑还是比较清晰明了的。
    undifined
    接下来就是编写脚本对数据进行解密,我写了一个IDAPython脚本,大致的工作就是在table_init函数中,分析util_memcpy的调用传参,获取到硬编码的table数据所在,然后使用解密逻辑进行解密:
    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
    import 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)
    执行结果如下,其中CNC地址和Telnet爆破成功数据返回地址就为cjfop.xyzsdfsd.xyz,经过验证这和抓包数据中的通信地址吻合。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    cjfop.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
2
3
4
5
6
7
8
9
10
11
12
// Set up passwords
add_auth_entry("\x50\x4D\x4D\x56", "\x5A\x41\x11\x17\x13\x13", 10); // root xc3511
add_auth_entry("\x50\x4D\x4D\x56", "\x54\x4B\x58\x5A\x54", 9); // root vizxv
add_auth_entry("\x50\x4D\x4D\x56", "\x43\x46\x4F\x4B\x4C", 8); // root admin
add_auth_entry("\x43\x46\x4F\x4B\x4C", "\x43\x46\x4F\x4B\x4C", 7); // admin admin
add_auth_entry("\x50\x4D\x4D\x56", "\x1A\x1A\x1A\x1A\x1A\x1A", 6); // root 888888
add_auth_entry("\x50\x4D\x4D\x56", "\x5A\x4F\x4A\x46\x4B\x52\x41", 5); // root xmhdipc
add_auth_entry("\x50\x4D\x4D\x56", "\x46\x47\x44\x43\x57\x4E\x56", 5); // root default
add_auth_entry("\x50\x4D\x4D\x56", "\x48\x57\x43\x4C\x56\x47\x41\x4A", 5); // root juantech
add_auth_entry("\x50\x4D\x4D\x56", "\x13\x10\x11\x16\x17\x14", 5); // root 123456
add_auth_entry("\x50\x4D\x4D\x56", "\x17\x16\x11\x10\x13", 5); // root 54321
add_auth_entry("\x51\x57\x52\x52\x4D\x50\x56", "\x51\x57\x52\x52\x4D\x50\x56", 5); // support support

在函数add_auth_entry中,会调用解密函数deobf将硬编码的加密数据进行解密,然后保存到auth_table结构体中。

1
2
3
4
5
6
7
8
9
10
11
12
13
static void add_auth_entry(char *enc_user, char *enc_pass, uint16_t weight)
{
int tmp;

auth_table = realloc(auth_table, (auth_table_len + 1) * sizeof (struct scanner_auth));
auth_table[auth_table_len].username = deobf(enc_user, &tmp);
auth_table[auth_table_len].username_len = (uint8_t)tmp;
auth_table[auth_table_len].password = deobf(enc_pass, &tmp);
auth_table[auth_table_len].password_len = (uint8_t)tmp;
auth_table[auth_table_len].weight_min = auth_table_max_weight;
auth_table[auth_table_len++].weight_max = auth_table_max_weight + weight;
auth_table_max_weight += weight;
}

静态分析

类似的,分析完源码之后,需要解决的就是两个问题:

  1. 定位到硬编码的加密字典
  2. 根据样本的解密逻辑对字典进行解密
    硬编码的解密字典可以在scanner_init函数中获取,只需要分析add_auth_entry函数的传参就可以得到。
    undifined
    对于解密逻辑,可以在函数add_auth_entry中得到,可以看到deobf函数已经被编译优化了,但是我们依旧可以看到解密逻辑仅仅是使用了简单的xor操作,对enc_userenc_pass逐个字节xor0xEA
    undifined
    因此可以对应写出字典提取的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
    54
    import 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
    98
    name: 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设备的视频应用程序,但明显不是本次应急设备的,因此猜测该样本还正在处于不断的开发调试中(毕竟连调试信息都没去除)。
undifined
从弱口令字典中可以看出,该样本应该是针对国内某些设备的,因为字典中包含的弱口令有几个是中国电信的光猫设备的常见默认密码。