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 debugprintf
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,.....J.b@ .|
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 to0x2a2
, 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 ofm
, which is hard coded to 64 bytes (which only makes sense).sigbuf
- hardcoded 512 byte signatures at offsets0x2dc
and0x4dc
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.orig → sourceFile (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:
- Magic bytes
- Length of decrypted firmware - padding
- Length of decrypted firmware in bytes
- Initialization vector value for AES_128_CBC to decrypt data
- SHA512 64 byte message digest of decrypted firmware + key
- SHA512 64 byte message digest of decrypted firmware
- SHA512 64 byte message digest of encrypted firmware
- 512 unused NULL bytes (0xdc to 0x2dc) # no colored box for this one
- 512 byte Signature 1
- 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 walkthrough .