前言
想学IoT安全,但前置知识过于庞杂,现有的教程往往默认读者有固件分析和逆向基础。我的基础并不好,几乎看不懂文章,不知道每一步在做什么,而系统补基础容易变成空中楼阁且劝退,因此陷入了两难的境地
这篇文章不分析漏洞,主要用于熟悉IoT安全的基本流程和逆向分析
环境搭建
固件获取
首先是通过漏洞编号确认路由器型号与固件版本,比如这篇文章的华为HG532 CVE-2017-17215 固件,得到一个二进制文件HG532eV100R001C01B020_upgrade_packet.bin,这就是整个设备的软件镜像
主要工具
固件分析用到的各种工具更适合Linux环境,因此我使用Ubuntu 24.04.4作为宿主机
binwalk:用于解包和分析固件,需要配套工具sasquatch
|
|
ida:对 ELF 程序进行静态分析qemu:路由器固件一般是一套嵌入式Linux系统,适配路由器主机,无法在ubuntu下直接运行。为了让程序真正跑起来,需要用到QEMU。它模拟了固件运行所需的真实设备环境,翻译不同架构的指令
|
|
固件初步处理
固件解包
使用binwalk进行解包,也就是从固件的二进制文件中提取出文件系统。解包后的文件系统在squashfs-root/目录下
|
|
意思是binwalk会扫描文件里的特征,找到文件系统后递归扫描里面的内容
-e:尝试提取-M: 提取后继续递归扫描里面的内容
文件分类
进入 squashfs-root 后,可以看到类似Linux系统的目录结构。bin 和 sbin 中是可执行程序,etc 中是配置文件,lib中是动态链接库
注意到vscode只能打开etc中的文件,可执行文件打开会乱码。这是因为它只能打开文本类文件,可执行文件是编译后的机器指令
使用下面的命令批量判断这些目录里每个文件的类型
|
|
这里筛选几个有代表性的输出:
|
|
这是一个 ELF 可执行程序
ELF:Linux 下常见的二进制文件格式32-bit:这是 32 位程序MSB:大端序,也就是高位字节放在前面MIPS:这个程序的架构是 MIPSMIPS32 rel2:MIPS 指令集版本dynamically linked:程序运行时需要依赖动态库interpreter /lib/ld-uClibc.so.0:程序启动时需要通过这个动态链接器加载运行no section header:文件中没有 section header,很多固件程序会被精简掉这部分信息
|
|
这种文件是指向BusyBox的符号链接。它们并不是实现命令的二进制程序,真正的程序是busybox。因此无需分析这类文件,分析 busybox即可
嵌入式设备为了节省存储空间,经常用一个BusyBox实现很多常用命令。比如系统调用
ls时,实际运行的是 BusyBox,只是 BusyBox 会根据自己被调用时的名字执行对应功能
|
|
init 和系统启动流程有关,也指向 BusyBox
|
|
这是 shell 脚本,是文本文件,可以直接打开阅读。ASCII text executable 表示它是文本且可执行。脚本能直接看出某些启动命令或配置逻辑,可以优先分析这类文件
|
|
指向 /dev/null,也就是 Linux 里的空设备。写入的东西会被丢弃,也读不出来正常内容。所以这是占位或被废弃的命令
|
|
这些是普通文本文件,可以直接打开阅读。一般是重要的配置类文件
|
|
证书和私钥相关文件
PEM certificate:PEM 格式证书PEM RSA private key:PEM 格式 RSA 私钥
|
|
这是普通目录,需要继续进入目录内部扫描文件
|
|
这是动态链接器相关文件。前面很多 ELF 程序里都有 interpreter /lib/ld-uClibc.so.0,说明这些程序运行时会通过这个路径找到动态链接器
ld-uClibc.so.0:程序要找的动态链接器路径ld-uClibc-0.9.30.so:实际存在的具体版本文件symbolic link:把通用名字指向具体版本
|
|
这是动态库,是供其他程序调用的库文件。固件中的程序运行时会依赖这些 .so 文件
shared object:共享库文件MIPS:库文件的架构是 MIPSdynamically linked:它本身也可能依赖其他库no section header:文件中没有 section header
启动流程
这个固件里 /sbin/init 指向 BusyBox,BusyBox 的 init 会读取 /etc/inittab 作为启动配置。所以观察启动流程首先看 etc/inittab
|
|
-
::sysinit:/etc/init.d/rcS:init启动后,会在初始化阶段执行/etc/init.d/rcS -
::respawn:-/bin/sh:反复拉起一个 shell。-表示让 shell 按 login shell 方式启动, login shell 会读取/etc/profile、~/.profile这类配置 -
# tty2::askfirst:-/bin/sh:控制台登录。这行被注释了,不会执行 -
#::ctrlaltdel:/bin/umount -a -r:响应 Ctrl+Alt+Del 这类重启动作,/bin/umount -a -r是卸载文件系统。这行被注释了,不会执行
这里主要需要查看两个文件:
etc/init.d/rcS和/etc/profile
etc/init.d/rcS只打印信息和设置 PATH,没有继续启动其他服务。这条线断掉了
|
|
/etc/profile才是真正的目标,摘取其中重要的命令如下
|
|
初步分析刚刚筛选出来和启动相关的程序:
bin/swapdev:环境准备的脚本,创建设备节点
bin/startbsp:环境初始化的脚本,挂载目录
bin/atserver:没有找到对应文件,可能是残留逻辑
bin/usbdiagd:没有找到对应文件,可能是残留逻辑
bin/atmcmdd:后台启动的 ELF 程序,常驻运行
bin/tcwdog:后台启动的 ELF 程序,和看门狗相关。这里真正被启动的是 tcwdog,/dev/watchdog 是传给它的设备节点参数
bin/mic:最后执行的ELF程序,没有&说明 profile 执行到这里后,它可能会接管后续流程
逆向分析
比较有分析价值的是
mic和atmcmdd,这里先分析mic
用ida打开,首先确认识别的文件类型与架构和之前file命令输出的一致,其他保持默认即可
打开Functions window窗口,它就是IDA 识别出来的函数目录
这里的函数名分为几类:
_init:前面带有下划线的函数,一般是ELF和运行库产生的,可以不重点分析sscanf:比较熟悉常见的名称,是libc 或系统接口,无需单独分析sub_40xxxx:真实命名未知的函数,以地址临时命名ATP_xxx_yyy:这里有大量ATP开头的函数,根据命名猜测是这个固件的功能函数。其中xxx是功能分类,yyy是具体操作ATP_UTIL_xxx:util分类是通用功能,比如进程、socket、定时器。如ATP_UTIL_ForkProcess是util下创建子进程的函数ATP_MSG_xxx:msg分类是消息与通信的实现,如发送、队列等。如ATP_MSG_SendSimpleMsg就是msg分类下发送简单信息的函数ATP_MIC_xxx:如ATP_MIC_SockSvrCreate是mic分类下创建socket服务的函数。暂时看不出这个分类具体是什么功能,注意到有些函数调用了msg
双击一个函数进入,可能出现两种情况:
- 只有短短一行,可以看到
.extern,说明引用了这个函数,但它的实现代码不在这个文件里。也就是说这部分保存的是调用关系
- 出现这样的流程图,说明真正进入了函数的实现逻辑。在这里可以按
F5反编译,查看伪代码分析具体功能
注意:如果
F5报错Sorry, the current file is not decompilable,检查是否用ida64打开了32位程序,分析32位程序时应该使用普通版本的ida
打开Imports窗口,这里记录了程序调用的外部库函数,也就是.extern
把光标放到函数上,然后按 X看交叉引用,这样就能看到程序里调用了这个外部函数的所有地方。这里主要分析调用时的上下文
注意到只有ATP_MIC_xxx在这个文件中实现,其他两个分类都是外部引用。下一步回到main函数,根据实际调用关系继续分析
Main函数分析
双击main函数进入它的实现逻辑界面,首先看一下程序的流程图
开头
这部分的意思是进入main后先检查启动参数。可以看到main(int argc, char **argv, ...), argc是参数个数,argv是参数内容。这里先判断参数个数是不是2,然后取出argv[1],调用strcmp把它和字符串"atpv"比较。如果返回0说明两个字符串相同,所以启动参数是atpv时,程序会进入左侧,否则进入正常启动流程
左侧这部分调用了GetVersion,并且在最后直接退出,判断是用于打印版本信息
初始化阶段
继续分析右侧。puts打印"Start mic now ..."说明这里是启动阶段。signal是注册信号处理函数。最后调用了init,是函数初始化部分
如果这里初始化失败,会输出错误信息,然后直接来到结束阶段
这里注意到报错输出直接说明了每一部分的作用
"Create inetd app failed: %x.\n"
创建一个inetd app,也就是一个服务入口表,每个app是其中一个服务项
"Append stdin to inetd app failed: %x.\n"
把stdin加到socket server里,说明它能接收来自标准输入的数据
"Create msg svr failed: %x.\n"
创建msg server,说明 mic 有消息通信功能
"Load cfm failed: %x.\n"
初始化 CFM 相关数据,也就是和配置与管理相关的部分。成功后便进入inetd app的启动阶段
启动阶段
这里首先打印了CFM正常的信息,然后检查dns服务。如果已经启动,会打印Init app already start.并继续,否则调用ATP_MIC_InetdLaunchApp进行启动。这里还出现了Init kill pid = %d. ,说明会清理已有进程
"Create monitor svr failed: %x.\n"
这部分是创建监听模块,用于监听后续事件
"Mic start return: %x.\n"
启动log和cms,让它们在后台运行,然后启动socket基础服务,让前面准备好的功能开始运行。调用结束后再检查返回值
结尾
最后进入 main 的结尾,清理前面创建的inetd和socket环境,然后返回0
到这里已经将
main函数的主要流程分析了一遍。后续可以通过main函数调用的功能函数来深入分析
QEMU分析
前面的静态分析可以看到,mic的main函数会检查启动参数。如果参数是atpv,程序会进入版本信息分支打印结果,然后退出
这里用 QEMU 进行验证。之前的file命令显示程序是大端 MIPS,所以用qemu-mips-static进行模拟,传入atpv参数
|
|
可以看到如之前分析所见,输出了版本信息