A-Journey-into-Synology-NAS-系列二——findhostd服务分析 | xxxA-Journey-into-Synology-NAS-系列二——findhostd服务分析 – xxx
菜单

A-Journey-into-Synology-NAS-系列二——findhostd服务分析

八月 30, 2021 - 安全客

robots

A-Journey-into-Synology-NAS-系列二——findhostd服务分析

 

前言

上一篇文章主要对群晖NAS进行了简单介绍,并给出了搭建群晖NAS环境的方法。在前面的基础上,本篇文章将从局域网的视角出发,对群晖NAS设备上开放的部分服务进行分析。由于篇幅原因,本文将重点对findhostd服务进行分析,介绍对应的通信机制和协议格式,并分享在其中发现的部分安全问题。

 

服务探测

由于NAS设备是网络可达的,假设我们与其处于同一个局域网中,首先对设备上开放的端口和服务进行探测。简单起见,这里直接通过netstat命令进行查看,如下。

A-Journey-into-Synology-NAS-系列二——findhostd服务分析

可以看到,除了一些常见的服务如smbdnginxminissdpdsnmpd等,还有一些自定义的服务如synovncrelaydiscsi_snapshot_comm_coresynosnmpdfindhostd等。与常见服务相比,这些自定义的服务可能less tested and more vulnerable,因此这里主要对这些自定义服务进行分析,包括findhostdiscsi_snapshot_comm_core

 

findhostd服务分析

findhostd服务主要负责和Synology Assistant进行通信,而Synology Assistant则用于在局域网内搜索、配置和管理对应的DiskStation,比如安装DSM系统、设置管理员账号/密码、设置设备获取IP地址的方式,以及映射网络硬盘等。

通过抓包分析可知,Synology Assistantfindhostd之间主要通过9999/udp端口(9998/udp9997/udp)进行通信,一个简单的通信流程如下。具体地,Synology Assistant首先发送一个广播query数据包,之后findhostd会同时发送一个广播包和单播包作为响应。在发现对应的设备后,Synology Assistant可以进一步发送其他广播包如quickconfmemory test等,同样findhostd会发送一个广播包和单播包作为响应。

A-Journey-into-Synology-NAS-系列二——findhostd服务分析

抓取的部分数据包如上图右侧所示。可以看到,两者之间通过9999/udp端口进行通信,且数据似乎以明文方式进行传输,其中包括mac地址、序列号和型号等信息。

协议格式分析

为了了解具体的协议格式,需要对findhostd(或Synology Assistant客户端)进行逆向分析和调试。经过分析可知,消息开头部分是magic (x12x34x56x78x53x59x4ex4f),然后存在一大段与协议格式相关的数据grgfieldAttribs,表明消息剩余部分的格式和含义。具体地,下图右侧中的每一行对应结构data_chunk,其包含6个字段。其中,pkt_id字段表明对应数据的含义,如数据包类型、用户名、mac地址等;offset字段对应将数据放到内部缓冲区的起始偏移;max_length字段则表示对应数据的最大长度。

A-Journey-into-Synology-NAS-系列二——findhostd服务分析

根据上述信息,可以将数据包按下图格式进行解析。具体地,消息开头部分为magic (x12x34x56x78x53x59x4ex4f),后面的部分由一系列的TLV组成,TLV分别对应pkt_iddata_lengthdata

A-Journey-into-Synology-NAS-系列二——findhostd服务分析

进一步地,为了更方便地对数据包格式进行分析,编写了一个wireshark协议解析插件syno_finder,便于在wireshark中直接对数据包进行解析,效果如下图所示。

A-Journey-into-Synology-NAS-系列二——findhostd服务分析

需要说明的是,在较新版本的Synology AssistantDSM中,增加了对数据包加密的支持(因为其中可能会包含敏感信息)。对应地,存在两个magic,分别用于标识明文消息和密文消息。同时,引入了几个新的pkt_id,用于传递与加解密相关的参数。

// magic  #define magic_plain “x12x34x56x78x53x59x4ex4f”  #define magic_encrypted “x12x34x55x66x53x59x4ex4f” // 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的方式。这里采用KittyScapy框架,来快速构建一个基于生成的黑盒fuzzerScapy是一个强大的交互式数据包处理程序,借助它可以方便快速地定义对应的协议格式,示例如下。

class IDPacket(Packet):      fields_desc = [          XByteField('id', 0x01),          FieldLenField('length', None, length_of='value', fmt='B', adjust=lambda pkt,x:x),          StrLenField('value', 'x01x00x00x00', 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', 'x12x34x56x78x53x59x4ex4f'),          PacketListField('id_packets', [], IDPacket)      ]  

Kitty是一个开源、模块化且易于扩展的fuzz框架,灵感来自于SulleyPeach Fuzzer。基于前面定义的协议格式,借助Kitty框架,可以快速地构建一个基于生成的黑盒fuzzer。另外,考虑到findhostdSynology Assistant之间的通信机制,可以同时对两端进行fuzz

host = '<broadcast>'  port = 9999  RANDSEED = 0x11223344    packet_id_a4 = qh_nas_protocols.IDPacket(id=0xa4, value='x00x00x02x01')  # ...  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, 'x00x00x02x01')          self.add_pkt_field(0xa6, 'x78x00x00x00')          self.add_pkt_field(0x01, p32(0x1))    # packet type          # ...          self.add_pkt_field(0xb9, 'x00x00x00x00x00x00x00x00')          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, 'x00x00x02x01')          self.add_pkt_field(0xa6, 'x78x00x00x00')          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, "x00x00x00x00x00x00x00x00")          # ...          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_id0x1时对应的值表示整个数据包的类型,常见的数据包类型如下。其中,netsettingquickconfmemory test数据包中包含加密后的管理员密码信息,对应的pkt_id0x2a

A-Journey-into-Synology-NAS-系列二——findhostd服务分析

A-Journey-into-Synology-NAS-系列二——findhostd服务分析

quickconf数据包为例,如上图所示。可以看到,pkt_id0x1时对应的值为0x4,同时pkt_id0x2a时对应的内容为BnvPxUcU5P1nE01UG07BTUen1XPPKPZX。通过逆向分析可知,函数MatrixDecode()用于对加密后的密码进行解密。因此,可以很容易地获取到管理员的明文密码。

~/DSM_DS3617xs_15284/hda1$ sudo chroot . ./call_export_func -d BnvPxUcU5P1nE01UG07BTUen1XPPKPZX  MatrixDecode(BnvPxUcU5P1nE01UG07BTUen1XPPKPZX) result: HITB2021AMS  

由于Synology Assistantfindhostd之间以广播的方式进行通信,且数据包以明文形式进行传输,在某些情形下,通过监听广播数据包,局域网内的用户可以很容易地获取到管理员的明文密码。

密码窃取

在对findhostd进行fuzz的过程中,注意到Synology Assistant中显示的DiskStation状态变为了"Not configured"。难道是某些畸形数据包对DiskStation进行了重置?经过分析后发现,是由于某些数据包欺骗了Synology AssistantDiskStation是正常的,而Synology Assistant却认为其处于未配置状态。

A-Journey-into-Synology-NAS-系列二——findhostd服务分析

通常情况下,管理员会选择通过Synology Assistant对设备进行重新配置,并设置之前用过的用户名和密码。此时,由于Synology Assistantfindhostd之间以广播的方式进行通信,且数据包以明文形式进行传输,故密码泄露问题又出现了。因此,在某些情形下,通过发送特定的广播数据包,局域网内的用户可以欺骗管理员对DiskStation进行”重新配置”,通过监听局域网内的广播数据包,从而窃取管理员的明文密码。另外,即使Synology AssistantDSM版本都支持通信加密,由于向下兼容性,这种方式针对最新的版本仍然适用。

null byte off-by-one

这个问题同样也和Synology Assistant有关。在fuzz的过程中,发现Synology Assistant中显示的一些内容比较奇怪。其中,"%n""%x""%p"等是针对string类型预置的一些fuzz元素。注意到,在"Server name"中显示的内容除了"%n"之外,尾部还有一些额外的内容如"00:11:32:8Fxxx",这些多余的内容对应的是"MAC address"。正常情况下,"MAC address"对应的内容不会显示到"Server name"中。

A-Journey-into-Synology-NAS-系列二——findhostd服务分析

通过对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. // Windows

The functions snprintf() and vsnprintf() write at most size bytes (including the terminating null byte (‘’)) to str. // Linux

因此,对于某些在内部缓冲区中处于邻近的pkt_id(如0x5b0x5c),通过构造特殊的数据包,可以使得前一项内容末尾的'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 Assistantfindhostd之间的通信机制、syno_finder协议格式的解析、协议fuzzing等。最后,分享了在其中发现的部分问题。

 

相关链接


Notice: Undefined variable: canUpdate in /var/www/html/wordpress/wp-content/plugins/wp-autopost-pro/wp-autopost-function.php on line 51