Decrypting DLINK Proprietary Firmware Images

Published: July 19, 2020

The DIR-3040 models of DLINK routers feature encrypted firmware images in the most recent versions of the firmware. https://support.dlink.com/ProductInfo.aspx?m=DIR-3040-US details the firmware images available for this product.

Unzipping the first reveals two files:

Running binwalk on DIR3040A1_FW111B02.bin reveals:

$ binwalk DIR3040A1_FW111B02.bin DECIMAL HEXADECIMAL DESCRIPTION --------------------------------------------------------------------------------

The lack of binwalk output almost surely means the firmware file is encrypted.

Unzipping the older firmware image reveals three files:

The last file ends with uncrypted.bin, which was my clue this version of the firmware image was not encrypted. Running binwalk on this file reveals:

$ binwalk DIR3040A1_FW102B03_uncrypted.bin DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 uImage header, header size: 64 bytes, header CRC: 0xDFA42E6F, created: 2019-09-18 07:16:48, image size: 17637193 bytes, Data Address: 0x81001000, Entry Point: 0x816357A0, data CRC: 0xAD066126, 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: 23062976 bytes 8476944 0x815910 MySQL ISAM compressed data file Version 4

Bingo, a uImage header and accompanying filesystem. We can extract this using binwalk -eM DIR3040A1_FW102B03_uncrypted.bin.

Looking at the filesystem, the first thing I did is look for certificates:

./etc_ro/public.pem ./etc_ro/cacert.pem ./etc_ro/mydlink/client-ca.crt.pem ./etc_ro/mosquitto/ssl/clientcrt.pem ./etc_ro/mosquitto/ssl/cacrt.pem ./etc_ro/mosquitto/ssl/cakey.pem ./etc_ro/mosquitto/ssl/servercrt.pem ./etc_ro/mosquitto/ssl/serverkey.pem ./etc_ro/mosquitto/ssl/clientkey.pem

Next I take a look at the binaries on the filesystem. One catches my eye: /bin/imgdecrypt. We could also have found this by grepping for /etc_ro/public.pem.

We load this into Ghidra and take a look:

int decrypt_firmare(int param_1,undefined4 *param_2) { int iVar1; char *local_20; int local_1c; undefined4 local_18; undefined4 local_14; undefined4 local_10; undefined4 local_c; local_18 = 0x33323130; local_14 = 0x37363534; local_10 = 0x42413938; local_c = 0x46454443; local_20 = "/etc_ro/public.pem"; if (param_1 < 2) { printf("%s <sourceFile>\r\n",*param_2); iVar1 = -1; } else { if (2 < param_1) { local_20 = (char *)param_2[2]; } iVar1 = FUN_004021ac(local_20,0); if (iVar1 == 0) { FUN_004025a4(&local_18); printf("key:"); local_1c = 0; while (local_1c < 0x10) { printf("%02X",(int)*(char *)((int)&local_18 + local_1c) & 0xff); local_1c = local_1c + 1; } puts("\r"); iVar1 = FUN_00401770(param_2[1],"/tmp/.firmware.orig",&local_18); if (iVar1 == 0) { unlink((char *)param_2[1]); rename("/tmp/.firmware.orig",(char *)param_2[1]); } RSA_free(DAT_00413220); } else { iVar1 = -1; } } return iVar1; }

The disassembly for this function looks like this:

************************************************************** * FUNCTION * ************************************************************** undefined __stdcall decrypt_firmare(undefined4 argc, und assume gp = 0x41b110 undefined v0:1 <RETURN> undefined4 a0:4 argc undefined4 a1:4 infile undefined4 Stack[0x4]:4 local_res4 XREF[6]: 00402624(W), 00402688(R), 004026d4(R), 004027e4(R), 00402828(R), 00402858(R) undefined4 Stack[0x0]:4 local_res0 XREF[3]: 00402620(W), 0040266c(R), 004026c0(R) undefined4 Stack[-0x4]:4 local_4 XREF[2]: 00402610(W), 004028b8(R) undefined4 Stack[-0xc]:4 local_c XREF[1]: 00402654(W) undefined4 Stack[-0x10]:4 local_10 XREF[1]: 00402650(W) undefined4 Stack[-0x14]:4 local_14 XREF[1]: 0040264c(W) undefined4 Stack[-0x18]:4 local_18 XREF[1]: 00402648(W) undefined4 Stack[-0x1c]:4 local_1c XREF[11]: 00402668(W), 004026fc(W), 00402700(R), 00402754(W), 00402768(R), 004027a0(R), 004027ac(W), 004027b0(R), 00402814(W), 00402818(R), 004028b4(R) undefined4 Stack[-0x20]:4 local_20 XREF[3]: 00402660(W), 004026e4(W), 004026e8(R) undefined4 Stack[-0x28]:4 local_28 XREF[11]: 0040261c(W), 004026b0(R), 004026f8(R), 0040272c(R), 00402750(R), 0040279c(R), 004027e0(R), 00402810(R), 00402854(R), 00402888(R), 004028b0(R) decrypt_firmare XREF[3]: Entry Point(*), main:00402b1c(c), 00413138(*) 0040260c c8 ff bd 27 addiu sp,sp,-0x38 assume gp = <UNKNOWN> 00402610 34 00 bf af sw ra,local_4(sp) 00402614 42 00 1c 3c lui gp,0x42 00402618 10 b1 9c 27 addiu gp,gp,-0x4ef0 0040261c 10 00 bc af sw gp=>_gp,local_28(sp) 00402620 38 00 a4 af sw argc,local_res0(sp) 00402624 3c 00 a5 af sw infile,local_res4(sp) 00402628 40 00 02 3c lui v0,0x40 0040262c 74 30 45 8c lw infile,offset s_0123456789ABCDEF_00403074(v0) = "0123456789ABCDEF" 00402630 74 30 43 24 addiu v1,v0,0x3074 00402634 04 00 64 8c lw argc,0x4(v1)=>s_456789ABCDEF_00403074+4 = "456789ABCDEF" 00402638 74 30 43 24 addiu v1,v0,0x3074 0040263c 08 00 63 8c lw v1,0x8(v1)=>s_89ABCDEF_00403074+8 = "89ABCDEF" 00402640 74 30 42 24 addiu v0,v0,0x3074 00402644 0c 00 42 8c lw v0,0xc(v0)=>s_CDEF_00403074+12 = "CDEF" 00402648 20 00 a5 af sw infile,local_18(sp) 0040264c 24 00 a4 af sw argc,local_14(sp) 00402650 28 00 a3 af sw v1,local_10(sp) 00402654 2c 00 a2 af sw v0,local_c(sp) 00402658 40 00 02 3c lui v0,0x40 0040265c 30 30 42 24 addiu v0,v0,0x3030 00402660 18 00 a2 af sw v0=>s_/etc_ro/public.pem_00403030,local_20(sp) = "/etc_ro/public.pem" 00402664 ff ff 02 24 li v0,-0x1 00402668 1c 00 a2 af sw v0,local_1c(sp) 0040266c 38 00 a2 8f lw v0,local_res0(sp) 00402670 00 00 00 00 nop 00402674 02 00 42 28 slti v0,v0,0x2 00402678 11 00 40 10 beq v0,zero,LAB_004026c0 0040267c 00 00 00 00 _nop 00402680 40 00 02 3c lui v0,0x40 00402684 44 30 43 24 addiu v1,v0,0x3044 00402688 3c 00 a2 8f lw v0,local_res4(sp) 0040268c 00 00 00 00 nop 00402690 00 00 42 8c lw v0,0x0(v0) 00402694 21 20 60 00 move argc=>s_%s_<sourceFile>_00403044,v1 = "%s <sourceFile>\r\n" 00402698 21 28 40 00 move infile,v0 0040269c 44 80 82 8f lw v0,-0x7fbc(gp)=>->printf = 00402e00 004026a0 00 00 00 00 nop 004026a4 21 c8 40 00 move t9,v0 004026a8 09 f8 20 03 jalr t9=>printf int printf(char * __format, ...) 004026ac 00 00 00 00 _nop 004026b0 10 00 bc 8f lw gp,local_28(sp) 004026b4 ff ff 02 24 li v0,-0x1 004026b8 2e 0a 10 08 j LAB_004028b8 004026bc 00 00 00 00 _nop LAB_004026c0 XREF[1]: 00402678(j) 004026c0 38 00 a2 8f lw v0,local_res0(sp) 004026c4 00 00 00 00 nop 004026c8 03 00 42 28 slti v0,v0,0x3 004026cc 06 00 40 14 bne v0,zero,LAB_004026e8 004026d0 00 00 00 00 _nop 004026d4 3c 00 a2 8f lw v0,local_res4(sp) 004026d8 00 00 00 00 nop 004026dc 08 00 42 8c lw v0,0x8(v0) 004026e0 00 00 00 00 nop 004026e4 18 00 a2 af sw v0,local_20(sp) LAB_004026e8 XREF[1]: 004026cc(j) 004026e8 18 00 a4 8f lw argc,local_20(sp) 004026ec 21 28 00 00 clear infile 004026f0 6b 08 10 0c jal FUN_004021ac undefined FUN_004021ac() 004026f4 00 00 00 00 _nop 004026f8 10 00 bc 8f lw gp,local_28(sp) 004026fc 1c 00 a2 af sw v0,local_1c(sp) 00402700 1c 00 a2 8f lw v0,local_1c(sp) 00402704 00 00 00 00 nop 00402708 04 00 40 10 beq v0,zero,LAB_0040271c 0040270c 00 00 00 00 _nop 00402710 ff ff 02 24 li v0,-0x1 00402714 2e 0a 10 08 j LAB_004028b8 00402718 00 00 00 00 _nop LAB_0040271c XREF[1]: 00402708(j) 0040271c 20 00 a2 27 addiu v0,sp,0x20 00402720 21 20 40 00 move argc,v0 00402724 69 09 10 0c jal FUN_004025a4 undefined FUN_004025a4() 00402728 00 00 00 00 _nop 0040272c 10 00 bc 8f lw gp,local_28(sp) 00402730 40 00 02 3c lui v0,0x40 00402734 58 30 42 24 addiu v0,v0,0x3058 00402738 21 20 40 00 move argc=>DAT_00403058,v0 = 6Bh k 0040273c 44 80 82 8f lw v0,-0x7fbc(gp)=>->printf = 00402e00 00402740 00 00 00 00 nop 00402744 21 c8 40 00 move t9,v0 00402748 09 f8 20 03 jalr t9=>printf int printf(char * __format, ...) 0040274c 00 00 00 00 _nop 00402750 10 00 bc 8f lw gp,local_28(sp) 00402754 1c 00 a0 af sw zero,local_1c(sp) 00402758 ec 09 10 08 j LAB_004027b0 0040275c 00 00 00 00 _nop LAB_00402760 XREF[1]: 004027bc(j) 00402760 40 00 02 3c lui v0,0x40 00402764 54 2f 43 24 addiu v1,v0,0x2f54 00402768 1c 00 a2 8f lw v0,local_1c(sp) 0040276c 18 00 a4 27 addiu argc,sp,0x18 00402770 21 10 82 00 addu v0,argc,v0 00402774 08 00 42 80 lb v0,0x8(v0) 00402778 00 00 00 00 nop 0040277c ff 00 42 30 andi v0,v0,0xff 00402780 21 20 60 00 move argc=>DAT_00402f54,v1 = 25h % 00402784 21 28 40 00 move infile,v0 00402788 44 80 82 8f lw v0,-0x7fbc(gp)=>->printf = 00402e00 0040278c 00 00 00 00 nop 00402790 21 c8 40 00 move t9,v0 00402794 09 f8 20 03 jalr t9=>printf int printf(char * __format, ...) 00402798 00 00 00 00 _nop 0040279c 10 00 bc 8f lw gp,local_28(sp) 004027a0 1c 00 a2 8f lw v0,local_1c(sp) 004027a4 00 00 00 00 nop 004027a8 01 00 42 24 addiu v0,v0,0x1 004027ac 1c 00 a2 af sw v0,local_1c(sp) LAB_004027b0 XREF[1]: 00402758(j) 004027b0 1c 00 a2 8f lw v0,local_1c(sp) 004027b4 00 00 00 00 nop 004027b8 10 00 42 28 slti v0,v0,0x10 004027bc e8 ff 40 14 bne v0,zero,LAB_00402760 004027c0 00 00 00 00 _nop 004027c4 40 00 02 3c lui v0,0x40 004027c8 5c 2f 44 24 addiu argc=>DAT_00402f5c,v0,0x2f5c = 0Dh 004027cc 40 80 82 8f lw v0,-0x7fc0(gp)=>->puts = 00402e10 004027d0 00 00 00 00 nop 004027d4 21 c8 40 00 move t9,v0 004027d8 09 f8 20 03 jalr t9=>puts int puts(char * __s) 004027dc 00 00 00 00 _nop 004027e0 10 00 bc 8f lw gp,local_28(sp) 004027e4 3c 00 a2 8f lw v0,local_res4(sp) 004027e8 00 00 00 00 nop 004027ec 04 00 42 24 addiu v0,v0,0x4 004027f0 00 00 43 8c lw v1,0x0(v0) 004027f4 20 00 a2 27 addiu v0,sp,0x20 004027f8 21 20 60 00 move argc,v1 004027fc 40 00 03 3c lui v1,0x40 00402800 60 30 65 24 addiu infile=>s_/tmp/.firmware.orig_00403060,v1,0x3060 = "/tmp/.firmware.orig" 00402804 21 30 40 00 move a2,v0 00402808 dc 05 10 0c jal decrypt_and_writeout undefined decrypt_and_writeout(u 0040280c 00 00 00 00 _nop 00402810 10 00 bc 8f lw gp,local_28(sp) 00402814 1c 00 a2 af sw v0,local_1c(sp) 00402818 1c 00 a2 8f lw v0,local_1c(sp) 0040281c 00 00 00 00 nop 00402820 1a 00 40 14 bne v0,zero,LAB_0040288c 00402824 00 00 00 00 _nop 00402828 3c 00 a2 8f lw v0,local_res4(sp) 0040282c 00 00 00 00 nop 00402830 04 00 42 24 addiu v0,v0,0x4 00402834 00 00 42 8c lw v0,0x0(v0) 00402838 00 00 00 00 nop 0040283c 21 20 40 00 move argc,v0 00402840 6c 80 82 8f lw v0,-0x7f94(gp)=>->unlink = 00402d60 00402844 00 00 00 00 nop 00402848 21 c8 40 00 move t9,v0 0040284c 09 f8 20 03 jalr t9=>unlink int unlink(char * __name) 00402850 00 00 00 00 _nop 00402854 10 00 bc 8f lw gp,local_28(sp) 00402858 3c 00 a2 8f lw v0,local_res4(sp) 0040285c 00 00 00 00 nop 00402860 04 00 42 24 addiu v0,v0,0x4 00402864 00 00 42 8c lw v0,0x0(v0) 00402868 40 00 03 3c lui v1,0x40 0040286c 60 30 64 24 addiu argc=>s_/tmp/.firmware.orig_00403060,v1,0x3060 = "/tmp/.firmware.orig" 00402870 21 28 40 00 move infile,v0 00402874 50 80 82 8f lw v0,-0x7fb0(gp)=>->rename = 00402dd0 00402878 00 00 00 00 nop 0040287c 21 c8 40 00 move t9,v0 00402880 09 f8 20 03 jalr t9=>rename int rename(char * __old, char * 00402884 00 00 00 00 _nop 00402888 10 00 bc 8f lw gp,local_28(sp) LAB_0040288c XREF[1]: 00402820(j) 0040288c 41 00 02 3c lui v0,0x41 00402890 20 32 42 8c lw v0,offset DAT_00413220(v0) = ?? 00402894 00 00 00 00 nop 00402898 21 20 40 00 move argc,v0 0040289c 80 80 82 8f lw v0,-0x7f80(gp)=>->RSA_free = 00402d10 004028a0 00 00 00 00 nop 004028a4 21 c8 40 00 move t9,v0 004028a8 09 f8 20 03 jalr t9=>RSA_free void RSA_free(RSA * r) 004028ac 00 00 00 00 _nop 004028b0 10 00 bc 8f lw gp,local_28(sp) 004028b4 1c 00 a2 8f lw v0,local_1c(sp) LAB_004028b8 XREF[2]: 004026b8(j), 00402714(j) 004028b8 34 00 bf 8f lw ra,local_4(sp) 004028bc 38 00 bd 27 addiu sp,sp,0x38 004028c0 08 00 e0 03 jr ra 004028c4 00 00 00 00 _nop

This function is responsible for decrypting the firmware. There is some sort of key derived from the local stack variables that is not straightforward to reverse. So instead of spending hours trying to figure out exactly what the code that produces the key does, why don't we just try to run the binary passing it our firmware image we want to decrypt?

$ file bin/imgdecrypt bin/imgdecrypt: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped

So we are working with a MIPSel binary. We can emulate this using qemu-mipsel-static. Copy that binary into the firmware filesystem at /usr/bin:

$ cp $(which qemu-mipsel-static) ./usr/bin

You will also need to mount /proc, /dev, and /sys in the firmware filesystem root in order to for the imgdecrypt binary to run successfully:

#!/bin/bash mount -t proc /proc proc/ mount --rbind /sys sys/ mount --rbind /dev/ dev/

Copy the firmware image you want to decrypt into the firmware filesystem root. In this case that file is DIR3040A1_FW111B02.bin

Next let's launch into a shell so that we can call the imgdecrypt binary:

$ sudo chroot . qemu-mipsel-static /bin/sh BusyBox v1.22.1 (2019-09-18 14:32:32 CST) built-in shell (ash) Enter 'help' for a list of built-in commands. / #

Run the imgdecrypt binary against the firmware image file:

/ # /bin/imgdecrypt DIR3040A1_FW111B02.bin key:C05FBF1936C99429CE2A0781F08D6AD8

Not only does it output the key to decrypt the firmware file image, but it also places the decrypted version in /tmp/.firmware.orig.

$ ls -a tmp . .. .firmware.orig

We can then run binwalk over the .firmware.orig file:

$ binwalk ./tmp/.firmware.orig DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 uImage header, header size: 64 bytes, header CRC: 0x339E3A96, created: 2020-05-09 03:35:18, image size: 17644523 bytes, Data Address: 0x81001000, Entry Point: 0x81637600, data CRC: 0x5C75FC87, 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: 23079360 bytes

Now we can extract using binwalk -eM ./tmp/.firmware.orig.

Back