Blog: Hardware Hacking

Breaking (bad) firmware encryption. Case study on the Netgear Nighthawk M1

G Richter 10 Aug 2019

TL;DR

The firmware encryption for the Netgear Nighthawk M1 is mainly XOR. It’s possible to derive the XOR key by statistical analysis, just from the firmware update file itself. It’s then possible to extract an AES key from what’s XOR’d, which can be used to decrypt other parts of the firmware file. Full decryption for inspection for other potential nasties! And a possible avenue for tampering.

This post is a high-level walkthrough of how one might fully decrypt of any current Netgear Nighthawk M1 firmware – using only easily-Googleable public information.

No CVE, no advisory. Netgear have told us this is a NOFIX. Not a problem for them, it sounds like. Have fun!

What’s the problem?

Firmware encryption. It’s generally quite a good way to make sure have-a-go hackers can’t have a look at the inner-workings of your device’s firmware. It’s a sensible way to raise the bar slightly and keep prying eyes off your device’s internals. That is, if it’s done correctly.

Most of the time, from an adversarial standpoint, we want firmware for static analysis. With that, we can reverse-engineer it and try to find bugs, which we can then try to use against the device itself.

That’s exactly what I wanted to do with the Nighthawk M1. It’s a new-ish CAT16 4G hotspot, a “top-of-the-line” consumer cellular router, with a new-ish (and extremely hard-to-find-public-information-about) Qualcomm chipset. I wanted to find bugs in it.

(source: https://www.netgear.com/images/Products/MobileBroadband/MobileRouters/MR1100/MR1100_hero.png)

Checking for low-hanging fruit

So, router in hand, I trawled the web for firmware update files. Usually in this scenario, I would try to unpack available firmware images and see if there’s anything interesting in there. But, on the Nighthawk M1, all the available update files I could find looked encrypted in some way or another (more on that later). The usual dumb low-hanging-fruit methods (binwalk, strings, etc) were coming up empty.

It’s encrypted in some way, we know that for sure. But sometimes, if the manufacturer has introduced firmware encryption after the device has been released, it’s possible to go back to older, non-encrypted, firmware images and try to reverse-engineer to find the encryption method and/or key. Then you can use that information to decrypt more recent firmware. But for this router, Netgear were only listing the most recent firmware update files. Popular Russian forums were hosting a few other firmware images, but they were also encrypted. So, it wasn’t entirely clear if encryption had been introduced recently, or if it had been baked in from the first release. But even if encryption had been introduced later, I didn’t have a pre-encryption firmware image to look at.

If we had a device we didn’t mind breaking, we could have de-soldered the flash memory and tried to read it in a socket. There were a few issues I had with this:

  • I only had one Nighthawk M1, and it was expensive, and I didn’t want to go through the eye-watering and wrist-numbing process of dead-bugging, then hand-reballing something.
  • It uses a relatively idiosyncratic combination flash/RAM BGA-package chip I didn’t have a socket for. Again – dead-bugging and hand-reballing = not a fun afternoon.
  • It’s quite a dense board, and there’s a small risk that I’d completely ruin it by putting too much hot air to it. I was doing this at home, rather than the lab.

This is the Nanya NM1484KSLAXAJ-3B combination LPDDR2/NAND Flash chip, just above the Qualcomm MDM9250.

So, in the end, all I had to work with were these massive (100MB+) encrypted firmware update files. Which looked from first glance to be encrypted. But, not all encryption is created equally.

How do you know it’s encrypted?

When you lay hands on any firmware package, you want to figure out exactly what’s inside it. There’s a few tools people like to use for this. You can find a few methods on our post about working with firmware dumps.

But what’s the almost-entirely-failsafe method of figuring out whether the firmware dump you’ve got is encrypted?

strings

Especially with larger firmware dumps, if strings doesn’t pick anything up (and you’re expecting at least *something*), it’s really likely to be encrypted. Make sure you run strings with the -e flag set to various encodings, in case you’re missing something.

$ strings -n16 MR1100-100EUS_23113509_NTG9x50C_12.06.03.00_00

_Generic_05.01.secc.spk

vl1T-W4m% ]e7 ^")

K,E>$r!!qxc`    a~S

[email protected]<-Gb$r+G9[j

hg9Dfs[31+.|~#y*4

v?_-=jO ?0n>#@[D

s]pD(6X#_tD&-NN-

dAsMJjD#=+x'LG4<b7

=GKw]I6VCDuTGvsv

E       *       [email protected] JUtn]#`

%z.rkn'-z:gVUUl1-i

?4WPG? z-L^;[email protected]

P~7^SLD0njEo:ALa+

That’s it. Nothing resembling a real word or sentence.

A lack of sensible-looking ASCII strings, in a firmware file this big (more than 100Mb), is usually an indication that the file is encrypted in some way. Searches for little-endian-ordered UTF-16 strings (using the strings -el flag) turned up absolutely nothing as well.

So, at this point, we know the M1 firmware is encrypted in _some_ way. We just don’t know *how* encrypted. If it’s encrypted badly, we might be able to decrypt it without

Entropy visualisation

This is probably the most effective way to get a sense of how encrypted a file might be. Binvis.io is pretty good for visualising smaller files but, in this case, since the file was huge, I had to use offline tools. First, binwalk.

The entropy is obviously high, with some small deviations. But, unfortunately, this 2D graph doesn’t give much as much visibility as would be useful in these situations. So, it’s worth cross-checking with the relatively elderly binvis C# application.

The file looks fuzzy, for sure. But there are definite chunks of repeating patterns. Something properly encrypted wouldn’t have repeating patterns like this, so it’s worth a lower-level look.

Well. A cursory look just at the head of the file is promising. There’s definite repeating 16-byte chunks.

This could mean any number of things. Maybe it’s something like AES in ECB mode? Maybe it’s XOR with a 16-byte key? Maybe it’s some other kind of obscure encryption? Maybe they rolled their own encryption somehow? It’s hard to tell at this point.

Let’s Try XOR

I mean, we might as well, right?

From the head of the file, you would be forgiven for assuming that if it were XOR-encrypted, the key would be 16 bytes. And, you might also be forgiven for thinking that the key might be 80404c21519bfdc5cdff2ed3660b8f6e.

Unfortunately, it’s not quite so simple. Attempting to XOR the file with that key just results in mainly trash:

We end up with some nice 0x10-aligned null-byte-delimiter-looking things. But still no sensible-looking data.

At this stage, we still don’t know if this thing really is XOR-encrypted, or actually encrypted in some other annoying and less-easily-hackable way.

Guessing a new key

So, let’s try to guess a new key. If the file is really XOR-encrypted, this is theoretically possible because we can make semi-sensible assumptions about some things:

  • There’s firmware in this file.
  • This file is so big it’s probably a bunch of different segments of different things.
  • Each of these segments probably contains executable binaries, compressed files, packed filesystems, etc.
  • Things like that tend to have lots of dead space full of null bytes.

Since JUNK ⊕ JUNK = 0x00, and lots of the unencrypted file is going to be 0x00s, if we find enough repeating chunks of JUNK within a given segment, and those chunks are a certain size, we can guess that those chunks are probably the key, and the key size is probably

There’s a nice tool out there called xortool. xortool can guess the key size for you – but it’s slow and not particularly clever on a file as big as this. Letting tools do this kind of detective work is probably going to lead to a lot more false positives and headache than if we just used a small amount of human brainpower.

So, even though it’s really unlikely a single key will just painlessly decrypt the whole lot, it’s still worth trying to see if we can figure out a key size, and then try to guess a key based on that – just to see if the inkling that is might be XOR’d is right. Even a pattern in the data which gives an indication that there IS a discernible pattern is enough to indicate that some XORing is going on.

So, how can we figure out a key size? This extremely slow bash one-liner:

hexdump -v -C file.bin | cut -d" " -f3-20 | sort | uniq -c | sort -nr | head -n 100

We hexdump the whole file (using the -v switch to make sure EVERYTHING is dumped – no * lines!), trim the address and the ASCII-attempt at the end of each line (using cut), sort the whole lot into some kind of order, and count how many of each unique line there are. The head at the end is just to avoid a total overflow of data to stdout.

What’s the rationale? If there’s a notably-higher number of unique lines than any other lines – we might have a something like a key. Or at least, a key size we can start working with.

Well, there we go. 64 lines of 16 bytes – each appearing (really roughly) 3800-ish times. The next most common 16-byte chunk only appears 521 times! This is a pretty clear indicator that the key is 16 * 64 = 1024 bytes long.

Let xortool do (some of) the hard work

xortool will try to guess an XOR key based on the length of key that you give it, and the most common byte you’d expect in the file. Then it’ll XOR the whole file for you with that key and dump it into ./xortool_out/. If it finds a few potential keys, it’ll do the same for all of those.

As it happens, xortool only finds 1024-long key. So what does the output file look like?

$ strings -n16 xortool_out/0.out

N8<Q$^BPUjfyPh()

`y7!pY:e,q"dp{zA

E!1VcCi1|RFsF=L}

[email protected]'<sbe!fTB

ALwwp9>n0_i~`W=>

Q       xAcL`%>?Q>VZ`:/L

hj;o9^>0Y+i/\lWmVpV

ppvkzn{m}sxyyxr}mzm{kupo

xm|qyxywt|o{o{lwpq

rpwozqzryuvwvxqynwownrrq

pqumynyo{rxxxws{ozoymvpp

un|p}u}vzzs{szlxkrkrnnun

kyVvkpfnviohognnesIr

g}roxlwoqlhrmrksgrlxzk{l{luuwvuv

?VdUfSinxn{k{zot

cehhihiq]wfy\t[x\szcrq|p

?gB]BVCUPY\]SS]ReO

+^8VCODNNKZHcVfKjA

s2ZIANGc7z)[\aSjOiLtL

EpRn]mPhP\PPJJUF`B

nkfq]vWwQnahUaeM

gwfx]sjqirbkrprpwhpppq=g`n_m%Yjuw

pvWqoqS_>[email protected]=

X_TgShRgPeZZv`xa

h~hlhdhRhJhBh1h%

[jK[jK[jK[jK[jK[jK[jK[jK[jK[jK[jK[j

[jK[jK[jK[jK[jK[jK[jK[jK[jK[jK[jK[jK[jK[jK[jK[jK[jK[jK[jK[jK[jK[jK[r

[jK[jK[jK[jK[jK[jK[jK[r

[jK[jK[jK[jK[jK[jK[jKZr

XrkXrkXzmXzmXpkX

YbCYbCYjEYbFYbFYbFYbFYbFYbFYbFYj

[jK[jK[jK[jK[jK[jK[jK[r

[jK[jK[jK[jK[jK[jK[jKZr

YbCYbCYjEYbFYbFYbFYbFYbFYbFYbFYj

Y^c$%uf<s<OY~v]a

!"#$%&'()*+,-./0123456789:;<=>[email protected][\]^_`abcdefghijklmnopqrstuvwxyz{|}~

!"#$%&'()*+,-./0123456789:;<=>[email protected][\]^_`abcdefghijklmnopqrstuvwxyz{|}~

!"#$%&'()*+,-./0123456789:;<=>[email protected][\]^_`abcdefghijklmnopqrstuvwxyz{|}~

!"#$%&'()*+,-./0123456789:;<=>[email protected][\]^_`abcdefghijklmnopqrstuvwxyz{|}~

!"#$%&'()*+,-./0123456789:;<=>[email protected][\]^_`abcdefghijklmnopqrstuvwxyz{|}~

!"#$%&'()*+,-./0123456789:;<=>[email protected][\]^_`abcdefghijklmnopqrstuvwxyz{|}~

b``^]d[ZhXWmUToRQuONzLK|[email protected]?g=<c:9}C<4?2Bk

!"#$%&'()*+,-./0123456789:;<=>[email protected][\]^_`abcdefghijklmnopqrstuvwxyz{|}~

!"#$%&'()*+,-./0123456789:;<=>[email protected][\]^_`abcdefghijklmnopqrstuvwxyz{|}~

!"#$%&'()*+,-./0123456789:;<=>[email protected][\]^_`abcdefghijklmnopqrstuvwxyz{|}~

uuu444|||QQQ[[[666

~}|{zyxwvutsrqponmlkjihgfedcba`_^]\[[email protected]?>=<;:9876543210/.-,+*)('&%$#"!

Wt[t_tCuGu+u/u3u

Wt[t_tCuGu+u/u3u

!"#$%&'()*+,-./0123456789:;<=>[email protected][\]^_`abcdefghijklmnopqrstuvwxyz{|}~

{ae`@6CROS:,=SPSYZXt

$#!%$"&$#&%$'&$('%)(&*('+)(+*(,+)-,*.,+/-,0.,0/-10.20/31/420431542643753864975:86:97;97<:8=;9><:?=;?>;@><[email protected]>CA?DB

HFCIGDIGEJHFKIGLJGMKHMKINLJOMKPNKQOLROMRPNSQOTROUSPVSQVTRWURXVSYWTZWUZXV[YV\ZW][X^[Y^\Z_]Z`^[a_\a_]b`^ca^db_eb`ecafdagebhfchfdigdjhekifljgmkhnlinljomjpnkqolqomrpnsqntrotrpusqvtqwurwusxvtywtzxuzyv{yw|zw}{x}|y~|z

nKmUcuE}EiuIaEa^inVQvFAFjnjRvr|FBlZ\tJLDbdX|xpLH`T

W^ZXQVTY^\]UVS]^[SQW[U__

]yIUqAy~NInfaVz~zJNJbVR|z|LjldRTxBDHlh`t

`kigmnkefcijenlajhnb`j

('%/#)+%&-)"%.,!*(." *

nKmUcuEMYQeQ~i^VQvF~zJnjbfb\z|tJtxbdh\hPL

LXP|dhbLxRldJBLzR\FJBVZRNFJ^VZ^vzAnfQ~viQ^iq~yiAeyqUEIuUYMuymME]me}}ucCMSc

NJbFBlZ\TRTx|xplH`T

[email protected]\TjbLzr|Vz|

]yIUqAy~NqNFafZ^ZrvJB

|Td>>pF|5klFIg#Gm

Now, that’s more promising. The key’s obviously not right – but we may be able to get a lot out of XORing this firmware in different ways.

Spending too much time in a hex editor

So, let’s test the hypothesis that the firmware is actually a series of other, smaller chunks all strung together in a single file.

If you recall, there was a big ol’ chunk of repeating 80404c21519bfdc5cdff2ed3660b8f6e lines at the file head. 0x100 to be precise.

Let’s take the biggest bunch of those, and see if they appear anywhere else in the file.

Hmm. There’s another chunk at 0x57B80.

And at 0x2332850…

Yep, there’s definitely some method to this madness.

A step back

So, what do we know so far?

  • We know that this firmware file is probably a bunch of different firmware parts, bundled together into a single, massive 100MB+ file.
  • We know that each part is probably XOR-encoded somehow.

But, we need to take a step back so we can figure this out in more depth. Is there any way we can make assumptions about what we are expecting to get out of the firmware file? Any kind of known(-ish) plaintext might help us here.

We can assume that Netgear didn’t build this cellular router from scratch – they’ve probably built on top of someone else’s stack. There are pre-cursors to Nighthawk M1 as well, the AirCard series of cellular routers.

What do we know about the AirCard devices? Well, they’re built by Sierra Wireless.

To cut a long, long story short – using a lot of Google and downloading a lot of Sierra Wireless router firmware images, I ended up with a normal Sierra Wireless firmware update file. It looks like this:

It’s pretty familiar-looking. The first 0x100 bytes are nulls, but after that we have some kind of header (from 0x100 to 0x144-ish), then nulls, then a date and another non-null (from 0x170 to 0x190). Then another header-looking chunk from 0x290 to 0x2b0. And another date/non-null from 0x300 to 0x320.

Let’s compare that to the header of our encrypted file:

Assuming the 80404c21519bfdc5cdff2ed3660b8f6e lines are “null” lines, there’s a very similar pattern. The first 0x100 bytes aren’t exactly “null” (they’re intermittent “null”/”non-null”), but a chunk of “non-null” data starts at 0x100 and ends at 0x140. Then there’s a non-null segment from 0x170 to 0x190. Then another “non-null” segment starts at 0x290, then a null line at 0x2F0, then more non-null data from 0x300.

The encrypted firmware follows a very similar pattern to the non-encrypted firmware. Assuming the date is part of each header, these header chunks seem to begin at 0x100 and 0x290 and last for 0x90 bytes. Furthermore, there’s 0x100 bytes of nulls before each header.

Remember our 0x100-long chunk of 80404c21519bfdc5cdff2ed3660b8f6e bytes from earlier? What if the presence of a 0x100-long chunk of these bytes indicated that a header for a particular sub-chunk of the firmware was about to begin? That would be cool, right? We could then start splitting up the massive file into smaller chunks, and work on each of those separately.

What’s in a header?

Well, in these Sierra Wireless firmware headers, not a huge amount. There’s a length field

Where do we go from here?

What do we know so far?

  • We know that this firmware file is probably a bunch of different firmware parts, bundled together into a single, massive 100MB+ file.
  • We know that the file is probably based on the Sierra Wireless firmware file format. There’s 0x100 nulls, a header, then the firmware body.
  • We know the firmware body is probably XOR encrypted.

What don’t we know?

  • We don’t know how the headers for each part are encrypted.
  • We don’t know exactly what each segment is meant to contain.

Breaking the encryption

By now we’ve got more than enough information needed to split the firmware file, run xortool against each chunk of known firmware body, and retrieve a key.

In practice, this was extremely fiddly and error-prone. Breaking encryption semi-blind is a hassle.

But, once you’ve got the key and decrypted the chunk of the firmware containing the system files, you can extract the appropriate binary, and figure out how the firmware is actually encrypted/decrypted by the device.

On the Nighthawk M1, the decryption was handled by a binary called NetgearWebApp. Here’s the header decryption function:

Turns out that AES is used to decrypt each header. The key’s hard-coded into the binary.

The binary calls the AES_decrypt function on each block of the encrypted header segment. One block at a time. Which is essentially just AES in ECB mode.

The AES key is hard-coded into the binary itself.

Then the XOR key is used to decrypt the body. As it turns out, the XOR key is the same for each segment, but shifted len(firmware body) % 32.

What do you do from here?

Doing the actual key-finding and decryption can be left as an exercise for the reader ?. A (cheap & nasty) script for playing with the firmware dumps is at https://github.com/pentestpartners/defcon27-4grouters.

How to do firmware encryption properly

Firmware encryption is often advised, but sometimes implemented badly. If you’re encrypting your firmware, here’s a few tips to make the process easier:

Don’t use XOR at all
XOR is a lightweight encryption that is too easy to break. As we’ve shown above, in many cases, you can just guess the key by statistical analysis.

Sign it, too
Sign your firmware, and properly check the signature as part of the firmware update process.

Go asymmetric
So, you encrypted the firmware. Only you, with your secret key, can decrypt it. But if you’re using symmetric cryptography, anyone who manages to extract the key can encrypt their own images. If you’re not properly signing the firmware, then that might be a problem.

Actual threat model
Do you really need to encrypt the firmware? If you’re using encryption where you actually just want assurance, then you might be better off signing.