The hacker’s nightmare cipher
As recently as 20 years ago, many microcontrollers still used OTP memory, and it was uncommon for embedded systems to support firmware updates. Today’s microcontrollers have flash memory, and most have minimal bootloaders built in to allow easy reprogramming. For a long time, the format for software updates was plain hex files as produced by most compiler/linker systems. Depending on the microcontroller, these hex files were programmed via UART, CAN or other communication interfaces. There was no encryption or authentication; anyone with access to the communication interface used could reprogram the microcontroller.
Today, many embedded systems have wireless interfaces. Sometimes wireless capability is added to systems designed years ago when no one could envision that remote access would open the systems to getting hacked. As published hacks have shown, once an intruder has gained access to this level (as with internal UART or CAN communications), microcontrollers can easily be reprogrammed with malicious code.
To address this vulnerability, many microcontrollers now feature security peripherals and functions that protect the bootloading/code-loading mechanisms. Cryptographic methods protect the software/firmware load processes with encryption and authentication. We at Embedded Systems Academy have introduced CANcrypt, a security framework for CAN communication.
However, the cryptographic methods commonly used for Internet communication can’t always be easily adapted to microcontrollers that don’t have security peripherals. A pure software solution might require too many resources (code or execution time) to fit an existing system. So what can we do to protect our minimalistic microcontroller devices from getting flashed with malicious code?
We want to protect our code even if the microcontroller has no security peripherals. We need a lightweight solution that is good enough not to be easily hacked. In recent years, I have seen various home brew bit shifting and mangling methods offer some protection.
However, when it comes to hacking encrypted hex files, such solutions have a downside. The same downside also exists with more common cryptographic methods like AES: Where the ciphertext is garbled, an attacker has a pretty good idea what the plaintext looks like. It will be either hex format or binary assembly code with recognizable patterns. Using trial and error methods with key guessing or brute force trials, you can determine if the decoding was a success just by seeing if the decoded version is a hex file or has some of the expected patterns. So you immediately know when decoding was a success.
Luckily, it’s easy to make a hacker’s life more difficult: just ensure that both the plaintext and the ciphertext look alike! If the encoded hex file looks just like another hex file with valid code (and can be flashed as such), then to truly determine if a decryption method was successful, an attacker needs to erase and re-flash the target device and give the program a try.
As a result, key guessing or brute force hacking is no longer an interesting option. Not only do the individual tries take too long, there is also the issue of the flash memory’s lifetime. Flash memory typically has a limited number of erase/write cycles so you might not have as many tries as you need for a brute force attempt.
The remaining question you might have is how to encode the hex code, the plaintext?
Well, it is time for the title of this article to kick in: What would cause the possible attacker the biggest nightmare? Yes of course: BUGS! And more BUGS!
To protect your software, simply introduce real bugs to your code. Weird concept, you say? Not if you do it in an automated, controlled way, based on a shared key only known to the sender and the receiver. It can be so easy and fun to purposely introduce bugs; there is so much room for creativity. I prefer the subtle ones: here and there replace a “branch on greater or equal” with a “branch on greater”. You can also modify branch and jump destination addresses or immediate values or switch registers; the options are endless...
If you make these changes to a hex file, make sure that all checksums in the file are modified accordingly. You still want the hex file to have the correct format; otherwise it would be easy to spot the substitutions.