Mirai源码阅读
代码可以从https://github.com/jgamblin/Mirai-Source-Code
下载。
一些基本的名称是这样的。攻击者将部署一个Commad & Control
CNC 的节点和一个loader
的服务器,然后将感染的设备称为bot
,并在其中运行程序payload
。
Marai 各个部分的主要功能如下:
- loader(
loader/src
): 监听bot的report,并上传payload到要感染的设备 - cnc(
mirai/cnc
): 即控制服务器,主要功能是处理用户登录和下发命令 - bot(
mirai/bot
): 运行僵尸程序
注:源码中其它部分(
mirai/tools
、script/
、dlr/
)不再关注。
代码中,CNC部分是由Go语言编写的,余下都由C语言编码完成。因此我们需要Go语言的环境。
Mirai的主要感染途径是通过设备的默认密码。在感染后,可以通过ssh和Telnet连接对其他设备进行感染,或在cnc的指挥下对其它网络设备发起DDos攻击。
Cnc 部分
cnc目录主要提供用户管理的接口、处理攻击请求并下发攻击命令。这个目录要求在安装的主机中存在Mysql。它会将管理员和bot的数据,甚至可以使用的命令以及历史存放在数据库中。
1 | admin.go 处理用户登录、创建新用户以及进行攻击 |
在main.go
中可以看到监听了23和101并分别调用了initialHandler
和apiHandler
两个函数。
首先跟随initialHandler
,若接受数据长度为4,且分别为00 00 00 x
(x>0)时,为bot监听,将对应的bot主机添加为新的bot。
否则,则判断是否是管理员并进行登录,如果成功登录,则可以通过命令发动攻击。而且如果是管理员账号,还可以通过命令执行管理员帐户添加adduser
和查询bot数量botcount
等。
ApiHandler
中则是提供了另一种访问方式,是为了更方便地调用bot进行攻击而设置的。
可以在attack.go
中看到,Mirai
所支持的攻击类型包括udp、vse、dns、syn、ack、stomp、GRE ip flood、GRE Ethernet flood、http等。(还有很多我并不认识。)当然这些进行攻击的类型都只是发一段特定的代码到bot,然后由所有bot一起进行即可。
Bot 部分(Pyload)
bot源码主要有:
- attack模块:解析下发的命令,发起DoS攻击
- scanner模块:扫描telnet弱口令登录,上报给loader
- killer模块:占用端口,kill同类僵尸(排除异己)
- public模块: utils
但是在此之前先看看main
函数中启动之前一通熟练地操作:
首先阻止
gdb
和watchdog
的调试。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// Signal based control flow
sigemptyset(&sigs);
sigaddset(&sigs, SIGINT);
sigprocmask(SIG_BLOCK, &sigs, NULL);
signal(SIGCHLD, SIG_IGN);
signal(SIGTRAP, &anti_gdb_entry);
// Prevent watchdog from rebooting device
if ((wfd = open("/dev/watchdog", 2)) != -1 ||
(wfd = open("/dev/misc/watchdog", 2)) != -1)
{
int one = 1;
ioctl(wfd, 0x80045704, &one);
close(wfd);
wfd = 0;
}gdb 会通过信号来停止程序,既然如此,就一旦接受到
SIGTRAP
就直接退出以禁止调试。然后向在特定位置的看门狗程序发送控制码
0×80045704
禁用看门狗,以防止自动重启。通常在嵌入式设备中,固件会实现一种叫看门狗(watchdog)的功能,有一个进程会不断的向看门狗进程发送一个字节数据,这个过程叫喂狗。如果喂狗过程结束,那么设备就会重启,因此为了防止设备重启,Mirai关闭了看门狗功能。然后是调用
ensure_single_instance()
用于确保只有一个实例的程序在运行。方法是绑定一个特定的端口48101。如果有进程已经占用了这个端口,就直接把它kill掉,这样每个同样的程序绑定这个端口的时候,就会被下一个启动的实例给kill掉。
但是同样,这个特点是检测网络设备中是否存在
Mirai
的最高效的检测方法。隐藏进程。
修改
args[0]
即运行程序的命令。将进程名变为随机的字符。
1
2
3
4
5
6
7
8
9
10
11// Hide argv0
name_buf_len = ((rand_next() % 4) + 3) * 4;
rand_alphastr(name_buf, name_buf_len);
name_buf[name_buf_len] = 0;
util_strcpy(args[0], name_buf);
// Hide process name
name_buf_len = ((rand_next() % 6) + 3) * 4;
rand_alphastr(name_buf, name_buf_len);
name_buf[name_buf_len] = 0;
prctl(PR_SET_NAME, name_buf);初始化攻击
attack_init()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21BOOL attack_init(void)
{
int i;
add_attack(ATK_VEC_UDP, (ATTACK_FUNC)attack_udp_generic);
add_attack(ATK_VEC_VSE, (ATTACK_FUNC)attack_udp_vse);
add_attack(ATK_VEC_DNS, (ATTACK_FUNC)attack_udp_dns);
add_attack(ATK_VEC_UDP_PLAIN, (ATTACK_FUNC)attack_udp_plain);
add_attack(ATK_VEC_SYN, (ATTACK_FUNC)attack_tcp_syn);
add_attack(ATK_VEC_ACK, (ATTACK_FUNC)attack_tcp_ack);
add_attack(ATK_VEC_STOMP, (ATTACK_FUNC)attack_tcp_stomp);
add_attack(ATK_VEC_GREIP, (ATTACK_FUNC)attack_gre_ip);
add_attack(ATK_VEC_GREETH, (ATTACK_FUNC)attack_gre_eth);
//add_attack(ATK_VEC_PROXY, (ATTACK_FUNC)attack_app_proxy);
add_attack(ATK_VEC_HTTP, (ATTACK_FUNC)attack_app_http);
return TRUE;
}在这之中,只是添加了一些可以进攻的方式,还没有实际进行攻击,所以这里面的函数我们稍后再看。
Killer 模块 killer.c
main()
函数在此后调用了killer模块 killer_init()
Killer模块主要是负责排除其他同类的病毒,以防止被抢走控制权。
在这个函数中,它会首先检测占用并杀死可能存在的进程,然后直接抢占 22/23/80 端口。这主要是为了排除异己,防止其他程序通过ssh/telnet/http的方式获得控制权。
在此后,他还会搜索特定的文件夹/proc/$pid/exe
,在这个文件夹中包含了所有正在运行中的进程的程序链接,然后它通过链接直接看程序的真实名称是否含有.anime
,一旦含有就直接杀死。
实际上这个程序在添加了其他逻辑之后,很快就能针对其他程序进行清除。这里大概只是用anime
做了一个典型而已。毕竟Mirai
还扫描了/proc/$pid/status
文件,在这个文件中存着进程的一些信息,Killer模块也能根据这些信息对特定的进程进行杀死。
Scanner 模块 Scanner.c
在killer之后,在主循环之前,main()
调用了一个Scanner模块scanner_init()
。Scanner即扫描器,他所做的是扫描网络中其它未被感染的主机,然后用弱口令尝试登陆,并将能登陆的主机的信息上报给loader,然后由loader对主机进行侵略。
在此模块中,扫描的ip地址是随机生成的,并会排除一定的ip地址
1 | do |
在此后列出了一系列的弱密码。
之后是快速扫描的秘密所在,下面这段代码批量对23和2323端口发送 SYN 数据包,只对有response的地址进行响应。
1 | if (fake_time != last_spew) |
由于使用的是UDP协议,要从获得的数据包中快速筛选出真正的响应的包
1 | errno = 0; |
我们会过滤掉:
- 不完整的包
- 目标非本机地址的包
- 目标非TCP协议的包
- 目标来源非23或2323的包
- 目标非特定端口的包
- 是SYN或ACK信号
- 不是RST和FIN信号
- 最后还判断其ACK序列号是否与前一个相同
之后将存活的设备保存到一个数组中。然后随机选取之前设置的弱口令进行爆破:
1 |
|
然后发送一系列命令判断登录成功与否。若成功,尝试一些操作,并上报loader。
上报loader的格式如下:
1 |
|
Attack 模块
在做完了上面这两个模块的内容之后,就进入了bot的主循环,它会主动连接CNC节点并等待CNC节点的指令使用attackparse
进行解析。
在这里有个小trick,在前面是设定了CNC节点的IP地址和端口FAKE_CNC_ADDR
和FAKE_CNC_PORT
,但是实际在连接中,这是一个虚假的IP地址和端口,用于迷惑对这个代码进行debug的开发者。真正的IP和端口是在table.c
中硬编码写入的cnc.changeme.com
用8.8.8.8
做DNS解析之后得到的,然后在连接前使用resolve_func()
函数对地址进行了修改写入了真的IP地址。
在建立连接后,bot
根据接收到的指令(目标数,IP地址,掩码),对目标进行攻击。
在attack_app.c
、attack_gre.c
、attack_tcp.c
和attack_udp.c
中分别定义了四大类的攻击类型,然后使用函数指针模拟多态地进行调用。其中攻击的方式大多是通过socket建立大量的SYN包,然后发给目标地址。这里具体就不提。
Loader 部分
loader代码的功能是向被感染设备上传相应架构的payload文件。
1 | headers/ 头文件目录 |
Loader中存放了针对各个平台编译后的可执行文件,其功能是用于加载Mirai的bot程序。在启动之初就会判断这个文件夹是否存在,然后启用了一个epoll架构的简单服务器,一旦有新的连接就启动一个新的worker线程。
在worker线程中,维护了一个状态机,即列出了几种状态,然后在状态之间转换。大概是为了应付一些随时要求验证的需求。
首先woker线程使用scanner提供的IP地址和账户密码信息登录IOT设备:
1 | case TELNET_USER_PROMPT: |
首先会执行/bin/busybox ps
和/bin/busybox cat /proc/mounts
命令查看设备挂载的分区。
1 | case TELNET_VERIFY_LOGIN: |
然后进行创建文件、使用chmod
命令调整文件权限至777,之后使用cpuinfo
命令判断设备运行平台,再使用wget
、tftp
或echo
三种方式将对应版本的恶意可执行文件上传至IOT设备。
1 | switch (conn->info.upload_method) |
在完成装载之后,还会根据下载的类型,运行相应的程序,到了这里,整个loader的工作才是做完了。
总结
花了蛮长时间,实际也是匆匆扫过。所以有些地方可以不甚仔细。
Mirai的程序,感觉与之前看的工业的代码似乎不完全一样,而且觉得有些地方仍有改进的余地。我感觉这是作者故意留了代码变异的空间,亦或者是根本没有完成。(代码中的DEBUG宏数量有点太多了。)
Mirai是瞄准IoT 设备的弱口令问题,因为很多人并不很在意物联网设备的安全问题,甚至会直接使用初始的用户名与密码,这样会造成物联网设备的大量沦陷。通过巨量的僵尸网络,恶意代码控制者可以进行DDos攻击以瘫痪网络服务器。
而随着物联网设备的应用拓宽,恐怕问题并不止于网络的DDos,更多的关于隐私或者其他的方面的问题会出现其中。我想这才是应该担忧的问题。