In our previous post we described an attack where we load an arbitrary value indirectly into the PC register using EMFI. During that attack we achieved our initial goal where we wanted to last year's research, where we turn Data Transfers into Code Execution, into practice.
As we already pointed out, the Flash Encryption feature of the ESP32 chip, which Espressif advises to use, significantly increases the complexity for bypassing Secure Boot. The typical approach of making modifications to the plain-text code or data stored in external flash is not possible any more once Flash Encryption is enabled.
In this post we describe an attack where we leverage the (hardware) vulnerability described in our previous post in combination with another vulnerability (i.e. CVE-2020-15048) in order to load an arbitrary value into the PC register.
The ESP32 supports Flash Encryption which is used to assure the confidentiality of the code and data stored in external flash. Interestingly, decryption of the data is done transparently to the processor by the flash controller. It uses a tweaked AES-256 hardware implementation to assure the plain-text data cannot be obtained from the cipher-text without knowledge of the decryption key.
The decryption key is stored in OTP-memory and cannot be access directly from software. This secure key storage was found to be vulnerable to a Fault Injection attack for which the results are published. Our attack is not related to this research.
It's important to note that the decryption key is actually not used as-is. It is actually used to generate unique keys for each 32-byte block in the external flash. This is likely done to mitigate several of the issues applicable when the standard AES-256 algorithm is used. These issues are mostly related to the cipher mode that is used as the underlying cryptographic algorithm is sufficiently secure. For example, when ECB mode is used it's possible to swap blocks and when CBC mode is used it's possible to flip bits.
Nonetheless, as the unique keys are only generated for each 32-byte block, and the block-size of the underlying cryptographic algorithm is 16 bytes, adjacent blocks are still encrypted with the same key. This is exactly what we leverage, in combination with several design and implementation issues, to bypass Secure Boot using a very similar attack as described in our previous post.
The chip's communication with the external flash, shown in the picture below, already indicated to us that there were more stages loaded during boot than just the bootloader.
We observe several patterns during which different parts of the external flash are copied from external flash to internal memory. Without further investigation we do not exactly know what the parts are.
Conveniently, the ROM prints during its execution, on the serial interface, information about these parts. This printing cannot be disabled as far as we know and is therefore always available. This information allows us to know, without analyzing the flash communication itself, exactly when what is copied.
We concluded the following:
+ load:0x3fff0008,len:4 <-- C
+ load:0x3fff000c,len:3220 <-- D
+ load:0x40078000,len:4816 <-- E
+ load:0x40080400,len:18640 <-- F
+ entry 0x40080740
The ROM extracts the load address and length for each part from the external flash itself. This information is stored in so-called headers which is stored at the beginning of each part. For example, the header for part 'C' is shown below. The last two 4-byte words are the load and len in the above snippet.
00001010 00 00 00 00 00 00 00 01 08 00 FF 3F 04 00 00 00
It's important to realize that this information is printed before any signature is verified. For example, the ROM will print the following if these values are set to
0x41414141, even though it will result in a chip that does not boot as these values are invalid. Note,
0x41414141 in decimal.
Unfortunately, all data is encrypted once Flash Encryption is enabled, including these headers. This means we cannot simply set the values contained in the header to arbitrary values as they will be decrypted into garbage as we do this research under the assumption that we do not have access to the decryption key.
Interestingly, Espressif's SDK stores the different parts of the flash image next to each other. This is shown in the picture below.
+ 00002F70 20 00 A8 08 92 AB FF 90 9A 10 C0 20 00 99 08 C0
+ 00002F80 20 00 A8 08 91 CE FB 90 5C 7A 00 40 00 99 08 8C
- 00002F90 12 65 ED FF 1D F0 00 00 00 04 08 40 D0 48 00 00
- 00002FA0 28 00 FF 3F 00 00 00 04 44 00 FF 3F 54 7D 00 40
The green data is loaded during part 'E' and the red data is loaded during part 'F'. The last 16-byte block of part 'E' is encrypted with the same key as the first 16-byte block of part 'F'.
Interestingly, because last block of part 'E' and the first block of part 'F' are encrypted with the same key, we can leak the last two 4-byte words of part 'E'. This works as the first block of part 'F' is actually the header that contains the information that is printed on the serial interface prior to the Secure Boot check.
For example, if we overwrite the last block of part 'E' with the first block of part 'F', the ROM prints the following.
- load:0x40007a5c,len:2349373696 # last two 4-byte words of part 'E'
+ entry 0x40080740
Please note that the both blocks are encrypted and we actually copy an encrypted block over another encrypted block. We cannot do anything else as we assume we do not have access to the decryption key, similarly as a real-world attacker.
This sequence of choices made during the design and implementation of the ESP32's Secure Boot implementation allows us to brute force arbitrary values of the last two 4-byte words of part 'E'. Nonetheless, brute forcing an arbitrary value, considering we do roughly 10 experiments per second, takes more than 10 years (i.e. search space is 2^32). Optimizations are likely possible, but we did not investigate this.
Regardless if we can optimize the brute force to something reasonable, we do not know if it's even feasible to load a value into the PC register if we can only control a single 4-byte value. Therefore, we decided to determine exactly that before we deal with the limitations imposed to us by the brute force requirement.
We are performing the attacks on a development board and therefore we are able to set the Secure Boot and Flash Encryption keys. It allows us to identify if the attack is applicable using a fully controlled environment. Even though a real-world attacker is unable to do so, it's not unlikely an attack will do the vulnerability identification on a development board as well.
We started our experiments by overwriting part 'E', in a plain-text flash image, entirely with address pointers to a function in ROM that prints something on the serial interfaces. This is the exact same address we used for identifying a successful glitch as during the attack described in the previous post. This part is stored in flash from address
0x1cc0 to address
Then, we encrypt the plain-text flash image with the correct encryption key and program the encrypted result using the ESP32 SDK. This allows us to make arbitrary modifications to the plain-text flash image. Of course, this is definitely not possible by a real-world attacker as the encryption key will not be known. Nonetheless, this is definitely a valid approach for working towards attack identification.
For our first experiments we injected glitches while part 'E' was being copied by the processor from external flash to internal memory. Even though this attack window is rather large, we are really affected by timing as we know that part 'E' is entirely overwritten with the address pointer.
The results of this campaign are shown in the picture below.
Interestingly, we observed four experiments for which we execute ROM function successfully. As anticipated, the successful results are spread out across the attack window. Like with the other attacks performed on the ESP32 chip so far, we are able to reproduce the attacks with a high success rate (i.e. higher than once per minute).
Working towards success
We set the glitch parameters to that of the successful glitch that is closest to the end of part 'E'. The two 4-byte words that we can potentially brute force are copied last as they are present in the last 16-byte block of part 'E'.
Then, we slow start overwriting part 'E' with 0x100 byte chunks of
0x41 bytes until we cannot reproduce the successful glitch any more. This tells us exactly, which 0x100 byte chunk of data contains the pointer that is loaded into the PC register. For this first result we concluded the pointer is taken from the chunk at offset
Then, we start targeting the remaining attack window again in order to find a successful glitch closer to the end of part 'E'. After a couple hours of testing we identified a successful glitch for which the result is shown in the picture below.
Again, we fix the glitch parameters and we slowly start overwriting part 'E' again until we cannot reproduce the attack anymore. This time we concluded the pointer is taken from the chunk at offset
Then, we start targeting the remaining attack window again in order to find a successful glitch closer to the end of part 'E' which is shown in the picture below.
At this point it was late at night and the temperature in our office dropped significantly. As a result, we observed alterations in the behavior of the chip compared to the previous experiments. The result was that we could not reproduce the successful glitches any more.
Therefore, we decided to randomize the glitch power in order to generate the successful glitches again. Moreover, as it was late, and some test runtime was ahead of us, we decided to simply set only the last two 4-byte words of part 'E' to the address pointer. If successful, it proves that the attack we envisioned works.
After roughly 300,000 experiments, which took roughly 12 hours, we got two successful glitches for which the results are shown in the picture below.
The only thing left to do was to verify which of the two 4-byte words was actually loaded into the PC register. At first, we tried to set only the last word to the address pointer, but we were unsuccessful in reproducing the attack.
Once we set only the other 4-byte word to the address pointer, we were able to reproduce the attack. This demonstrates that, it's practical to load an arbitrary value into the PC register of the ESP32 during boot, assuming you can control a single 4-byte value. The latter of course is affected by the brute force described earlier in this post.
We demonstrated an attack on the ESP32 that bypasses both Secure Boot, even when its Flash Encryption feature is enabled. The attack allows an attacker to load an arbitrary value into the ESP32's PC register. The attack can be described using our FIRM as follows.
Nonetheless, due to the Flash Encryption feature, an attacker needs to brute-force a 4-byte word which is leaked via the serial interface. This requirement is somewhat restrictive and definitely costly time-wise. Espressif requested a CVE (i.e. CVE-2020-15048) for this vulnerability for which they released an advisory.
We acknowledge the limitations imposed by the brute-force requirement and therefore we did not pursue this path any further. Nonetheless, we believe the results described in this post are important as it shows that Fault Injection attacks are effective even when control over the environment is (very) limited.
Stay tuned for our final post in this series on the ESP32 where we describe an attack that bypasses Secure Boot and Flash Encryption in order to extract the plain-text data from external flash using only a single glitch.