小米路由器固件仿真模拟方案

15 篇文章 1 订阅
8 篇文章 0 订阅

前言

由于我们学校实验室里正好用的是小米AX9000这款路由器,因此当时我就选了这款设备挖,也就没有对固件仿真了。既有女朋友又努力上进的ZIKH26师傅与我这个摆烂人不同,在复现我这个洞的时候,想要研究一下小米路由器的仿真模拟。正好最近我也没啥事,于是也就一起看了看。

恰巧前几天也有师傅私信我关于小米路由器该如何仿真的问题,并且网上目前似乎还没有师傅分析过小米固件的仿真,因此我就写了此文放了出来(所以,我说FirmAE是对某些固件特制的学术玩具会不会被打,逃)。

除了一些特殊的设备,其实对于固件的仿真模拟,总结成一句话就是:IoT设备的仿真过程就是不断地改报错,只要与外设硬件无关,都是小问题。也就是说,主要是要有耐心。

这里依然以小米路由器AX9000为例,小米其他型号的路由器也都可以用如下的方案仿真。在本文的最后,会用我们仿真模拟的路由器环境验证CVE-2023-26315这个漏洞。

环境配置

小米路由器AX9000相对其他一些型号来说,仿真稍微复杂一些,因为小米AX9000的固件是AArch64el架构的,而网上似乎还没有公开的AArch64的内核与文件系统。因此,我们需要自己装一个AArch64的虚拟机,并从中extract提取出内核与磁盘镜像。可参考这篇文章中的步骤完成。

我本人比较懒,装AArch64虚拟机很耗时间和内存,所以这部分是ZIKH26师傅提取的内核与磁盘镜像,在此表示崇高的敬意。


关于网络环境配置的问题,这里就不再多说了。只简单提一句,qemu-system-aarch64启动后,可以用ip addr看到enp0s1网卡是DOWN的状态,且没有分配IP地址。

因此,我们需要手动分配一个IP地址(与网桥br0处于同一网段即可,qemuenp0s1接口与宿主机的eth0接口通过虚拟网桥br0转发数据),然后再将enp0s1网卡给UP启用即可,命令如下:

1

2

ip add add 192.168.192.132/24 dev enp0s1

ip link set enp0s1 up

完成后,enp0s1网卡的状态应该正常了,再检测下qemu虚拟机与宿主机能否互相正常通信:

下面,就正式开始仿真模拟了。

起手式

将固件解压后,首先将文件系统用scp传入qemu虚拟机,然后就是最经典的三行起手式:

1

2

3

mount --bind /proc proc

mount --bind /dev dev

chroot . /bin/sh

好吧,这一步我竟然还截了一张图:

根据openwrt的内核初始化流程,按理说应该先启动/etc/preinit,其中会执行/sbin/init进行初始化,但是在这套固件仿真的时候,这样会导致qemu重启,所以我们首先先执行/sbin/init中最重要的/sbin/procd &,启动进程管理器即可。

启动httpd服务

下面就该启动httpd服务了,简单检索一下,发现有uhttpdmihttpdsysapihttpd。进一步查看一下配置文件(如/etc/sysapihttpd/sysapihttpd.conf),发现sysapihttpd其实就是nginx,监听了80端口,有了nginx自然就不需要再启动uhttpd了。而mihttpd中监听了8198端口,定义了一些文件上传下载的API,可以暂时先不启。

所以,只需要启动sysapihttpd,执行/etc/init.d/sysapihttpd start即可:

首先,报错缺失/var/lock/procd_sysapihttpd.lock这个文件,这个创建一下对应的目录和文件就行了。接着,会报错Failed to connect to ubus,很显然这里是用到了ubus总线通信,我们需要启动/sbin/ubusd &。但是,接下来又继续报错usock: No such file or directory,但是并没有给出缺失哪个文件,因此我们需要将ubusd拖进IDA定位一下报错点。

很容易定位到sub_20B0函数,我们执行的是ubusd,而不是它的软链接tbusd,因此v8会是路径/var/run/ubus.sock,接着其作为参数传入usock函数中,当usock函数的返回值错误时,就会走到perror("usock")报错。

显然我们缺少/var/run/ubus.sock这个文件,创建后ubusd即可正常启动,接着sysapihttpd服务也启动成功(这里缺失/dev/nvram芯片可以先不用管,因为后续没有用到相关操作,暂时不用hook):

到这里,检查一下进程里相关程序都已经挂起,且netstat查看web端口也正常对外开放:

崩溃&排查

虽然这一切看上去没有任何问题,nmap也能扫到端口,但是我们访问IP就会报错,显示服务器没有传回任何数据。

此外,查看进程,可以发现sysapihttpd的主进程进程号没变,但是子进程的进程号却已经改变,这就说明当我们访问IP的时候,server崩溃了,所以子进程crash了,主进程作为守护进程又开了个新的子进程。

因此,我们可以用strace跟踪一下子进程的进程号,判断出现了什么问题导致sysapihttpdcrash的。这里直接在外面的qemu文件系统中用apt或者dpkg安装一下strace_arm64deb包,然后再ssh开个窗口跟踪进程即可。先attach上去卡住,然后访问IP后,strace跟踪的结果如下:

可以看到最后是发生了段错误导致了crash的。我们需要解决的是上面的三个报错,前两个错误都是由于缺少/etc/TZ文件所导致的,这是一个和时区有关的配置文件,echo "WAUST-8WAUDT" > /etc/TZ创建一下即可。

对于后面getsockopt的报错,我们可以将/usr/sbin/sysapihttpd拖进IDA,定位一下getsockopt参数一致(主要关注第三个参数是0x50)的位置,可以找到sub_1EDACsub_27570两个函数。至于是哪个函数中getsockopt调用的位置才是关键,结合strace上下文,根据上面调用了accept4系统调用,可以确定sub_27570函数中getsockopt的位置才是调用点。

根据上面的代码,结合strace后面输出的log报错信息,可以得知这里是由于getsockopt发生了错误,从而重定向到了0.0.0.1:65535,导致了后续的崩溃。因此,最简单粗暴的办法就是绕过这个if分支,不执行其中的内容,即将CBZpatch成相反的CBNZ即可:

patch后的sysapihttpd程序按照如下的步骤更新至固件的文件系统中,并重启httpd服务:

然后,访问IP并重定向至/init.html,可以看到路由器初始化配置页面终于是成功出现了:

跳过初始化配置

然而,由于我们只启动了部分服务,以及有部分配置与硬件相关,所以毫无疑问按照路由器初始化配置页面中的流程进行配置的过程中一定会出现各种问题。当然,我们仿真模拟的环境并不需要完整的配置,只是为了验证或调试漏洞而已,所以我们也没必要折腾这部分,直接跳过就行了。

我们通过grep在固件文件系统中查找是在哪里重定向至/init.html的,很容易定位到/usr/lib/lua/luci/view/web/sysauth.htm文件:

其中,相关代码如下:

这里最简单粗暴的办法当然就是将这几行都注释掉,这样虽然的确可以跳过初始化配置,但是在之后漏洞验证的时候会出点小问题,留到后文再说。

我们这里还是更优雅地想办法去满足这个if判断的条件,使之不重定向至/init.html。根据这里调用的函数,很容易定位到如下代码的调用关系(这里的源码来自于小米路由器4 Pro 稳定版,这个版本的Lua源码没有编译,小米各个型号路由器的Lua部分变化不大,源码当然比反编译出来的伪代码好看很多):

很容易弄清这里的逻辑,只需要按照如下命令设置uci配置项,标记为已初始化即可:

1

2

uci set xiaoqiang.common.INITTED=1

uci commit

uci set设置后,再次访问IP,即可绕过初始化配置,跳转至登录页面:

设置登录密码

由于我们跳过了初始化配置阶段,并没有设置路由器后台的登录密码,且我们需要验证的CVE-2023-26315是一个授权认证后的漏洞,因此我们需要设置登录密码以登录进后台拿到token的值(当然,上文说过有些型号的设备固件中Lua是没有编译的源码,因此也可以拿这部分源码把相关API改成未授权的,然后用固件里自带的/usr/bin/luac编译一下替换即可,但这样确实太暴力了)。

上篇文章提过,身份校验的过程在/usr/lib/lua/luci/dispatcher.luajsonauth函数中,其中调用了checkUser函数根据从POST报文中获取的usernamepasswordnonce(现时)字段进行身份验证。

/usr/lib/lua/xiaoqiang/util/XQSecureUtil.luacheckUser函数中,首先获取了系统uci配置项中存储的密码,这里的XQPreference.get函数在本文的上一节中已经给出,可分析出此处的配置项为account.common.(用户名)。接着,需要POST报文中传入的现时字段nonce与系统中uci存储的password的值拼接后进行sha1哈希的结果等于POST报文中传入的密码字段。

因此,接下来,我们需要确定POST报文中传入的密码和用户名字段是什么。很显然,POST请求报文中的密码字段不可能是明文的形式,不然随便拦截一下就寄了。故而,一定会有相关的JavaScript代码对用户提交的密码进行加密(哈希)后再进行传输。所以,可以直接在浏览器登录页面中,查看一下相关的web代码:

可以看出,报文中的用户名字段固定就是admin,而密码字段是通过oldPwd()函数加密后的结果。这里的oldPwd()函数将用户提交的密码明文与一个固定的key值(a2ffa5c9be07488bbb04a3a47d3c5f6a)拼接后,进行sha1哈希,再将结果继续与现时nonce拼接后,再sha1哈希一次,作为POST请求报文中的密码字段。

结合上述分析,我们需要将account.common.admin这个uci配置项设置为sha1(登录密码+key),比如说登录密码设置为winmt,那么这个值就是sha1(winmta2ffa5c9be07488bbb04a3a47d3c5f6a)=b264db0fca361ef8eca919fa28e70d7a57d4c2db

可通过如下命令set设置uci的配置项:

1

2

uci set account.common.admin=b264db0fca361ef8eca919fa28e70d7a57d4c2db

uci commit

设置好account.common.admin这个uci配置项后,用设定的密码winmt即可成功登录入路由器后台:

此时,token值为789c292990be8ca7d36947145aaea700

一个小插曲

在“跳过初始化配置”一节中,提到过:如果将sysauth.htm中重定向的if分支都注释掉的话,在后面进行漏洞验证的时候会出现一些问题。具体来说,是当访问/api/xqdatacenter/request这个API的时候,会出现503报错。

下面来分析一下这个问题的成因。

/usr/lib/lua/luci/controller/api/xqdatacenter.lua中,对于/api/xqdatacenter/request这个entry{}没有设置第五个参数与权限有关的flag位:

1

entry({"api""xqdatacenter""request"}, call("tunnelRequest"), _(""), 301)

具体在/usr/lib/lua/luci/dispatcher.lua文件中,可以看到entry{}的相关定义:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

--- Create a new dispatching node and define common parameters.

-- @param   path    Virtual path

-- @param   target  Target function to call when dispatched.

-- @param   title   Destination node title

-- @param   order   Destination node order value (optional)

-- @param   flag    For extension (optional)

-- @return          Dispatching tree node

function entry(path, target, title, order, flag)

    local c = node(unpack(path))

    c.target = target

    c.title  = title

    c.order  = order

    c.flag   = flag

    c.module = getfenv(2)._NAME

    return c

end

同样是在/usr/lib/lua/luci/dispatcher.lua文件中,其内的dispatch函数中对API作了最顶层的权限校验(sysauth_authenticator内的函数等各种模式是在其中被调用做进一步验证的)。在dispatch函数中,有一段如下代码:

1

2

3

4

if not _noinitAccessAllowed(track.flag) then

    luci.http.status(403"Forbidden")

    return

end

其中,_noinitAccessAllowed函数的代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

function _noinitAccessAllowed(flag)

    local xqsys = require("xiaoqiang.util.XQSysUtil")

    if xqsys.getInitInfo() then

        return true

    else

        if flag == nil then

            return false

        end

        if bit.band(flag, 0x08== 0x08 then

            return true

        else

            return false

        end

    end

end

综上,也就是说只有getInitInfo()函数的返回值为trueflag位为0x08(或& 0x08 == 0x08),才能绕过这个_noinitAccessAllowed的校验,不然就会报503错误。

这里的getInitInfo()函数我们已经不陌生了,在“跳过初始化配置”一节中出现过,也就是xiaoqiang.common.INITTED这个uci配置项。这样也就解释了为什么在处理sysauth.htm中重定向的if分支时,还是最好设置一下相关uci配置项。当然,直接找一份xqdatacenter.lua的源码,在对应entry{}中加上flag0x08,再用固件自带的luac编译后替换一下也是可以的。

验证漏洞

接下来,我们完成本次仿真模拟的最终目的:验证CVE-2023-26315这个漏洞。根据我上篇文章中的分析,想要完成此漏洞的验证,还需要启动datacenter以及plugincenter,命令如下:

1

2

/usr/sbin/datacenter &

/usr/sbin/plugincenter &

启动后,可能会有一些报错,但并不影响,netstat查看90909091端口正常即可:

最后,跑一遍exp即可成功拿到shell,漏洞验证完成:

完结撒花,Are You OK

  • 19
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
小米路由器mini固件降级是指将现有的路由器固件版本退回到较早的版本。固件降级可能由于以下原因进行:修复与现有固件版本相关的问题、尝试旧版本的功能或界面、或者避免新版本的兼容性问题。然而,小米并不提供官方的固件降级支持,因此进行固件降级要谨慎且自担风险。 要降级小米路由器mini的固件,你可以尝试以下步骤: 1. 确保你已备份路由器的所有设置和数据,因为降级过程可能会导致数据丢失或配置重置。 2. 在电脑上下载目标固件版本的ROM文件。你可以访问小米官方网站或第三方固件网站查找适用于你的路由器型号的旧版本固件。 3. 登录小米路由器的管理界面,一般通过输入路由器的IP地址(例如192.168.31.1)进入。 4. 在管理界面中找到固件升级选项,可能位于"系统设置"或"高级设置"等菜单中。 5. 在固件升级页面中,点击"手动选择固件"或类似的选项,然后浏览并选择你下载的旧版本固件ROM文件。 6. 点击"开始升级"或"确认"等按钮,开始固件降级过程。过程中可能需要耐心等待,并确保不要中断电源或断开路由器的连接。 7. 降级完成后,可能需要重新配置路由器的设置和重新连接设备。 需要注意的是,固件降级有风险,可能会导致不可预测的问题,甚至使设备无法正常工作。因此,在降级之前,建议先了解你要解决的问题是否可以通过其他方式解决,并在降级前备份重要数据。如果不熟悉路由器固件升级和降级的操作,最好寻求专业人士的帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值