前言
上一篇文章主要对群晖NAS
进行了简单介绍,并给出了搭建群晖NAS
环境的方法。在前面的基础上,本篇文章将从局域网的视角出发,对群晖NAS
设备上开放的部分服务进行分析。由于篇幅原因,本文将重点对findhostd
服务进行分析,介绍对应的通信机制和协议格式,并分享在其中发现的部分安全问题。
服务探测
由于NAS
设备是网络可达的,假设我们与其处于同一个局域网中,首先对设备上开放的端口和服务进行探测。简单起见,这里直接通过netstat
命令进行查看,如下。
可以看到,除了一些常见的服务如smbd
、nginx
、minissdpd
和snmpd
等,还有一些自定义的服务如synovncrelayd
、iscsi_snapshot_comm_core
、synosnmpd
和findhostd
等。与常见服务相比,这些自定义的服务可能less tested and more vulnerable
,因此这里主要对这些自定义服务进行分析,包括findhostd
和iscsi_snapshot_comm_core
。
findhostd服务分析
findhostd
服务主要负责和Synology Assistant
进行通信,而Synology Assistant
则用于在局域网内搜索、配置和管理对应的DiskStation
,比如安装DSM
系统、设置管理员账号/密码、设置设备获取IP
地址的方式,以及映射网络硬盘等。
通过抓包分析可知,Synology Assistant
和findhostd
之间主要通过9999/udp
端口(9998/udp
、9997/udp
)进行通信,一个简单的通信流程如下。具体地,Synology Assistant
首先发送一个广播query
数据包,之后findhostd
会同时发送一个广播包和单播包作为响应。在发现对应的设备后,Synology Assistant
可以进一步发送其他广播包如quickconf
、memory test
等,同样findhostd
会发送一个广播包和单播包作为响应。
抓取的部分数据包如上图右侧所示。可以看到,两者之间通过9999/udp
端口进行通信,且数据似乎以明文方式进行传输,其中包括mac
地址、序列号和型号等信息。
协议格式分析
为了了解具体的协议格式,需要对findhostd
(或Synology Assistant
客户端)进行逆向分析和调试。经过分析可知,消息开头部分是magic
(\x12\x34\x56\x78\x53\x59\x4e\x4f
),然后存在一大段与协议格式相关的数据grgfieldAttribs
,表明消息剩余部分的格式和含义。具体地,下图右侧中的每一行对应结构data_chunk
,其包含6个字段。其中,pkt_id
字段表明对应数据的含义,如数据包类型、用户名、mac
地址等;offset
字段对应将数据放到内部缓冲区的起始偏移;max_length
字段则表示对应数据的最大长度。
根据上述信息,可以将数据包按下图格式进行解析。具体地,消息开头部分为magic
(\x12\x34\x56\x78\x53\x59\x4e\x4f
),后面的部分由一系列的TLV
组成,TLV
分别对应pkt_id
、data_length
和data
。
进一步地,为了更方便地对数据包格式进行分析,编写了一个wireshark
协议解析插件syno_finder,便于在wireshark
中直接对数据包进行解析,效果如下图所示。
需要说明的是,在较新版本的Synology Assistant
和DSM
中,增加了对数据包加密的支持(因为其中可能会包含敏感信息)。对应地,存在两个magic
,分别用于标识明文消息和密文消息。同时,引入了几个新的pkt_id
,用于传递与加解密相关的参数。
// magic
#define magic_plain “\x12\x34\x56\x78\x53\x59\x4e\x4f”
#define magic_encrypted “\x12\x34\x55\x66\x53\x59\x4e\x4f” // introduced recently
// new added
000000c3 00000001 00002f48 00000004 00000000 00000000 # support_onsite_tool
000000c4 00000000 00002f4c 00000041 00000000 00000000 # public key
000000c5 00000001 00002f90 00000004 00000000 00000000 # randombytes
000000c6 00000001 00002f94 00000004 00000000 00000000
协议fuzzing
在了解了协议的格式之后,为了测试协议解析代码的健壮性,很自然地会想到采用fuzz
的方式。这里采用Kitty
和Scapy
框架,来快速构建一个基于生成的黑盒fuzzer
。Scapy
是一个强大的交互式数据包处理程序,借助它可以方便快速地定义对应的协议格式,示例如下。
class IDPacket(Packet):
fields_desc = [
XByteField('id', 0x01),
FieldLenField('length', None, length_of='value', fmt='B', adjust=lambda pkt,x:x),
StrLenField('value', '\x01\x00\x00\x00', length_from=lambda x:x.length)
]
# ...
def post_build(self, pkt, pay):
if pkt[1] != 4 and pkt[1] != 0xff:
packet_max_len = self._get_item_max_len(pkt[0])
if len(pkt[2:]) >= packet_max_len:
if packet_max_len == 0:
pkt = bytes([pkt[0], 0])
else:
pkt = bytes([pkt[0], packet_max_len-1])+ pkt[2:2+packet_max_len]
return pkt + pay
class FindHostPacket(Packet):
fields_desc = [
StrLenField('magic_plain', '\x12\x34\x56\x78\x53\x59\x4e\x4f'),
PacketListField('id_packets', [], IDPacket)
]
Kitty是一个开源、模块化且易于扩展的fuzz
框架,灵感来自于Sulley
和Peach Fuzzer
。基于前面定义的协议格式,借助Kitty
框架,可以快速地构建一个基于生成的黑盒fuzzer
。另外,考虑到findhostd
和Synology Assistant
之间的通信机制,可以同时对两端进行fuzz
。
host = '<broadcast>'
port = 9999
RANDSEED = 0x11223344
packet_id_a4 = qh_nas_protocols.IDPacket(id=0xa4, value='\x00\x00\x02\x01')
# ...
packet_id_2a = qh_nas_protocols.IDPacket(id=0x2a, value=RandBin(size=240))
# ...
pakcet_id_rand1 = qh_nas_protocols.IDPacket(id=RandByte(), value=RandBin(size=0xff))
pakcet_id_rand2 = qh_nas_protocols.IDPacket(id=RandChoice(*qh_nas_protocols.PACKET_IDS), value=RandBin(size=0xff))
findhost_packet = qh_nas_protocols.FindHostPacket(id_packets=[packet_id_a4, packet_id_2a, ..., packet_id_rand1, packet_id_rand2])
findhost_template = Template(name='template_1', fields=[ScapyField(findhost_packet, name='scapy_1', seed=RANDSEED, fuzz_count=100000)])
model = GraphModel()
model.connect(findhost_template)
target = UdpTarget(name='qh_nas', host=host, port=port, timeout=2)
fuzzer = ServerFuzzer()
fuzzer.set_interface(WebInterface(host='0.0.0.0', port=26001))
fuzzer.set_model(model)
fuzzer.set_target(target)
fuzzer.start()
此外,基于前面定义好的协议格式,也可以实现一个简易的Synology Assistant
客户端。
class DSAssistantClient:
# ...
def add_pkt_field(self, pkt_id, value):
self.pkt_fields.append(qh_nas_protocols.IDPacket(id=pkt_id, value=value))
def clear_pkt_fields(self):
self.pkt_fields = []
def find_target_nas(self):
self.clear_pkt_fields()
self.add_pkt_field(0xa4, '\x00\x00\x02\x01')
self.add_pkt_field(0xa6, '\x78\x00\x00\x00')
self.add_pkt_field(0x01, p32(0x1)) # packet type
# ...
self.add_pkt_field(0xb9, '\x00\x00\x00\x00\x00\x00\x00\x00')
self.add_pkt_field(0x7c, '00:50:56:c0:00:08')
self.build_send_packet()
def quick_conf(self):
self.clear_pkt_fields()
self.add_pkt_field(0xa4, '\x00\x00\x02\x01')
self.add_pkt_field(0xa6, '\x78\x00\x00\x00')
self.add_pkt_field(0x01, p32(0x4)) # packet type
self.add_pkt_field(0x20, p32(0x1)) # packet subtype
self.add_pkt_field(0x19, '00:11:32:8f:64:3b')
self.add_pkt_field(0x2a, 'BnvPxUcU5P1nE01UG07BTUen1XPPKPZX')
self.add_pkt_field(0x21, 'NAS_NEW')
# ...
self.add_pkt_field(0xb9, "\x00\x00\x00\x00\x00\x00\x00\x00")
# ...
self.add_pkt_field(0x7c, "00:50:56:c0:00:08")
self.build_send_packet()
# ...
if __name__ == "__main__":
ds_assistant = DSAssistantClient("ds_assistant")
ds_assistant.find_target_nas()
# ...
安全问题
前面提到,pkt_id
字段表明对应数据的含义,如数据包类型、用户名、mac
地址等。其中,pkt_id
为0x1
时对应的值表示整个数据包的类型,常见的数据包类型如下。其中,netsetting
、quickconf
和memory test
数据包中包含加密后的管理员密码信息,对应的pkt_id
为0x2a
。
以quickconf
数据包为例,如上图所示。可以看到,pkt_id
为0x1
时对应的值为0x4
,同时pkt_id
为0x2a
时对应的内容为BnvPxUcU5P1nE01UG07BTUen1XPPKPZX
。通过逆向分析可知,函数MatrixDecode()
用于对加密后的密码进行解密。因此,可以很容易地获取到管理员的明文密码。
~/DSM_DS3617xs_15284/hda1$ sudo chroot . ./call_export_func -d BnvPxUcU5P1nE01UG07BTUen1XPPKPZX
MatrixDecode(BnvPxUcU5P1nE01UG07BTUen1XPPKPZX) result: HITB2021AMS
由于Synology Assistant
和findhostd
之间以广播的方式进行通信,且数据包以明文形式进行传输,在某些情形下,通过监听广播数据包,局域网内的用户可以很容易地获取到管理员的明文密码。
在对findhostd
进行fuzz
的过程中,注意到Synology Assistant
中显示的DiskStation
状态变为了"Not configured"
。难道是某些畸形数据包对DiskStation
进行了重置?经过分析后发现,是由于某些数据包欺骗了Synology Assistant
:DiskStation
是正常的,而Synology Assistant
却认为其处于未配置状态。
通常情况下,管理员会选择通过Synology Assistant
对设备进行重新配置,并设置之前用过的用户名和密码。此时,由于Synology Assistant
和findhostd
之间以广播的方式进行通信,且数据包以明文形式进行传输,故密码泄露问题又出现了。因此,在某些情形下,通过发送特定的广播数据包,局域网内的用户可以欺骗管理员对DiskStation
进行”重新配置”,通过监听局域网内的广播数据包,从而窃取管理员的明文密码。另外,即使Synology Assistant
和DSM
版本都支持通信加密,由于向下兼容性,这种方式针对最新的版本仍然适用。
这个问题同样也和Synology Assistant
有关。在fuzz
的过程中,发现Synology Assistant
中显示的一些内容比较奇怪。其中,"%n"
、"%x"
和"%p"
等是针对string
类型预置的一些fuzz
元素。注意到,在"Server name"
中显示的内容除了"%n"
之外,尾部还有一些额外的内容如"00:11:32:8Fxxx"
,这些多余的内容对应的是"MAC address"
。正常情况下,"MAC address"
对应的内容不会显示到"Server name"
中。
通过对6.1-15030
版本的DSAssistant.exe
进行分析和调试,函数sub_1272E10()
负责对string
类型的数据进行处理,将其从接收的数据包中拷贝到对应的内部缓冲区。前面提到过,针对每个pkt_id
项,都有一个对应的offset
字段和max_length
字段。当对应数据长度的大小正好为max_length
时,额外的'\x00'
在(1)
处被追加到缓冲区末尾,而此时该'\x00'
其实是写入了邻近缓冲区的起始处,从而造成null byte off-by-one
。
size_t __cdecl sub_1272E10(int a1, _BYTE *a2, int a3, int a4, size_t a5, int a6, int a7)
{
// ...
v7 = (unsigned __int8)*a2;
if ( (int)v7 > a3 - 1 )
return 0;
if ( !*a2 )
return 1;
if ( a5 < v7 )
return 0;
snprintf((char *)(a4 + a7 * a5), v7, "%s", a2 + 1); // 将string类型的数据拷贝到内部缓冲区的指定偏移处
*(_BYTE *)(v7 + a4) = 0; // (1) null byte off-by-one
return v7 + 1;
}
The
_snprintf()
function formats and stores count or fewer characters and values (including a terminating null character that is always appended unless count is zero or the formatted string length is greater than or equal to count characters) in buffer. // WindowsThe functions
snprintf()
andvsnprintf()
write at most size bytes (including the terminating null byte (‘\0’)) to str. // Linux
因此,对于某些在内部缓冲区中处于邻近的pkt_id
(如0x5b
和0x5c
),通过构造特殊的数据包,可以使得前一项内容末尾的'\x00'
被下一项内容覆盖,从而可能会泄露邻近缓冲区中的内容。
pkt_id offset max_len
0000005a 00000000 00000aa8 00000080 00000000 00000000
0000005b 00000000 00000b28 00000080 00000000 00000000 <===
0000005c 00000000 00000ba8 00000004 00000000 00000000
小结
本文从局域网的视角出发,对群晖NAS
设备上的findhostd
服务进行了分析,包括Synology Assistant
与findhostd
之间的通信机制、syno_finder
协议格式的解析、协议fuzzing
等。最后,分享了在其中发现的部分问题。
发表评论
您还未登录,请先登录。
登录