Breaking the D-Link DIR3060 Firmware Encryption - Static analysis of the decryption routine - Part 2.2

Welcome back to part 2.2 of this series! If you have not yet checked out part 1 or part 2.1 please do so first as they highlight important reconnaissance steps as well as the first half of the disassembly analysis!

ToC:


Let’s recall the current functionality we’ve encountered based on the developed source code:

#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <openssl/aes.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

static RSA *grsa_struct = NULL;
static unsigned char iv[] = {0x98, 0xC9, 0xD8, 0xF0, 0x13, 0x3D, 0x06, 0x95,
                             0xE2, 0xA7, 0x09, 0xC8, 0xB6, 0x96, 0x82, 0xD4};
static unsigned char aes_in[] = {0xC8, 0xD3, 0x2F, 0x40, 0x9C, 0xAC,
                                 0xB3, 0x47, 0xC8, 0xD2, 0x6F, 0xDC,
                                 0xB9, 0x09, 0x0B, 0x3C};
static unsigned char aes_key[] = {0x35, 0x87, 0x90, 0x03, 0x45, 0x19,
                                  0xF8, 0xC8, 0x23, 0x5D, 0xB6, 0x49,
                                  0x28, 0x39, 0xA7, 0x3F};

unsigned char out[] = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
                       0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46};

int check_cert(char *pem, void *n) {
  OPENSSL_add_all_algorithms_noconf();

  FILE *pem_fd = fopen(pem, "r");
  if (pem_fd != NULL) {
    RSA *lrsa_struct[2];
    *lrsa_struct = RSA_new();
    if (!PEM_read_RSAPublicKey(pem_fd, lrsa_struct, NULL, n)) {
      RSA_free(*lrsa_struct);
      puts("Read RSA private key failed, maybe the password is incorrect.");
    } else {
      grsa_struct = *lrsa_struct;
    }
    fclose(pem_fd);
  }
  if (grsa_struct != NULL) {
    return 0;
  } else {
    return -1;
  }
}

int aes_cbc_encrypt(size_t length, unsigned char *key) {
  AES_KEY dec_key;
  AES_set_decrypt_key(aes_key, sizeof(aes_key) * 8, &dec_key);
  AES_cbc_encrypt(aes_in, key, length, &dec_key, iv, AES_DECRYPT);
  return 0;
}

int call_aes_cbc_encrypt(unsigned char *key) {
  aes_cbc_encrypt(0x10, key);
  return 0;
}

int actual_decryption(char *sourceFile, char *tmpDecPath, unsigned char *key) {
  int ret_val = -1;
  size_t st_blocks = -1;
  struct stat statStruct;
  int fd = -1;
  int fd2 = -1;
  void *ROM = 0;
  int *RWMEM;
  off_t seek_off;
  unsigned char buf_68[68];
  int st;

  memset(&buf_68, 0, 0x40);
  memset(&statStruct, 0, 0x90);
  st = stat(sourceFile, &statStruct);
  if (st == 0) {
    fd = open(sourceFile, O_RDONLY);
    st_blocks = statStruct.st_blocks;
    if (((-1 < fd) &&
         (ROM = mmap(0, statStruct.st_blocks, 1, MAP_SHARED, fd, 0),
          ROM != 0)) &&
        (fd2 = open(tmpDecPath, O_RDWR | O_NOCTTY, 0x180), -1 < fd2)) {
      seek_off = lseek(fd2, statStruct.st_blocks - 1, 0);
      if (seek_off == statStruct.st_blocks - 1) {
        write(fd2, 0, 1);
        close(fd2);
        fd2 = open(tmpDecPath, O_RDWR | O_NOCTTY, 0x180);
        RWMEM = mmap(0, statStruct.st_blocks, PROT_EXEC | PROT_WRITE,
                     MAP_SHARED, fd2, 0);
        if (RWMEM != NULL) {
          ret_val = 0;
        }
      }
    }
  }
  puts("EOF part 2.1!\n");
  return ret_val;
}

int decrypt_firmware(int argc, char **argv) {
  int ret;
  unsigned char key[] = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
                         0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46};
  char *ppem = "/tmp/public.pem";
  int loopCtr = 0;
  if (argc < 2) {
    printf("%s <sourceFile>\r\n", argv[0]);
    ret = -1;
  } else {
    if (2 < argc) {
      ppem = (char *)argv[2];
    }
    int cc = check_cert(ppem, (void *)0);
    if (cc == 0) {
      call_aes_cbc_encrypt((unsigned char *)&key);

      printf("key: ");
      while (loopCtr < 0x10) {
        printf("%02X", *(key + loopCtr) & 0xff);
        loopCtr += 1;
      }
      puts("\r");
      ret = actual_decryption((char *)argv[1], "/tmp/.firmware.orig",
                              (unsigned char *)&key);

      if (ret == 0) {
        unlink(argv[1]);
        rename("/tmp/.firmware.orig", argv[1]);
      }
      RSA_free(grsa_struct);
    } else {
      ret = -1;
    }
  }
  return ret;
}

int encrypt_firmware(int argc, char **argv) { return 0; }

int main(int argc, char **argv) {
  int ret;
  char *str_f = strstr(*argv, "decrypt");

  if (str_f != NULL) {
    ret = decrypt_firmware(argc, argv);

  } else {
    ret = encrypt_firmware(argc, argv);
  }

  return ret;
}

We left of right after the setup stuff:

The last thing we analyzed was re-mapping the file located at /tmp/.firmware.orig back into memory with some minor adjustments to file permissions. Again if the mapping fails we’ll take a non wanted path so we will ignore that in the analysis again. So in case of success the still mapped sourceFile is being prepared as a function argument to a function called check_magic :

First decryption related code after all the file prep work earlier

check_magic:

check_magic is pretty straightforward. It takes a mapped memory (at least read-only permissions) segment as an argument and calls int memcmp(mapped_sourceFile, "SHRS", 4) to check whether the first 4 byte in the binary correspond to " SHRS " (0x53485253).


According to the return value of memcmp , which is stored in $v0 , sltiu $v0, 1 sets the return value of this function as follows:

  • If $v0 == 0 (== full match) → $v0 = 1
  • Else $v0 = 0

The disassembly graph earlier shows that if the return value of check_magic is != 0 the branch to magic_succ is taken ( bnez $v0, magic_succ ). This little function serves as a sanity check and first confirmation that an encrypted image starts with a valid header. We already saw this string earlier in the hex dump in part 1. So this part at least clears up the confusion about this seemingly non-random string. It was indeed non-random after all! The snippet below again confirms the presence of " SHRS" in an encrypted image file.

> hd DIR_882_FW120B06.BIN -n 16
00000000  53 48 52 53 00 d1 d9 a6  00 d1 d9 b0 67 c6 69 73  |SHRS........g.is|
00000010
> # 0x53 0x48 0x52 0x53 == "SHRS"

So once we pass this check and are back in the actual_decryption function control flow continues here:


The first two chunks that lead up two a call to uint32_t htonl(uint32_t hostlong) are best explained with an example. Once again we’re using the header of an encrypted firmware sample:

$v0:
          ↓
00000000: 5348 5253 00d1 d9a6 00d1 d9b0 67c6 6973  SHRS........g.is
00000010: 51ff 4aec 29cd baab f2fb e346 fda7 4d06  Q.J.)......F..M.
[...]

lwl $v1 11($v0):
                                      ↓
00000000: 5348 5253 00d1 d9a6 00d1 d9b0 67c6 6973  SHRS........g.is
00000010: 51ff 4aec 29cd baab f2fb e346 fda7 4d06  Q.J.)......F..M.
[...]
--> $v1 = b0d9

move $a0, $v1
--> $a0 = b0d9 XXXX

lwr $a0, 8($v0):
                              ↓
00000000: 5348 5253 00d1 d9a6 00d1 d9b0 67c6 6973  SHRS........g.is
00000010: 51ff 4aec 29cd baab f2fb e346 fda7 4d06  Q.J.)......F..M.
[...]
-->   $a0 = b0d9 d100

nop
move $v0, $a0
--> $v0 = b0d9 d100

move $a0, $v0
--> $a0 = b0d9 d100

htonl(b0d9 d100)

In MIPS we need this pair of lwl, lwr instructions for unaligned memory access. Basically you’re providing the lwl instruction with the address of the most significant byte of an unaligned memory location. It will automatically put the corresponding bytes into the upper bytes of the destination register ( $v1 / $a0 respectively). lwr works analogously, with the only difference being that the picked bytes are put in the lower bytes of the destination register. The results are combined by merging both byte values (so these instructions can be used in either order!).

# datalen_2
int.from_bytes(b'\xb0\xd9\xd1\x00', byteorder='little', signed=False)
13752752

# Alternatively, we can use the socket package
from socket import htonl
htonl(0xb0d9d100)
13752752

# datalen_1 
int.from_bytes(b'\xa6\xd9\xd1\x00', byteorder='little', signed=False)
13752742

Both return values are saved on the stack ( 0x128+datalen_2($sp) / 0x128+datalen_1($sp) ). The question remains what are they used for and why are there two almost identical values ( \xb0\xd9\xd1\x00 vs. \xa6\xd9\xd1\x00 ). Neither of those two seem to be the full length of the encrypted firmware binary:

> wc -c DIR_882_FW120B06.BIN
13759047 DIR_882_FW120B06.BIN

> wc -c DIR_882_FW120B06.BIN | cut -d' ' -f1 | xargs printf '0x%x\n'     
0xd1f247

Their purpose will become clear soon as one of these values is used right away! After this fun memory access magic a function call to calcSha512Digest is being prepared with 3 arguments: calcSha512Digest(mapped_sourceFile + 0x6dc, datalen_2, buf) .

calcSha512Digest:

As the name already gives away this function calculates a SHA512 message digest. Based on the function arguments this invocation will use the mapped encrypted firmware at offset 0x6dc , with length datalen_2 , which we just calculated earlier in the htonl part! When taking this offset into account could datalen_2 = total size - offset ?

> # total size - offset - datalen_2
> pcalc 0xd1f247 - 0x6dc - 0xd1d9b0
	4532            	0x11b4            	0y1000110110100'
> # Clearly not..

The provided buffer variable is used to store the hash result memory. The function itself just uses the SHA512 library functions. If you look a little bit more closely you’ll notice that this holy trinity of SHA512_Init , SHA512_Update , and SHA512_Final all belong to OpenSSL and are taken from libcrypto which we found in the strings output in part 1!

Right after exiting calcSha512Digest the basic block continues with setting up another memcmp to compare the just calculated hash digest to an expected value:

This value is hard coded in the encrypted binary (which is still mapped as RO into memory at this point) at offset 0x9C . The third supplied argument to the int memcmp(const void *s1, const void *s2, size_t n) call is again the size and as expected has to be 0x40 bytes as SHA512 returns a 512 bit digest (== 64 bytes == 0x40 bytes). We can easily extract said value from our encrypted firmware:

> pcalc 0x40
    64              	0x40              	0y1000000

> pcalc 0x9c
    156             	0x9c              	0y10011100

> hd DIR_882_FW120B06.BIN -s 0x9c -n 64 -e '156/1 "%x" "\n"' | cut -d
0000009c  68 bf e5 30 a0 49 b9 e8  5d a0 bb 81 71 87 05 cd  |h..0.I..]...q...|
000000ac  70 25 18 f2 8f af d6 21  35 05 31 7e fd af 60 56  |p%.....!5.1~..`V|
000000bc  d8 ed e7 71 6c 39 d1 68  0d a7 13 f4 04 41 87 58  |...ql9.h.....A.X|
000000cc  e9 97 36 73 99 78 8b 01  10 ee 12 d6 b6 3b 69 ec  |..6s.x.......;i.|
000000dc

> hd DIR_882_FW120B06.BIN -s 0x9c -n 64 -e '156/1 "%x" "\n"' | cut -d$'\n' -f2
68bfe530a049b9e85da0bb8171875cd702518f28fafd621355317efdaf6056d8ede7716c39d168da713f44418758e997367399788b110ee12d6b63b69ec

If there is a mismatch control flow is, as usual, redirected to an early exit. However, in case of success control is redirected to the part I renamed as hash1_succ ( memcmp returns (in $v0 ) 0 on an exact match, which is followed by a branching beqz $v0, hash1_succ instruction).

The first part in hash1_succ sets up a another call to aes_cbc_encrypt with five arguments:

  • arg1 ( $a0 ): mapped source File + 0x6dc
  • arg2 ( $a1 ): datalen_2
  • arg3 ( $a2 ): decryption key (still the one from the debug printf to stdout earlier)
  • arg4 ( $a3 ): IVEC (same as before)
  • arg5 (on stack): mapped file at /tmp/ location

We already covered earlier what this function does. However, instead of calculating the decryption key with size 0x40 a large chunk of memory from the mapped and encrypted firmware is being used in combination with the decryption key. This can only mean one thing: This code block is responsible for decrypting the whole thing!

So after aes_cbc_encrypt returns we basically have access to the fully decrypted firmware already. Directly afterwards there is another call to calcSha512Digest :

This time with our suspected freshly decrypted firmware, datalen_1 , and a buffer as the arguments. The result of the SHA512 operation is compared against an expected value at encrypted_firmware + 0x5c . Once again we’re talking about a 64 byte value here:

> hd DIR_882_FW120B06.BIN -s 0x5c -n 64 -e '92/1 "%x" "\n"' | cut -d$'\n' -f2
1657d3b7d77c9e11ec721dfb87a25b18ec538285b98439b6b4dd85def0283d36ebeaad09d71b0ba3e2640e8c54ceb32eb0e8f721d73aaad14d5f7e872

Analogous to other memcmp operations earlier another beqz $v0, hash2_succ redirects control flow based on the result. We’ve seen this multiple times by now so we’re directly taking a look at the wanted branching result.

I’ll be pretty quick about this one. We have yet another function call where a SHA512 message digest is calculated. This time the only difference being that the digest is calculated over the whole decrypted firmware image concatenated with the decryption key. And as always the result is compared against a hard coded value in the encrypted firmware at offset 0x1c :

> hd DIR_882_FW120B06.BIN -s 0x1c -n 64 -e '92/1 "%x" "\n"' | cut -d$'\n' -f2
fda74d6a466e6adbfc49d13f3f7d112986b2a351de9085b783f74d3a2a255ab813cfb2a177ab29946066ebc2589882748e3541ee2514442e8d68e466e2c

So before quickly moving onto the next part denoted as hash3_succ let’s recap what datalen_1 and datalen_2 are used for!

Analysis of the datalen variables:

Just earlier we saw the two variables used almost interchangeably. The invested reader might have already figured it out. But the one call to aes_cbc_encrypt earlier gave away that datalen_2 at offset 0x8 - 0x12 corresponds op the length of the decrypted firmware binary. At the end of this series we will be able to decrypt these firmware samples just fine so here is a little foreshadowing with a decrypted sample:

# datalen_2
> hd DIR_882_FW120B06.BIN -n 4 -s 0x8
00000008  00 d1 d9 b0                                       |....|
0000000c

> wc -c decrypted_DIR_882_FW120B06.BIN | cut -d' ' -f1 | xargs printf '%x\n'
d1d9b0

But what about datalen_1 at offset 0x4 - 0x8?

# datalen_1
> hd DIR_882_FW120B06.BIN -s 0x4 -n 4
00000004  00 d1 d9 a6                                       |....|
00000008
# datalen_1
int.from_bytes(b'\xb0\xd9\xd1\x00', byteorder='little')
13752742

It’s almost identical in size compared to datalen_2 with only a difference of 10 bytes. However, as it is smaller it cannot be the size of the decrypted payload. We also already ruled out that it is the size of the encrypted firmware image before. So what is it?

Let’s backtrack! Right after a call to aes_cbc_encrypt the datalen_1 is used to calculate two SHA512 message digests over the decrypted firmware image, once without the decryption key put in the cipher and once with it in there. That does not seem to be any helpful… So what about the missing 10 bytes (difference between these two length variables) that are disregarded in the hash calculation?

> hd decrypted_DIR_882_FW120B06.BIN -s 0xd1d9a6
00d1d9a6  00 00 00 00 00 00 00 00  00 00                    |..........|
00d1d9b0
> # Last 10 bytes in the decrypted firmware

When seeking to datalen_1 and getting the hex dump until EOF we can see that it is just NULL bytes! So these 10 bytes cannot be some kind of check sum or any other relevant metadata. If you look very close at the addresses you can see that the decrypted firmware ends at 0x1d9b0 . What’s so special about this address? Turns out it’s not the address that is special but the properties of it:

> pcalc 0xd1d9b0 % 16
	0               	0x0               	0y0
> # 16 byte alignment matters!

Now why is a 16 byte alignment needed in the first place? The answer is in the disassembly where datalen_2 is used in combination with the call to aes_cbc_encrypt ! As the RFC for AES_CBC_ENCRYPT for IPSec nicely states:

2.4. Block Size and Padding
The AES uses a block size of sixteen octets (128 bits).
Padding is required by the AES to maintain a 16-octet (128-bit)
blocksize. Padding MUST be added, […], such that
the data to be encrypted ([…]) has a length that is a multiple of 16 octets.
Because of the algorithm specific padding requirement, no additional
padding is required to ensure that the ciphertext terminates on a 4-
octet boundary […]. Additional padding MAY be included, as
specified in [ESP], as long as the 16-octet blocksize is maintained.

We can conclude that datalen_2 is based on datalen_1 and the difference between them will always be a dynamically calculated value between 1 and 15 to keep the firmware 16-byte aligned. So summarize this further:

  • datalen_1 → size of decrypted payload
  • datalen_2 → size of decrypted payload with 16 byte alignment

Offset analysis: The curious case of 0x6dc:

Another thing that may have caused confusion is why the first SHA512 digest calculation as well as the decryption started at this particular offset of 0x6dc ? When you look at the fist 2k bytes of an encrypted firmware image they look rather arbitrary:

> hd DIR_882_FW120B06.BIN -n 2048
00000000  53 48 52 53 00 d1 d9 a6  00 d1 d9 b0 67 c6 69 73  |SHRS........g.is|
00000010  51 ff 4a ec 29 cd ba ab  f2 fb e3 46 fd a7 4d 06  |Q.J.)......F..M.|
00000020  a4 66 e6 ad bf c4 9d 13  f3 f7 d1 12 98 6b 2a 35  |.f...........k*5|
00000030  1d 0e 90 85 b7 83 f7 4d  3a 2a 25 5a b8 13 0c fb  |.......M:*%Z....|
00000040  2a 17 7a b2 99 04 60 66  eb c2 58 98 82 74 08 e3  |*.z...`f..X..t..|
00000050  54 1e e2 51 44 42 e8 d6  8e 46 6e 2c 16 57 d3 0b  |T..QDB...Fn,.W..|
00000060  07 d7 7c 9e 11 ec 72 1d  fb 87 a2 5b 18 ec 53 82  |..|...r....[..S.|
00000070  85 b9 84 39 b6 b4 dd 85  de f0 28 3d 36 0e be aa  |...9......(=6...|
00000080  d0 9d 71 b0 ba 3e 26 40  e8 c5 4c 0e 0b 32 eb 00  |..q..>&@..L..2..|
00000090  e8 f7 21 d7 3a aa 0d 14  d5 f7 e8 72 68 bf e5 30  |..!.:......rh..0|
000000a0  a0 49 b9 e8 5d a0 bb 81  71 87 05 cd 70 25 18 f2  |.I..]...q...p%..|
000000b0  8f af d6 21 35 05 31 7e  fd af 60 56 d8 ed e7 71  |...!5.1~..`V...q|
000000c0  6c 39 d1 68 0d a7 13 f4  04 41 87 58 e9 97 36 73  |l9.h.....A.X..6s|
000000d0  99 78 8b 01 10 ee 12 d6  b6 3b 69 ec 00 00 00 00  |.x.......;i.....|
000000e0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
[...]
000002d0  00 00 00 00 00 00 00 00  00 00 00 00 19 26 c0 d3  |.............&..|
000002e0  3e 88 4a c4 58 90 72 09  d6 86 8d bb d4 72 52 54  |>.J.X.r......rRT|
000002f0  ee ef f3 10 0d a5 44 ff  ce 08 ba b1 ac 84 ce bf  |......D.........|
00000300  7c e8 e8 0c 11 a2 d4 d1  cc 9d 89 65 43 f0 72 cf  ||..........eC.r.|
00000310  91 f7 53 45 b2 51 b6 d7  7c 13 d9 2a 3e ed 2c 2e  |..SE.Q..|..*>.,.|
00000320  73 e8 5c d0 9e 77 65 e5  22 91 ee a2 51 f7 e7 2f  |s.\..we."...Q../|
00000330  2a 4a 68 db c9 ea c9 74  6d aa 04 93 77 33 48 89  |*Jh....tm...w3H.|
00000340  bf 56 17 fd 11 77 b5 1c  d9 67 f6 9d 09 3f a0 5b  |.V...w...g...?.[|
00000350  a3 9f 2c 89 13 91 b0 8e  bc 45 19 ae 9a 46 b2 0e  |..,......E...F..|
00000360  2b ca 39 78 50 2c ce 07  6c 0d c5 95 5c 4b 50 14  |+.9xP,..l...\KP.|
00000370  3b 3f 22 c2 28 03 f9 02  ad b9 4f 54 70 d0 cb d3  |;?".(.....OTp...|
00000380  a8 df 7a 4a f1 51 44 c7  a4 93 c6 9b 4d 15 ac 76  |..zJ.QD.....M..v|
00000390  d7 c1 f2 cc 78 d5 06 1f  a0 b4 42 6e 48 2a 78 6c  |....x.....BnH*xl|
000003a0  18 8b 57 23 38 9e e4 e9  61 f2 ed 58 d0 f8 51 58  |..W#8...a..X..QX|
000003b0  a1 54 5f 89 cc 72 73 65  74 58 a5 96 24 25 31 c2  |.T_..rsetX..$%1.|
000003c0  c4 1f b8 de a1 30 e1 0a  70 7f 6d 10 0a 53 04 c0  |.....0..p.m..S..|
000003d0  e8 f1 d9 bc de c2 3a 62  b3 bb 21 16 cf 53 f3 f7  |......:b..!..S..|
000003e0  24 55 02 28 47 85 f8 e2  2d 00 c6 44 eb b7 64 fd  |$U.(G...-..D..d.|
000003f0  e4 23 63 cd 48 f1 70 4e  43 ce 74 78 28 be 89 96  |.#c.H.pNC.tx(...|
00000400  a9 29 3c 2f 0c a9 32 43  28 76 6a 96 12 6c 8b ff  |.)</..2C(vj..l..|
00000410  22 da 4e 45 ac 6f e1 34  3e b0 d5 be 9a 9a 65 50  |".NE.o.4>.....eP|
00000420  c4 52 05 02 1c d6 4a 46  48 8e 20 2f ea 6a 1b b0  |.R....JFH. /.j..|
00000430  62 61 00 8d 64 b9 ab 88  cb 28 98 01 6a 33 82 79  |ba..d....(..j3.y|
00000440  e1 02 9f 56 1d 5d b1 8f  0e 19 02 5e 44 02 2d b8  |...V.].....^D.-.|
00000450  ab 96 1d 42 2c db 13 c8  d6 dc 4a bb 62 40 20 85  |...B,[email protected] .|
00000460  9f c9 6f f1 fb 18 d1 09  0e b8 c7 30 2c 99 e7 3f  |..o........0,..?|
00000470  1a f5 e0 f3 f6 09 ed 7e  da 00 24 8b 80 b2 66 1b  |.......~..$...f.|
00000480  15 1c 49 ea 05 f9 21 70  f9 18 ae 18 6c 31 31 38  |..I...!p....l118|
00000490  1a ff 71 d8 a1 3b 7e 8d  5f 00 a8 d5 fd e2 3b 58  |..q..;~._.....;X|
000004a0  82 16 af aa 9e 16 08 5c  ea 6f 8a e7 20 53 54 96  |.......\.o.. ST.|
000004b0  00 eb 1d 75 38 f9 03 2d  f9 70 25 12 65 d2 82 b1  |...u8..-.p%.e...|
000004c0  35 4b d2 d9 eb 8c c2 70  b2 78 58 f1 c2 3f 19 e1  |5K.....p.xX..?..|
000004d0  ab d6 38 7f d8 be e4 db  94 f3 13 59 98 d3 d5 9f  |..8........Y....|
000004e0  f5 cb 4d d9 71 cf 45 6e  03 5e 0a 87 32 4e b2 49  |..M.q.En.^..2N.I|
000004f0  98 ff 0a 4e dc 95 8f fe  38 c3 34 c8 60 01 e5 d8  |...N....8.4.`...|
00000500  9d ca ad a7 16 92 a3 09  ec 25 82 d3 51 51 0e d0  |.........%..QQ..|
00000510  30 30 28 2c 8b 0b cd 91  bc 45 65 26 6d dc 54 30  |00(,.....Ee&m.T0|
00000520  c4 73 da 18 5f 21 eb 0a  59 c1 70 17 a8 ef ec 53  |.s.._!..Y.p....S|
00000530  2a b5 d5 d6 86 31 9c 4d  f6 22 7a 6d 01 b2 b1 46  |*....1.M."zm...F|
00000540  6c 7f 81 8b ea 51 88 3c  89 bf e6 e0 fb 4b ec f8  |l....Q.<.....K..|
00000550  54 d0 f5 fe 47 92 87 54  6a 75 74 34 64 a2 2a 56  |T...G..Tjut4d.*V|
00000560  50 d7 ee 1b 41 c8 85 c1  00 d9 e4 99 ca a0 25 4e  |P...A.........%N|
00000570  72 8e d8 fe db 42 0c 1b  10 85 42 fe 77 a3 53 d6  |r....B....B.w.S.|
00000580  a6 f0 44 04 72 58 04 09  a7 5d c0 b1 60 54 09 f0  |..D.rX...]..`T..|
00000590  71 08 f1 86 f9 2d 39 3a  4b 83 52 3a 07 d5 92 1e  |q....-9:K.R:....|
000005a0  e7 f6 4e f4 ed c1 07 d2  98 3b 75 48 6c b1 fc 8d  |..N......;uHl...|
000005b0  5b c2 f6 df 0e 1f f0 f8  ac 49 50 85 52 49 24 83  |[........IP.RI$.|
000005c0  a8 7c 2b bc 1f 46 5c 71  58 6f 8c ce ea 02 e7 af  |.|+..F\qXo......|
000005d0  2d da 8e ce 9e fa 77 be  ea 7b 6f 5e ea 7d 3b cf  |-.....w..{o^.};.|
000005e0  a0 8e 68 5a e6 8c c9 c3  d9 39 e8 f2 77 89 0c b9  |..hZ.....9..w...|
000005f0  3e 95 20 87 d3 35 46 91  dc 83 24 f3 a3 e8 66 74  |>. ..5F...$...ft|
00000600  b6 47 c7 86 01 50 17 cd  39 7e 1e 85 18 18 80 c4  |.G...P..9~......|
00000610  b2 01 b6 97 de 00 a3 2d  9b 4a d9 18 10 16 86 93  |.......-.J......|
00000620  10 ba cd 01 1d 34 35 46  7f c6 ff fb 61 94 47 92  |.....45F....a.G.|
00000630  60 72 88 10 de 0c 3b 03  c3 da 70 b9 17 01 01 a0  |`r....;...p.....|
00000640  63 49 65 aa 2f 7f 68 15  0c 5a 47 0c 82 93 e2 ef  |cIe./.h..ZG.....|
00000650  78 c6 1a 0a 2a dd 32 81  b1 9c 35 d4 d5 7e 1d fc  |x...*.2...5..~..|
00000660  33 5a e7 35 0f 74 27 a2  20 a6 2c fd e0 ab cb 42  |3Z.5.t'. .,....B|
00000670  ef bd 5f 17 36 bb af dc  6a 2b 4f b8 ae ef b7 c4  |.._.6...j+O.....|
00000680  21 2c c0 64 ec 3e 75 21  94 9b d5 87 33 25 81 dc  |!,.d.>u!....3%..|
00000690  13 a7 3b 7f da c8 fb ea  7b 3d 6d 5e 58 bb 0b 52  |..;.....{=m^X..R|
000006a0  14 a4 38 f5 fa 84 48 29  e6 ae 0e 75 5e 3d 8d bd  |..8...H)...u^=..|
000006b0  5a d1 42 07 93 99 f1 d3  f6 77 96 02 9d 52 9f f2  |Z.B......w...R..|
000006c0  a7 91 ec 10 bf 0c 53 52  ca 2d 4c 7a 2c e4 18 12  |......SR.-Lz,...|
000006d0  ec 8d 3c 1b a7 5b 2c 14  63 a8 d9 76 0b 84 6a e5  |..<..[,.c..v..j.|
000006e0  73 66 ef bb c5 0f 33 a7  79 16 d0 7d 8e 53 fc 0a  |sf....3.y..}.S..|
000006f0  8e dd b7 a7 6c 5e d9 78  78 0e e1 c1 17 6c 31 41  |....l^.xx....l1A|
00000700  53 ec 46 db 01 89 4c 98  53 e6 a9 8b b2 c1 ed 0f  |S.F...L.S.......|
00000710  b6 f7 49 98 84 fd e9 54  89 94 e3 17 2d 61 2b 7e  |..I....T....-a+~|
00000720  92 7d 0a b0 3d d3 45 c4  63 e9 f1 14 cc b3 9e b2  |.}..=.E.c.......|
00000730  79 62 0a 36 0d f7 7c af  b7 03 10 0f 98 95 27 3d  |yb.6..|.......'=|
00000740  07 bf a1 ea d7 99 95 14  74 64 0f 68 c7 ad 28 19  |........td.h..(.|
00000750  cc d3 4c 07 ef 95 25 0e  e5 36 f7 3f bf 89 77 31  |..L...%..6.?..w1|
00000760  43 21 f9 e1 dc db 7f c1  93 56 cf d1 eb 24 82 55  |C!.......V...$.U|
00000770  3d 9f 32 4d 8b 5c 02 f5  61 4c 8f e5 ba 11 ed ae  |=.2M.\..aL......|
00000780  ba a4 c4 0f 8a 87 d5 cb  d3 2d c9 34 ab 06 67 17  |.........-.4..g.|
00000790  66 d5 44 ff 35 0e ae 2b  37 63 f8 b3 67 29 4d 24  |f.D.5..+7c..g)M$|
000007a0  4f ba 22 37 8d 2a 55 b0  b2 5e 3c 5b 67 da fe 63  |O."7.*U..^<[g..c|
000007b0  9c 75 85 14 27 cb a2 a0  06 5b 03 68 98 b3 8e c9  |.u..'....[.h....|
000007c0  f3 d9 34 16 d0 2b 33 0b  32 aa c3 79 49 df 77 99  |..4..+3.2..yI.w.|
000007d0  09 c9 a5 16 bd 6c 49 58  87 a6 1e 35 b6 14 f2 72  |.....lIX...5...r|
000007e0  2b fc bf d1 45 73 e1 86  47 61 97 25 ac 34 b5 bf  |+...Es..Ga.%.4..|
000007f0  a9 3f 8b 27 1e d2 46 20  de fb 5f c1 3e e3 5e c3  |.?.'..F .._.>.^.|
00000800
> # First 2048 bytes of an encrypted firmware image

However, by now we already figured out most of what the first 0xdb bytes hold. Nevertheless, there is a clear cut at offset 0xdb to 0x2db which is exactly 512 NULL bytes. This looks like the security header may end at 0xdb . With the assumption based on one of the calls to aes_cbc_encrypt that our data payload only starts at 0x6dc what are these 1024 seemingly random bytes that as of right now do not hold either relevant metadata nor parts of the encrypted payload?

> pcalc 0x6db - 0x2db
	1024            	0x400             	0y10000000000

To figure that out we need to dive back right into the control flow over at hash3_succ !

Here a new function call to sha512_checker is being prepared which is invoked a second time (with different arguments) once the first call succeeds and with that returns 1. You can see that both of these function calls basically have the following signature func(mapped_enc_fw_base_address + security_header_offset, 64, mapped_enc_fw_base_address + yet_unidentified_offset, 512) as their arguments. This function will answer all the remaining questions regarding the first yet unknown bytes. So what does it do?

sha512_checker:

This function is used for two additional integrity checks with calls to int RSA_verify(int type, const unsigned char *m, unsigned int m_len, unsigned char *sigbuf, unsigned int siglen, RSA *rsa) .

This library call verifies that the signature sigbuf of size siglen matches a given message digest m of size m_len . type denotes the message digest algorithm that was used to generate the signature. Lastly, as expected, RSA_verify returns 1 on success. rsa is the signer’s public key, which in this case defaults to the /etc_ro/public.pem (if not overwritten by argv[2] ). When following along until here the function parameters should all make sense by now.

  • type - As seen in the disassembly graph the type is hard coded to 0x2a2 , which corresponds to SHA512.
  • m - SHA512 hash digest that are 64 bytes in size. In our case these corresponds to the digest of the size of the encrypted and decrypted firmware.
  • m_len - size of m , which is hard coded to 64 bytes (which only makes sense).
  • sigbuf - hardcoded 512 byte signatures at offsets 0x2dc and 0x4dc in the encrypted firmware.
  • siglen - fixed size of 512 bytes because we’re dealing with signatures of these sizes (as the distance between the two offsets from sigbuf show as well).
  • RSA_cert - Is the struct in memory loaded with the values from reading the public key that is by default located in /etc_ro/public.pem .
> pcalc 0x2a2
674             	0x2a2             	0y1010100010

> echo '#include <openssl/obj_mac.h>' | gcc -E - -dM | grep 674
#define NID_SHA512 674

Ultimately, we do not have to include these two checks in a custom decryption PoC, since we’re already past the point of the actual decryption. Furthermore, we made sure the SHA512 digests match the expected values. These additional checks are most likely put there to verify the origin of the encrypted firmware image.

This basically concludes the complete decryption routine. The only thing I left out are some minor bad paths we are not interested in anyway. Following this is only code that sets the return value of this function (0 on success, -1 otherwise) and the function tear down, which takes care of all the un-mapping/closing from all the IO related operations:


decrypt_firmware tear down:

So successfully completing the actual_decryption routine returns 0 to the decrypt_firmware function. At this point this function only cleans up as well by un-linking (removing) the original sourceFile (encrypted firmware image) and renaming the decrypted one from /tmp/.firmware.origsourceFile (with sourceFile obviously being the name of the supplied encrypted firmware sample).


The last step is returning to main and with that preparing the process tear down. So that’s it! That was the complete firmware decryption scheme D-Link is using for their recent router firmware. Re-implementing everything we’ve seen so far results in the following C-code:

#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <openssl/aes.h>
#include <openssl/ossl_typ.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/sha.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

RSA *grsa_struct;
static unsigned char iv[] = {0x98, 0xC9, 0xD8, 0xF0, 0x13, 0x3D, 0x06, 0x95,
                             0xE2, 0xA7, 0x09, 0xC8, 0xB6, 0x96, 0x82, 0xD4};
static unsigned char aes_in[] = {0xC8, 0xD3, 0x2F, 0x40, 0x9C, 0xAC,
                                 0xB3, 0x47, 0xC8, 0xD2, 0x6F, 0xDC,
                                 0xB9, 0x09, 0x0B, 0x3C};
static unsigned char aes_key[] = {0x35, 0x87, 0x90, 0x03, 0x45, 0x19,
                                  0xF8, 0xC8, 0x23, 0x5D, 0xB6, 0x49,
                                  0x28, 0x39, 0xA7, 0x3F};
unsigned char out[] = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
                       0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46};
const static char *null = "0";

int check_cert(char *pem, void *n) {
  RSA *lrsa_struct[2];
  OPENSSL_add_all_algorithms_noconf();
  FILE *pem_fd = fopen(pem, "r");
  if (pem_fd != NULL) {
    lrsa_struct[0] = RSA_new();
    if (PEM_read_RSAPublicKey(pem_fd, lrsa_struct, NULL, n) == NULL) {
      RSA_free(lrsa_struct[0]);
      puts("Read RSA private key failed, maybe the password is incorrect.");
    } else {
      grsa_struct = lrsa_struct[0];
    }
    fclose(pem_fd);
  }
  if (grsa_struct == NULL) {
    return -1;
  } else {
    return 0;
  }
}

int aes_cbc_encrypt(unsigned char *in, unsigned int length,
                    unsigned char *user_key, unsigned char *ivec,
                    unsigned char *out) {
  AES_KEY dec_key;
  unsigned char iv[16];
  memcpy(iv, ivec, 16);

  AES_set_decrypt_key(user_key, 0x80, &dec_key);
  AES_cbc_encrypt(in, out, (unsigned int)length, &dec_key, iv, AES_DECRYPT);
  return 0;
}

int call_aes_cbc_encrypt(unsigned char *key) {
  aes_cbc_encrypt(aes_in, 0x10, aes_key, iv, key);
  return 0;
}

int check_magic(void *mapped_mem) {
  return (unsigned int)(memcmp(mapped_mem, "SHRS", 4) == 0);
}

int calc_sha512_digest(void *mapped_mem, u_int32_t len, unsigned char *buf) {
  SHA512_CTX sctx;
  SHA512_Init(&sctx);
  SHA512_Update(&sctx, mapped_mem, len);
  SHA512_Final(buf, &sctx);
  return 0;
}

int calc_sha512_digest_key(void *mapped_mem, u_int32_t len, void *key,
                           unsigned char *buf) {
  SHA512_CTX sctx;
  SHA512_Init(&sctx);
  SHA512_Update(&sctx, mapped_mem, len);
  SHA512_Update(&sctx, key, 0x10);
  SHA512_Final(buf, &sctx);
  return 0;
}

int check_sha512_digest(const unsigned char *m, unsigned int m_length,
                        unsigned char *sigbuf, unsigned int siglen) {
  return RSA_verify((int)0x2a2, m, m_length, sigbuf, siglen, grsa_struct);
}

int actual_decryption(char *sourceFile, char *tmpDecPath, unsigned char *key) {
  int ret_val = -1;
  size_t st_blocks = -1;
  struct stat stat_struct;
  int _fd;
  int fd = -1;
  void *ROM = (void *)0x0;
  unsigned char *RWMEM = (unsigned char *)0x0;
  off_t seek_off;
  unsigned char hash_md_buf[68] = {0};
  unsigned int mcb;
  uint32_t datalen_1;
  uint32_t datalen_2;

  memset(&hash_md_buf, 0, 0x40);
  memset(&stat_struct, 0, 0x90);
  _fd = stat(sourceFile, &stat_struct);
  if (_fd == 0) {
    fd = open(sourceFile, O_RDONLY);
    st_blocks = stat_struct.st_size;
    if (((-1 < fd) &&
         (ROM = mmap(NULL, st_blocks, PROT_READ, MAP_SHARED, fd, 0),
          ROM != 0)) &&
        (_fd = open(tmpDecPath, O_RDWR | O_NOCTTY | O_CREAT, S_IRUSR | S_IWUSR),
         -1 < _fd)) {
      seek_off = lseek(_fd, st_blocks - 1, 0);
      if (seek_off == st_blocks - 1) {
        write(_fd, null, 1);
        close(_fd);
        _fd = open(tmpDecPath, O_RDWR | O_NOCTTY, S_IRUSR | S_IWUSR);
        RWMEM =
            mmap(NULL, st_blocks, PROT_READ | PROT_WRITE, MAP_SHARED, _fd, 0);
        if (RWMEM != 0) {
          mcb = check_magic(ROM);
          if (mcb == 0) {
            puts("No image matic found\r");
          } else {
            datalen_1 = htonl(*(uint32_t *)(ROM + 8));
            datalen_2 = htonl(*(uint32_t *)(ROM + 4));
            calc_sha512_digest((ROM + 0x6dc), datalen_1, hash_md_buf);
            _fd = memcmp(hash_md_buf, (ROM + 0x9c), 0x40);
            if (_fd == 0) {
              aes_cbc_encrypt((ROM + 0x6dc), datalen_1, key,
                              (unsigned char *)(ROM + 0xc), RWMEM);
              calc_sha512_digest(RWMEM, datalen_2, hash_md_buf);
              _fd = memcmp(hash_md_buf, (ROM + 0x5c), 0x40);
              if (_fd == 0) {
                calc_sha512_digest_key(RWMEM, datalen_2, (void *)key,
                                       hash_md_buf);
                _fd = memcmp(hash_md_buf, (ROM + 0x1c), 0x40);
                if (_fd == 0) {
                  _fd = check_sha512_digest((ROM + 0x5c), 0x40, (ROM + 0x2dc),
                                            0x200);
                  if (_fd == 1) {
                    _fd = check_sha512_digest((ROM + 0x9c), 0x40, (ROM + 0x4dc),
                                              0x200);
                    if (_fd == 1) {
                      ret_val = 0;
                      puts("We in!");
                    } else {
                      ret_val = -1;
                    }
                  } else {
                    ret_val = -1;
                  }
                } else {
                  puts("check sha512 vendor failed\r");
                }
              } else {
                printf("check sha512 before failed %d %d\r\n", datalen_2,
                       datalen_1);
                int ctr = 0;
                while (ctr < 0x40) {
                  printf("%02X", *(hash_md_buf + ctr));
                  ctr += 1;
                }
                puts("\r");
                ctr = 0;
                while (ctr < 0x40) {
                  printf("%02X",
                         *(unsigned int *)(unsigned char *)(ROM + ctr + 0x5c));
                  ctr += 1;
                }
                puts("\r");
              }
            } else {
              puts("check sha512 post failed\r");
            }
          }
        }
      }
    }
  }
  if (ROM != (void *)0x0) {
    munmap(ROM, stat_struct.st_blocks);
  }
  if (RWMEM != (unsigned char *)0x0) {
    munmap(RWMEM, st_blocks);
  }
  if (-1 < (int)fd) {
    close(fd);
  }
  return ret_val;
}

int decrypt_firmware(int argc, char **argv) {
  int ret;
  unsigned char key[] = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
                         0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46};
  char *ppem = "/tmp/public.pem";
  int loopCtr = 0;
  if (argc < 2) {
    printf("%s <sourceFile>\r\n", argv[0]);
    ret = -1;
  } else {
    if (2 < argc) {
      ppem = (char *)argv[2];
    }
    int cc = check_cert(ppem, (void *)0);
    if (cc == 0) {
      call_aes_cbc_encrypt((unsigned char *)&key);
      printf("key: ");
      while (loopCtr < 0x10) {
        printf("%02X", *(key + loopCtr) & 0xff);
        loopCtr += 1;
      }
      puts("\r");
      ret = actual_decryption((char *)argv[1], "/tmp/.firmware.orig",
                              (unsigned char *)&key);
      if (ret == 0) {
        unlink(argv[1]);
        rename("/tmp/.firmware.orig", argv[1]);
      }
      RSA_free(grsa_struct);
    } else {
      ret = -1;
    }
  }
  return ret;
}

int encrypt_firmware(int argc, char **argv) {
  puts("TODO\n");
  return -1;
}

int main(int argc, char **argv) {
  int ret;
  char *str_f = strstr(*argv, "decrypt");
  if (str_f != NULL) {
    ret = decrypt_firmware(argc, argv);
  } else {
    ret = encrypt_firmware(argc, argv);
  }
  return ret;
}

Summary:

When combining what we found out during analysis we can conclude that the encrypted firmware has a 1756 byte security header as shown below:



It contains the following fields in this exact order:

  1. Magic bytes
  2. Length of decrypted firmware - padding
  3. Length of decrypted firmware in bytes
  4. Initialization vector value for AES_128_CBC to decrypt data
  5. SHA512 64 byte message digest of decrypted firmware + key
  6. SHA512 64 byte message digest of decrypted firmware
  7. SHA512 64 byte message digest of encrypted firmware
  8. 512 unused NULL bytes (0xdc to 0x2dc) # no colored box for this one
  9. 512 byte Signature 1
  10. 512 byte Signature 2

With this in mind, we can clearly see that the header continues until offset 0x6dc before the actual encrypted data payload starts. This seems consistent across all images I looked at. Hence, the total size of the security header is 1756 bytes, with 220 bytes for the initial 7 bullet points containing various meta checks, the unused 512 NULL byte area and the two 512 byte verification signatures at the end. The NULL byte area could potentially be reserved space for a future verification check or an additional signature.

Finally, I noticed that the /etc_ro/public.pem file differs across devices:

Testing against other encrypted D-Link images:

My custom script (linked at the bottom) was evaluated against additional newer firmware samples of different routers and the decryption always worked flawlessly. This shows that the security header information as well as the imgdecrypt binary itself did not change. The following firmware samples were tested for validation:

Device Router release Sample name Md5sum encrypted FW release
DIR-882 Q2 2017 2_DIR-882_RevA_Firmware122b04.bin a59e7104916dc1770ad987a13c757075 07/10/19
DIR-882 Q2 2017 DIR882A1_FW130B10_Beta_for_security_issues_Stackoverflow_20191219.bin 339f98563ea9c0b2829a3b40887dabbd 02/20/20
DIR-1960 Q2 2019 DIR-1960_RevA_Firmware103B03.bin f2aff7a08e44d77787c7243f60c1334c 10/30/19
DIR-2660 Q4 2019 DIR-2660_RevA_Firmware110B01.bin ba72e99a3cea77482bab9ea757d33dfc 11/25/19
DIR-3060 Q4 2019 DIR-3060_RevA_Firmware111B01.bin 86e3f7baebf4178920c767611ec2ba50 10/22/19

Unpacking D-Link DIR3060 - A quick analysis:

So let’s test our final script against a random DIR3060 firmware that I downloaded:

> ./dlink-dec.py DIR_3060/DIR-3060_RevA_Firmware111B01.bin DIR_3060/decrypted_DIR-3060_RevA_Firmware111B01 dec
[*] Calculating decryption key...
	[+] OK!
[*] Checking magic bytes...
	[+] OK!
[*] Verifying SHA512 message digest of encrypted payload...
	[+] OK!
[*] Verifying SHA512 message digests of decrypted payload...
	[+] OK!
	[+] OK!
[+] Successfully decrypted DIR-3060_RevA_Firmware111B01.bin!

> file DIR_3060/decrypted_DIR-3060_RevA_Firmware111B01.bin
decrypted_DIR-3060_RevA_Firmware111B01.bin: u-boot legacy uImage, Linux Kernel Image, Linux/MIPS, OS Kernel Image (lzma), 18080478 bytes, Mon Jan  6 08:45:07 2020, Load Address: 0x81001000, Entry Point: 0x81643D00, Header CRC: 0xCF5AE78D, Data CRC: 0x70C7567F

> binwalk DIR_3060/decrypted_DIR-3060_RevA_Firmware111B01.bin
DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             uImage header, header size: 64 bytes, header CRC: 0x342C3662, created: 2019-09-16 06:46:37, image size: 18030334 bytes, Data Address: 0x81001000, Entry Point: 0x816357A0, data CRC: 0x35F99EE4, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: lzma, image name: "Linux Kernel Image"
160           0xA0            LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 23452096 bytes

Looks promising! The script finished without any errors and file as well as binwalk report proper file sections that are typical for a firmware image. How about unpacking?

We can just use binwalk or whip out FACT the Firmware Analysis and Comparison Tool that ships with the FACT extractor, which includes additional file signatures and carvers for extraction!

>./extract.py decrypted_DIR-3060_RevA_Firmware111B01.bin -o tmp
[...]
>./extract.py extraced_ubootLZMA4/8834EC -o dir3060_file_system
WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap.
[2020-03-20 11:00:20][unpackBase][INFO]: Plug-ins available: ['TPWRN702N', 'adobe', 'ambarella', 'ambarella_romfs', 'arj', 'avm_sqfs_fake', 'dahua', 'deb', 'dji_drones', 'dlm', 'dsk', 'dsk_extended', 'generic_carver', 'generic_fs','intel_hex', 'jffs2', 'nop', 'patool', 'pjl', 'postscript', 'raw', 'ros', 'sevenz', 'sfx', 'sit', 'squash_fs', 'srec', 'tek', 'tpltool', 'ubi_fs', 'ubi_image', 'uboot', 'uefi', 'untrx', 'update_stream', 'xtek', 'yaffs', 'zlib']
[2020-03-20 11:00:29][unpack][INFO]: 1903 files extracted
[2020-03-20 12:00:30][extract][WARNING]: Now taking ownership of the files. You may need to enter your password.
{
    "plugin_used": "PaTool",
    "plugin_version": "0.5.2",
    "output": "137449 blocks\npatool: Extracting /tmp/extractor/input/8834EC ...\npatool: running '/bin/cpio' --extract --make-directories --preserve-modification-time --no-absolute-filenames --force-local --nonmatching \"*\\.\\.*\" < '/tmp/extractor/input/8834EC'\npatool:     with shell='True', cwd='/tmp/fact_unpack_o4kgxvd5'\npatool: ... /tmp/extractor/input/8834EC extracted to `/tmp/fact_unpack_o4kgxvd5'.\n",
    "analysis_date": 1584702021.1047733,
    "number_of_unpacked_files": 1887,
    "number_of_unpacked_directories": 97,
    "summary": [
        "no data lost"
    ],
    "entropy": 0.5405103347033559,
    "size_packed": 61216856,
    "size_unpacked": 225761852
}

> cd dir3060_file_system && ls
bin  etc  etc_ro  init  lib  rom  sbin  share  usr

> cp $(which qemu-mipsel-static) .

> sudo chroot . ./qemu-mipsel-static bin/imgdecrypt Hello_0x00sec       
key:C05FBF1936C99429CE2A0781F08D6AD8

We’ve done it! We successfully unpacked the encrypted firmware image and can directly see a known constant in form of the etc_ro folder, which contains the certificate we saw right at the beginning!

Just for fun we can snoop around the file system locally now without having to have a device at hand which is handy for static analysis:

> cat /etc/shadow
root:$1$ZVpxbK71$2Fgpdj.x9SBOCz5oyULHd/:17349:0:99999:7:::
daemon:*:0:0:99999:7:::
ftp:*:0:0:99999:7:::
network:*:0:0:99999:7:::
nobody:*:0:0:99999:7:::

> cat etc/shadow | cut -d$'\n' -f1 | cut -d":" -f2 > hash.txt

> cat hash.txt
$1$ZVpxbK71$2Fgpdj.x9SBOCz5oyULHd/

> hashcat -m 500 -a 0 -o cracked.txt hash.txt rockyou.txt -O

> cat cracked.txt
$1$ZVpxbK71$2Fgpdj.x9SBOCz5oyULHd/:root

So we got a root:root login on the newest flagship router model from last year and no privilege separation whatsoever with everything running as root as usual.

Last but not least, instead of enumerating the extracted file system locally we could have also dumped the decrypted firmware file into FACT instead of only using the FACT extractor to get tons of more interesting metadata about this firmware:

As we can see the encrypted firmware sample in the first screenshot is not extracted at all. The file type only matches the newly added signature for the D-Link “SHRS” magic byte sequence. However, after applying the firmware decryption we can see a fully unrolled Linux file tree as well as a very verbose file-type with lots of meta-information in addition to the correctly cracked password :).

Note: Ignore the first lengthy password tag in the decrypted firmware, that’s a bug in the output parser from the password cracker :D.

Conclusion:

In this blog post I showcased how we can circumvent a firmware encryption by finding the path of least resistance and I guess also the path of minimal purchase costs. Our research shows that our results from analyzing the D-Link DIR882 directly translate up to the flagship model D-Link DIR3060 among others. This shows that the encryption mechanism and more importantly the structure of the security header never changed over the course of the past 2-3 years! Moreover, the decryption key stayed the same as well.

IMO this was a rather fun challenge and utilizing a dynamically resolved decryption key that is based on three hard coded constants in addition to that public certificate mechanism is not such a horrible solution for securing firmware updates. I was only able to crack this due to the physical access to the device. That said I never much poked around the other potential vulnerable components like the OTA firmware update mechanism or the web GUI as this would be beyond the scope of this article.

The final decryption script, the re-constructed C code and everything else can be found on my GitHub. The full script also includes a basic encryption routine that mimics the original encryption.

This first blog post series was already rather lengthy even though I omitted the whole hooking up to the serial console and dumping the binary of interest. Nevertheless, I hope you enjoyed reading through it. I’m more than happy to receive any feedback so please hit me up on Twitter.

PS: Show FACT some love:

PPS: As of now the custom unpacker is part of FACT by default so there is no need to manually unpack these images anymore :)!

PPPS: Sorry for the often wonky naming of basic blocks and variables! Did that irritate you and do you prefer a blank IDA database next time or was it not that bad?

PPPPS: If you feel like dynamically debugging the original MIPS binary, it is certainly possible from within GDB:


And with that its time to say EOF. Hope you enjoyed this walktrhough.

<< Part 2.1

5 Likes