Another way to grab the flash of the device is by taking a look at the software updates. Luckily, Coolermaster seems to be actively working on the firmware, killing bugs and adding lighting modes, and you can download the latest version from their website. The website actually has two versions: seemingly there's an EU and an US version of the firmware. That's because the hardware between the two is actually a tad different: the EU version has a key extra.
The firmware updater is a single executable, with the firmware itself tucked into it somehow. Before I could take a look at that, I needed to separate it from the rest of the executable. I used the fact that there are two firmwares available to my advantage here. My line of thought was that the UI and upgrade functions of both versions of the keyboard should be the same; there probably aren't any differences in the PC code that handles the upgrade. That means most or all of the differences in the two firmware executables should be in the firmware that's sent to the keyboard. To test this, I ran a binary diff over the two files and indeed, all differences were concentrated in an 16K block at the end of the executable, neatly separated from the rest of the file by zero padding.
I cut out this section of the executable, and took a better look at it. It seemed indeed to be firmware-related: for example it started with the characters '1.1.7' which is the version number of the firmware update I downloaded.
The rest didn't really strike me as something that belonged in the flash of a microcontroller. For example, this is an USB keyboard and you'd expect the USB descriptor strings to be in the firmware somewhere. The USB descriptor strings are UTF-16 strings indicating the name, manufacturer etc of the device to the OS. Here, no UTF-16-string could be found at all, though. If this actually was the firmware, it would either be compressed or encrypted.
To see what was going on, I took a better look at the actual bytes using a hex-editor. Here's a pretty
telling bit of it:
Two things can be seen here. First of all, there's a repeating string of 'A5 CA 88 A5' going on. No compression worth its salt would leave those repetitions here; they can be compressed even with a trivial compression system like RLE. Secondly, the byte 'A5' seems to be occurring an awful lot in this firmware, and not only in the bit I've shown. That's not a byte you'd expect to occur that much. Normally, you'd expect the byte '00' to do this, mostly because it's used as padding, when you e.g. store an 8-bit value into a 32-bit variable.
So there seems to be something going on that as one of the effects replaces 00 by A5. There's only one widely-used encryption that's byte-granular and has that effect, and that is XOR-encryption. So I tried reversing the encryption by XORring every byte with 0xA5 and lo and behold, USB-16 description strings show up! (I later found out that apart from the XOR encoding, also a few of the first 1K blocks were swapped.)
Now I had a working firmware dump, right? Well, throwing the decrypted firmware into a disassembler indeed gave me a fair amount of valid-looking ARM code. It's not the entire flash contents, though: there are some jumps that jump outside of the bit of code I had here. Also, there's a large block that seems to be corrupted somehow: there are code jumps into it, but no valid ARM-code. All in all, while this helps, it's no solid base to start programming Snake on yet.
There is another thing you can do with an executable file, and that's to actually execute it. With an USB sniffer (in this case USBPcap) in the background, I could then review the packets going back and forth and see if I could glance something from that. Also, my own keyboard firmware still was an old version, so updating would probably be useful anyway.
After looking at the packet captures, I found out that the firmware update handles the communication to the keyboard by sending packets to USB endpoint 3 and receiving them from endpoint 4. One of those packets actually seems to switch the keyboard into a dedicated flash mode: the keyboard re-enumerates with a different PID. The update packets have this format:
How are those packets used? Well, this is what happened during a firmware upgrade, plus what I deduced the packets could mean:
|1:2||0x2800-0x2808||(kb->pc) "1.1.5"||<- Read version|
|4:1||0||(Device re-enumerates)||<- Enter flash mode|
|0:8||0x2800-0x2808||-||<- Erase version|
|0:8||0x2c00-0x6ad4||-||<- Erase firmware|
|1:1||0x2c00-0x6ad4||Multiple, uploads fw||<- Write fw|
|1:0||0x2c00-0x6ad4||Multiple, uploads fw||<- Verify fw?|
|1:1||0x2800-0x2808||(pc->kb) "1.1.7"||<- Write new version|
|4:0||1||(Device re-enumerates)||<- Exit flash mode|
Now, there's something interesting going on here. To erase the old and write the new firmware version number, the firmware update sends the same commands as used to erase and flash the firmware itself, but to a different region: 0x2800 instead of 0x2c00. From what, I could conclude that the firmware version is just stored in a separate sector in the flash, simplifying the management of it: the firmware wouln't need to implement a completely different set of commands just to manage the version number.
But wait, the same 0x2800 address is is used to read the firmware version. If the firmware version actually isn't more than data that happens to be in flash, it would make sense that the firmware version read command actually isn't anything more than a general flash read command, useful for the entirety of the flash. That would be incredible useful to me: by running that command over the entire flash, I could essentially grab all the data that's in there.
I whipped up a small program using libusb that would do exactly that. The program ran without a hitch, but what I got back wasn't exactly what I expected: aside from the sector the version was in, the reads returned all zeroes. I didn't seem wrong in my assumption that the command to read the version was a generalized flash read command; for example, when I increased the address by one, I would get all the data one byte shifted. It looked like something else was keeping me from reading the interesting bits. Well, a big bunch of the firmware update I decoded earlier was still corrupted, but maybe I could find some info in the bits that were readable?
And after a bit of searching, I found some relevant code. This little chunk is called every time a byte is read from the flash. It basically checks if the byte is read from an allowed region, and if so it jumps out to the code that sends it. If not, the last line in the bit of code will replace it with zero. This code would indeed result in the behaviour I saw. In theory, this is easily fixable: removing the final line and replacing it with a NOP would mean the read byte would be passed as-is.
The replacement of the code with a NOP means a modification of exactly one byte, and after some re-encrypting and twiddling with offsets, I knew exactly what byte I had to change in the original firmware in order to have it flashed to the keyboard. By changing this byte, however, I made what's essentially a corrupted firmware update. I didn't see any CRC checks that I could easily spot that would stop the keyboard from running the code, but I might have missed a check that would make the keyboard reject the upgrade. And then? Would it still accept a new firmware or would I have a dead keyboard on my hands? Only one way to find out...
Well, after starting up Windows and running the executable, nothing special happened: it looked like
the firmware updater at least doesn't check itself. Plugging in the keyboard and starting the update
also worked without a hitch. After a while, my fears were relieved: the keyboard did actually light
up again after the firmware update was done. But did the new firmware actually take hold? I ran my
program again and this time the dumped data looked much better:
After running the dumped image through the disassembler, I could indeed confirm that this looked very much like it was a complete flash dump. No weird corrupted bits, no links into the middle of nowhere, just everything you'd expect from an unencrypted firmware dump. Seems like I had a backup of the keyboard! Apart from me now being able to reverse engineer everything, this also meant that if I ever were to make a boo-boo while developing Snake which bricks the keyboard, I could just use the ROM bootloader to revert the flash to a working state. It also meant my JTAG/SWD jig got a lot more usable: by doing a bulk erase of the microcontroller and then restoring the backup I had, I could disable the nasty protection bit and e.g. single-step through the flash code again.