A Journey into Synology NAS — Part 2: Analyzing findhostd Service

cq674350529
8 min readMar 7, 2023

--

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 findhostd 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 TLVs, 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. // Windows

The functions snprintf() and vsnprintf() 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.

References

--

--