For various reasons, I decided to try writing a SD card bootloader for my Ultimaker2.
The project is open source and on my GitHub here.
My goal was to install this new bootloader without having physical access to the circuitry. Thus I cannot use a ISP tool and must be done through the bootloader that is already present on the Ultimaker2. The only way to do this is to partition off a portion of application memory region for a secondary bootloader that executes after the original bootloader. But the ATmega2560 has a restriction that prevents anything in the application memory region from modifying the flash memory at all. Overcoming this restriction is what this hack is all about, continue reading if you are interested in learning more.
Popular open source 3D printers use a circuit that is derived from the Arduino Mega which is based on the ATmega2560 microcontroller. The Arduino Mega 2560 edition uses an open source STK500v2 protocol bootloader. Typically, on AVR microcontrollers, you may only edit the bootloader using a ISP tool or debugger. The bootloader should not modify itself, because if a bootloader corrupts itself, the entire circuit will be bricked, as the bootloader won’t be able to recover itself if it is already corrupted. In this case, only a ISP tool can be used to restore it.
Memory Layout of the ATmega2560
This represents the memory usage of the ATmega2560
Important to note: any code executed inside the application region is not allowed to perform any sort of flash memory erase or write operations at all. Citing ATmega2560 datasheet section 29.2.1
The Application section can never store any Boot Loader code since the SPM instruction is disabled when executed from the Application section.
This is the biggest challenge to overcome, and I’ll show you how I did it.
More In-Depth View of Memory and Execution Flow
All AVR applications have a vector table that will jump the execution to the actual executable code, skipping over an area reserved for initialization data (for strings and global variables).
The vector table also jumps to correct the locations of interrupt service routines when the corresponding interrupts occur.
When the circuit powers up, the bootloader is entered first (this can be configured by the config fuse bits). When the bootloader is done, it jumps to the very beginning of memory, where the application vector table is. The vector table will execute the actual code, in the case of the Ultimaker2, it usually executes a piece of firmware called Marlin.
The New Plan, Changes I Made
The green area is the SD card bootloader, it handles reading the file system, it is placed just before the original bootloader.
The green area is a part of the application region, so it cannot perform any flash memory erasing or writing. To overcome this problem, I put the flash memory erase and write functions inside the orange area labelled “injected code”. Whenever a flash memory erase or write operation is required, the green/app region can call the function in the orange/injected region.
The vector table of the Marlin firmware need to be modified so it executes the SD card bootloader instead of actually executing Marlin. If this was not done, then the SD card would never execute.
The yellow “trampoline” is essentially a copy of the original Marlin vector table. The SD card bootloader uses it to execute Marlin. Without it, the bootloader will just execute the bootloader again in an infinite loop.
Difficulties Encountered, and Their Solutions
The factory default Arduino Mega 2560 STK500v2 bootloader is only around 4Kbytes in size, but the AVR chip is configured for a 8Kbyte bootloader region. This fact gave me plenty of space to inject code into. Note that a typical bootloader for the Arduino Uno takes up only around 2Kbytes, and the chip would be configured for 2Kbyte bootloader, which would give me no space to inject code.
The STK500v2 bootloader doesn’t protect itself, as in, it never checks the address it writes to. Hence why I can tell it to write stuff to the empty space behind it, as well as in front of it. I just have to make sure it never actually overwrites useful bootloader code. This is done by modifying the STK500v2 Python code that is shipped with Cura (the UM2 slicing software).
Although the write address is never checked, the erase address is checked. So once the injected code is injected, it cannot be removed or modified again. So… if you do this update, you only have one chance to get it right, and you cannot do any other updates that uses a technique like this. Sorry. But I did design the injected code to be universal, so it can be reused by other types of bootloaders. (for example, update by USB thumb drive, or update over network)
To see where this vulnerability is, pay attention to this file, line numbers: 977 is where the address can be loaded without being checked, 998 is where the erase is protected, 1006 shows the write is unprotected.
By the way, did you know you can brick any Arduino Mega 2560 by simply bootloading a really big sketch? This flaw has existed for years and it doesn’t seem like anybody is complaining. My hack here depends on this flaw to work so I hope they never actually patch it.
The injected code is written in assembly, and placed precisely at a known address. It is in a file called spmfunc.S
The vector table and trampoline are generated automatically by the Makefile, and also automatically generated by the SD card bootloader code whenever new code is bootloaded from the SD card. Look in my makefile and code for the words “retargeted” and “trampoline” and “reset vector”. Most of this work is done with the help of disassembly and assembly tools, but the bootloader itself also does it during runtime (look at this file for the functions named “make_jmp”, “make_rjmp”, where it says “if (fa < SPM_PAGESIZE) // If is very first page", where it says "else if (fa == (BOOT_ADR - SPM_PAGESIZE)) // If is trampoline", and where it says "asm volatile("jmp (__vectors - 4)");") My makefile has some hilariously ugly code, sometimes because GNU Make sucks for math and string manipulation. There are grep, sed, and awk scripts being used to overcome the limitations of GNU Make. If you enjoy how this bootloader works, you might also be interested in learning about the ATtiny85 V-USB bootloader. It deals with similar challenges, needs a more complicated trampoline and more complicated modifications to the vector table, but it doesn’t need to inject code because SPM is unrestricted on ATtiny.