-
-
[原创]小米某25年路由器固件分析
-
发表于: 2026-7-28 17:58 1348
-
在小米官网上扒的固件,直接拿来用了
首先binwalk扫描一下,发现是ubi文件系统
手工提取ubi需要ubireader_extract_images,并且还有字节对齐要求,所以先用binwalk直接解包试试
成功提取了两个有效文件,一个是kernel,一个是rootfs

虽然文件尾缀是ubifs,但是hexdump一下可知开头的魔数是 68 73 71 73,也就是squashfs,同时留意到binwalk的报告和hexdump下面还有7zXZ,所以我尝试解压了一下

不过,直接用binwalk或者unsquashfs就能提取了,下面出现了一堆warnning,应该都是软链接丢失的告警,稍后需要修复,这里先把它们存成txt便于一会儿用sed和awk处理

又出现了一个0.squashfs.bin,扫描后发现跟之前的扫描结果差不多,应该是备份或者神秘报错

先不管它,核心文件夹是squashfs-root

随便切一个目录进去,找个文件检查一下架构,发现是aarch64

检查一下inittab,fstab等文件,确定启动方式,挂载文件等
是非常经典的rcS脚本读取rc.d目录下的服务文件并按照S*的顺序启动,不过我没找到rcS脚本,但这个影响不大,证明已经被procd替代了,但procd也会按照那个顺序读取并启动
读etc下的fstab,发现啥也没有?
inittab没有mount的硬编码,S*里也没有mount,一会儿模拟固件时只能尝试挂一下tmp,proc和sys


写一个仿真脚本和一个软链接修复脚本,然后启动
随便找一个之前坏的软链接检查一下,发现软链接已经被修复

现在开始检查rc.d里的启动服务
有不少眼熟的东西,从S*按编号找(也就是启动顺序),有firewall,nginx,cron,telnet,mosquitto,baidupan,mi_docker,除此之外还有一些小米的自研服务
先观察一下telnet的脚本,开头是#!/bin/sh /etc/rc.common,这意味着内核会去掉#!后执行:
[解释器] [可选参数] [脚本路径] [脚本参数]
也就是/bin/sh /etc/rc.common /etc/init.d/telnet boot(rcS自带的boot,procd也一样)


去检查一下rc.common,就是把调用/etc/init.d/telnet时后面跟的参数读出来存进$action,放进procd框架里执行,然后运行$action "$@"(其实这个$@啥也没有)
观察到rc.common里有一个start_service()函数,里面只有一个return 0,但是后面又有一个. "$initscript",这意味着它会被telnet脚本里的这个函数覆盖
因此,telnet启动的完整流程就是:
执行rc.common telnet start
$action=start,执行start函数,由于source $initscript的存在,start会调用telnet脚本里的start_service,也就是这东西:

看起来它是检查ssh是否有公钥并且存在于/etc/shadow里,如果不存在才能执行$PROG,也就是/usr/sbin/telnetd,但很遗憾的是,它被注释了
因此telnet必定启动,然后检查login.sh,也就是PROG指定的登录脚本,它是决定身份认证和是否给shell的关键

这是login.sh的脚本,判断root的密码的状态,如果是正常哈希就不使用login,直接拒绝;如果是空密码或者锁定状态,就会进一步判断ft_mode是否等于1,可能是factory_test工厂模式,如果ft_mode=1,那么就可以无需密码,直接用telnet登录,否则就需要密码

这里我尝试通过/etc/init.d/telnet启动它来模拟登录情况,但是由于是qemu-user模式所以没有ubus,所以失败了,后面可以用qemu-system模拟一下
总地来说,这部分没什么漏洞可言,如果想利用,前提必须是ft_mode=1并且root密码出问题,而如果想达成这样的条件,那必然需要任意文件写
接下来分析其他东西
之前注意到有一个nginx,START=21,这个东西也可以研究一下,一般来讲iot设备的nginx为了符合需求,都是自己的魔改版本,所以可能会有特殊漏洞
之前已经分析了rc.common的模式,所以这里它的流程跟telnet区别不大,我们可以直接来分析start_service函数:

这里可以看到它open_instance后执行/usr/bin/spawn-fcgi -a 127.0.0.1 -p 8920 -U nobody -F 1 -- /usr/bin/fcgi-cgi
随后有两个[ -d xxxxx] || mkdir,这是在判断,如果不存在/var/log/nginx和/var/lib/nginx目录,就自动创建这两个目录
然后调用init_config,实现如下:


这是将/etc/config/default-locations.conf里的client_max_body_size做一个替换,来源是local
_body_size=$(uci -q get nginx.main.client_max_body_size),可以直接执行uci -q
get nginx.main.client_max_body_size查看,结果是50M
再依葫芦画瓢执行一下uci -q get nginx.main.force_https,发现输出是0,那第二个if-fi就不用看了
第三个if是把“include /etc/nginx/default-locations.conf”放进$HTTP_LOCATIONS里,但此变量的值暂时未知
最后执行/usr/sbin/nginx -c /etc/nginx/nginx.conf -g 'daemon off;'
感觉这个过程可以被模拟一下,我们先手动替换/etc/nginx/default-locations.conf里的max_body_size,顺便创建那两个目录,也写进启动脚本里避免麻烦
随后,可以把/usr/bin/spawn-fcgi,/usr/bin/fcgi-cgi和/usr/sbin/nginx提取出来,一会儿用IDA研究一下

先读一下/usr/bin/spawn-fcgi的help
所以/usr/bin/spawn-fcgi -a 127.0.0.1 -p 8920 -U nobody -F 1 -- /usr/bin/fcgi-cgi
是绑定本机8920端口,将程序的所创建的Unix domain socket的属主改为“nobody”的id,并创建一个子进程运行fcgi-cgi

这里用qemu自带的strace观察一下spawn的系统调用,但是跟踪到clone以后就停了,不会追踪子进程,查阅文档发现qemu的strace并不提供-f这种追踪子进程选项,所以需要逆向一下,找到它传给子进程的参数
先检查基本信息:

其中ldd是需要对应架构,这是输出结果:
/ # ldd ./usr/bin/spawn-fcgi
/lib/ld-musl-aarch64.so.1 (0x780e21c90000)
libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x400000827000)
libc.so => /lib/ld-musl-aarch64.so.1 (0x780e21c9000)

打开IDA,定位主函数位置是VA:0x12A0位置
节头表被删了,导致got.plt现在只能显示LOAD


尝试用gdb和qemu自带的gdbserver动态分析,但是gdb直接崩了,可能还是/proc的问题,失去了动态分析手段,只能根据strace的系统调用尝试静态分析
有一个clone系统调用,IDA里没有clone,但是有一个fork创建子进程,直接跳转


这是我还原的子进程执行的代码,i64会放在帖子末尾留待下载
这部分做的事就是根据-F参数后面的值来决定循环创建几个子进程,然后根据某变量的正负决定是否把它写进环境变量(关于v30部分,我猜测是用0号文件描述符来替代v30这个fd),接着,如果fork是被允许的,就把自身守护进程化,最后执行路径程序
这是一个完完全全的父进程,fork后生命周期基本就结束了,不属于daemon,因此我没有尝试找内存破坏漏洞。
接下来看fcgi-cgi
gdb尝试调试,依旧崩溃

strace查调用发现有epoll,显然是一个daemon进程,这东西比之前的spawn-fcgi重要得多,这里我们选择用qemu-system,势必要让动态分析跑起来
qemu-system和内核离不开关系
当启动一台设备时,整个流程大概是:
cpu执行第一条指令,跳转到引导程序入口的物理地址开始执行,然后从flash里读出内核头,自解压程序,内核核心文件和设备树,放到RAM上
随后,自解压程序被执行,同时引导程序让cpu的pc寄存器指向内核核心文件的入口,开始执行内核代码,同时内核收到引导程序存在寄存器中的设备树地址,根据设备树的内容启动各种驱动程序和模块,给根目录挂载文件系统,最后初始化完成后,挂载其他文件系统,pc指向init,进入用户态
而核心就是内核头,自解压程序,内核核心文件和设备树以及根文件系统,根文件系统可以是我们已有的squashfs-root,而前四个属于内核的文件,我们可以用openwrt官方提供的镜像
(这里我尝试使用最早binwalk提取出的kernel文件,但是始终启动不了)
但是,我们必须先制作一个文件系统,squashfs-root是只读文件系统,而qemu-system需要可写的,所以使用ext4,这是较为常用和支持较多的文件系统

这里先用dd生成一个有一定大小的空文件留给文件系统使用
随后,将它用mkfs制作成一个ext4文件
用mount把这个文件挂载到目录下,需要使用-o loop选项,也就是为文件分配一个loop虚拟块设备(mount必须使用块设备)

把原文件系统的目录复制过来
此时这个ext4文件就制作完成了

从openwrt官网获取内核镜像
将ext4文件和内核镜像文件放在一个目录下,然后执行
qemu-system-aarch64 -M virt 指定machine,这里virt是qemu提供的通用虚拟开发板 -cpu cortex-a57 指定cpu型号 -m 256M 指定内存大小 -kernel openwrt-22.03.5-armvirt-64-Image 指定镜像
-drive file=rootfs.ext4,if=virtio,format=raw 指定文件系统 -append "root=/dev/vda console=ttyAMA0" 追加参数,告知根文件系统存在于哪个块设备/设置控制台为ttyAMA0串口 -nographic 不使用图形化

然后qemu-system开始启动,但后面出现了一大堆报错,大概是缺少一些硬件设备

这种情况可以选择在append后面追加一个参数init=/bin/sh,这样内核初始化完成后就会设置pid=1的进程为/bin/sh,这样的好处是不会因为init找不到个别硬件设备卡死,坏处就是这意味着它不会拉起任何daemon或其他进程,需要我们根据shell脚本手动拉起
最后弹出/#,启动成功
今天在路由器内核上耽误了很久,只有这些了,明天继续调试
文件较大难以传输,这里是云盘,里面有一些大型文件:
通过网盘分享的文件:rc01
链接: 299K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6H3j5h3&6Q4x3X3g2T1j5h3W2V1N6g2)9J5k6h3y4G2L8g2)9J5c8Y4y4Q4x3V1j5I4e0K6W2w2x3$3A6s2b7W2k6c8f1g2x3%4N6X3f1K6P5e0m8A6b7i4S2X3b7g2)9K6c8Y4m8%4k6q4)9K6c8o6j5$3y4U0j5`. 提取码: 6666
---来自百度网盘超级会员v3的分享