Mirai源代码精读(bot部分)

前言

虽然感觉前人分析挺多,但我觉得很多文章干看看不懂,很多地方跳来跳去,没铺垫我真的有点乱,也没有讲通我的疑惑点。感觉还是很想自己来读一下,毕竟那么多样本都是基于这套代码缝缝补补,加入各种东西

本文重点:

  • 想自己彻底搞清楚整个逻辑,而不是跳来跳去看一点点代码,反正跟着别人的文章看我是有点看不下去,问题越看越多,干脆直接看源码
  • 搞清楚恶意行为者,总是改变和万变不离其宗的代码部分,是什么,下次分析的时候大概的架构心中有数
  • 搞清楚初代mirai的恶意行为者有什么手段来给安全人员制造麻烦(〃>皿<)

读这篇文章可以知道,mirai是怎么kill同行,怎么让机器难以人工修复,如何横向传播感染,如何反调试,有什么攻击向量,以及,怎么把c2字符串都藏起来,等等

分析

mirai的bot部分就是日常所抓到的木马样本的代码实现部分,主要有这些模块,当然,有些.h宏定义还是有东西的,这个后面单独补充不然太臃肿
a3358d095eb50640ee6af69ff6a1d69.png
attack没什么好看的,其实挺千遍一律的。。。checksum也不需要说吧,那么接下来按每个文件,展开讲讲

从编译部分开始讲起

从编译的指令开始说,是因为看代码的时候刚开始非常疑惑为什么会有那么多#ifdef DEBUG分支
image.png

mirai有两套编译的参数,分属两个sh
一套是包含debug的,主要是,有个–DDEBUG,这样的话就会保留代码里面大量#ifdef DEBUG的代码逻辑
image.png
另一套则没有–DDEBUG,这是真正的木马使用的编译参数
image.png
那么,-O3的优化等级将会优化掉所有#ifdef DEBUG部分的逻辑,因为没有–DDEBUG参数,这样就能把调试的log都消失掉,而不给安全人员了解到功能的执行
当然这一点也方便了阅读源码的人了解功能

自实现函数工具箱:util.c

为了防止很多感染机器(特别是各类rtos)的lib库里面没有mirai希望使用的函数,提高兼容性,所以mirai自己实现了一套常用函数放在木马里面

  1. 字符串处理工具
    • util_strlen: 自定义字符串长度计算
    • util_strncmp/util_strcmp: 字符串比较
    • util_strcpy: 字符串复制
    • util_stristr: 不区分大小写的字符串子串搜索
  2. 内存操作工具
    • util_memcpy: 内存块复制
    • util_zero: 内存清零
    • util_memsearch: 内存块中搜索指定字节序列
  3. 数据类型转换
    • util_atoi: 字符串转整数
    • util_itoa: 整数转字符串
  4. 网络与文件系统接口
    • util_local_addr: 获取本地 IPv4 地址
    • util_fdgets: 从文件描述符逐字符读取行数据
  5. 字符类型判断工具
    • util_isupper/util_isdigit/util_isspace 等: 内联函数判断字符类型
      总的来说没什么好看的,哦,怎么没有内存管理相关,原来直接用系统原生的了

随机数生成:rand.c

Mirai-Source-Code/mirai/bot/rand.c
mirai专门写了块代码rand.c来负责随机生成东西,还是有点想法的,做到了兼容、性能、防猜测的综合考虑
这块还挺有意思的,虽然和攻击无关,不想了解可以跳过随机数生成这块
伪随机种子尽可能混合安全随机源以增熵,但是有可能是照顾到某些机器没有/dev/urandom所以没加那个
image.png
伪随机方案rand_next:使用Xorshift伪随机数生成器,
image.png
这个LFSR线性反馈移位寄存器的周期为2\^96-1,什么意思呢,
image.png

长的周期可以防止被推测下一个随机数;同时,其性能非常好,因为仅仅使用位移、异或、赋值操作。其最终会输出随机无符号的32位整形
此外还有一个借助rand_next生成给定长度随机unsign int的,其实就是类型强制转换,在剩余字节长度大于4的时候会直接用rand_next(正是4字节长)的结果,而不足4大于等于2剩余长度、不足2剩余长度的时候也有其各自的逻辑

image.png
当然,也有借助rand_next生成给定长度随机小写字母和数字
image.png
这样就梳理了mirai任何随机生成的东西的过程,看起来好像做到了兼容、安全、性能的某种最佳了

横向移动感染:scanner.c

Mirai-Source-Code/mirai/bot/scanner.c

爆破用账密如何保存、加载、选取

很好奇,如果攻击者要更新账密信息怎么办呢
image.png

实际上,账密信息在代码里的保存结果,是经过混淆的,在使用之前,首先被解混淆,再被放到内存里待用。而auth_entry函数正是负责这个的

add_auth_entry,顾名思义,这就是添加一个关于认证的,entry,entry应该理解成程序进行auth逻辑的入口,
四舍五入的意思就是,add_auth_entry是用来选择账密的

image.png
可以看到,其内部实现主要是开辟了一块内存区域来存放信息,然后,信息包含解混淆(deobf)后的账密以及它们的长度,还有就是有个不知道是什么的weight_min和weight_max,看起来意思是权重,紧接着就会用到来辅助随机选取的东西

解混淆算法如下,其实就是该字节与0xDE,0xAD,0xBE,0xAF逐一进行异或,想必这里的算法是可以灵活改变的
image.png

random_auth_entry 负责随机选择auth_table内部的一个账密来尝试,使用的是rand_next()来进行随机选取
image.png
uint16_t r = (uint16_t)(rand_next() % auth_table_max_weight);作用是选取随机数 r,而这个r会负责选择权重
接下来的逻辑是,遍历auth_table每个位置,如果r小于其weight_min,即权重最小值,那就什么都不做,继续遍历;如果r小于其权重最大值,那么,就返回当前auth_table位置
意思其实就是,每个auth_table[i]都有其权重min max范围,r如果落到该范围,就会选中该auth_table[i],即账密
说起来复杂,其实还是随机选取那一套,

只是,为什么要搞的那么麻烦呢?
我觉得作者如此大费周章的搞一个随机选取逻辑,是因为“贴心照顾”到很多机器的兼容性,不管怎么说,这套只用到了一些c语言的基础操作,如realloc和各种数组指针操作,几乎每台被感染机器都能执行

总的来说,
random_auth_entry负责选择add_auth_entry,
add_auth_entry负责保存账密、账密长度和生成供random_auth_entry随机选择的权重,并调用deobf解混淆账密,再将以上信息放入内存区域,
deobf负责解混淆账密信息
这样就完成了对爆破账密的保存与读取、解混淆、随机选取、加载

实际上可以在scanner上看到,其仅仅对telnet,23和2323image.png
进行爆破,没有使用任何漏洞来横向传播
image.png

C2信息配置:config.h

mirai/bot/config.h
配置c2信息的地方,放心,包有混淆的啦
image.png
然后发现table.c引用了这些神必文字,想必也是解混淆和加载c2的地方了

重要信息加载:table.c

mirai/bot/table.c
这块大量引用的add_entry,提醒一下,跟前面的add_auth_entry不一样,前面说的那个方法自带一个混淆,这个没有
image.png
可以看到就是开辟内存空间,memcpy了一下并记录长度,然后,如果在debug状态,就会在locked上了个锁,实际上这个位不影响木马执行,后面会说lock和unlock的机制

然后我们见到了熟悉的,大量的信息加载,这些肯定全都按字节去混淆过,而且是c2和某些字符串信息,table.h会有代号对应它们,用的时候只会用代号

image.png

那怎么解混淆,这次相比前述解混淆机制其实是一样的,对原文使用key进行逐字节异或,区别在于这里对key的逐字节取逻辑,性能上也许更好,就是用的右移+取低1字节的操作
image.png

放这里的toggle_obf,即混淆机制,toggle_obf混淆恢复的逻辑,仅仅被table_unlock_val、table_lock_val所调用,
image.png
这个解混淆方法调用范围如下,看起来几乎贯穿木马全文了
image.png
实际上这对东西是干嘛的呢,为什么一个unlock一个lock却长的完全一样
其实解混淆和混淆是一样的过程,所以内部都是toggle_obf(),

只要成对使用即可,在需要用到某个值时,mirai便会首先table_unlock_val,toggle_obf()解掉混淆,用完立马再toggle_obf()一次混淆,确保给安全人员逆向的时候添麻烦

请求特征:table.h

mirai/bot/table.h
可以看到其存放了一些值,而值的含义,项目拥有者已经帮我们标注出来了,
其主要有:

  • 当前僵尸bot掌握的基本信息,比如c2的域名ip端口,以及成功执行的标志位,进程参数
  • killer的数据,没看到后面有点不知道,这块根据国外的分析,其实放的是一些别的僵尸控制进程的特征
    image.png
    看起来是对付Qakbot(Qbot)、Zollard、Remaiten以及有UPX壳魔数并且加载到内存中的的进程,
    (论修改UPX壳头部的必要性,不仅能规避杀软检测,还能防止同行搞事,哈哈,提心吊胆的)
  • scanner的数据,主要包括了欲感染的目标域名(ip)或端口,然后是拿shell的一些指令定义,还有专门的验证登录成功的字符串,连接僵尸bot的密码,以及杀死同行僵尸控制进程的东西
  • attack的数据,主要是请求头那些东西
  • User-agent头的数据,想必修改这里能规避某些特征检测
    image.png
    全都使用代号来对应table.c内部的混淆字符串

占领进程防止同行,占领端口防止修复:killer.c

mirai/bot/killer.c
首先,如果定义要kill telnet,那就使用killer_kill_by_port(htons(23))
image.png

关闭其它控制进程:killer.c

killer进程的自身复制

首先,killer会不断创建进程,且避开先前的kill区域
image.png
这样就能保证,处在子进程即fork出来的子killer进程,继续执行下去,而处在父进程,即创建无数killer的父killer进程,会return到调用killer_init的地方,然后以后回来继续fork东西
注意这里的父和子都是killer
image.png

killer子进程

kill端口并防止复活

首先会kill一些端口,然后绑定,绑定是为了防止复活,以不让他人连上僵尸机器
kill 23端口相关进程,并预先绑定防止相关进程重启占用:
image.png
kill 22端口相关进程,并预先绑定防止相关进程重启占用:
image.png
kill 80端口相关进程,并预先绑定防止相关进程重启占用:
image.png

内存匹配特定字符串并kill进程

然后就会为下一个步骤做准备,检查proc里面的exe,即保存真实地址的东西是否能够访问
image.png

其实这里分析过这类木马就知道了,会常常遇到,就是,木马经常会读取/proc/$pid/exe,但仍然有点不完全了解什么作用,这下就能在源码里面分析到了。
从这个大循环开始,一直都是mirai在寻找同行或者某些特征的进程,然后kill -9的过程
image.png

首先,mirai会搜索/proc/文件夹下的每个文件夹,以自己的服务器为例,可以见到有很多文件夹的名字带有字母,而mirai仅仅匹配以数字即pid命名的文件夹,file->d_name即匹配到的文件夹名字
image.png

然后,int rp_len, fd, pid = atoi(file->d_name);这句代码,把pid保存了起来
紧接着是mirai killer的进程保活机制

image.png
紧接着,有个条件,image.png
看起来是readlink 了/proc/pid/exe,那么作用是什么呢
image.png
实际上readlink /proc/pid/exe可以得到真实路径,
然后,mirai会专门寻找anime僵尸网络的特征字符串,然后删掉其文件,kill掉其进程
image.png
特别的是,如果一个进程删掉了本体,而进程还在,说明其很有可能也是僵尸网络进程,mirai也会kill掉
然后,如果内存里面有这个进程对应/proc/pid/exe的路径,说明可能是同行,就跟目前在做的事情一样,那就kill掉。而kill upx壳特征的进程不知道为什么注释掉了
image.png
最后,mirai会把自身进程的/proc/pid/exe和/proc/pid/status,里面的信息都抹掉,防止别人用这招对付自身。哦对了,/proc/pid/status会有当前程序的名称
image.png

killer_kill_by_port函数

killer_kill_by_port(port_t port)函数就是,以port来搜对应的进程然后kill掉
image.png

memory_scan_match函数

memory_scan_match函数里面可以看到mirai的扫内存行为,其主要用于发现同行,即其它感染进程。这里也是攻击者可以定义的地方,预先保存了同行特征,需要时就会解压

image.png
解混淆以后立刻又会混淆,table_unlock_val和table_lock_val成对使用
其中的内存匹配逻辑mem_exists其实就是逐字匹配了
image.png
好,那么,当检测到确实存在可以访问的,当前进程pid的exe,即has_exe_access()条件满足,其就不会提前return
然后程序进入了一个 while (TRUE)循环
image.png

多种攻击向量:attack.c

这部分好像没怎么变吧,会有对应的位来表示当前攻击向量
可以去mirai/bot/attack.h看定义
image.png

一、UDP 协议攻击

  1. attack_udp_generic
    • 攻击类型:通用 UDP 洪泛攻击(无协议载荷定制)。
    • 行为:发送大量 UDP 垃圾数据包,耗尽目标带宽或资源。
    • 目标:IP + 端口,攻击流量简单但难以过滤。
  2. attack_udp_vse
    • 攻击类型:针对 Valve Source Engine(游戏服务器) 的漏洞攻击。
    • 行为:伪造 Steam 服务器查询请求,触发反射 / 放大效应。
    • 历史事件:曾用于攻击《CS:GO》《Dota 2》等游戏服务器。
  3. attack_udp_dns
    • 攻击类型:DNS 反射 / 放大攻击。
    • 行为:伪造源 IP 向开放 DNS 服务器发送大型查询(如 ANY 请求),反射流量至目标。
    • 放大倍数:可达 50 倍以上(取决于查询类型)。
  4. attack_udp_plain
    • 攻击类型:纯 UDP 载荷定制攻击(如特定字节模式)。
    • 用途:绕过基础流量过滤规则,针对特定服务干扰。

二、TCP 协议攻击

  1. attack_tcp_syn
    • 攻击类型:SYN 洪泛攻击。
    • 行为:发送大量伪造源 IP 的 SYN 包,耗尽目标 TCP 连接队列。
    • 特征:目标服务器 SYN_RECV 状态连接激增。
  2. attack_tcp_ack
    • 攻击类型:ACK 洪泛攻击。
    • 行为:发送大量 ACK 或 ACK+PSH 包,消耗目标处理资源。
    • 绕过策略:部分防火墙不检查 ACK 包状态,攻击更易穿透。
  3. attack_tcp_stomp
    • 攻击类型:TCP 连接滥用攻击(如 STOMP 协议漏洞)。
    • 行为:建立 TCP 连接后发送畸形协议数据(如超大头部、非法指令),导致服务崩溃。
    • 典型目标:消息队列中间件(如 RabbitMQ)。

三、GRE 协议攻击

  1. attack_gre_ip
    • 攻击类型:GRE IP 隧道封装攻击。
    • 行为:将攻击流量封装在 GRE IP 数据包中,绕过基于常规协议的黑名单。
    • 隐蔽性:需目标网络允许 GRE 协议,否则易被拦截。
  2. attack_gre_eth
    • 攻击类型:GRE 以太网帧封装攻击。
    • 行为:在 GRE 隧道中封装完整以太网帧,用于二层网络渗透。
    • 场景:针对内部网络或 VPN 隧道进行穿透攻击。

四、应用层攻击

  1. attack_app_proxy
    • 攻击类型:代理协议滥用攻击(如 SOCKS/HTTP 代理反射)。
    • 行为:利用开放代理服务器转发攻击流量,隐藏真实 IP。
    • 示例:发送大量代理请求至开放代理,反射流量至目标。
  2. attack_app_http
    • 攻击类型:HTTP 洪泛攻击(Layer 7)。
    • 行为
      • 标准模式:高频请求 GET/POST 消耗服务器资源。
      • 慢速攻击:维持长时间连接(如 Slowloris)。
    • 特征:模拟 User-Agent 和 Referer,伪装合法流量。

标志位,C2下发指令用的
image.png

核心主程序:main.c

mirai/bot/main.c
其不仅导入了前述的很多文件的宏定义
image.png
而且还有些很有意思的函数,其内部调用前述逻辑
其实这堆函数就基本上是bot的整套逻辑,之前看的细节现在从整体上理解就通了
image.png

main函数

上来先把自己删了以免被拿到样本分析
image.png

紧接着kill dog,以免设备重启
image.png
然后更狗的是,mirai会有个假的C2地址和端口,挺有迷惑性的
image.png
接下来就是调用
unlock_tbl_if_nodebug(args[0]);
anti_gdb_entry(0);
ensure_single_instance();
rand_init();
attack_init();
killer_init();
scanner_init();
本文以上部分都有涉及,详见其它地方做完这些init,就进入了while true的循环状态,主要维护和c2的通信,以及遵照c2下发的指令等
image.png
存活心跳机制如下,把0x00000001和自己的状态arg[1],存于id_buf,发给C2
image.png

与C2沟通的细节部分,我打算放在另外一篇文章

然后就是读C2指令,然后attack,ddos
image.png

anti_gdb_entry:劫持gdb断点依赖的SIGTRAP信号

首先,resolve_func本身被赋予了util_local_addr的指针,

image.png
这个本应是前面一个获取本机ip地址用的image.png
在355行处定义了一个反gdb函数,sig是信号的意思,这个函数用于劫持signal信号机制的处理函数
image.png
GDB 默认使用 SIGTRAP 处理断点,而mirai把该信号的处理函数劫持到anti_gdb_entry了,自然无法断点调试
image.png

原来只是反断点信号,感觉可以在前面进程kill那里,把gdb attach上来的进程也kill掉?这部分暂时不知道

resolve_cnc_addr

这部分是mirai用来解析C2的,可以看到用的还是table解混淆函数那套,
image.png

ensure_single_instance

这是mirai用来维持占用端口的
image.png

unlock_tbl_if_nodebug

简单来说就是实现了一个,把table_init执行前进行解混淆和迷惑操作的函数,使得在解压table出来之前也会相当麻烦
image.png

总结

这里面的技战术也许有些过时,比如,没有chacha20加密,没有使用漏洞只是telnet爆破等,但是其保活机制,排除同行的机制,以及内存搜索,反调试的实现过程等,都是共通的

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇
粤ICP备20015830号