漏洞简介
PSV-2020-0211
对应Netgear
R8300
型号路由器上的一个缓冲区溢出漏洞,Netgear
官方在2020年7月31日发布了安全公告,8月18日SSD
公开了该漏洞的相关细节。该漏洞存在于设备的UPnP
服务中,由于在处理数据包时缺乏适当的长度校验,通过发送一个特殊的数据包可造成缓冲区溢出。利用该漏洞,未经认证的用户可实现任意代码执行,从而获取设备的控制权。
该漏洞本身比较简单,但漏洞的利用思路值得借鉴,下面通过搭建R8300
设备的仿真环境来对该漏洞进行分析。
漏洞分析
环境搭建
根据官方发布的安全公告,在版本V1.0.2.134
中修复了该漏洞,于是选取之前的版本V1.0.2.130
进行分析。由于手边没有真实设备,打算借助qemu
工具来搭建仿真环境。文章通过qemu system mode
的方式来模拟整个设备的系统,我个人更偏向于通过qemu user mode
的方式来模拟单服务。当然,这两种方式可能都需要对环境进行修复,比如文件/目录缺失、NVRAM
缺失等。
用binwalk
对固件进行解压提取后,运行如下命令启动UPnP
服务。
# 添加`--strace`选项, 方便查看错误信息, 便于环境修复
<extracted squashfs-root>$ sudo chroot . ./qemu-arm-static --strace ./usr/sbin/upnpd
运行后提示如下错误,根据对应的目录结构,通过运行命令mkdir -p tmp/var/run
解决。
18336 open("/var/run/upnpd.pid",O_RDWR|O_CREAT|O_TRUNC,0666) = -1 errno=2 (No such file or directory)
之后再次运行上述命令,提示大量的错误信息,均与NVRAM
有关,该错误在进行IoT
设备仿真时会经常遇到。NVRAM
中保存了设备的一些配置信息,而程序运行时需要读取配置信息,由于缺少对应的外设,因此会报错。一种常见的解决方案是"劫持"
与NVRAM
读写相关的函数,通过软件的方式来提供相应的配置。
网上有很多类似的模拟NVRAM
行为的库,我个人经常使用Firmadyne
框架提供的libnvram
库:支持很多常见的api
,对很多嵌入式设备进行了适配,同时还会解析固件中默认的一些NVRAM
配置,实现方式比较优雅。采用该库,往往只需要做很少的改动,甚至无需改动,就可以满足需求。
参考libnvram
的文档,编译后然后将其置于文件系统中的firmadyne
路径下,然后通过LD_PRELOAD
环境变量进行加载,命令如下。
<extracted squashfs-root>$ sudo chroot . ./qemu-arm-static --strace -E LD_PRELOAD=./firmadyne/libnvram.so.armel ./usr/sbin/upnpd
运行后提示缺少某个键值对,在libnvram/config.h
中添加对应的配置,编译后重复进行测试,直到程序成功运行起来即可,最终libnvram/config.h
的变化如下。
diff --git a/config.h b/config.h
index 9908414..6598eba 100644
--- a/config.h
+++ b/config.h
@@ -50,8 +50,10 @@
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_bipaddr", nvram_set, "192.168.0.255") \
+ /* ENTRY("lan_ipaddr", nvram_set, "192.168.0.50") */ \
+ ENTRY("lan_ipaddr", nvram_set, "192.168.200.129") \
+ /* ENTRY("lan_bipaddr", nvram_set, "192.168.0.255") */ \
+ ENTRY("lan_bipaddr", nvram_set, "192.168.200.255") \
ENTRY("lan_netmask", nvram_set, "255.255.255.0") \
/* Set default timezone, required by multiple images */ \
ENTRY("time_zone", nvram_set, "EST5EDT") \
@@ -70,6 +72,10 @@
/* 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") \
+ /* Used by Netgear router: enable upnpd log */ \
+ ENTRY("upnpd_debug_level", nvram_set, "3") \
+ /* Used by "Netgear R8300" */ \
+ ENTRY("hwrev", nvram_set, "MP1T99")
# 成功运行
<extracted squashfs-root>$ sudo chroot . ./qemu-arm-static -E LD_PRELOAD=./firmadyne/libnvram.so.armel ./usr/sbin/upnpd
nvram_get_buf: upnpd_debug_level
sem_lock: Triggering NVRAM initialization!
nvram_init: Initializing NVRAM...
# ... <omit>
nvram_match: upnp_turn_on (1) ?= "1"
nvram_match: true
ssdp_http_method_check(203):
ssdp_discovery_msearch(1007):
ST = 20
ssdp_check_USN(212)
service:dial:1
USER-AGENT: Google Chrome/84.0.4147.125 Windows
漏洞分析
在upnp_main()
中,在(1)
处recvfrom()
用来读取来自socket
的数据,并将其保存在v55
指向的内存空间中。在(2)
调用ssdp_http_method_check()
,传入该函数的第一个参数为v55
,即指向接收的socket
数据。
int upnp_main()
{
char v55[4]; // [sp+44h] [bp-20ECh]
// ...
while ( 1 )
{
// ...
if ( (v20 >> (dword_C4580 & 0x1F)) & 1 )
{
v55[0] = 0;
v28 = recvfrom(dword_C4580, v55, 0x1FFFu, 0, (struct sockaddr *)&v63, (socklen_t *)&v71); // (1)
// ...
if ( v29 )
{
if ( v28 )
{
// ...
if ( acosNvramConfig_match("upnp_turn_on", "1") )
ssdp_http_method_check( v55, (int)&v59, (unsigned __int16)(HIWORD(v63) << 8) | (unsigned __int16)(HIWORD(v63) >> 8)); // (2)
// ...
在ssdp_http_method_check()
中,在(3)
处调用strcpy()
进行数据拷贝,其中v40
指向栈上的局部缓冲区,v3
指向接收的socket
数据。由于缺乏长度校验,当构造一个超长的数据包时,拷贝时会出现缓冲区溢出。
signed int ssdp_http_method_check(const char *a1, int a2, int a3)
{
int v40; // [sp+24h] [bp-634h]
v3 = a1;
// ...
wrap_vprintf(3, "%s(%d):\n", "ssdp_http_method_check", 203);
if ( dword_93AE0 == 1 )
return 0;
strcpy((char *)&v40, v3); // (3) stack overflow
// ...
漏洞利用
upnpd
程序启用的缓解措施如下,可以看到仅启用了NX
机制。另外,由于程序的加载基址为0x8000
,故.text
段地址的最高字节均为\x00
,而在调用strcpy()
时存在NULL
字符截断的问题,因此在进行漏洞利用时需要想办法绕过NULL
字符限制的问题。
$ checksec --file ./upnpd
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
SSD
公开的漏洞细节中给出了一个方案:通过stack reuse
的方式来绕过该限制。具体思路为,先通过socket
发送第一次数据,往栈上填充相应的rop payload
,同时保证不会造成程序崩溃;再通过socket
发送第二次数据用于覆盖栈上的返回地址,填充的返回地址用来实现stack pivot
,即劫持栈指针使其指向第一次发送的payload
处,然后再复用之前的payload
以完成漏洞利用。SSD
公开的漏洞细节中的示意图如下。
实际上,由于recvfrom()
函数与漏洞点strcpy()
之间的路径比较短,栈上的数据不会发生太大变化,利用stack reuse
的思路,只需发送一次数据即可完成利用,示意图如下。在调用ssdp_http_method_check()
前,接收的socket
数据包保存在upnp_main()
函数内的局部缓冲区上,而在ssdp_http_method_check()
内,当调用完strcpy()
后,会复制一部分数据到该函数内的局部缓冲区上。通过覆盖栈上的返回地址,可劫持栈指针,使其指向upnp_main()
函数内的局部缓冲区,复用填充的rop gadgets
,从而完成漏洞利用。
另外在调用strcpy()
后,在(4)
处还调用了函数sub_B60C()
。通过对应的汇编代码可知,在覆盖栈上的返回地址之前,也会覆盖R7
指向的栈空间内容,之后R7
作为参数传递给sub_B60C()
。而在sub_B60C()
中,会读取R0
指向的栈空间中的内容,然后再将其作为参数传递给strstr()
,这意味[R0]
中的值必须为一个有效的地址。因此在覆盖返回地址的同时,还需要用一个有效的地址来填充对应的栈偏移处,保证函数在返回前不会出现崩溃。由于libc
库对应的加载基址比较大,即其最高字节不为\x00
,因此任意选取该范围内的一个不包含\x00
的有效地址即可。
在解决了NULL
字符截断的问题之后,剩下的部分就是寻找rop gadgets
来完成漏洞利用了,相对比较简单。同样,SSD
公开的漏洞细节中也包含了完整的漏洞利用代码,其思路是通过调用strcpy gadget
拼接出待执行的命令,并将其写到某个bss
地址处,然后再调用system gadget
执行对应的命令。
在给出的漏洞利用代码中,strcpy gadget
执行的过程相对比较繁琐,经过分析后,在upnpd
程序中找到了另一个更优雅的strcpy gadget
,如下。借助该gadget
,可以直接在数据包中发送待执行的命令,而无需进行命令拼接。
.text:0000B764 MOV R0, R4 ; dest
.text:0000B768 MOV R1, SP ; src
.text:0000B76C BL strcpy
.text:0000B770 ADD SP, SP, #0x400
.text:0000B774 LDMFD SP!, {R4-R6,PC}
补丁分析
Netgear
官方在R8300-V1.0.2.134_1.0.99
版本中修复该漏洞。函数ssdp_http_method_check()
的相关伪代码如下,可以看到,在补丁中调用的是strncpy()
而非原来的strcpy()
,同时还对局部缓冲区&v40
进行了初始化。
signed int ssdp_http_method_check(const char *a1, int a2, int a3)
{
int v40; // [sp+24h] [bp-Ch]
v3 = a1;
// ...
memset(&v40, 0, 0x5DCu);
v52 = 32;
sub_B814(3, "%s(%d):\n", "ssdp_http_method_check", 203);
if ( dword_93AE0 == 1 )
return 0;
v51 = &v40;
strncpy((char *)&v40, v3, 0x5DBu); // patch
// ...
小结
本文通过搭建Netgear
R8300
型号设备的仿真环境,对其UPnP
服务中存在的缓冲区溢出漏洞进行了分析。漏洞本身比较简单,但漏洞利用却存在NULL
字符截断的问题,SSD
公开的漏洞细节中通过stack reuse
的方式实现了漏洞利用,思路值得借鉴和学习。