D-Link DWR-978 (MT7622 with 5G NR modem)

Hi everyone,

I'm currently working on DWR-978, it's an MT7622 with AC2600 wifi (with MT7615).

Ethernet took quite some time to get working, eventually turned out this uses a Realtek RTL8367S switch rather than MT7531 :innocent: .

The 5G modem is a Compal RXM-G1 M.2 card, connected via both USB and PCIE, using the MHI protocol which is already available in Linux (got it working with LuCI modemmanager at some point :slightly_smiling_face: ).

Current progress:


  • MAC addresses
  • factory image
  • lots of polishing: required vs. unneeded packages, make 5G work smoothly (it somehow doesn't on my latest build :see_no_evil: ), use nvmem-cells correctly etc., find remaining GPIOs (reset for 5G modem? maybe reset + power enable to enter fastboot mode? Modem is based on Snapdragon X55).
  • figure out if GNSS can be enabled (should work with the existing Sub6 antennas)
  • make wiki article to paste bootlogs

The modem also has some unused connectors for mmWave antennas, I found those special three-pin connectors are discontinued (Murata MM3929-2701A03, to connect to Qualcomm QTM525), but maybe we can get some for experiments...

Does anyone own this device already? They are still quite expensive, but sometimes available for less than 300 €, used on ebay.

Next thing to look at would be factory image I guess (maybe @RolandoMagico wants to have a look as well :blush: ), it is a 0x78 0x74 bytes header followed by xor, similar to ELX, but something different... Hopefully there's no RSA signature etc., since I could not find any means of recovery by pressing the reset button on boot (but as an MT7622 device, maybe we could achieve something via Mediatek SP Flash Tool and a matching scatterfile?)

I had a short look in the images on the D-Link server. Looks quite encrypted :slight_smile:
The data on offset 0x2E (32 bytes wich looks like hex in ascii) look similar to the AES salt on the M30/M32/R32.
Didn't find any keys in the GPL sources for this device, the readme also states that the created image would be unencrypted.
But if it is really AES, the key should be somewhere in flash (maybe in the config or factory partition?)

Which image did you have a look at and how do you get to 0x78?

I had looked at v01.00.0037 from the D-Link TSD site. Are there others / country variants etc.?

$ hexdump -C dwr-978_v01.00.0037.bin | head
00000000  03 1d 61 29 07 44 57 52  2d 39 37 38 00 00 00 00  |..a).DWR-978....|
00000010  00 00 00 00 00 30 31 2e  30 30 2e 30 30 33 37 00  |.....01.00.0037.|
00000020  00 00 00 00 00 62 35 32  32 61 35 30 63 00 62 37  |.....b522a50c.b7|
00000030  38 36 31 34 31 33 62 37  33 30 34 63 65 35 61 36  |861413b7304ce5a6|
00000040  61 61 33 34 65 39 64 38  62 33 65 38 61 64 00 32  |aa34e9d8b3e8ad.2|
00000050  30 32 32 2d 30 37 2d 30  34 00 00 40 00 00 00 02  |022-07-04..@....|
00000060  20 03 00 02 00 00 01 f6  d0 00 87 69 49 16 57 9a  | ..........iI.W.|
00000070  b8 47 00 00 f9 99 b4 c8  12 d6 e3 8e 29 94 4a 1d  |.G..........).J.|
00000080  12 d6 e7 de 29 94 4a 0d  12 09 84 d3 29 94 4a 35  |....).J.....).J5|
00000090  12 09 84 c2 29 94 4a 55  12 d6 e6 26 29 94 4a 25  |....).JU...&).J%|

I have not yet de-xored it, but I recall I even saw the code in the GPL tarball, but it's been too long ago :innocent: Will have to search again, bringing up ethernet wasted way too much time for this device.

// edit: I just checked my ghidra project for this device, there is
./PKGS_3RD/fwhandle/fwhandle_util available in the GPL tarball, which is used by sysifd which is located in the PKGS_3RD directory as well.

"Encode Key" is just for the xor stuff, seems to be derived from something, so we don't even need to extract it from the padding at the end of the file :joy: But I forgot most of it already, ghidra project was started in early january -.-

Sorry, I should have collected all the information I previously gathered before opening a thread for the device...

The most difficult part was to figure out the parameters of fwhandle_util, since it does not print a help. But x should be the correct for extract, the fw_with_header is the file from the D-Link server here:

./fwhandle_util -x fw_with_header -t kernel_pad_apps
------- Header Info ----------
Image Header Size      : 0x0074
Image Header Magic Code: 0x031d6129
Image Data Size        : 32952320
Image Type             : 7 [kernel_pad_apps]
Compression Type       : TBD
Model Name             : DWR-978
Model ID               : 0x00422003
Build Date             : 2022-07-04
Version Firmware       : 01.00.0037
Version Code SCM       : b522a50c
Version Config         : b7861413b7304ce5a6aa34e9d8b3e8ad
Data CRC Checksum      : 0x87694916
Header CRC Checksum    : 0x579ab847

t seems to be the string that is used to generate the actual XOR value (though we can get it from the image padding of course), version code SCM looks like a git hash... about Version Config I'm not sure.

I had also grepped through the GPL tarball to find scripts that use this tool, this helped a lot in finding which parameters are passed which values:

/ # grep -rn fwhandle .
./www/cgi-bin/enap.fcgi:49521:fwhandle -s /storage/token.dat -d /tmp/token.tmp -t token -i DWR-978 -v 01.00.0034 -m %s -c 0x%x -p 0x%x -D %s
./www/cgi-bin/dlcfg.cgi:2283:fwhandle -s /storage/token.dat -d /tmp/token.tmp -t token -i DWR-978 -v 01.00.0034 -m %s -c 0x%x -p 0x%x -D %s
./apps/sbin/cli_factory:16907:fwhandle -t kernel_pad_apps -x %s
./apps/sbin/sysifd:51004:fwhandle -s /storage/token.dat -d /tmp/token.tmp -t token -i DWR-978 -v 01.00.0034 -m %s -c 0x%x -p 0x%x -D %s
./apps/sbin/sysifd:51008:fwhandle -x /tmp/token.tmp -t token
./apps/sbin/sysifd:51031:fwhandle -x /tmp/fw_with_header -t kernel_pad_apps
./apps/sbin/sysifd:52355:fwhandle -t kernel_pad_apps -x /tmp/fw_with_header
./apps/www/cgi-bin/dlcfg.cgi:2283:fwhandle -s /storage/token.dat -d /tmp/token.tmp -t token -i DWR-978 -v 01.00.0034 -m %s -c 0x%x -p 0x%x -D %s
./apps/www/cgi-bin/enap.fcgi:49521:fwhandle -s /storage/token.dat -d /tmp/token.tmp -t token -i DWR-978 -v 01.00.0034 -m %s -c 0x%x -p 0x%x -D %s

I'm still eager to find the actual algorithm for generating the XOR (which by itself seems to be using some multi-stage xor mangling...), although it's technically not necessary :slightly_smiling_face:
I hope I will find sometime in the coming weeks, at least now that the ethernet switch stuff is finally solved...

The firmware header has looked quite familiar all the time, but only now I see which device it resembles: COVR-2200 (available as COVR-2202 set).

The header magic 0x011D6129 is identical, but the rest is slightly different, will have to check the GPL tarball and see if there could be a single util to support both devices.
COVR-2200 was designed by T&W, built by SGE. DWR-978 is also from T&W, though it seems they used a different manufacturer(?).

So this format is probably for more T&W devices, also from other brands (and their own). I guess I have to dig the 2200 back out (the issue was that the second 5GHz wifi did not come up (maybe missing some LNA pins etc.) :slightly_smiling_face:

Quick update on the factory image generation, it seems the type 0x07 image of DWR-978 and type 0x03 used with COVR-2200 are quite different, even if mostly in the offsets and order of elements (besides minor differences, e.g. COVR-2200 uses gmtime() format to encode firmware build date).

Not sure if we need e.g. the SCM version (i.e. git hash) at all, can we even expect every OpenWrt build to happen from a correctly initialized git repository without breaking things? And that would not even include uncommited changes in the working directory...

As for now, cheated with the header and used a template for the first values, only data size, data checksum and header checksum are calculated after xor-ing the input file. At least this recreates the identical factory image for DWR-978 after it was previously unpacked with fwhandle_util :slightly_smiling_face:

DWR-978 factory image
import binascii

infile_path, outfile_path = "source.bin", "factory.bin"

header_template = bytes.fromhex(
    "03 1D 61 29 07 44 57 52 2D 39 37 38 00 00 00 00"
    "00 00 00 00 00 30 31 2E 30 30 2E 30 30 33 37 00"
    "00 00 00 00 00 62 35 32 32 61 35 30 63 00 62 37"
    "38 36 31 34 31 33 62 37 33 30 34 63 65 35 61 36"
    "61 61 33 34 65 39 64 38 62 33 65 38 61 64 00 32"
    "30 32 32 2D 30 37 2D 30 34 00 00 40 00 00 00 02"
    "20 03 00 02 00 00")

xor_pattern = bytes.fromhex("29 94 4A 25 12 09 84 C2")

with open(infile_path, 'rb') as infile:
    header = bytearray(header_template)
    source = bytearray(infile.read())

    xorpos = 0
    for i in range(len(source)):
        source[i] ^= xor_pattern[xorpos]
        xorpos += 1
        if xorpos >= len(xor_pattern):
            xorpos = 0
    header += len(source).to_bytes(4, byteorder = 'big')    # data size
    header += binascii.crc32(source).to_bytes(4, byteorder = 'big') # data crc
    header += bytes([0] * 6)
    header[0x6e:0x72] = binascii.crc32(header).to_bytes(4, byteorder = 'big') # header crc

    with open(outfile_path, 'wb+') as outfile:

Wasted hours messing with Makefiles and bash, but couldn't find a way to generate factory header without Python...

this would introduce space characters after each line
define Build/dwr978-header
	echo -ne "\x03\x1D\x61\x29\x07\x44\x57\x52\x2D\x39\x37\x38\x00\x00\x00\x00" \
			 "\x00\x00\x00\x00\x00\x30\x31\x2E\x30\x30\x2E\x30\x30\x33\x37\x00" \
			 "\x00\x00\x00\x00\x00\x62\x35\x32\x32\x61\x35\x30\x63\x00\x62\x37" \
			 "\x38\x36\x31\x34\x31\x33\x62\x37\x33\x30\x34\x63\x65\x35\x61\x36" \
			 "\x61\x61\x33\x34\x65\x39\x64\x38\x62\x33\x65\x38\x61\x64\x00\x32" \
			 "\x30\x32\x32\x2D\x30\x37\x2D\x30\x34\x00\x00\x40\x00\x00\x00\x02" \
			 "\x20\x03\x00\x02\x00\x00" > "$@.new"
	@imagesize="$$(stat -c%s $@)";
	datacrc=$$(gzip -c $@ | tail -c 8 | od -An -tx4 --endian little | cut -d " " -f2);
	echo -ne $(imagesize) >> "$@.new"
	echo -ne $(datacrc) >> "$@.new"
	echo -ne "\0\0\0\0\0\0" >> "$@.new"
	headercrc=$$(gzip -c $@.new | tail -c 8 | od -An -tx4 --endian little | cut -d " " -f2);
	#todo: inject header crc
	echo -ne $(headercrc) >> "$@.new"
	cat $@ >> $@.new
	mv $@.new $@

(...and the rest with sizes and crc probably won't work as well, so I'll just go back to Python instead -.-)

// edit: updated branch https://github.com/s-2/openwrt/tree/dwr978_github to generate factory image, can be successfully flashed via OEM web updater :slightly_smiling_face:

The actual XOR'ing was moved to the buildstep for performance.

There is unfortunetately no recovery, at least not accessible by pressing reset button during power-on, so UART would be required to unbrick (to revert to OEM, the unpacked firmware can be flashed to firmware partition using mtd write).