小米IoT私有协议的漏洞挖掘尝试(一)

AR502H的分析遇到了一些瓶颈,缺少大量本该存在的二进制程序和脚本文件,目前又卡在一个证书的校验上,所以最近挑了一个看起来简单一些的换换脑子,另一个继续慢慢做,有进展了就继续往下写。

miio_client二进制程序

从小米25年出厂的某个固件上提取并获得,该路由器基于Openwrt完成二次开发,与过去的init启动方式不同,采用了openwrt的procd体系,/sbin/init拉起pid = 1后并调用其他程序加载大量模块后就执行exec,将pid = 1交给procd,由procd跟着init.d目录下的软链接拉起daemon和网络服务。而小米自己加入了一些魔改的私有协议和服务,这些闭源的协议不像openwrt的开源实现那样经过大量审计,更容易遇到漏洞。

miio_client就是这样一个程序,它通过私有协议与小米云以及用户的小米app进行通信,对此我查阅了一些相关资料:

QQ20260921-192542.png

QQ20260921-192620.png

此外,github上似乎还有一些放出的私有协议的具体报文格式,以及社区维护的工具,但相关性并不是很强。

本文主要面向该程序进行漏洞挖掘和逆向工作,因此关于固件提取不做详细记录。

根据官方文档可知,miio_client在小米很多的诸多IoT设备上都有部署,而不局限于路由器。它的部署主要分为nomqtt和带mqtt两种,这里我获取的是nomqtt(无mosquitto集成)的版本。

它主要与miio_client_helper_nomqtt.sh脚本相互发送json文本进行通信,启动后就监听127.0.0.1的54322端口,等待该脚本的主动连接。

这里我使用qemu-user-static搭配chroot建立实验环境,阅读过init.d下的miio_client的相关启动脚本后做出尝试:

QQ20260921-193718.png

成功运行,但是出现了报错,打开IDA通过搜索相关字符串锁定报错位置,简单逆向和重命名后还原如下:

QQ20260921-193853.png

可以确定主要报错原因是缺少data的路径,于是根据启动脚本手动mkdir了一个文件夹,并重新尝试启动:

QQ20260921-194110.png

观察其warning日志不难确定,它正在等待helper脚本的连接。

找到启动脚本运行:

QQ20260921-194409.png

/proc/mtd缺失,这里modprobe手动加载mtd模块并再次尝试运行helper脚本:

QQ20260921-194537.png

QQ20260921-194611.png

可以看到,helper脚本这边已经不再报错,一切正常,而miio_client这边出现了新的错误:did not int.

继续使用IDA搜索字符串定位逻辑:

QQ20260921-194838.png

这次的报错所在的逻辑位置较为深入和具体,需要至少6-7次交叉引用才能定位到主函数,所以我选择先恢复IDA符号,搭建动态调试环境并尝试追踪这条调用链展开逆向。

使用IDApython编写脚本

这里写了一个IDApython脚本用于恢复符号。虽然它是一个stripped的程序,**但是由于并不是采用plt桩来完成got表寻址,而是在text中直接寻址,因此got表虽然会被IDA识别出符号,却因为没有plt的引用,不会直接对got表项进行重命名,而是用off_xxx的格式对其进行命名,恢复时需要对其进行手动修改,让IDA识别符号,比较繁琐,所以用它来自动恢复:

from ida_segment import get_segm_by_name,getseg
from idautils import XrefsTo,XrefsFrom
from ida_name import get_name,set_name,SN_FORCE

def extern_find():
    extern_seg = get_segm_by_name("extern")
    extern_start = extern_seg.start_ea
    extern_end = extern_seg.end_ea
    extern_addr = (extern_start,extern_end)
    return extern_addr

def traverse(start,end):
    total = (end - start) / 8 + 1
    total = int(total)
    for i in range(0,total):
        ea = start + i * 8
        name = get_name(ea)
        if name is not None:
            text_member = XrefsTo(ea,0)
            for txref in text_member:
                found = False
                got_member = XrefsFrom(txref.frm,0)
                for dxref in got_member:
                    seg = getseg(dxref.to)
                    if dxref.to < start and seg.perm == 6:
                        res = set_name(dxref.to,"_" + name,SN_FORCE)
                        print(f"rename {name} success.")
                        found = True
                        break   
                if found:
                    break
def main():
    extern_start,extern_end = extern_find()
    traverse(extern_start,extern_end)

if __name__=="__main__":
    main()

随后搭建动态分析环境,使用qemu-user自带的-g参数,可以拉起它内置的gdbserver,监听使用qemu-user启动的程序;同时启动gdb-multiarch远程连接:

QQ20260921-201529.png

QQ20260921-201744.png

但是段错误崩溃了。使用gdb查看coredump文件,发现段错误发生在gdb内部,同时还有残留的/lib/ldxxx字符串,看到这里我大概明白应该是根路径识别错误的问题,上网搜了解决方案,得知可以用dbg的set sysroot来设置根目录,从而让dbg正确地识别到所需的库和动态链接器:

QQ20260921-202116.png

又对json的解析做了恢复,下次一起放出来。

评论