作者 | 海康威視
一、前言
二、嘗試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這類自動化模擬工具的瓶頸,因此在實際工作中,我們常常通過手工調試的方式來修復這一過程,提升固件模擬的成功率。本文將以Cisco RV100W為例,展示如何修復一個Firmadyne仿真失敗的固件。3. 修復文件系統如libnvram.so、常見的設備文件等,並打包鏡像;4. 獲取目標固件的網絡配置信息,並生成qemu的啟動腳本;下面我們嘗試使用Firmadyne直接模擬Cisco RV100W設備的固件。2.1 提取文件系統固件仿真的第一步就是提取目標固件的文件系統,firmadyne項目提供了基於binwalk的文件系統提取腳本,能夠自動將文件系統提取並打包為tar.gz:由於firmadyne腳本需要,將解析出來的rootfs重命名為99.tar.gz,後續對該固件的操作均使用99替代:2.2 識別固件架構使用getArch.sh腳本識別固件架構為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/loop02.4 獲取網絡配置在上一步生成image.raw後,使用inferNetwork.sh腳本生成虛擬機的啟動腳本。根據腳本的輸出,可以看到網絡配置並沒有成功,不過仍然在對應目錄下生成了一個不包含網絡配置的啟動腳本,名為run.sh:Running 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:nvram_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的符號連結:/mnt/sbin/init: symbolic link to rc對rc簡單進行分析一下,發現該文件的實現與busybox相似,通過軟連接或者重命名的方式將程序命名為對應的名字,執行的過程當中通過程序的文件名確定執行的具體邏輯:/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。使用binwalk對固件手動解包,發現除了系統的根目錄之外還有一名為3C的文件,抓取該文件中的字符串,發現了內核的啟動參數,同時也驗證了內核參數為init=/sbin/preinit的想法:…………root=/dev/mtdblock2 console=ttyS0,115200 init=/sbin/preinit……所以init進程退出的原因是啟動參數不正確,當執行init程序的時候,由於沒有對應的代碼邏輯,所以進程會直接返回,導致kernel panic。故修改run.sh,添加qemu的內核啟動參數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中添加對應的文件即可:add map loop0p1 (253:0): 0 2095104 linear 7:0 2048bin/ data/ dev/ etc/ firmadyne/ lib/ lost+found/ mnt/ proc/ sbin/ sys/ tmp/ usr/ var@ www//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/arg3.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調用。根據函數的調用樹,發現如果執行了wps_button_pushed,那麼最後會執行start_single_service,如果我們能夠防止reset_button_pushed被執行,那麼就可以讓服務正常啟動:最後發現一個nvram的鍵,名為resetbutton_disable,該值控制resetbutton的行為,如果將將該值設為1,那麼將禁用resetbutton,防止設備進行重啟:所以,接下來在libnvram.override中添加對應的文件,禁用resetbutton:# printf "1" | tee /mnt/firmadyne/libnvram.override/resetbutton_disable13.4 順利進入系統在這次啟動之前,我們先重新編譯libnvram.so,將DEBUG這個宏定義為0,關閉調試信息,避免大量的nvram調試信息影響判斷:/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此時再次嘗試執行run.sh,可以看到最後成功進入到了shell環境當中,並且web服務和telnet服務均監聽各自的埠:……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.Active 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 Path4.1. 更新網絡配置至此,我們已經能夠成功的使用qemu啟動固件進入shell環境,並且根據埠監聽的情況可以判斷web和telnet等核心進程均成功啟動,所以為了能夠使用宿主機訪問該這臺模擬的設備,還需要進行最後的配置網絡。由於inferNetwork.sh這個腳本調用的虛擬機啟動腳本為script/run.mipsel.sh,所以我們需要編輯run.mipsel.sh,添加init=/sbin/preinit:qemu-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:Running 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! -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,證明網絡連通性沒有問題:Trying 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.在/usr/sbin/httpd這個二進位程序中發現該報錯信息,並且通過上下文,發現wl_is_ap_mgmt_vlan_mac的返回值導致了這條報錯的產生:wl_is_ap_mgmt_vlan_mac為動態連結的函數,在libcbt.so中實現。通過逆向分析,發現當wl_ap_mgmt_vlan_id與wl_vlan_id的值相同的時候,函數返回1,將不會產生這條報錯信息。通過同樣的方法,添加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:/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 4.3 仿真成功最後,我們成功仿真成功了該固件,使用默認用戶名密碼cisco:cisco,成功登入web控制臺:Firmadyne作為一個自動化的固件仿真框架,根據其論文中的數據,在能夠成功提取出文件系統的8893例固件樣本中,有1971例能夠通過網絡測試,成功率約為22%。根據廠商不同,Dlink的固件約有40%的成功率,Netgear的固件約有50%的成功率,然而Cisco的設備固件無一通過網絡測試。本文通過使用逆向分析的方式,使用firmadyne的框架,成功仿真Cisco RV100W的設備固件,希望能夠為手動仿真Cisco的其他設備固件提供一些思路。同時,在分析的過程當中,結合firmadyne的論文,我們發現設備自動化仿真的成功率是和廠商強相關的。所以如果要想在firmadyne的基礎上增加仿真的成功率,那麼一個有效的思路就是針對廠商或者產品線進行適配,可以定向的增加特定廠商的固件仿真成功率。