Patch diff an old vulnerability in Synology NAS
前言
之前在浏览群晖官方的安全公告时,翻到一个Critical
级别的历史漏洞Synology-SA-18:64。根据漏洞公告,该漏洞存在于群晖的DSM(DiskStation Manager)
中,允许远程的攻击者在受影响的设备上实现任意代码执行。对群晖NAS
设备有所了解的读者可能知道,默认条件下能用来在群晖NAS
上实现远程代码执行的漏洞很少,有公开信息的可能就是与Pwn2Own
比赛相关的几个。由于该漏洞公告中没有更多的信息,于是打算通过补丁比对的方式来定位和分析该公告中提及的漏洞。
环境准备
群晖环境的搭建可参考之前的文章《A Journey into Synology NAS 系列一: 群晖NAS介绍》,这里不再赘述。根据群晖的安全公告,以DSM 6.1
为例,DSM 6.1.7-15284-3
以下的版本均受该漏洞影响,由于手边有一个DSM 6.1.7
的虚拟机,故这里基于DSM
6.1.7-15284
版本进行分析。
补丁比对
首先对群晖的DSM
更新版本进行简单说明,方便后续进行补丁比对。以DSM 6.1.7
版本为例,根据其发行说明,存在1
个大版本6.1.7-15284
和3
个小版本6.1.7-15284 Update 1
、6.1.7-15284 Update 2
及6.1.7-15284 Update 3
。其中,大版本6.1.7-15284
对应初始版本,其镜像文件中包含完整的系统文件,而后续更新的小版本则只包含与更新相关的文件。另外,Update 2
版本中包含Update 1
中的更新,Update 3
中也包含Update 2
中的更新,也就是说最后1
个小版本Update 3
包含了全部的更新。
从群晖官方的镜像仓库中下载6.1.7-15284
、6.1.7-15284-2
和6.1.7-15284-3
这三个版本对应的pat
文件。在Update x
版本的pat
文件中除了包含与更新相关的模块外,还有一个描述文件DSM-Security.json
。比对6.1.7-15284-2
和6.1.7-15284-3
这2个版本的描述文件,如下。
可以看到,在6.1.7-15284 Update 3
中更新的模块为libfindhost
与netatalk-3.x
,与对应版本发行说明中的信息一致。
借助Bindiff
插件对版本6.1.7-15284
和6.1.7-15284 Update 3
中的libfindhost
模块进行比对,如下。可以看到,主要的差异在函数FHOSTPacketRead()
中。后面的其他函数很短,基本上就1~2
个block
,可忽略。
两个版本中函数FHOSTPacketRead()
内的主要差异如下,其中在6.1.7-15284 Update 3
中新增加了3
个block
。
对应的伪代码如下。可以看到,在6.1.7-15284 Update 3
中,主要增加了对变量v34
的额外校验,而该变量会用在后续的函数调用中。因此,猜测漏洞与v34
有关。
漏洞分析
libfindhost.so
主要是与findhostd
服务相关,用于在局域网内通过Synology Assistant
工具搜索、配置和管理对应的NAS
设备,关于findhostd
服务及协议格式可参考之前的文件《A Journey into Synology NAS 系列二: findhostd服务分析》。其中,发送数据包的开始部分为magic (\x12\x34\x56\x78\x53\x59\x4e\x4f)
,剩余部分由一系列的TLV
组成,TLV
分别对应pkt_id
、data_length
和data
。
另外,在libfindhost.so
中存在一大段与协议格式相关的数据grgfieldAttribs
,表明消息剩余部分的格式和含义。具体地,下图右侧中的每一行对应结构pkt_item
,其包含6
个字段。其中,pkt_id
字段表明对应数据的含义,如数据包类型、用户名、mac
地址等;offset
字段对应将数据放到内部缓冲区的起始偏移;max_length
字段则表示对应数据的最大长度。
实际上,
libfindhost.so
中的grgfieldAttribs
,每一个pkt_item
包含8
个字段;而在Synology Assistant
中,每一个pkt_item
包含6
个字段。不过,重点的字段应该是前几个,故这里暂且只关注前6
个字段。
findhostd
进程会监听9999/udp, 9998/udp, 9997/udp
等端口,其会调用FHOSTPacketRead()
来对接收的数据包进行初步校验和解析。以DSM 6.1.7-15284
版本为例, FHOSTPacketRead()
的部分代码如下。首先,在(1)
处会校验接收数据包的头部,校验通过的话程序流程会到达(2)
,在while
循环中依次对剩余部分的pkt_item
进行处理。在(2)
处会从数据包中读取对应的pkt_id
,之后在grgfieldAttribs
中通过二分法查找对应的pkt_item
,查找成功的话程序流程会到达(3)
。在(3)
处会读取对应pkt_item
中的pkt_index
字段,如果pkt_index=2
,程序流程会到达(4)
。如果v39 == pkt_id
,则会执行++v36
,否则在(5)
处会将pkt_id
赋值给v39
。之后,在(6)
处会根据pkt_index
的值调用相应的FHOSTPacketReadXXX()
。
1 | // in libfindhost.so |
地址off_7FFFF7DD7FC0
实际指向的内容如下。其中,函数FHOSTPacketReadString()
会使用传入的第7
个参数v36
。另外,FHOSTPacketReadArray()
内部直接调用FHOSTPacketReadString()
,因此这两个函数是等价的。
1 | LOAD:00007FFFF7DD7FC0 off_7FFFF7DD7FC0 dq offset grgfieldParsers |
函数FHOSTPacketReadString()
的部分代码如下。正常情况下,程序流程会到达(7)
处,读取数据包中对应data_length
字段,如果其值小于剩余数据包的总长度,程序流程会到达(8)
。如果(8)
处的条件成立,在(9)
处会调用snprintf()
将对应的data
拷贝到内部缓冲区的指定偏移处,其中snprintf()
的第1
个参数为(char *)(a4 + a7 * pkt_max_length)
,用到了传进来的v36/a7
参数。
1 | __int64 FHOSTPacketReadString(__int64 a1, _BYTE *a2, signed int remain_pkt_length, __int64 a4, unsigned __int64 pkt_max_length, __int64 a6, unsigned int a7) |
回到前面的(4)/(5)
处,可以发现,如果发送的数据包中包含多个对应pkt_index=0x2
的pkt_item
,如pkt_id=0xbc/0xbd/0xbe/0xbf
,则可以触发多次++v36
。由于缺乏对v36
的适当校验,通过发送伪造的数据包,可造成后续在调用FHOSTPacketReadString()
出现越界写。进一步地,在(6)
处传递的v38
与FHOSTPacketRead()
函数的第4
个参数有关,而在findhostd
程序中调用FHOSTPacketRead()
时第4
个参数为指向栈上的缓冲区,因此,利用该越界写操作可覆盖栈上的返回地址,从而劫持程序的控制流。
DSM 6.1.7-15284
版本中的findhostd
文件似乎经过混淆了,无法直接采用IDA Pro
等工具进行分析,可以在gdb
中dump
出findhostd
进程,然后对其进行分析。另外,在较新的版本如VirtualDSM 6.2.4-25556
中,对应的findhostd
文件未被混淆,可直接分析。
1 | // in findhostd |
另外,由于Synology Assistant
客户端对协议数据包的处理过程与findhostd
类似,因此其早期的版本也会受该漏洞影响。
漏洞利用
查看findhostd
启用的缓解机制,如下,同时设备上的ASLR
等级为2
。其中,显示"NX disabled"
,不知道是否和程序被混淆过有关。在设备上查看进程的内存地址空间映射,确实看到[stack]
部分为rwxp
。考虑到通用性,这里还是采用ret2libc
的思路来获取设备的root shell
。
1 | checksec.exe --file ./findhostd |
由于越界写发生在调用snprintf()
时,故存在'\x00'
截断的问题。通过调试发现,利用越界写覆盖栈上的返回地址后,在返回地址的不远处存在发送的原始数据包内容,因此可借助stack pivot
将栈劫持到指向可控内容的地方,从而继续进行rop
。
在实际进行利用的过程中,本来是想将cmd
直接放在数据包中发送,然后定位到其在栈上的地址,再将其保存到rdi
寄存器中,但由于未找到合适的gadgets
,故采用将cmd
写入findhostd
进程的某个固定地址处的方式替代。同时,发现区域0x00411000-0x00610000
不可写(正常应该包含.bss
区域?),而.got.plt
区域可写,故将cmd
写到了该区域。
1 | root@NAS_6_1:/# cat /proc/`pidof findhostd`/maps |
最终效果如下。
One More Thing
获取到设备的root shell
后,相当于获取了设备的控制权,比如可以查看用户共享文件夹中的文件等。但是如何登录设备的Web
管理界面呢?这里给出一种简单的方案:利用synouser
和synogroup
命令增加1
个管理员用户,然后使用新增的用户进行登录即可。当然,synouser
命令支持直接更改现有用户的密码,且无需原密码,但改了之后正常用户就不知道其密码了 :(
1 | 增加一个用户名为cq, 密码为cq674350529的用户 |
小结
本文基于群晖DSM 6.1.7-15284
版本,通过补丁比对的方式对群晖安全公告Synology-SA-18:64
中提及的漏洞进行了定位和分析。该漏洞与findhostd
服务相关,由于在处理接收的数据包时缺乏适当的校验,通过发送伪造的数据包,可触发out-of-bounds write
,利用该操作可覆盖栈上的返回地址,从而劫持程序控制流,达到任意代码执行的目的。通常情况下,findhostd
服务监听的端口不会直接暴露到外网,故该漏洞应该是在局域网内才能触发。
相关链接
- Synology Security Advisory: Synology-SA-18:64 DSM
- 群晖镜像仓库
本文首发于Seebug Paper,文章链接:https://paper.seebug.org/2038/