elttam recently had the pleasure of being a gold sponsor for DownUnderCTF (DUCTF), Australia and New Zealand’s largest capture the flag competition.Not only is it the largest, but it’s also one of the most creative CTF’s around which forces players to push the boundaries of what they know and learn something new in the process.This blog post describes the author’s approach to solving a Medium difficulty pwn challenge titled “ESPecially Secure Boot”, which required writing an exploit for CVE-2018-18558.
Dockerfiles and associated code have been published on the elttam GitHub repo here, which the reader is encouraged to use and follow along with the writeup.Quick setup steps are as follows:
git clone https://github.com/elttam/DUCTF-ESPecially-secure-boot/./setup.shdocker compose up --detachdocker compose stopdocker compose exec challenge /bin/bashdocker compose exec solution /bin/bashThe challenge description clearly indicates the goal for players will be to craft a malicious application image for the ESP32, which can be used to bypass the Second Stage bootloader’s Secure Boot implementation:
“The ESP-IDF 2nd stage Bootloader implements functions related to the Secure Boot feature.In previous releases of ESP-IDF releases (2.x, 3.0.5, 3.1), the 2nd stage Bootloader did not sufficiently verify the load address of binary image sections.If the Secure Boot feature was used without the Flash Encryption feature enabled, an attacker could craft a binary which would overwrite parts of the 2nd stage Bootloader’s code whilst the binary file is being loaded.Such a binary could be used to execute arbitrary code, thus bypassing the Secure Boot check.”
The challenge itself listens on port 1337, and executes run.py for each new connection.The run script is responsible for copying the original firmware in flash-base.bin to a new temporary file, asking the user for a base64 blob which will be decoded and written to the file at offset 0x20000 (thereby “flashing” our new application image), and will finally use QEMU to emulate it.

The readme.txt file also provides some useful information about the challenge environment:
“The QEMU binary used in this challenge is compiled from https://github.com/espressif/qemu.No modifications have been made to QEMU or the ROMs provided.The bootloader was built from https://github.com/espressif/esp-idf/tree/v3.1-rc2 with relatively standard configs and secure boot V1 enabled.No modifications have been made to the bootloader.This is an old version of ESP-IDF, make sure to check for any known vulnerabilities!You can find the flag in flash.”
By knowing the specific bootloader release being used (v3.1-rc2), this information will assist with vuln triage and creating type and function signature databases for reversing as shown later in the article.
The Espressif Advisory says “ESP-IDF V3.1.1 and V3.0.6 contain the fix”.Therefore, we can diff one of these with a previous version (v3.1.1 with v3.1 in this example) to find the vulnerability.The second stage bootloader code responsible for segment loading can be found under components/bootloader_support/esp_image_format.c:process_segment(), so we’ll use this knowledge to narrow our focus to code which has changed in its proximity:
What is immediately obvious in this diff is the introduction of an else{} block which will ensure the load_addr and load_end variables don’t overlap the region between loader_iram_start and loader_iram_end.These new variables are initialized like so:
Digging a little deeper, it just so happens _loader_text_start and _loader_text_end are defined by the esp32.bootloader.ld linker script:
What this essentially says is _loader_text_start will be equal to the start address of iram_loader_seg (0x40078000), and _loader_text_end will be equal to the end of the second stage bootloader (known only at link time, in my debug setup it’s 0x4007b8b9).
Therefore, we now understand that vulnerable versions did not sufficiently protect the region of memory at iram_loader_seg which contains… you guessed it… the second stage bootloader.If we review the vulnerable process_segment() function, we see there’s insufficient verification of load_addr and end_addr, and it just so happens these values are calculated from our application image… Now we’re getting somewhere.
The vulnerable process_segment() function looks like so:
The astute reader will see this function doesn’t actually read segment data into memory, it’s simply enforcing some validation rules:
The actual segment loading happens in process_segment_data(), which we’ll look at soon.
We now have enough context about the underlying vulnerability that needs to be exploited, we can now shift our attention to how to trigger it.
If we’re going to be manipulating segment load address information, we’ll need to learn a little about the application image format - which is fortunately very well documented in App Image Format.The author also created an 010 Editor Template which can be found here, and is useful for quickly navigating around the image segments in a hex editor and having a colourful visualisation of what’s happening.

The overall structure of an application image is as follows:
esp_image_header_t structure (let’s call this header).header.segment_count entries of type esp_image_segment_header_t (lets call this segmenthdr).segmenthdr is a data blob of segmenthdr.data_len size (lets call this segmentdata).segmentdata, there may be up to 15 padding bytes followed by a checksum.header.hash_appended is set, a SHA256 digest of all data after header is appended to the image for integrity verification.esp_secure_boot_sig_block_t structure will be appended for Secure Boot verification.For easy reference, the structures we’ve just talked about are defined as:
We’ll be manipulating the esp_image_segment_header_t.load_addr members of our application image to abuse the fact that process_segment() does insufficient validation resulting in a write-what-where primitive in process_segment_data().
If our primary objective is to bypass secure boot, our secondary objective is to have an application which if executed can read the flag.The esp-idf SPI Flash API docs mention the esp_flash_read() function, which we can use to read data from an arbitrary flash address.
To confirm which address we want to read, we can search for the unique identifier “DUCTF” in flash - which is present at address 0x133370.
Putting this information together, our application should look like so:
The esp-idf framework comes with some really useful and ready to use example applications, so we’ll repurpose the esp-idf/examples/get-started/hello_world/ project for our testing. Simply replace the main/hello_world_main.c file with the above code and run make.
What are we going to write? The obvious answer is “some bytes which will bypass secureboot verification”.For example, we might want to change the opcodes or operands of a branch instruction to invert the logic so execution continues on signature verification failure.This seems straightforward, however there’s a catch.The process_segment_data() function will obfuscate image data in RAM, and not deobfuscate until after secureboot verification.This means we don’t have reliable control over the “what” in our write-what-where primitive.
The author believes this is why the run.py script in the challenge files use the -seed 1234 argument to QEMU, which will force the guest to use a deterministic PRNG, seeded with 1234.If the state of the ram_obfs_value array is dumped, it should allow for reliable exploitation.However, the author decided to live dangerously and let the gods of chaos choose what the data is.By not caring what specific bytes we’re writing and passing that level of control over to the PRNG, we hope that at least some of the time a byte sequence is produced which is advantageous to the player.
We want to write to a location in the second stage bootloader which will be executed soon after the load.The ideal location will be the branch statement which makes the decision to continue or exit application execution after secure boot verification.Revisiting the esp_image_load(), function which is the caller of process_segment(), we see a call to verify_secure_boot_signature() to check the application signature, followed by the conditional statement we want to abuse.
The real question is, where in this 4MB firmware dump can we find this check? We can take the following approach:
flash-base.bin using the open source esp32knife project.Loading this into Ghidra, we can enable the “Function ID” analysis step which will use our custom FIDdb to restore function names.We can also apply type archive to the identified functions to restore type information.As a result, it was simple to navigate to esp_image_load() and find the corresponding check which happens at address 0x4007a3f4.This will be the target of our overwrite and answers our question of “write where?”.

With all this information at hand, we can now write a proof of concept.Our code will do as follows:
0x4007a3f4, data_size set to 0x04, and 4 bytes that can be anything.The proof of concept exploit can be found here, and when run should look something like this:

This challenge forced players to learn about the internals of the ESP-IDF second stage bootloader, often used on ESP32 microcontrollers.The challenge had several interesting obstacles such as obfuscation and data constraints, which made it enjoyable to play in a CTF.And finally, it required writing an exploit for CVE-2018-18558 which, at the time of publishing this post, had no such PoC.
During the process of solving the challenge, there were also several interesting observations which the reader is encouraged to play with, especially if wanting to exploit this issue on real hardware.
qemu-efuse.bin binary, which gives attackers read-access to the usually secret eFuse blocks EFUSE_BLK1 and EFUSE_BLK2.If flash encryption were enabled, could we still exploit this issue?0x00000000 to ram_obfs_value[0], it will make ram_obfs_value[0] = ram_obfs_value[1].As the Xtensa ISA allows 2 and 3 byte instructions, is this a useful primitive?0x3fff0000 range, we need the end to be >= 0x40000000.If our data is nothing but 0x00 bytes, it seems we can leak the key of ram_obfs_value in error messages.Is this a useful primitive?The author would like to thank the DUCTF organisers for running a very enjoyable event, as well as the challenge creators joseph and HexF for the really interesting puzzle.
If you enjoyed this post and the topic of Secure Boot and MCUs interest you, come and check out elttam’s upcoming presentation at BSides Canberra 2024 on “Boot Security in the MCU”.If you’re an Australian player of DUCTF, solved multiple challenges, and are interested in a career at elttam please feel free to get in touch.