"No grave but the SIP": Reversing a VoIP phone firmware

When conducting internal intrusion tests, one can find interesting to access the phones used by a client, as they are often connected to an internal network and can provide some kind of persistent access. This article presents the research done for getting a good grasp on the firmware of Yealink VoIP phones, which enables us to analyze further the underlying system. This research permitted to find several security issues in the firmware upgrade system.

During this diving session, we will encounter wild animals as a custom (semi) ciphered filesystem, some cryptographic oddities, and even a probable developer birth date! So, grab your reverser diving suit and your hexviewer fins, and let's start!

Context

The target of this analysis is Yealink T4 Series phones [1] , but most of the findings also apply to other models from this constructor.

Firmware exploration

As the firmware updates are available online, we tried to extract it using tools as binwalk. However, this quick approach did not yield interesting results, so here is a summary of the diving inside the upgrade file format!

Decrypting the upgrade file

The first step in this process was to identify interesting information in the upgrade file header. If we dump the first bytes in hexadecimal form, we can see what seems to be a 0x80 bytes header followed by some patterns.

00000000: ad24 ec0b 0000 0080 7731 6601 0300 0003  .$......w1f.....
00000010: 0000 0002 01bd 7eb0 ffff ffff 0000 0005  ......~.........
00000020: 0000 0042 5434 3653 0000 0000 0000 0000  ...BT46S........
00000030: 0000 0000 4e55 4c4c 0000 0000 0000 0000  ....NULL........
00000040: 0000 0000 3636 2e38 342e 302e 3135 0000  ....66.84.0.15..
00000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000060: 0000 0000 4143 4400 0000 0000 0000 0000  ....ACD.........
00000070: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000080: 2053 3e98 3b27 e128 7912 99a7 6339 0dfc   S>.;'.(y...c9..
00000090: 3552 84e2 a37f b05d dd2f 3314 016e 4fa9  5R.....]./3..nO.
000000a0: f7bc 74df 3b27 e1a8 8df9 9529 4a1a a7fb  ..t.;'.....)J...
000000b0: 3552 83e2 a383 f6bd af43 3ec4 a46f 802f  5R.......C>..o./
000000c0: f7bc 74e1 3b27 e1a8 fb9e 471c 6339 0dfb  ..t.;'....G.c9..
000000d0: 3552 83e2 a383 f6bd dd2b 5cf4 ca9f 6201  5R.......+\...b.
000000e0: f7bc 74e1 3b27 e1a8 fb9e 471c 6339 0dfb  ..t.;'....G.c9..
000000f0: 3552 83e2 a383 f6bd dd2b 5cf4 ca9f 6201  5R.......+\...b.
00000100: 8cf9 381b 3a26 e0a8 fb9e 471c 6339 0dfb  ..8.:&....G.c9..

Quick cryptanalysis

If we look at the data between offsets 0x80 and 0x100 bytes, we can see patterns every 32 bytes, which tends to indicate some sort of block cipher algorithm used in ECB mode. However, if we consider two similar blocks (at offsets 0x90 and 0xB0 for example), we can see that there is some difference between them, which tends to indicate a repeating key XOR pattern.

00000090: 3552 84e2 a37f b05d dd2f 3314 016e 4fa9  5R.....]./3..nO.
--diff--  0000 0100 0011 1110 1111 0110 1101 1111
000000b0: 3552 83e2 a383 f6bd af43 3ec4 a46f 802f  5R.......C>..o./

If we look at the distribution of 32 bits values inside the firmware, we can assess that the cryptographic algorithm is probably not very strong.

Failure: trivial xor decryption with cleartext attack

In order to avoid costly cryptanalysis or reverse engineering, it is worth a try to assume that the repeating pattern is the ciphertext of null bytes, and with this assertion guess a possible key.

This method yields a possible key, but trying to decrypt the full firmware with it only produced garbage.

The conclusion of this failure is that either:

  • The cleartext is not null bytes
  • The algorithm is not only based on xor

Understanding firmware format from an older cleartext firmware

Instead of doing a full cryptanalysis of the firmware, we downloaded firmware files for several models of phone from the same constructor, and we finally found an older one which was partially unencrypted.

This firmware contained both a secondary header and the firmware update installer in the form of an ARM ELF binary.

Reversing this binary, thanks to numerous debug strings, allowed us to reconstruct the firmware file format, which is provided in the yealink_rom.ksy file (using Kaitai Struct syntax [5] ).

Cryptographic algorithms

The analysis of the updater binary yielded not one, but two crypto algorithms!

Dubbed cypher3 and cypher4, these algorithms are based on simple XOR and ADD operations on hardcoded keys (256 bits for cypher3 and 512 bits for cypher4).

Cypher4 also contains a simple mixing step, which inverts some bytes position in the ciphertext.

Interestingly, cypher4 was not found to be used in any of the download firmware images, but the newer ones are using the 3DES and AES algorithms (which are not defined in this updater).

Besides these algorithms, the blocks of the firmware can also be compressed using LZMA.

Combining all the information

Having defined the headers in a useable format, and having the cryptographic primitives in hand, we wrote a Python script to decrypt and split the firmware images in separate blocks. This script can be found in the repository [2].

Decrypting the filesystem

Having separated the firmware upgrade file in several parts, we want to look at what's inside it! However, this was not so easy as tools like binwalk and hachoir indicated that there were files inside it, but were unable to extract them.

Maybe it's time for another decryption round!

Identifying the underlying filesystem

$binwalk app.bin
DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
10560         0x2940          ELF, 32-bit LSB shared object, ARM, version 1 (SYSV)
18555         0x487B          mcrypt 2.2 encrypted data, algorithm: blowfish-448, mode: CBC, keymode: 8bit
191190        0x2EAD6         Unix path: /home/shenjp/qt463-t52/lib
1001088       0xF4680         ELF, 32-bit LSB shared object, ARM, version 1 (SYSV)
1022208       0xF9900         ELF, 32-bit LSB shared object, ARM, version 1 (SYSV)
1106688       0x10E300        ELF, 32-bit LSB shared object, ARM, version 1 (SYSV)
1455794       0x1636B2        Unix path: /var/run/dbus/system_bus_socket
1481288       0x169A48        Unix path: /sys/fs/cgroup

Let's start by dumping the first 0x800 bytes:

00000000: f8bc 74e1 3a27 e1a8 048f 471c 6339 0dfb  ..t.:'....G.c9..
00000010: 3552 83e2 a383 f6bd dd2b 5cf4 ca9f 6201  5R.......+\...b.
00000020: f7bc 74e1 3b27 e1a8 fb9e 471c 6339 0dfb  ..t.;'....G.c9..
00000030: 3552 83e2 a383 f6bd dd2b 5cf4 ca9f 6201  5R.......+\...b.
00000040: f7bc 74e1 3b27 e1a8 fb9e 471c 6339 0dfb  ..t.;'....G.c9..
00000050: 3552 83e2 a383 f6bd dd2b 5cf4 ca9f 6201  5R.......+\...b.
00000060: f7bc 74e1 3b27 e1a8 fb9e 471c 6339 0dfb  ..t.;'....G.c9..
00000070: 3552 83e2 a383 f6bd dd2b 5cf4 ca9f 6201  5R.......+\...b.
00000080: f7bc 74e1 3b27 e1a8 fb9e 471c 6339 0dfb  ..t.;'....G.c9..
00000090: 3552 83e2 a383 f6bd dd2b 5cf4 ca9f 6201  5R.......+\...b.
000000a0: f7bc 74e1 3b27 e1a8 fb9e 471c 6339 0dfb  ..t.;'....G.c9..
000000b0: 3552 83e2 a383 f6bd dd2b 5cf4 ca9f 6201  5R.......+\...b.
000000c0: f7bc 74e1 3b27 e1a8 fb9e 471c 6339 0dfb  ..t.;'....G.c9..
000000d0: 3552 83e2 a383 f6bd dd2b 5cf4 ca9f 6201  5R.......+\...b.
000000e0: f7bc 74e1 3b27 e1a8 fb9e 471c 6339 0dfb  ..t.;'....G.c9..
000000f0: 3552 83e2 a383 f6bd dd2b 5cf4 ca9f 6201  5R.......+\...b.
00000100: f7bc 74e1 3b27 e1a8 fb8f 10b5 a3f8 0dfb  ..t.;'..........
00000110: 3552 83e2 a383 f6bd dd2b 5cf4 ca9f 6201  5R.......+\...b.
00000120: f7bc 74e1 98fc 42f3 048f 10b5 e0aa 364e  ..t...B.......6N
00000130: b803 0edb 060a 1568 f40a 8f8d f322 4f68  .......h....."Oh
00000140: 0c73 a582 98fc 42f3 048f 10b5 e0aa 364e  .s....B.......6N
00000150: b803 0edb 060a 1568 f40a 8f8d f322 4f68  .......h....."Oh
00000160: 0c73 a582 98fc 42f3 048f 10b5 e0aa 364e  .s....B.......6N
00000170: b803 0edb 060a 1568 f40a 8f8d f322 4f68  .......h....."Oh
00000180: 0c73 a582 98fc 42f3 048f 10b5 e0aa 364e  .s....B.......6N
00000190: b803 0edb 060a 1568 f40a 8f8d f322 4f68  .......h....."Oh
000001a0: 0c73 a582 98fc 42f3 048f 10b5 e0aa 364e  .s....B.......6N
000001b0: b803 0edb 060a 1568 f40a 8f8d f322 4f68  .......h....."Oh
000001c0: 0c73 a582 98fc 42f3 048f 10b5 6339 0dfb  .s....B.....c9..
000001d0: b803 0edb 060a 1568 f40a 8f8d f322 4f68  .......h....."Oh
000001e0: 0c73 a582 98fc 42f3 048f 10b5 e0aa 364e  .s....B.......6N
000001f0: b803 0edb 060a 1568 f40a 8f8d f322 4f68  .......h....."Oh
00000200: ffff ffff ffff ffff ffff ffff ffff ffff  ................
*
00000800: ffff 0010 00ff ff00 0107 e1a4 3dec a92d  ............=..-

We can retrieve patterns issued from the cypher3 algorithm in the first 512 bytes, however between 512 and 2048 the buffer is full of 0xFF. If we continue to examine it, we find some interesting values, for example an ELF executable header at offset 0x2940. The best hypothesis for this analysis is that the filesystem headers are encrypted, but the content of the files is in cleartext.

After decryption, we obtain the following header:

00000000: 0300 0000 0100 0000 ffff 0000 0000 0000  ................
00000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000060: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000070: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000080: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000090: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000100: 0000 0000 0000 0000 00ff ffff c041 0000  .............A..
00000110: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000120: 0000 0000 ffff ffff ffff ffff ffff ffff  ................
00000130: ffff ffff ffff ffff ffff ffff ffff ffff  ................
00000140: ffff ffff ffff ffff ffff ffff ffff ffff  ................
00000150: ffff ffff ffff ffff ffff ffff ffff ffff  ................
00000160: ffff ffff ffff ffff ffff ffff ffff ffff  ................
00000170: ffff ffff ffff ffff ffff ffff ffff ffff  ................
00000180: ffff ffff ffff ffff ffff ffff ffff ffff  ................
00000190: ffff ffff ffff ffff ffff ffff ffff ffff  ................
000001a0: ffff ffff ffff ffff ffff ffff ffff ffff  ................
000001b0: ffff ffff ffff ffff ffff ffff ffff ffff  ................
000001c0: ffff ffff ffff ffff ffff ffff 0000 0000  ................
000001d0: ffff ffff ffff ffff ffff ffff ffff ffff  ................
000001e0: ffff ffff ffff ffff ffff ffff ffff ffff  ................
000001f0: ffff ffff ffff ffff ffff ffff ffff ffff  ................
00000200: 0843 9562 44f8 22eb 046f c825 9c4a 360c  .C.bD."..o.%.J6.
00000210: 36f3 8c23 e484 0946 e2f4 6f0d cba0 af06  6..#...F..o.....

While this header may seem illegible, binwalk can help us identifying it as a YAFFS filesystem header.

$ binwalk header.bin
DECIMAL       HEXADECIMAL     DESCRIPTION
-------------------------------------------------------------
0             0x0             YAFFS filesystem, little endian

If we decrypt the whole file using the cypher3 algorithm, we now have what seems to be metadata of the filesystem, as illustrated by the strings contained inside.

$ strings decrypted_buffer.bin
libVP2008.so
/boot/lib/libDeviceLib-arm.so
libfreetype.so
/phone/lib/libfreetype.so.6
libQtNetwork.so.4
[...SNIP...]

The hypothesis stating that only the filesystem headers are encrypted seems validated, but let's confirm it by looking at the code!

After trying to dump directly the filesystem from the phone, it appeared that not only the update file but also the filesystem on flash memory is encrypted in a similar manner.

Next logical step was: if flash memory is encrypted, the kernel must decrypt it!

The kernel is available in the update file, but it is also ciphered with the cypher3 algorithm.

However, after decryption we directly obtain an u-boot image for a Linux-2.6.27.47 ARM system, so we are able to disassemble it.

After identifying the cypher3 algorithm and tracing cross references to the decoding function, we found the following interesting code:

YAFFS object header decryption

So, we now have confirmation from the code that the first 512 bytes of the YAFFS headers are indeed encrypted!

Reconstructing the filesystem

The next and final step was to reconstruct the filesystem using the decrypted headers.

This task is similar to the techniques used by Matt Boyer for his PyYaffs project [3] or techniques used for mobile phone forensics [4] .

The system is based on a YAFFS2 filesystem using 2048 bytes chunks and 64 bytes tags stored after each chunk. These tags seem to be packed using a different method than the standard one, as they contain the magic 0x19860815 (which might be the developer birth date).

An example of such tags, preceding a cleartext block containing an ELF binary, can be seen below:

00000000: ffff 0010 00ff ff00 040f 1166 91dd c074  ...........f...t
00000010: 0100 0000 0000 0000 080c 9110 52cc 7122  ............R.q"
00000020: 0000 1508 8619 0f00 000c 9110 52cc 7122  ............R.q"
00000030: 000d 0000 000d 0000 000c 9110 52cc 7122  ............R.q"
00000040: 7f45 4c46 0101 0100 0000 0000 0000 0000  .ELF............

We can now extract the filesystem, as we have all the information needed:

  • Filenames and file hierarchy is contained in the decrypted headers
  • File content is contained in cleartext blocks
  • Objects IDs, blocks IDs and sequence IDs are contained in the packed tags

Released tools

If you want to dive yourself in the firmware of these VoIP phones, we publish the following tools in our repository [2] :

  • Three KaitaiStruct models useable to analyze the upgrade files and Yealink's custom YAFFS filesystem and tags
  • The script yealink_crypto implementing the cryptographic primitives used in the firmware upgrade process
  • The script yealink_rom_dump.py used for dumping the upgrade files
  • The script yaffs_decrypt.py which is able to list or dump files from encrypted YAFFS filesystem

Firmware upgrade security issues

Now that the firmware is fully extracted, we found the following deficiencies in the firmware management system:

  • The firmware is not signed
  • It is possible to downgrade the firmware to a lower version
  • The custom encryption scheme of the upgrade file is very weak, and the new ones are all using the same shared AES key in ECB mode.
  • Even if complicated by the custom upgrade file format and encryption, it should be possible to craft a custom firmware to persistently implant the phone.

While these issues are common in embedded / IOT systems, they are quite problematic given the usage of this product as a communication platform.

Conclusion

While it was quite interesting to reverse engineer this particularly challenging upgrade system, we are now able to explore more deeply the system.

Stay tuned for more fishes in the SIP!

References