CVE-2018-5767
CVE-2018-5767 是 TENDA-AC15 型号路由器上的一个漏洞,产生的原因是没有限制用户的输入,使用函数 sscanf 直接将输入拷贝到栈上,导致栈溢出。
存在该漏洞的固件是 Tenda cn Ac15_firmware:15.03.1.16
提取固件
首先需要明确一点,IoT 设备的系统通常与 linux、Windows 不同,由于设备的资源有限,它们的操作系统相对比较简洁,而且许多 IOT 设备使用的都是一些不是很常见的指令集(多数使用了精简指令集),这就需要我们了解这些 CPU 架构以及指令集的特性,才能分析固件中的程序。
幸运的是,TENDA(至少是 AC15)的 CPU 是 ARM 架构的,提取的程序可以直接使用 IDA 进行反编译。
首先要下载官方的路由器软件包,打开TENDA 的官网,在里面找到 AC15 路由器的支持页面下载固件,链接。
下载到本地的是一个.bin 文件,大小约为 10 MB,file 一下发现这其实是一个迷你的镜像
1 | ➜ Desktop file US_AC15V1.0BR_V15.03.1.16_multi_TD01.bin |
file 指出它是 u-boot legacy uImage 格式的镜像文件,在百度上可以找到一些详细的解释,简单来说,它就是 Linux 为 ARM 架构打造的小型启动镜像,我们可以将它理解为类似于 IOS 的镜像。
既然是镜像,就能够通过一些手段来提取它内部的文件,首先明确此镜像的文件系统格式,通常可以使用 binwalk ,以这个固件为例,用 binwalk 跑一下得到的信息:
1 | DECIMAL HEXADECIMAL DESCRIPTION |
第三项指出了镜像的文件系统格式为Squashfs,类似的文件格式还有许多,一般都能被 binwalk 所识别。
接着就可以着手将文件系统取出来,使用dd 命令
1 | dd if=US_AC15V1.0BR_V15.03.1.16_multi_TD01.bin bs=1 count=8549574 skip=1674524 of=image |
if 参数设置输入的文件,bs 是输入输出块的大小,count 是输入输出块的个数(抽取的总字节数可以理解为 bs * count),skip 是从文件头开始的偏移量,of 指定输出字节的保存位置,这些数据在 binwalk 中都能找到。
执行这条命令需要比较长的时间,跑完得到一个名为image 的文件,这时再来 file 一下
1 | image: Squashfs filesystem, little endian, version 4.0, 8549574 bytes, 741 inodes, blocksize: 131072 bytes, created: Thu Nov 19 09:36:43 2015 |
文件系统已经提取出来了,接下来就是解压出文件,和 NTFS FAT32 类似,Squashfs 是一种磁盘格式,可以直接使用来工具解码文件,这里我们用到的工具是 squashfs-tools,在 github 上面能找到源代码,也可以使用下面的命令直接安装
1 | git clone https://github.com/mirror/firmware-mod-kit.git |
然后找到工具中的unsquashfs_all.sh,使用下面的命令解压镜像
unsquashfs_all.sh image
到这里,路由器的固件已经被提取出来了,下面准备复现漏洞。
漏洞分析
在bin 文件夹中找到存在漏洞的文件 httpd ,使用 checksec 看一下开启的保护:
注:部分情况下checksec 检测的结果并不准确,需要通过动态调试进一步确定。
32 位程序,只开启了 NX,丢进 IDA 开始分析,由于是复现漏洞,所以直接定位到函数 R7WebsSecurityHandler,按F5 查看反编译代码(代码比较多,只把关键的地方放上来)
结合上下文来理解这段代码,第100 行使用了函数 strstr 尝试在 (v22 + 46) 处找到 “password=” 字符串的位置,其中 v22 + 46 是用户的 HTTP 请求,password 是请求中 cookie 的一个字段,如果找到了 password,就会进入接下来的逻辑,在第 102 行调用了函数 sscanf,使用正则匹配 “password=” 之后的字符串,并将匹配出的结果存入变量 v35 中,结合 v35 的变量定义
1 | char v35; // [sp+304h] [bp-1C0h] |
很明显这里是存在栈溢出的,代码没有判断字段的最大长度,直接将内容拷贝到栈上,如果用户精心构造一个password,那么可能有机会控制整个程序的流程,进而达到远程执行代码的目的。
在复现漏洞的时候经常会摸不到头脑,比如凭什么说v22 + 46 代表 http 请求,这个函数究竟想要干什么等等,加上没有真正的路由器,一切只能在 qemu 中模拟,想要真正从根本上理解漏洞的成因是比较困难的,解决办法只能是不断地分析关键函数并且根据程序功能猜测这些究竟是什么,有条件的话可以考虑搞来一台路由器模拟真实的环境,这或许能够解决一部分问题。
搭建调试环境
操作之前先拍摄快照!
在从前,想要跨平台模拟运行其他平台的程序是很困难的,幸好一路大佬开发出了QEMU,关于 QEMU 的具体信息可在其官方网站上找到,实际上它就是一个虚拟机,能够虚拟大部分硬件设备,ARM 自然也包括在内,下面是一张关于 QEMU 的简图
它在宿主机上模拟了相应硬件的环境,并在这些模拟的环境上面运行客户系统,这种机制类似于VMware,无需关机重启,即可在一套硬件上运行多种不同的系统。
想要运行本例的程序,就需要先搭建好qemu 的环境,具体的操作步骤就不细说了,在百度上有大把的文章,重点是如何搭建 QEMU + GDB 的调试环境。
1 | sudo apt-get install qemu |
如果你已经搭建好了qemu,应该可以使用下面的命令启动 httpd
qemu-arm -L <抽取出的固件根目录> httpd
如果不能启动,可能是由于QEMU 没有装好或者 -L 选项的参数不正确。
接下来要安装支持多种架构的gdb 调试器, linux 自带的 gdb 并不支持 ARM 指令集,需要安装 gdb-multiarch
sudo apt-get install gdb-multiarch
安装完成之后启动看看是否正常,推荐装上pwndbg 这个 gdb 插件,方便调试。
如果一切正常,下面就可以开始调试了,首先使用qemu 将程序跑起来
1 | qemu-arm-static -g 1234 -L <抽取出的固件根目录> httpd |
然后打开gdb-multiarch,依次用下面的命令进行初始化设置并附加到程序上
1 | gdb-multiarch |
不出意外的话就能使用gdb 调试程序了。
搭建网络环境
操作之前先拍摄快照!!!
如果你按照上面的步骤操作完之后,调试程序应该是没有问题的,但是很可能会在一个名为check_network 函数上面栽跟头,按照函数名来推测,它应该是用来检测当前网络连接是否可用的,我们虽然搭建起了调试环境,但是并没有设置好网络环境,所以这个函数不会正常工作(返回值为 0),如果你尝试将它的返回值修改为 1,程序可以正常运行,但是最终出现的 IP 地址会非常诡异,我分别使用两台机器进行了实验,一次 IP 地址位于日本,而另一次位于墨西哥….. 这就导致程序无法正常接收我们的请求,没法儿进行下一步操作了。
所以要先配置好qemu 的网络参数,首先安装必要的工具
1 | apt-get install bridge-utils uml-utilities |
接着要修改网络配置,编辑/etc/network/interfaces
1 | interfaces(5) file used by ifup(8) and ifdown(8) |
新建一个shell 脚本 /etc/qemu-ifup
1 | !/bin/sh |
别忘了给脚本足够的权限,然后重启网络服务
1 | sudo /etc/init.d/networking restart |
如果其中哪一步报错,尝试重启一下虚拟机。
这样,我们的网络环境就配置完成了,接下来开始着手复现漏洞。
触发漏洞
启动程序的命令
1 | sudo chroot . ./qemu-arm-static -g 10000 ./bin/httpd |
启动之后在IDA 中选择 remote GDB debugger ,配置相应的参数然后连接上去即可调试。
首先试试看能不能触发漏洞,让程序崩溃。直接运行程序时会输出WeLoveLinux ,之后就没什么反应了,猜测进入了假死状态,使用 IDA 找到用到这个字符串的位置下断点,单步看看会发生什么。
经过调试推测这个函数连接了某个服务,在正常情况下应该会返回1,但是我们使用 QEMU 模拟的环境,它连接不到相关服务,就会返回 0。我们需要手动 patch,可以使用 IDA 自带的 patch,也可以使用 keypatch 插件,将返回值 patch 成 1。
patch 好之后就可以正常运行啦,附一个截图
如果你看到类似的界面(特别是 IP 地址,一般是你的本机地址),那就说明环境搭建的没有问题。
接着在有漏洞的函数R7WebsSecurityHandler 开头打一个断点,先打开具有拦截数据包功能的软件(比如 burp suite),然后用浏览器访问 http://<IP>/goform/execCommand
抓一个数据包,这个包的内容应该如下
1 | GET /goform/execCommand HTTP/1.1 |
为了触发漏洞,我们要手动添加一个Cookie 字段,例如我将数据包改造为
1 | GET /goform/execCommand HTTP/1.1 |
重放数据包,这时候程序应该会断在R7WebsSecurityHandler 函数头部,再 F9 运行程序(整个过程中可能会弹出一些警告,直接忽略,然后在接下来的窗口中点击 pass to app),不出意外的话程序会终止运行,并且终端中会打印出段错误。
接下来就是想办法利用漏洞了,其实IoT 的漏洞和 CTF 中的 pwn 题比较类似,利用思路也有部分相通之处,过程依旧是先检查保护,然后寻找漏洞,根据漏洞类型和开启的保护类型找到合适的利用方式,最后编写 exp。
首先让我们看看漏洞发生时stack 空间的情况。
上图是sscanf 之前,我们看到它的三个参数和 IDA 伪代码识别的一致,单步步过 sscanf,然后检查对应的栈空间发现已经被字符 a 填充,继续单步,在步过 0x2dd48 代码之后就会触发异常,检查异常发生时的程序环境:
程序试图访问r3 寄存器指向的地址,但是此时 R3 已经变成了 0x61616161,也就是我们填入的内容,由于访问了非法地址,导致程序出错,异常退出。
发生这种情况就说明输入覆盖了stack 上的关键数据,也就是说有机会通过精心构造一些数据来控制程序流程!参考网上的文章,如果 password 字段中包含 .gif 字符串,那么就会直接从当前函数中 return,而不会进入到读入 R3 寄存器这一支流程,所以我们重新构造一个满足条件的 payload
1 | GET /goform/execCommand HTTP/1.1 |
再次发送数据包,崩溃时的情况如图
当前的PC 指针(相当于 x86 中的 EIP)指向了 0x61616160,也就是我们填充的内容。
到这里可以大致推测一下这个函数的功能,应该是用来过滤一些非法输入的,但是由于本身的逻辑没有做好,导致本来应该保护软件安全的函数却自己出了问题。
下面需要计算一下到返回地址的偏移,首先需要知道一个重要的知识点,和x86 不同,ARM 指令集在函数返回时并不是依靠存储在栈上的地址,而是依靠 LR 寄存器,只有修改了 LR 寄存器,才能在函数返回时控制程序流程,幸运的是,我们分析的函数在返回时调用了 pop 指令,将存储在栈上的地址存放到 LR 寄存器中,所以给了我们控制程序流程的机会。构造以下 payload
1 | GET /goform/execCommand HTTP/1.1 |
然后正常操作,当执行到pop 指令的时候观察 stack 如下
通过当前状态,可以推算出padding 的长度为 447 个字节。
有了padding,接下来要考虑构造 ROP,与 ctf 中不同的一点是这种 web 服务在崩溃后一般会立刻重启,并且重启之后的 libc 地址和之前是相同的,所以爆破 libc 是比较常用的攻击手段,不过 qemu 没办法提供这种爆破 libc 的条件。
我们尝试手动修改一些地址,搞一个可以看到效果的利用链。
首先运行到0x2de94 ,单步走几次到 pop 指令之前,然后使用命令
1 | set {int}<your stack address> = <target address> |
修改几个stack 的地址,比如我修改了几个地址如图:
单步运行,会在终端中看到效果
成功执行了puts 函数并输出我们的内容。
payload如下
1 | GET /goform/execCommand HTTP/1.1 |
附:写完本文之后我又进行了几次调试,发现system 函数始终不能正常被执行,原因是 execve 这个调用一直提示 No such file or directory . google 到几篇文章也没有具体的解决办法,初步猜测是 qemu 模拟环境不完善,导致找不到 /bin/sh ,不知道在真机上面能不能得到改善。
参考文章
https://www.freebuf.com/vuls/160040.html
- Title: CVE-2018-5767
- Author: Catalpa
- Created at : 2019-03-19 00:00:00
- Updated at : 2024-10-17 08:45:52
- Link: https://wzt.ac.cn/2019/03/19/CVE-2018-5767/
- License: This work is licensed under CC BY-SA 4.0.