PSV-2020-0211:Netgear R8300 UPnP栈溢出漏洞分析
漏洞简介
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服务。
1 | 添加`--strace`选项, 方便查看错误信息, 便于环境修复 |
运行后提示如下错误,根据对应的目录结构,通过运行命令mkdir -p tmp/var/run解决。
1 | 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环境变量进行加载,命令如下。
1 | <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的变化如下。
1 | diff --git a/config.h b/config.h |
需要说明的是,libnvram还会尝试去定位固件中的全局符号router_defaults和Nvrams,并加载其中存在的键值对,对应的代码如下。其中,调用nvram_set_default_*的顺序为:nvram_set_default_builtin(),nvram_set_default_table(a)。也就是说,上面NVRAM_DEFAULTS中的键值对会先被加载,然后再加载全局符号router_defaults和Nvrams中存在的键值对。因此,在libnvram/config.h中添加的键值对有可能会被覆盖(比如lan_ipaddr),为了保证自定义的键值对生效,对libnvram/nvram.c的修改如下。
1 | int nvram_set_default(void) { |
1 | diff --git a/nvram.c b/nvram.c |
程序成功运行效果如下。
1 | <extracted squashfs-root>$ sudo chroot . ./qemu-arm-static -E LD_PRELOAD=./firmadyne/libnvram.so.armel ./usr/sbin/upnpd |
漏洞分析
在upnp_main()中,在(1)处recvfrom()用来读取来自socket的数据,并将其保存在v55指向的内存空间中。在(2)调用ssdp_http_method_check(),传入该函数的第一个参数为v55,即指向接收的socket数据。
1 | int upnp_main() |
在ssdp_http_method_check()中,在(3)处调用strcpy()进行数据拷贝,其中v40指向栈上的局部缓冲区,v3指向接收的socket数据。由于缺乏长度校验,当构造一个超长的数据包时,拷贝时会出现缓冲区溢出。
1 | signed int ssdp_http_method_check(const char *a1, int a2, int a3) |
漏洞利用
upnpd程序启用的缓解措施如下,可以看到仅启用了NX机制。另外,由于程序的加载基址为0x8000,故.text段地址的最高字节均为\x00,而在调用strcpy()时存在NULL字符截断的问题,因此在进行漏洞利用时需要想办法绕过NULL字符限制的问题。
1 | checksec --file ./upnpd |
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,可以直接在数据包中发送待执行的命令,而无需进行命令拼接。
1 | .text:0000B764 MOV R0, R4 ; dest |
补丁分析
Netgear 官方在R8300-V1.0.2.134_1.0.99版本中修复该漏洞。函数ssdp_http_method_check()的相关伪代码如下,可以看到,在补丁中调用的是strncpy()而非原来的strcpy(),同时还对局部缓冲区&v40进行了初始化。
1 | signed int ssdp_http_method_check(const char *a1, int a2, int a3) |
小结
本文通过搭建Netgear R8300型号设备的仿真环境,对其UPnP服务中存在的缓冲区溢出漏洞进行了分析。漏洞本身比较简单,但漏洞利用却存在NULL字符截断的问题,SSD公开的漏洞细节中通过stack reuse的方式实现了漏洞利用,思路值得借鉴和学习。
相关链接
- Security Advisory for Pre-Authentication Command Injection on R8300, PSV-2020-0211
- SSD Advisory – Netgear Nighthawk R8300 upnpd PreAuth RCE
- Netgear Nighthawk R8300 upnpd PreAuth RCE 分析与复现
- Firmadyne libnvram
本文首发于安全客,文章链接:https://www.anquanke.com/post/id/217606