One of the most common hurdles we come up against when analyzing firmware images is encryption. While there are some resources out there on generic approaches to decrypting firmware images, today we'll do a short walkthrough on how we extracted an encryption key for a subset of D-Link routers - in particular the
D-Link DIR-X1560. This device is part of the same router generation as
D-Link DIR-X5460, which was featured in a recent Wi-Fi router security check conducted jointly by Chip - a popular German technology magazine - and IoT Inspector.
D-Link Router Firmware Encryption
SHRS
SHRS
00000000 53 48 52 53 01 13 91 5D 01 13 91 60 67 C6 69 73 SHRS‘]‘`gÆis
00000000 53 48 52 53 01 13 91 5D 01 13 91 60 67 C6 69 73 SHRS‘]‘`gÆis
00000000 53 48 52 53 01 13 91 5D 01 13 91 60 67 C6 69 73 SHRS‘]‘`gÆis
0xricksanchezvery nice writeupDIR-3060, which we recently published an advisory forimgdecrypt
imgdecrypt
00000000 65 6e 63 72 70 74 65 64 5f 69 6d 67 02 0a 00 14 |encrpted_img....|
00000000 65 6e 63 72 70 74 65 64 5f 69 6d 67 02 0a 00 14 |encrpted_img....|
00000000 65 6e 63 72 70 74 65 64 5f 69 6d 67 02 0a 00 14 |encrpted_img....|
encrpted_img
encrpted_img
SHRS
SHRS
encrpted_img
encrpted_img
encrpted_img
encrpted_img
D-Link tends to encrypt the firmware images for its routers, with a custom firmware update file format. Many D-Link routers in the DIR range use a firmware update file format with the header:
This firmware format and encryption scheme has already been publicly documented. A researcher named published a documenting finding the key for SHRS firmware images (including the ). They extracted the encryption key and IV from the binary, which they gain access to by a UART shell on a model in a similar series.
However, we recently came across a router in the DIR-X range, the firmware for which has a slightly different header:
The header is just the string , then a 32-bit big-endian field containing the size of the image.
The keys for the firmware images don’t work for these images. So, we needed to find a different way to extract the decryption keys from one of the devices that uses this firmware format.
Obviously, we are unable to extract the encryption key from the encrypted firmware image because it’s encrypted. So, we need to find another way get our hands on the key.
If we are able to execute code on the device somehow, then it’s simple enough. Given sufficient local privileges, we can access everything that’s on the device while it’s running. This is the firmware image after it's been decrypted.
If the device manufacturer has only recently introduced firmware encryption, then it may be possible to track down an older firmware image that hasn't been encrypted yet (most likely the firmware version immediately before encryption is introduced) and check if the key can be extracted from there.
Another technique we can resort to is to directly read the device’s physical flash memory. On flash, the firmware is very unlikely to be encrypted. We would take one of the devices apart, de-solder the flash memory, dump this, and read out the filesystem. However, this is quite destructive (plus wasteful and expensive!).
We won’t dig too deep into the options here, as many others have gone into great depth on this issue. One of the more comprehensive writeups on this matter is a post . Check this out if you’re interested in strategies you might consider when trying to figure out firmware encryption.
Shellanigans
busybox tar
busybox tar
nc
nc
nc –nvlp [PORT] > filesystem.tar.gz
nc –nvlp [PORT] > filesystem.tar.gz
nc –nvlp [PORT] > filesystem.tar.gz
tar -cvz /bin/ /data/ /etc/ /etc_ro/ /lib/ /libexec/ /mnt/ /opt/ /sbin/ /usr/ /var/ /webs/ | nc [IP ADDRESS] [PORT]
tar -cvz /bin/ /data/ /etc/ /etc_ro/ /lib/ /libexec/ /mnt/ /opt/ /sbin/ /usr/ /var/ /webs/ | nc [IP ADDRESS] [PORT]
tar -cvz /bin/ /data/ /etc/ /etc_ro/ /lib/ /libexec/ /mnt/ /opt/ /sbin/ /usr/ /var/ /webs/ | nc [IP ADDRESS] [PORT]
In our case, it was relatively easy to get shell access to a DIR-X1560 via the physical UART debug interface. Once we had an interactive shell on the DIR- X1560, we could easily dump the whole filesystem. One very easy way to do this with the in-built and commands. First set up a listener on your own box:
Then run the following on the device, just passing all the root folders that you want to exfiltrate:
You’ll end up with a nice tar.gz’d image of whatever parts of the filesystem you want, sent over the network.
Finding the Decryption Routine
imgdecrypt
imgdecrypt
Binary file bin/fota matches
Binary file bin/httpd matches
Binary file bin/prog.cgi matches
$ grep -r encrpted_img
Binary file bin/fota matches
Binary file bin/httpd matches
Binary file bin/prog.cgi matches
$ grep -r encrpted_img
Binary file bin/fota matches
Binary file bin/httpd matches
Binary file bin/prog.cgi matches
prog.cgi
prog.cgi
fota
fota
prog.cgi
prog.cgi
encrpted_img
encrpted_img

FUN00033144
FUN00033144

gj_decode()
gj_decode()

Unlike in the “SHRS” firmware images, there’s no obvious binary to focus on. Therefore, we can start following the trail from the firmware upload process and see if we can track down exactly where the decryption takes place.
Luckily, the firmware header string can be used as a nice unique “egg” to hunt for in the system:
Here we find and - two binaries which probably handle these encrypted firmware images in some way or another.
In , we can quite easily track down the firmware upload routine, based on where we find the string:
By following the variable, which is a pointer to the encrypted portion of the firmware image, we see that this function is called, with the pointer as its first argument:
Within this function, one very likely candidate for a firmware decryption function emerges: .
Jumping into the Library
gj_decode()
gj_decode()
libcmd_util.so
libcmd_util.so
aes_set_key
aes_set_key
aes_cbc_decrypt
aes_cbc_decrypt
libcmd_util.so
libcmd_util.so
ndaes_set_key()
aes_set_key()
ndaes_cbc_decrypt
aes_cbc_decrypt

key_loc
key_loc
key_loc + 4
key_loc + 4
00031ba3
00031ba3
key_loc+4
key_loc+4
00031bc3
00031bc3
00031bc5
00031bc5
key_loc
key_loc
00031bd5
00031bd5
00031ba3
00031ba3

00031bc5
00031bc5
00031ba3
00031ba3
00031bc5
00031bc5
is defined in . It’s a really small function, but critically it calls two functions with encryption-related names: and . These are also defined within the same library, but we don’t necessarily need to get too deep into them. The function names give us enough information to go off – we're very likely looking at AES encryption in CBC mode.
We can also quite quickly see that the 2 argument passed to is probably the AES key, and the 2 argument passed to is probably the IV.
The decompilation here is a bit messy, but it’s relatively straightforward to see what’s going on, if you don’t obsess over the details too much. There are two loops, which copy data from global variables to local buffers. These loops iterate over the bytes at these global addresses until they reach some defined end address.
The local pointers to these buffers are at and . In the first loop, the buffer at is copied to until it reaches . In the second, the buffer at is copied to the address at , until it reached the byte at .
Indeed, at , we can see that there is an array of bytes:
A very similar pattern can be seen at .
As such, we can probably guess that the AES key itself is at , and the IV is at .
We’re not publishing the actual keys here today. But those who pay attention and follow along would likely be very capable of extracting these keys themselves. It is, as they say, left as an exercise for the interested reader.
We can test the hypothesis quickly and easily by using . I tend to open this up most times I want to quickly workshop anything cryptography-related. Yes, it’s written by the UK’s GCHQ. But if you’re the kind of person who is suspicious of things written by civil servants, it’s written in pure JavaScript, and can be run in whatever browser you want, on whatever air gapped box you want 😊.
Just copy/pasting the first 0x1000 or so bytes from an encryption firmware image as ASCII HEX into the CyberChef Input field and using the “AES Decrypt” operation in CyberChef with our extracted key and IV, we get a very promising result.
We can see the top of an UBI erase block here. Which means we’re well on the way to fully decrypting the entire firmware update package.
Once we know that the key and IV work, it’s relatively quick and easy to write up a full decryption script. In this case, there were another couple of small alignment hurdles to overcome before we had a fully-coherent image we could unpack – but these were easy enough to solve.
In many cases, Firmware encryption in embedded devices is not a massively difficult issue to solve. Once you have the physical device to hand, the process can be hugely simplified. It’s worth remembering that firmware encryption for embedded devices is implemented mainly in firmware update packages, and encryption is only rarely implemented at the storage level (as opposed to state-of-the-art practices for mobile phones and notebooks, where full-disk (or at least the-important-part-of-the-disk) encryption is common practice).
This kind of setup is likely to change in the future, at least partially. For instance, Android has been moving towards and, since 7.0, . Since Android 10, file-based encryption is required. It should be noted, however, that the term “full-disk” encryption is misleading in Android – it’s only the partition that is encrypted.