Firmadyne仿真修复案例

  • A+
所属分类:安全文章

作者 | 海康威视

一、前言

二、尝试Firmadyne自动模拟

2.1 提取文件系统

2.2 识别固件架构

2.3 修复文件系统&镜像打包

2.4 获取网络配置

三、修复启动过程

3.1 错误的init进程

3.2 缺失的nvram键值

3.3 绕过系统reset逻辑

3.4 顺利进入系统

四、完成仿真

4.1 更新网络配置

4.2 修复web访问权限

4.3 仿真成功

五、总结

一、前言

在设备安全研究中,为了更方便的开展动态分析工作,常常会考虑对固件进行仿真模拟。Firmadyne是目前最流行的开源固件模拟工具,很多开源固件模拟方案就是对Firmadyne的封装。
由于嵌入式设备的碎片化,固件模拟的通用性难以保证,在实际模拟中,即使是Firmadyne的失败率也非常高。以思科路由器为例,Firmadyne论文中共测试了61个思科设备固件,成功提取文件系统的有43例,识别系统架构成功的有39例,而网络配置仅成功2例,最后没有固件能够通过网络连通性测试。
根据Firmadyne的论文,绝大多数失败都产生在推断网络配置信息的阶段:
Firmadyne仿真修复案例
Firmadyne通过修改内核源码,监听设备启动时初始化网络配置的过程,从而获取设备的默认网络配置。但由于设备启动的初始化逻辑与硬件关联紧密、通用性差,是Firmadyne这类自动化模拟工具的瓶颈,因此在实际工作中,我们常常通过手工调试的方式来修复这一过程,提升固件模拟的成功率。本文将以Cisco RV100W为例,展示如何修复一个Firmadyne仿真失败的固件。

二、尝试Firmadyne自动模拟

Firmadyne自动仿真固件大致分为六个步骤:
1. 解析固件提取文件系统;
2. 根据提取的文件系统,判断固件的架构信息;
3. 修复文件系统如libnvram.so、常见的设备文件等,并打包镜像;
4. 获取目标固件的网络配置信息,并生成qemu的启动脚本;
5. 执行生成的启动脚本,开始仿真;
下面我们尝试使用Firmadyne直接模拟Cisco RV100W设备的固件。

2.1 提取文件系统

固件仿真的第一步就是提取目标固件的文件系统,firmadyne项目提供了基于binwalk的文件系统提取脚本,能够自动将文件系统提取并打包为tar.gz:
# ./sources/extractor/extractor.py -b Cisco -np -nk "./Cisco_RV110W_FW_1.2.2.5.bin" images./Cisco_RV110W_FW_1.2.2.5.bin>> MD5: 10ca3292c5aeb5b4c77ddb98c0b6d663>> Tag: Cisco_RV110W_FW_1.2.2.5.bin_10ca3292c5aeb5b4c77ddb98c0b6d663>> Temp: /tmp/tmpe2qv2gq5>> Status: Kernel: True, Rootfs: False, Do_Kernel: False, Do_Rootfs: True>> Recursing into archive ...>>>> Squashfs filesystem, little endian, non-standard signature, version 3.0, size: 9188808 bytes, 1074 inodes, blocksize: 65536 bytes, created: 2019-07-24 08:12:22>>>> Found Linux filesystem in /tmp/tmpe2qv2gq5/_Cisco_RV110W_FW_1.2.2.5.bin.extracted/squashfs-root!>> Skipping: completed!>> Cleaning up /tmp/tmpe2qv2gq5...
由于firmadyne脚本需要,将解析出来的rootfs重命名为99.tar.gz,后续对该固件的操作均使用99替代:

2.2 识别固件架构

使用getArch.sh脚本识别固件架构为mipsel:
# ./scripts/getArch.sh images/99.tar.gz./bin/busybox: mipsel

2.3 修复文件系统&镜像打包

使用makeImage.sh脚本修复rootfs,同时将修复好的文件系统打包为qemu的启动镜像:
# ./scripts/makeImage.sh 99 mipsel----Running----./scratch/99/./scratch/99/image.raw./scratch/99/image/./binaries/console.mipsel./binaries/libnvram.so.mipsel----Copying Filesystem Tarball--------Creating QEMU Image----Formatting './scratch/99/image.raw', fmt=raw size=1073741824----Creating Partition Table----……----Mounting QEMU Image--------Creating Filesystem----mke2fs 1.45.6 (20-Mar-2020)……----Making QEMU Image Mountpoint--------Mounting QEMU Image Partition 1--------Extracting Filesystem Tarball--------Creating FIRMADYNE Directories--------Patching Filesystem (chroot)----Creating /etc/TZ!Creating /etc/hosts!Creating /etc/passwd!Warning: Recreating device nodes!Removing /etc/scripts/sys_resetbutton!----Setting up FIRMADYNE--------Unmounting QEMU Image----loop deleted : /dev/loop0

2.4 获取网络配置

在上一步生成image.raw后,使用inferNetwork.sh脚本生成虚拟机的启动脚本。根据脚本的输出,可以看到网络配置并没有成功,不过仍然在对应目录下生成了一个不包含网络配置的启动脚本,名为run.sh: 
# ./scripts/inferNetwork.sh 99 mipselRunning firmware 99: terminating after 60 secs...qemu-system-mipsel: terminating on signal 2 from pid 1244313 (timeout)Inferring network...Interfaces: []Done!# tree scratch/99scratch/99├── image├── image.raw├── qemu.initial.serial.log└── run.sh1 directory, 3 files
根据firmadyne做网络配置推断的原理,在这一阶段产生错误大多是因为系统没有执行到网络配置的阶段,产生这个问题的原因可能有很多,这时往往需要手动执行qemu启动脚本,根据报错信息另行判断。

三、修复启动过程

3.1 错误的init进程

尝试手动执行run.sh。qemu的标准输出记录在qemu.final.serial.log中,通过观察qemu的日志,发现最后几行报错的信息如下。在对应的2.6.39内核版本中,该报错产生于kernel/exit.c中的find_new_reaper函数。如果父进程在子进程之前退出,该函数尝试为子进程找到一个新的父进程,但是当退出的父进程为init时,就会产生这条报错信息。由此可推断,init进程因为某种原因结束了,导致最后的kernel panic:
# cat scratch/99/qemu.final.serial.log | tailnvram_set_default_image: Copying overrides from defaults folder!sem_get: Key: 410c001ccp: cannot stat '/firmadyne/libnvram.override/*': No such file or directorysem_get: Key: 410c001csem_get: Key: 410c001csem_get: Key: 410c001cnvram_get_buf: = "-08 1 1"[    2.932000] Kernel panic - not syncing: Attempted to kill init!QEMU: Terminated
所以接下来,我们需要分析一下init进程,在固件中,我们发现init进程实际为rc的符号链接:
# file /mnt/sbin/init/mnt/sbin/init: symbolic link to rc
对rc简单进行分析一下,发现该文件的实现与busybox相似,通过软连接或者重命名的方式将程序命名为对应的名字,执行的过程当中通过程序的文件名确定执行的具体逻辑:
# file /mnt/sbin/* | grep 'to rc'/mnt/sbin/blink_diag_led:     symbolic link to rc……/mnt/sbin/client6:            symbolic link to rc……/mnt/sbin/preinit:            symbolic link to rc……/mnt/sbin/snmpdc:             symbolic link to rc……/mnt/sbin/wl_nvram:           symbolic link to rc……/mnt/sbin/yutest:             symbolic link to rc
但在在对rc的逆向分析中,没有见到对”init”字符串的匹配,取而代之的是有一个判断程序名是否为”preinit”的判断,preinit与init意思相近,所以猜测内核的启动参数可能为init=/sbin/preinit。
Firmadyne仿真修复案例Firmadyne仿真修复案例
使用binwalk对固件手动解包,发现除了系统的根目录之外还有一名为3C的文件,抓取该文件中的字符串,发现了内核的启动参数,同时也验证了内核参数为init=/sbin/preinit的想法:
# binwalk -e Cisco_RV110W_FW_1.2.2.5.bin……# strings _Cisco_RV110W_FW_1.2.2.5.bin.extracted/3C | grep -P 'init='……root=/dev/mtdblock2 console=ttyS0,115200 init=/sbin/preinit……
所以init进程退出的原因是启动参数不正确,当执行init程序的时候,由于没有对应的代码逻辑,所以进程会直接返回,导致kernel panic。故修改run.sh,添加qemu的内核启动参数init=/sbin/preinit:
# cat scratch/99/run.sh | grep 'init=/sbin/preinit' -drive if=ide,format=raw,file=${IMAGE} -append "root=${QEMU_ROOTFS} console=ttyS 0 nandsim.parts=64,64,64,64,64,64,64,64,64,64 rdinit=/firmadyne/preInit.sh rw debug ignore_loglevel print-fatal-signals=1 user_debug=31 firmadyne.syscall=0 init=/sbin/preinit"

3.2 缺失的nvram键值

修复了启动参数后,再次尝试启动run.sh,发现最后造成Kernel panic的原因发生了变化,这次打印出了内核的函数调用栈。可以看到在preinit中产生了signal 11,也就是说在执行preinit的过程当中,产生了无效的内存引用:
# cat scratch/99/qemu.final.serial.log | tail -n 30[   10.004000]         7fe0b074 7fe0afda 7fe0af88 7fe0af58 00000001 7fe0af68 004b0000 00471ce0[   10.004000]         ...[   10.004000] Call Trace:[   10.004000][   10.008000][   10.008000] Code: 00c08821  00e0b021  00808021[   10.008000]  8d020000  00051840  00621821  94620000  30420020[   10.008000] preinit/1: potentially unexpected fatal signal 11.[   10.008000][   10.008000] Cpu 0[   10.008000] $ 0   : 00000000 1000a400 00000000 00000001[   10.008000] $ 4   : 00000000 00000000 0000000a 00000001[   10.012000] $ 8   : 2adaf004 00000003 00000001 8ffc5480[   10.012000] $12   : 8ffc5480 2adaf004 00010000 8ffc515c[   10.012000] $16   : 00000000 0000000a 7fe0b074 7fe0afda[   10.012000] $20   : 00000001 00000000 00000001 004b0000[   10.012000] $24   : 00000002 2ad64ac0[   10.012000] $28   : 2adb7530 7fe0ae98 00000001 0046f968[   10.016000] Hi    : 00000001[   10.016000] Lo    : 00000001[   10.016000] epc   : 2ad64b08 0x2ad64b08[   10.016000]     Not tainted[   10.016000] ra    : 0046f968 0x46f968[   10.016000] Status: 0000a413    USER EXL IE[   10.016000] Cause : 10800008[   10.016000] BadVA : 00000000[   10.016000] PrId  : 00019300 (MIPS 24Kc)[   10.020000] Kernel panic - not syncing: Attempted to kill init!QEMU: Terminated
经过观察qemu的日志发现,在启动的过程中产生了大量缺失键值的nvram报错:
# cat scratch/99/qemu.final.serial.log | grep -i 'unable to open key' | tailnvram_get_buf: Unable to open key: /firmadyne/libnvram/boot_hw_model!nvram_get_buf: Unable to open key: /firmadyne/libnvram/wl0_radio!nvram_get_buf: Unable to open key: /firmadyne/libnvram/wl0_vifs!nvram_get_buf: Unable to open key: /firmadyne/libnvram/wl0_radio!nvram_get_buf: Unable to open key: /firmadyne/libnvram/wl0_bss_enabled!nvram_get_buf: Unable to open key: /firmadyne/libnvram/wl0_vlan_id!# cat scratch/99/qemu.final.serial.log | grep -i 'unable to open key' | sort | uniq | wc -l283
在继续进行修复过程之前,需要先简单介绍一下模拟nvram设备的原理。我们将镜像挂载到/mnt目录下,观察一下firmadyne新增的目录。对于nvram的模拟,我们主要关注libnvram.so,firmadyne通过强制加载这个函数库的方式,将原有nvram的设备操作替换为文件读写,如果需要手动调整nvram的某个键值,只需要在libnvram.override中添加对应的文件即可:
# kpartx -a -s -v scratch/99/image.rawadd map loop0p1 (253:0): 0 2095104 linear 7:0 2048# mount /dev/mapper/loop0p1 /mnt# ls /mntbin/  data/  dev/  etc/  firmadyne/  lib/  lost+found/  mnt/  proc/  sbin/  sys/  tmp/  usr/  [email protected]  www/# tree /mnt/firmadyne/mnt/firmadyne├── console├── libnvram├── libnvram.override├── libnvram.so├── preInit.sh└── ttyS1
根据以上的信息,猜测可能是因为缺失nvram的键值,从而导致了程序对libnvram.so中函数返回的空指针进行了引用。所以为了修复这个问题,我们尝试根据缺失的键名,批量产生空的文件到libnvram.override中:
# cat scratch/99/qemu.final.serial.log | grep 'Unable to'| sort | uniq | awk 'BEGIN{FS="!|/"}{print $4}' | xargs -I arg touch /mnt/firmadyne/libnvram.override/arg

3.3 绕过系统reset逻辑

在修复了缺失的nvram键值后,我们发现报错信息产生了根本性的改变,这次preinit进程没有异常结束,而是在最后尝试对设备进行重启,调用了sys_reboot被firmadyne捕获到:
at scratch/99/qemu.final.serial.log | tail……[   26.340000] firmadyne: sys_reboot[PID: 1 (preinit)]: magic1:fee1dead, magic2:28121969, cmd:1234567[   26.340000] firmadyne: sys_reboot: removed CAP_SYS_BOOT, starting init...QEMU: Terminated
同时,经过观察,在qemu的日志中发现了大量的RESET字样:
nvram_get_buf: resetbutton_disable……Reset Button pushed!count[0] RESET_WAIT_COUNT[100]Reset Button pushed!count[1] RESET_WAIT_COUNT[100]Reset Button pushed!count[2] RESET_WAIT_COUNT[100]……Reset Button pushed!count[98] RESET_WAIT_COUNT[100]Reset Button pushed!count[99] RESET_WAIT_COUNT[100]Reset Button pushed!count[100] RESET_WAIT_COUNT[100]resetbutton: factory default.Receiving restore commond from resetbutton ...
在rc中寻找”RESET_WAIT_COUNT”这段字符串,发现其在函数reset_button_pushed中被使用,reset_button_pushed又被period_check调用。
Firmadyne仿真修复案例
Firmadyne仿真修复案例
根据函数的调用树,发现如果执行了wps_button_pushed,那么最后会执行start_single_service,如果我们能够防止reset_button_pushed被执行,那么就可以让服务正常启动:
Firmadyne仿真修复案例
最后发现一个nvram的键,名为resetbutton_disable,该值控制resetbutton的行为,如果将将该值设为1,那么将禁用resetbutton,防止设备进行重启:
Firmadyne仿真修复案例
所以,接下来在libnvram.override中添加对应的文件,禁用resetbutton:
# printf "1" | tee /mnt/firmadyne/libnvram.override/resetbutton_disable1

3.4 顺利进入系统

在这次启动之前,我们先重新编译libnvram.so,将DEBUG这个宏定义为0,关闭调试信息,避免大量的nvram调试信息影响判断:
# cat config.h | grep DEBUG#define DEBUG               0# CC=/usr/bin/mipsel-linux-gnu-gcc make 2>/dev/null/usr/bin/mipsel-linux-gnu-gcc -c -O2 -fPIC -Wall nvram.c -o nvram.o/usr/bin/mipsel-linux-gnu-gcc -shared -nostdlib nvram.o -o libnvram.so# cp libnvram.so /mnt/firmadyne/libnvram.so
此时再次尝试执行run.sh,可以看到最后成功进入到了shell环境当中,并且web服务和telnet服务均监听各自的端口:
# ./scratch/99/run.sh……Hit enter to continue...BusyBox v1.7.2 (2019-04-22 16:08:01 CST) built-in shell (ash)Enter 'help' for a list of built-in commands.# netstat -lnActive Internet connections (only servers)Proto Recv-Q Send-Q Local Address           Foreign Address         Statetcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTENtcp        0      0 0.0.0.0:81              0.0.0.0:*               LISTENtcp        0      0 192.168.1.1:23          0.0.0.0:*               LISTENtcp        0      0 0.0.0.0:443             0.0.0.0:*               LISTENtcp        0      0 0.0.0.0:444             0.0.0.0:*               LISTENtcp        0      0 :::80                   :::*                    LISTENtcp        0      0 :::81                   :::*                    LISTENtcp        0      0 :::443                  :::*                    LISTENtcp        0      0 :::444                  :::*                    LISTENudp        0      0 0.0.0.0:53518           0.0.0.0:*udp        0      0 127.0.0.1:53            0.0.0.0:*udp        0      0 192.168.1.1:53          0.0.0.0:*udp        0      0 0.0.0.0:67              0.0.0.0:*udp        0      0 0.0.0.0:69              0.0.0.0:*udp        0      0 127.0.0.1:38032         0.0.0.0:*udp        0      0 0.0.0.0:5353            0.0.0.0:*udp        0      0 :::42626                :::*raw        0      0 0.0.0.0:255             0.0.0.0:*               7Active UNIX domain sockets (only servers)Proto RefCnt Flags       Type       State         I-Node Path

四、完成仿真

4.1. 更新网络配置

至此,我们已经能够成功的使用qemu启动固件进入shell环境,并且根据端口监听的情况可以判断web和telnet等核心进程均成功启动,所以为了能够使用宿主机访问该这台模拟的设备,还需要进行最后的配置网络。
由于inferNetwork.sh这个脚本调用的虚拟机启动脚本为script/run.mipsel.sh,所以我们需要编辑run.mipsel.sh,添加init=/sbin/preinit: 
# cat scripts/run.mipsel.sh | grep init=/sbin/preinitqemu-system-mipsel -m 256 -M malta -kernel ${KERNEL} -drive if=ide,format=raw,file=64,64,64,64,64,64,64,64,64 rdinit=/firmadyne/preInit.sh rw debug ignore_loglevel print-fatal-signals=1 init=/sbin/preinit" -serial file:${WORK_DIR}/qemu.initial.serial.log -serial unix:/tmp/qemu.${IID}.S1,server,nowait -monitor unix:/tmp/qemu.${IID},server,nowait -display none -netdev socket,id=s0,listen=:2000 -device e1000,netdev=s0 -netdev socket,id=s1,listen=:2001 -device e1000,netdev=s1 -netdev socket,id=s2,listen=:2002 -device e1000,netdev=s2 -netdev socket,id=s3,listen=:2003 -device e1000,netdev=s3
接下来重新执行inferNetwork.sh。这样就成功获得了正确的IP配置。最后,还需要重新编辑一下run.sh,添加内核的启动参数init=/sbin/preinit:
# ./scripts/inferNetwork.sh 99 mipselRunning firmware 99: terminating after 60 secs...qemu-system-mipsel: terminating on signal 2 from pid 1271009 (timeout)Inferring network...Interfaces: [('br0', '192.168.1.1')]Done! # cat scratch/99/run.sh | grep 'init=/sbin/preinit'        -drive if=ide,format=raw,file=${IMAGE} -append "root=${QEMU_ROOTFS} console=ttyS    0 nandsim.parts=64,64,64,64,64,64,64,64,64,64 rdinit=/firmadyne/preInit.sh rw debug ignore_loglevel print-fatal-signals=1 user_debug    =31 firmadyne.syscall=0 init=/sbin/p    reinit" 

4.2 修复web访问权限

网络配置完成后,再次执行run.sh,此时固件正常启动进入shell环境,尝试使用默认口令Admin123能够登入telnet,证明网络连通性没有问题:
# telnet 192.168.1.1Trying 192.168.1.1...Connected to 192.168.1.1.Escape character is '^]'.RV110W login: adminPassword:BusyBox v1.7.2 (2019-04-22 16:08:01 CST) built-in shell (ash)Enter 'help' for a list of built-in commands.#
但是当访问web界面的时候,有如下报错信息: 
Firmadyne仿真修复案例
在/usr/sbin/httpd这个二进制程序中发现该报错信息,并且通过上下文,发现wl_is_ap_mgmt_vlan_mac的返回值导致了这条报错的产生:
Firmadyne仿真修复案例
wl_is_ap_mgmt_vlan_mac为动态链接的函数,在libcbt.so中实现。通过逆向分析,发现当wl_ap_mgmt_vlan_id与wl_vlan_id的值相同的时候,函数返回1,将不会产生这条报错信息。
Firmadyne仿真修复案例
Firmadyne仿真修复案例
通过同样的方法,添加nvram的键值,wl_ap_mgmt_vlan_id和wl_vlan_id,确保两个键对应的值相同即可:
# printf "10" | tee /mnt/firmadyne/libnvram.override/wl_ap_mgmt_vlan_id# printf "10" | tee /mnt/firmadyne/libnvram.override/wl_vlan_id
另外的一个问题,就是在firmadyne提供的libnvram.so中并未提供wl_nvram_get这个函数,所以我们需要在libnvram.so中添加wl_nvram_get的实现,令其与nvram_get的实现相同即可:
nvram.h:36 char *wl_nvram_get(const char *key);
nvram.c 336 char *wl_nvram_get(const char *key) {  337     return nvram_get(key);  338 }   
最后重新编译libnvram.so,替换镜像中的libnvram.so:
# cat config.h | grep DEBUG#define DEBUG               0 # CC=/usr/bin/mipsel-linux-gnu-gcc make 2>/dev/null/usr/bin/mipsel-linux-gnu-gcc -c -O2 -fPIC -Wall nvram.c -o nvram.o/usr/bin/mipsel-linux-gnu-gcc -shared -nostdlib nvram.o -o libnvram.so # cp libnvram.so /mnt/firmadyne/libnvram.so

4.3 仿真成功

最后,我们成功仿真成功了该固件,使用默认用户名密码cisco:cisco,成功登入web控制台:
Firmadyne仿真修复案例

五、总结

Firmadyne作为一个自动化的固件仿真框架,根据其论文中的数据,在能够成功提取出文件系统的8893例固件样本中,有1971例能够通过网络测试,成功率约为22%。根据厂商不同,Dlink的固件约有40%的成功率,Netgear的固件约有50%的成功率,然而Cisco的设备固件无一通过网络测试。
本文通过使用逆向分析的方式,使用firmadyne的框架,成功仿真Cisco RV100W的设备固件,希望能够为手动仿真Cisco的其他设备固件提供一些思路。同时,在分析的过程当中,结合firmadyne的论文,我们发现设备自动化仿真的成功率是和厂商强相关的。所以如果要想在firmadyne的基础上增加仿真的成功率,那么一个有效的思路就是针对厂商或者产品线进行适配,可以定向的增加特定厂商的固件仿真成功率。


原文来源:关键基础设施安全应急响应中心
Firmadyne仿真修复案例

本文始发于微信公众号(网络安全应急技术国家工程实验室):Firmadyne仿真修复案例

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: