This page describes several ways of 0wning your r0ket (i.e. retrieving the original “vendor” keys for encrypting&signing new l0dables and for encrypting&signing messages on the wireless mesh).
The r0ket uses a mesh-network to convei time, highscore and other messages. To protect these values from evil hackers, the mesh encrypts and signs messages sent to the mesh using a secret key, stored read-protected on the flash of the processor.
Looking at the firmware-source uncovers a severe bug in the font-handler (commit). Here, a static sized buffer of 600 bytes (charBuf
) is filled with data read from the font-file. The amount of data is dependent on the width and the height of the font divided by 8.
height = (font->u8Height-1)/8+1
Setting u8Height
to 255 results in a height-value of 32. With a maximum width of 255, width*height can reach 8160 bytes, which is nearly the size of the whole SRAM of the chip (8KB).
objdump
then shows that the charBuf
is located somewhere at the beginning of SRAM (0x10000bb0 in my case). The buffer is not located on the stack, so one cannot immediately overwrite the return address.
A modified firmware (reveals the stack-pointer at the relevant code in DoChar to be around 0x10001560 (__asm( “mov %0, sp\n” : “=r” (x) :);
)
However, the actual overwrite will happen somewhere deeper (f_read
→ memcpy
…) but this is not so much of interest.
To test stuff, I inserted the blink code (from l0dable/blink.c) into the firmware and used objdump to retrieve its address. Now I create a custom font, with a height of 255 and a width of 78 (0x1560-0xbb0/32 + 1 = 78) (play around a little with these values if it doesn't work at first). The font format is a little obscure:
u8Width // 0 normal, 1 compressed u8Height u8FirstChar // first char present in font u8LastChar // last char present in font uint16_t extras // number of additional chars present extras * uint8_t extra_char u8Width // width of this char char data[]; ...
The data part is then filled with the desired return address and make sure, that you pad correctly, so that the return address actually starts at a 4-byte boundary.
After a couple of hours of no success, you will learn about ARM’s thumb mode which requires you, to add 1 to the destination address. Loading the font, my r0ket now starts blinking :D yay
Time, to write an exploit that dumps the rom to the dataflash. It’ll be located at the charBuf address, so we use the l0dable build process (which loads the binary into sram and executes) but adopt for the location using a custom .ld
file.
MEMORY { sram(rwx): ORIGIN = 0x10000bc4, LENGTH = 600 } INCLUDE ram.ld
Now all that’s left to do, is to fill the charBuf with the compiled binary, and pad the rest of it with address of charBuf
(+1!). Running it dumps the ROM. And fortunately the exploit works without modifications on the original firmware.
Extracting the keys from the dumped ROM is then easy, if you compare it to a self-compiled on with known keys and location, as the surrounding data will stay mostly constant.
If you're not so keen on building your own ARM binary exploit, there is another bug in the font renderer which is a lot easier to exploit, but which makes dumping the keys a bit, mh, cumbersome. Here's how (see firmware/lcd/render.c):
When a new font is selected, the font data structure (efont.def) is filled with metadata from the “external font” file (u8Width, u8Height, u8FirstChar, u8LastChar). Note that the data pointer (au8FontTable) and extras pointer (charExtra) are not changed.
For u8Width == 0 and u8Width == 1, the DoChar() renderer distinguishes properly between internal and external fonts - but it doesn't for other values. So by installing a .f0n file which only contains the bytes “\x07\x08\x20\x80\xee\xee” (the last two bytes are necessary because an “extras” value is read, too), you can now use the internal “7×8” font as an external font, as you can test in the 'nick→chooseFont' menu. By increasing the value of lastChar and the dimensions of the glyphs you can even look far beyond the actual data table of the internal font. (Note that “look” is meant very literal - if you write an increasing byte sequence into 'nick.cfg', you literally get a display dump of a certain memory area.)
Unfortunately, the keys are located _before_ the font data in the firmware image, as you can easily check in your own builds. This is where the “extra glyphs” feature comes into play, which was originally meant to allow fonts to include glyphs outside the [u8FirstChar,u8LastChar] range…
The offset from the start of the font data pointer is calculated as:
toff = c * font->u8Width * ((font->u8Height-1)/8+1) data = &font->au8FontTable[toff];
c is the glyph index and ought to be in the range [0,u8LastChar-u8FirstChar+numExtras], but it is at least type 'int', so it is theoretically possible to make the outcome negative. And indeed, the responsible function _getIndex() is vulnerable to fonts like this:
07 08 f0 00 ee ee f0 00 f1 00 f2 00 ...
Input characters in the range '0xf0-0xff' are then mapped through the “extras” feature because they are greater than u8LastChar, and their index value is calculated as (u8LastChar+1+matched_extras_entry-u8FirstChar), which happens to be (-0xef) for input 0xf0. This trick enables us to read far enough to discover the keys, encrypt&sign a proper firmware-dump l0dable and retrieve the rest… although we must admit that reading all those pixels from the display and converting them back to data was painful.
We (from Chaostreff Heilbronn village) also discovered some attack vectors. Even if we didn't extracted the 128bit holy grail, these vectors should be closed to prevent others from p0wning your device
Finally we want to say Thank You too all the people who inspired us (CCCP, deadbyte, red becon, TkkrLab, and others).
Basically by using the charBuf overflow already mentioned above, you're able to overwrite some special data structure, containing up-codes. These up-codes are executed in IAP (In Application Programming) context. This means, you can place custom ones there. PoC may follow.
Additionally, this overflow is able to write into the section, where the l0dables
are located. This means you can do some kind of heap spraying by using special prepared custom fonts.
We've discovered, that 0x00 0x00
is a possible equivalent to NOP.
Countermeasure: fix charBuf buffer overflow
The method filetransfer_receive()
within recvcard.c
doesn't validate
the incoming file name but using the file name directly from the received meta data.
Thus it's possible to send l0dables (*.c0d).
This could be an option for remote exploits, when combining with other vectors.
Countermeasure: Extra check the incoming meta data and create only valid VCARD files.
We didn't were scared about the XXTEA crypto algorithm which protected the l0dables, but also tried to circumvent the checks. In the end we came up with a two staged approach, which may work, if some one fixes the “firmware bug”
Stage 1:
Prepare some l0dable with your shell code by
a) do not encrypt it b) add 16 bytes of padding in front of your code (using NOPs and/or hex editor) c) make sure the file size is not a multiple by 16 (simply add some 0x00s)
Put this l0dable on your original protected device and execute it.
This should result in error code size!
. But our shell code is now loaded right in place.
Stage 2:
Now we want to execute our code. Therefore we spotted out two possible options,
where no special checks/validations are existing.
a) Zero-size l0dable: Create a file with size=0 and execute it right after stage 1. This will keep our stage 1 code in place and bypass the size check. Unluckily this will end up in a integer overflow within the ''xxtea_cbcmac()'' see ''len-4''. b) 16-byte l0dable: Create a file with size=16, containing 0x00 and execute it right after stage 1. This will also keep our stage 1 code in place, bypass the size check and bypass the MAC check. Unluckily this will end up in a division by zero exception within ''xxtea_decode_words()''. At this point, only a programming bug prevents us from successfully running our code! Because every static code analyzer will spot this as a devision by 0 programming error, there is some hope, that someday this could be fixed and exploited. Remember the OpenSSL issue CVE-2008-0166? ;-)
Countermeasure: (1) Clean the memory before loading new l0dables. (2) Do more checks before executing loaded code.