Backstory

During a red teaming exercise conducted for one of our customers, we abused weak passwords to obtain an administrative access to some Zyxel ZyWALL Unified Security Gateway (USG) appliances that were used as both firewalls and VPN concentrators in their branch offices. These appliances are targeted at small and medium businesses and are somewhat popular, at least according to Shodan.

Thanks to our administrative access, we were able to dump the configurations and we noticed that a series of passwords stored on these devices were encrypted in some way. When we did a Google search we could not find any public information on the Internet about how these passwords were stored. Based on our observations, they had to be encrypted with a reversible algorithm because they were passwords that the device itself used, such as the PSKs of VPNs.

Since we had some spare budget, we decided to buy a similar device on eBay and spend some time auditing it on our own. Our next articles will cover the results of our analysis carried out on the physical device. In the meantime, let’s focus on the work we did while we were waiting for the device to arrive.

In this first article of our Zyxel audit series we will cover firmware extraction and password decryption against Zyxel ZyWALL Unified Security Gateway (USG) appliances.

Firmware extraction

First of all, we downloaded from the official website the same firmware images (version 4.10 and 4.70) for the USG310 device that were deployed by our customer.

We started working on version 4.10 of the firmware: the file 410AAPJ2C0.bin is the firmware image in ZIP format. Unfortunately, the ZIP archive was password protected. Searching online we were unable to find any information on the passwords used by Zyxel.

In the PDF documents distributed with the firmware, however, a recovery method is described in the event that the upgrade procedure is unsuccessful.

A lower level procedure is also available in case the previously described method does not work:
https://kb.zyxel.com/KB/searchArticle!gwsViewDetail.action?articleOid=006845&lang=EN

Based on the information in the low level recovery guide we can assume that the “.ri” file is directly executed by the system to install the fundamental components in order to then be able to flash the firmware from the “.bin” file.

We extracted “410AAPJ2C0.ri” with binwalk:

[email protected]:w$ binwalk -e 410AAPJ2C0.ri

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
512           0x200           uImage header, header size: 64 bytes, header CRC: 0xF9EC0106, created: 2014-12-06 03:27:53, image size: 4948010 bytes, Data Address: 0x5000000, Entry Point: 0x80101400, data CRC: 0xD43F46E, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: lzma, image name: "Linux Kernel Image"
576           0x240           LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 16814648 bytes

[email protected]:w$

And the files inside it:

[email protected]:w/_410AAPJ2C0.ri.extracted$ binwalk -e 240

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             ELF, 64-bit MSB MIPS32 rel2 executable, MIPS, version 1 (SYSV)
5095576       0x4DC098        Linux kernel version 2.6.32
11385704      0xADBB68        gzip compressed data, maximum compression, from Unix, last modified: 2014-12-06 01:57:21
11487616      0xAF4980        DES SP2, big endian
11488128      0xAF4B80        DES SP1, big endian
11511376      0xAFA650        CRC32 polynomial table, little endian
11910064      0xB5BBB0        Unix path: /usr/bin/magic-seed
11919441      0xB5E051        Unix path: /sys/module/perf_counters/parameters/counter{0,1}
11919495      0xB5E087        Unix path: /sys/module/perf_counters/parameters/l2counter{0-3}
11948723      0xB652B3        PARity archive data - file number 16717
12987688      0xC62D28        Unix path: /home/sdd1/source/410/formal/p2/usg310/src/kernel/arch/mips/include/asm/bugs.h
13224040      0xC9C868        Neighborly text, "NeighborSolicits"
13224064      0xC9C880        Neighborly text, "NeighborAdvertisementsorts"
13227391      0xC9D57F        Neighborly text, "neighbor %.2x%.2x.%.2x:%.2x:%.2x:%.2x:%.2x:%.2x lost on port %d(%s)(%s)"
13231704      0xC9E658        Flattened device tree, size: 10827 bytes, version: 17
13242536      0xCA10A8        Flattened device tree, size: 11728 bytes, version: 17
13622896      0xCFDE70        Unix path: /usr/local/zld_udev/sbin/uevent_helper.sh
14045184      0xD65000        gzip compressed data, maximum compression, from Unix, last modified: 2014-12-06
15042374      0xE58746        Zip archive data, at least v2.0 to extract, compressed size: 226984, uncompressed size: 229081, name: etc_writable/zyxel/secuextender/SecuExtender_x64.cab
15269496      0xE8FE78        Zip archive data, at least v2.0 to extract, compressed size: 610493, uncompressed size: 610989, name: etc_writable/zyxel/secuextender/ssltun.jar
15880177      0xF24FF1        Zip archive data, at least v2.0 to extract, compressed size: 493928, uncompressed size: 516542, name: etc_writable/zyxel/secuextender/sslapp.jar

[email protected]:w/_410AAPJ2C0.ri.extracted$

And again:

[email protected]:w/_410AAPJ2C0.ri.extracted/_240.extracted$ binwalk -e D65000

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             ASCII cpio archive (SVR4 with no CRC), file name: ".", file name length: "0x00000002", file size: "0x00000000"
112           0x70            ASCII cpio archive (SVR4 with no CRC), file name: "zyinit", file name length: "0x00000007", file size: "0x00000000"
232           0xE8            ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/zyinit", file name length: "0x0000000E", file size: "0x00040DEC"
266064        0x40F50         ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/etc_inittab", file name length: "0x00000013", file size: "0x00000BB8"
269196        0x41B8C         ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/e2fsck", file name length: "0x0000000E", file size: "0x0006B64C"
709204        0xAD254         ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/zld_mrd.ko", file name length: "0x00000012", file size: "0x00001788"
715356        0xAEA5C         ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/rw.zip", file name length: "0x0000000E", file size: "0x002106C8"
2879904       0x2BF1A0        ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/mke2fs", file name length: "0x0000000E", file size: "0x0004239C"
3151288       0x3015B8        ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/switchdev.ko", file name length: "0x00000014", file size: "0x0000AF10"
3196236       0x30C54C        ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/zld_fsextract", file name length: "0x00000015", file size: "0x00017098"
3290728       0x323668        ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/sw_cn60xx.ko", file name length: "0x00000014", file size: "0x00005418"
3312388       0x328B04        ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/zld_udev", file name length: "0x00000010", file size: "0x0001347C"
3391488       0x33C000        ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/db.zip", file name length: "0x0000000E", file size: "0x00001428"
3396772       0x33D4A4        ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/zyinit_gpl", file name length: "0x00000012", file size: "0x00055128"
3745356       0x39264C        ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/fwversion", file name length: "0x00000011", file size: "0x0000014A"
3745816       0x392818        ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/switchdev_char.ko", file name length: "0x00000019", file size: "0x00005800"
3768480       0x3980A0        ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/unzip", file name length: "0x0000000D", file size: "0x0002B8B0"
3946956       0x3C39CC        ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/lkm.lst", file name length: "0x0000000F", file size: "0x00000050"
3947164       0x3C3A9C        ASCII cpio archive (SVR4 with no CRC), file name: "zyinit/platform_support.ko", file name length: "0x0000001B", file size: "0x00004CC8"
3966960       0x3C87F0        ASCII cpio archive (SVR4 with no CRC), file name: "init", file name length: "0x00000005", file size: "0x0000000D"
3967092       0x3C8874        ASCII cpio archive (SVR4 with no CRC), file name: "TRAILER!!!", file name length: "0x0000000B", file size: "0x00000000"

[email protected]:w/_410AAPJ2C0.ri.extracted/_240.extracted$

Looking at the extracted files, it is possible to identify the “zyinit” binary, which contains some firmware related strings.

By analyzing it, it is possible to see that it launches other external commands, in particular the “zld_fsextract” command:

Searching online for these executable binaries, only an interesting URL was identified (https://www.dslreports.com/forum/remark,26961186), which gave us some information about the ZIP password:

Searching in the “zld_fsextract” binary for the password, it is possible to identify some good starting points for the analysis, like this one:

These options are used by the “unzip” binary to unzip a file with a specific password which is defined in the parameter “-P”. Based on information found online and on a quick analysis, it seems that the binary calculates the unzip password in some way based on the binary name or the binary content.

The fastest way to extract the files is to emulate the MIPS processor and execute the binary.

Launching a Linux/MIPS virtual machine:

> wget https://people.debian.org/~aurel32/qemu/mips/vmlinux-3.2.0-4-5kc-malta
> wget https://people.debian.org/~aurel32/qemu/mips/debian_wheezy_mips_standard.qcow2
> qemu-system-mips64.exe -M malta -kernel vmlinux-3.2.0-4-5kc-malta -hda debian_wheezy_mips_standard.qcow2 -append "root=/dev/sda1 console=tty0" -net nic -net user,hostfwd=tcp:127.0.0.1:2222-:22

Getting firmware image information using the “zld_fsextract” binary:

[email protected]:~# ./zld_fsextract 410AAPJ2C0.bin -s list
name                :kernel
scope               :-f kernelusg310.bin -f kernelchecksum -D /
nc_scope            :-f kernelusg310.bin
version             :2.6.32
build_date          :2014-12-06 11:27:35
checksum            :6e4e1ad212be0a8a3ce89484f3c5dc1e
core_checksum       :a028057f7c742ea52bd3d0408f38a673

name                :code
scope               :-f bmusg310.bin -f bmchecksum -f kernelusg310.bin -f kernelchecksum -d wtp_image -d db -i -D /rw
scope               :-d db/etc/zyxel/ftp/conf -D /
nc_scope            :-f fwversion -f filechecksum -f wtpinfo
version             :4.10(AAPJ.2)
build_date          :2014-12-09 08:51:18
checksum            :899be95dac4a5bdcfd2f694035f16746
core_checksum       :e25ab639d4ff432bb7ffdc8c1bd39be3

name                :WTP_wtp_image/400AAS4C0.bin
scope               :-f wtp_image/400AAS4C0.bin -D /db
nc_scope            :
version             :4.00(###.4)
build_date          :2013-08-31 20:36:43
checksum            :07773b3deb39a1dd9c2036201097529c
core_checksum       :07773b3deb39a1dd9c2036201097529c

name                :WTP_wtp_image/400AADG4C0.bin
scope               :-f wtp_image/400AADG4C0.bin -D /db
nc_scope            :
version             :V4.00(###.4)
build_date          :2013-08-31 20:35:40
checksum            :aabb0606887cec28a07b8598e699f027
core_checksum       :aabb0606887cec28a07b8598e699f027

[email protected]:~#

And using the zld_fsextract binary to extract the firmware without the need to specify a password:

[email protected]:~# ./zld_fsextract 410AAPJ2C0.bin ./unzip -s extract -e code
...
[email protected]:~#

[email protected]:~# ls -al /rw/
total 57192
drwxr-xr-x  3 root root     4096 Nov 23 12:56 .
drwxr-xr-x 24 root root     4096 Nov 23 12:56 ..
-r--r--r--  1 root root 58511360 Dec  6  2014 compress.img
drwxr-xr-x  5 root root     4096 Dec  9  2014 etc_writable
-rw-r--r--  1 root root      139 Dec  9  2014 filechecksum
-rw-r--r--  1 root root    21415 Dec  9  2014 filelist
-rw-r--r--  1 root root      326 Dec  9  2014 fwversion
-rw-r--r--  1 root root     1671 Dec  9  2014 wtpinfo
[email protected]:~#

Then uncompressing the new image:

[email protected]:w$ binwalk -e compress.img

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------

0             0x0             Squashfs filesystem, little endian, version 4.0, compression:gzip, size: 58509745 bytes, 4958 inodes, blocksize: 131072 bytes, created: 2014-12-06 03:26:20

[email protected]:w$


After seeing that the process worked correctly with a fully emulated environment, we decided to try the single binary emulation provided by qemu. By using “strace” we can quickly see how the “unzip” binary was launched to find the ZIP password.

strace -f -s 199 qemu-mipsn32-static ./zld_fsextract 410AAPJ2C0.bin ./unzip -s extract -e code

Here is the output.

The binary that calculates the ZIP password is statically compiled, therefore it is not so easy to understand how that password is generated, and above all we are lazy…

Password encryption algorithm

Now that we have access to the filesystem, let’s start looking for information on the algorithm used to store passwords in the configuration files. In the configurations available to us, all passwords used the $4$ format and were preceded by the following keywords:

  • encrypted-key
  • encrypted-keystring
  • encrypted-password
  • encrypted-presharekey
  • password-encrypted
  • wpa-psk-encrypted

First step, identify which executables deal with these types of keywords:

[email protected]:w$ grep "encrypted-" _compress.img.extracted/squashfs-root/bin/*
Binary file _compress.img.extracted/squashfs-root/bin/zysh matches
Binary file _compress.img.extracted/squashfs-root/bin/zyshd matches
[email protected]:w$

Looking in the code of “zyshd” for the string “$4$” which identifies the encrypted password:

Looking inside the schrodinger routine. This part generates the salt:

Encryption is performed by the following code:

And then the BASE64 encoding at the end:

Looking inside the “RIJ_cbc_encrypt” function, it is possible to identify the routine “RIJ_decrypt” which leads us to the AES algorithm. This is also confirmed by the name of the function (RIJ = Rijndael) and by the 0xC0 used on the set key, which is 192. AES is one of the few encryption algorithms that support 192-bit keys.

Looking inside the encryption routine, it is possible to identify AES parameters like Td0 memory; it is also possible to use the FindCrypt Ghidra plugin that helps identify the encryption routines in use.

We can assume that the encryption/decryption function works as follows:

RIJ_cbc_encrypt(destination, source, len, unknown, IV, encryption/decryption byte)

At this point we only need to identify the parameters passed to the function.

The IV is:

The encryption key is:

So:

aes_key = "001200054A1F23FB1F060A14CD0D018F5AC0001306F0121C"
aes_iv = "0006001C01F01FC0FFFFFFFFFFFFFFFF"

The same process can be followed on the firmware version 4.70 to identify the $5$ encryption scheme.

To summarize, in the case of a password hash starting with $4$ the algorithm is as follows:

iv = static_iv
key = static_key
salt = generate_8_bytes_from_random()
to_encrypt = salt + password (the password is repeated to reach the 80 bytes)
AES_SET_KEY(aes,key,192)
AES_DECRYPT(aes,cyphertext,to_encrypt,key,iv)
final_encrypted = base64(cyphertext)
final_string = "$4$" + salt + "$" + final_encrypted "$"

In the case of a password hash starting with $5$ the algorithm is as follows:

iv = static_iv
key = static_key

salt = generate_8_bytes_from_random()
to_encrypt = salt + password (the password is repeated to reach the 80 bytes)
AES_SET_KEY(aes,key,192)
AES_DECRYPT(aes,cyphertext,to_encrypt,key,iv)

salt = generate_8_bytes_from_random()
to_encrypt = salt + password (the password is repeated to reach the 80 bytes)
AES_SET_KEY(aes,key,192)
AES_DECRYPT(aes,cyphertext,to_encrypt,key,iv)
step1 = base64(cyphertext)

salt1 = generate_8_bytes_from_random()
to_encrypt = salt1 + step1
AES_SET_KEY(aes,key,192)
AES_DECRYPT(aes,cyphertext,to_encrypt,key,iv)
final_encrypted = base64(cyphertext)

final_string = "$5$" + salt1 + "$" + salt2 + "$" + final_encrypted "$"

We developed a decryption tool that can be downloaded from: https://github.com/inode-/zyxel_password_decrypter

Another possible way to reverse these algorithms is to use dynamic analysis, but it seems QEMU does not fully support the SoC used by Zyxel and generates some illegal instruction fault on most executables. There is a potentially working QEMU patch at https://github.com/amir-mehmood/QEMU-Octeon-MIPS64, but we did not test it.

Finally, when we received the physical device we found out that the /usr/sbin/zencrypt binary present in the filesystem can be used to decrypt these passwords as well:

[email protected]:~# zencrypt -k 'E8AzAwOyYBwVHJJjtUTDqdAxLDPIr/ncDNV5HCfZFRJlqhYkSqbGvX9BP06YyBg5fGp2HQrwtHsp9mQLNAtBeEIPejrUv5jw/ZRmUx20nuwCNK0UZdeklaFWc945oR3zWVupR2i/KakLhb9d46q72geNfFnXggbJdsl7ObjGKUNLfRF6tMnvxGtPO54PJKpL8E9Af4LAqStAAEPu3S70og8h9PzUER9/Qq246kyTfjONyyVF49EX1l+vg6rD0y+5UnHamBedqvzr/e7az469oViXUIxfMnr/CpMgR5hgzKn3efqldNVSEqmS07UN6CkjHiWaQgnDTxhc4tjbZQjaT3CGVpghF7W3GzmYtMqQlYQ$'
pluto


Stay tuned for our upcoming articles in this series, along with some juicy vulnerability disclosures!