A Journey into Synology NAS — Part 2: Analyzing findhostd Service
Preface
In the previous article, we mainly introduced the Synology NAS, as well as the methods used for setting up a Synology NAS environment. In the following articles, we will focus on analyzing some services on the Synology NAS from the perspective of a local area network. Specifically, we will mainly analyze the findhost
d service, including its communication mechanism, protocol format, and some security issues discovered during this process.
Service Detection
Assuming we are in the same local network as the NAS device, and since the device is network reachable, we can try to detect the ports and services that are open on the device. For simplicity, netstat
is used to check the open ports and services, as shown below.
As can be seen, in addition to some common services such as smbd
, nginx
, minissdpd
, and snmpd
, there are also some custom services such as synovncrelayd
, iscsi_snapshot_comm_core
, synosnmpd
, and findhostd
. Compared to common services, these custom services may be less tested and more vulnerable. Hence, we will focus on analyzing these custom services, including findhostd
and iscsi_snapshot_comm_core
.
findhostd Service Analysis
The findhostd
service is mainly responsible for communicating with Synology Assistant, which is used to search, configure, and manage NAS devices within the local area network. With the Synology Assistant, We can install the DSM system, configure administrator account/password and device's network, mapping network drives, and so on.
By analyzing the packets between the Synology Assistant and findhostd
, it turned out that they mainly communicate through the 9999/udp
port (9998/udp
, 9997/udp
). A simple communication process is as follows. Specifically, Synology Assistant first sends a broadcast query
packet, and then findhostd
responds with both a broadcast packet and a unicast packet. After discovering the corresponding device, Synology Assistant can further send other broadcast packets such as quickconf
, memory test
, and findhostd
will respond with a broadcast packet and a unicast packet.
As shown in the above picture, they communicated with each other through the 9999/udp
port. And the data seemed to be transmitted in plain text, which included its MAC address, serial number, model and so on.
Protocol Format Analysis
In order to understand the specific protocol format, it’s necessary to reverse engineer and debug findhostd
(or Synology Assistant client). It turns out that the message starts with a magic
(\x12\x34\x56\x78\x53\x59\x4e\x4f
), and there is a large amount of data grgfieldAttribs
related to the protocol format, indicating the format and meaning of the remaining parts of the message. Specifically, each row in the right part of the figure corresponds to the data_chunk
structure, which contains six fields. Among them, the pkt_id
field indicates the meaning of the corresponding data, such as packet type, username, mac address. The offset
field corresponds to the start offset of data in the internal buffer, and the max_length
field represents the maximum length of the corresponding data.
According to the above information, we can disassemble the packets into the following format. Specifically, the beginning of the message is the magic
(\x12\x34\x56\x78\x53\x59\x4e\x4f
) field, and the rest of the message consists of a series of TLV
s, where each TLV
corresponds to pkt_id
, data_length
, and data
, respectively.
Further, to facilitate the analysis of packet format, a Wireshark plugin called syno_finder was built to parse the data packet. An example of the plugin’s output in Wireshark is shown in the following figure.
It should be noted that in newer versions of Synology Assistant and DSM, packet encryption mechanism is introduced, for the packet may contain sensitive information. Correspondingly, there are two magic
values, one for plain text messages and the other for encrypted messages. Additionally, several new pkt_id
values have been introduced for passing encryption/decryption related parameters.
// 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
Protocol Fuzzing
After having a basic understanding of the protocol format, fuzzing is used to test the robustness of the protocol parsing code. Specifically, we choose Kitty and Scapy frameworks to quickly build a black-box fuzzer. As you know, Scapy is a powerful interactive packet manipulation program that can be used to define protocol format easily and quickly, as shown below.
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 is an open-source, modular and easily extensible fuzzing framework inspired by Sulley and Peach Fuzzer. Based on the defined protocol format, a black-box fuzzer can be quickly built using the Kitty framework. In addition, due to the communication mechanism between findhostd
and Synology Assistant, we can fuzz them simultaneously.
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()
What’s more, a simple Synology Assistant client can also be implemented by re-using the defined protocol format.
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()
# ...
Security Issues
Password Leakage
As mentioned earlier, the pkt_id
field indicates the meaning of the corresponding data, such as packet type, username, MAC address, and so on. If pkt_id
equals to 0x1
, its value represents the type of the entire data packet. And some common packet types are as follows. Among them, netsetting
, quickconf
, and memory test
data packets are more appealing, for they may contain encrypted administrator password, whose pkt_id
is 0x2a
.
Taking the quickconf
packet as an example, as shown in the above figure, the value of pkt_id=0x1
is 0x4
, and the value of pkt_id=0x2a
is BnvPxUcU5P1nE01UG07BTUen1XPPKPZX
. Through reverse engineering, it turns out that function MatrixDecode()
is used to decrypt the encrypted password. Therefore, it is trivial to obtain the plain text password of the administrator.
~/DSM_DS3617xs_15284/hda1$ sudo chroot . ./call_export_func -d BnvPxUcU5P1nE01UG07BTUen1XPPKPZX
MatrixDecode(BnvPxUcU5P1nE01UG07BTUen1XPPKPZX) result: HITB2021AMS
Since the Synology Assistant and findhostd
communicate via broadcast, and the data is transmitted in plain text, by monitoring these broadcast packets, in some cases, an adversary on the local network can easily obtain the plain text password of the administrator.
Password Stealing
During fuzzing, I noticed that my already configured device became to “Not configured”. Did some crafted packets reset my DiskStation? After investigating it, it turned out that some crafted packets deceived the Synology Assitant. My DiskStation was ok, but the Synology Assitant though it was not configured.
Normally in this case, you may choose to re-configure the device via Synology Assistant, and set the username and password used before. Again, since the messages are sent via broadcast in plain text, password leakage comes back. In summary, an adversary can cheat the administrator into re-configuring the device, then steal the plain text administrator password by monitoring the broadcast traffic.
In addition, even if both Synology Assistant and DSM
versions support communication encryption, due to backward compatibility, it's still possible to do password stealing.
Null Byte Off-by-One
During fuzzing, it was discovered that some contents displayed in Synology Assistant were strange, like the “Server name” field shown below. In addition to those predefined items such as “%n”, “%x”, “%p”, there were some additional tail contents, such as “00:11:32:8Fxxx”, corresponding to the MAC address. Normally, the MAC address content should not be displayed in the “Server name” field.
After analyzing the DSAssistant.exe
(version 6.1-15030
), function sub_1272E10()
was found to be responsible for processing string
type data, and would copy them from the received packet into the corresponding internal buffer. As mentioned earlier, for each pkt_id
item, there is a corresponding offset
field and max_length
field. When the corresponding data length is exactly equal to max_length
, an additional '\x00'
is appended to the end of the buffer at (1)
. In our case, this '\x00'
is actually written to the start of the adjacent buffer, causing a null byte off-by-one
issue.
size_t 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); // copy data into the corresponding internal buffer
*(_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
As a result, for some pkt_id
values that are adjacent in the internal buffer (such as 0x5b
and 0x5c
), by constructing a special data packet, the '\x00'
at the end of the previous item can be overwritten by the next item, which may lead to leakage of the content in the adjacent buffer.
pkt_id offset max_len
0000005a 00000000 00000aa8 00000080 00000000 00000000
0000005b 00000000 00000b28 00000080 00000000 00000000 <===
0000005c 00000000 00000ba8 00000004 00000000 00000000
Summary
In this article, we first analyze the findhostd
service on Synology NAS from the perspective of a local area network, including the communication mechanism between Synology Assistant and findhostd
, the syno_finder
protocol format, and protocol fuzzing. Finally, we share some security issues found during the analysis.