Discover the intricacies of CVE-2024-28183, a critical vulnerability in ESP-IDF’s OTA update process that allows attackers to bypass anti-rollback protections through a TOCTOU exploit, posing significant security risks to devices using ESP32.

1. Overview

Anti-rollback is a security mechanism implemented in the ESP32 as part of the over-the-air (OTA) update process.

This feature prevents attackers from “downgrading” firmware to older and potentially less secure versions.

It is implemented through the use of a 32-bit eFuse whose bits represent the latest acceptable secure_version for an application image.

The secure_version value is set at build time for an application image and is burned into the eFuse after a successful upgrade.

A Time-of-Check-Time-of-Use (TOCTOU) vulnerability was discovered in the implementation of the ESP-IDF bootloader which could allow an attacker with physical access to a device to bypass anti-rollback protections.

This issue was found to affect the latest version of ESP-IDF (v5.3-dev) at the time of discovery.

2. Description

Anti-rollback can be enabled in the second stage bootloader by setting the CONFIG_BOOTLOADER_APP_ANTI_ROLLBACK option before building.

The process begins at the call_start_cpu0 function which is the entrypoint for the second stage bootloader. This function reads and parses the partition table from flash and selects the application image to load.

When anti-rollback is enabled, some checks are performed within the bootloader_utility_get_selected_boot_partition function so that only applications with a high enough secure version can be considered for booting.

After the boot partition has been selected, the bootloader_utility_load_boot_image function is called to load the app image.

This function steps through the possible partitions to find one that can be booted.

The final anti-rollback checks are performed here, but are performed before the application image is loaded (refetched from flash), leading to a TOCTOU issue.

components/bootloader_support/src/bootloader_utility.c (added comments marked with //!):

void bootloader_utility_load_boot_image(const bootloader_state_t *bs, int start_index)
{
    int index = start_index;
    esp_partition_pos_t part;
    esp_image_metadata_t image_data = {0};

    if (start_index == TEST_APP_INDEX) {
        if (check_anti_rollback(&bs->test) && try_load_partition(&bs->test, &image_data)) { //! [1] TOCTOU
            load_image(&image_data);
        } else {
            ESP_LOGE(TAG, "No bootable test partition in the partition table");
            bootloader_reset();
        }
    }

    /* work backwards from start_index, down to the factory app */
    for (index = start_index; index >= FACTORY_INDEX; index--) {
        part = index_to_partition(bs, index);
        if (part.size == 0) {
            continue;
        }
        ESP_LOGD(TAG, TRY_LOG_FORMAT, index, part.offset, part.size);
        if (check_anti_rollback(&part) && try_load_partition(&part, &image_data)) { //! [2] TOCTOU
            set_actual_ota_seq(bs, index);
            load_image(&image_data);
        }
        log_invalid_app_partition(index);
    }

    /* failing that work forwards from start_index, try valid OTA slots */
    for (index = start_index + 1; index < (int)bs->app_count; index++) {
        part = index_to_partition(bs, index);
        if (part.size == 0) {
            continue;
        }
        ESP_LOGD(TAG, TRY_LOG_FORMAT, index, part.offset, part.size);
        if (check_anti_rollback(&part) && try_load_partition(&part, &image_data)) { //! [3] TOCTOU
            set_actual_ota_seq(bs, index);
            load_image(&image_data);
        }
        log_invalid_app_partition(index);
    }

    if (check_anti_rollback(&bs->test) && try_load_partition(&bs->test, &image_data)) { //! [4] TOCTOU
        ESP_LOGW(TAG, "Falling back to test app as only bootable partition");
        load_image(&image_data);
    }

    ESP_LOGE(TAG, "No bootable app partitions in the partition table");
    bzero(&image_data, sizeof(esp_image_metadata_t));
    bootloader_reset();
}

In each of the lines marked at [1][2][3][4], the anti-rollback check is performed before trying to load the partition.

The first argument to the try_load_partition function is a esp_partition_pos_t type which only specifies the position and size of the partition in flash.

The actual image data is refetched within this function, despite the anti-rollback check having been performed on potentially different data.

As a result, an attacker with precise control of the device’s flash could replace the application image with an older version after the anti-rollback checks have occured and just before the image is loaded to be booted.

3. Proof Of Concept

Environment Setup

This section outlines steps for setting up the testing environment to reproduce the issue with QEMU.

For convenience of developing the proof of concept, the example has flash encryption disabled.

However, it is noted that this issue also affects devices with flash encryption enabled as it only involves replacing an entire application image with a previous version.

For convenience of reproducing the issue, the attached bootloader image, eFuse file and application image can be directly used instead of rebuilding them.

That is, the ESP-IDF section can be safely skipped if using the attached files.

QEMU

The Espressif QEMU fork is used for dynamic testing. It can be built with the following commands:

git clone https://github.com/espressif/qemu.git
cd qemu
./configure --target-list=xtensa-softmmu --enable-gcrypt --enable-debug --disable-strip --disable-user --disable-capstone --disable-vnc --disable-sdl --disable-gtk
ninja -C build

For more information click here.

LEAVE A REPLY

Please enter your comment!
Please enter your name here