Skip to content

finngineering/ch55x_firmware_extractor

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CH55x firmware extractor

The CH55x firmware extractor is used to read out firmware from CH55x integrated circuits. There is no in-built support in the devices for directly reading out the firmware through the bootloader. However, there is a functionality to verify the firmware contents 8 bytes at a time against provided data. This functionality is vulnerable to a text book timing attack and is what is exploited here to allow firmware extraction from these devices. The bootloader can be accessed either through USB or UART. This firmware extractor works only over UART, because the higher latency of USB would make this attack difficult. Tested chips are CH552 and CH554, and bootloader version 2.4 and 2.5. The hardware solution for the firmware extractor is based on a STM32 Blue Pill, because those are readily available, cheap, and have the necessary performance.

Anatomy of the exploit

The bootloader has previously been read out from the device and its communication protocol has been reverse engineered. Below is an approximately correct verify command and the verify function used in the bootloader. There are some guards in place, which require the length to be a multiple of 8, the address to be aligned to an 8 byte boundary, the address to be under 0x3800 and that there are no previous verify errors. The last guard means a restart of the CH55x is needed after each unsuccesful verification.

We can see that the verification function returns immediately when one byte of the verification fails. This means the more bytes that are correct, the longer the verification function will take. This is the text book example of a timing attack than can be exploited.

unsigned char verifycmd[] = {
//  0x57, 0xab, // UART magic not included to verify function
    0xa6,       // Verify command
    5 + len,    // Constant 5 plus length of data to verify
    0,          // Unused
    addr_low,   // Low byte of address
    addr_high,  // High byte of address
    0, 0, 0,    // Unused
    0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, // Data to verify against
    checksum
}

unsigned char verify(unsignec char *cmdbuffer)
{
    static char prev_verify_error;

    unsigned char len = cmdbuffer[1]-5
    unsigned short addr = cmdbuffer[3] + cmdbuffer[4] << 8;
    if (len & 0x07 || addr & 0x07 || addr > 0x3800 || prev_verify_error) {
        return 0xfe;
    }

    for (int i=0; i < len; i++) {
        // Key can be set through bootloader, and CBYTE[] means code memory
        if(key[i & 0x07] ^ cmdbuffer[8+i] ^ CBYTE[addr+i]) {
            prev_verify_error = 1;
            return 0xf5;
        }
    }
    return 0;
}

Through trial and error we found that each correct byte extends the execution time of the verify function by approximately 4.2 µs. The baud rate used by the bootloader for UART communication is 57600 (regardless of what you read elsewhere), meaning that the transmission of one bit takes approximately 17.4 µs. The relattion between these two times are important, because it appears that the the reply is sent with a jitter of also ~17 µs. We are then trying to distinguish responses that differ by 4.2 µs in response time from the verify function, but may differ by up to 17 µs because of the UART jitter (clock timing). This appears to be a difficult task, but it is possible to do through statistical means.

We can determine if a byte was correct or not by trying to verify it several times and recording the results. Say for instance that the shortest possible time to receive a reply if the first byte is wrong is 30 µs. Then with the UART jitter we can expect a maximum reply time of 30 µs + 17.4 µs = 47.4 µs for an invalid first byte. At the same time, a valid first byte and invaid second byte would push this "range" to 34.2 µs to 51.6 µs. Adding some margins, we can conclude that the first character was invalid in case the reply time is shorter than ~33 µs. Similarily, we can say that the first character was valid in case the reply time was higher than 48 µs. This is the basis of the timing attack used to extract the firmware.

Using the firmware extractor

This is not a completely automatic tool for firmware extraction - modification of the source code and recompilation will be necessary (using VS Code with PlatformIO). The main reason for this is that the exact timing characteristics for each byte differs between setups and will need to be tuned. The tuning process could be automated, but this was not the goal of this project. The main tuning is done though the prober_limits variable. For instance, prober_limits[0] contains the limits for byte 0 in the 8 bytes to be verified. In case the response time is under .invalid_under_time, we know the byte was invalid. In case it's over .valid_over_time, we know the byte was correct. There is also a .min_delta that allows advancing to the next byte prior to knowing for certain if the byte is correct or not.

struct ProberByteLimits prober_limits[8] = {
    {
        // Byte 0
        .invalid_under_time = 33,
        .valid_over_time = 50,
        .min_delta = 30
    }, // ...

To find the proper valus to use, its recommended to put .invalid_under_time to 0, .valid_over_time to e.g. 100, and .min_delta can be kept at 30. This means that the prober will fail to find the first correct byte, but progress will be printed to host PC UART. You will see something akin to:

[0x0000]=0x01? min=31 max=47 tries=63
               min=31 max=48 tries=127
               min=31 max=48 tries=191
               min=30 max=48 tries=255
               min=30 max=48 tries=319

Provided that the first character tested is invalid, we can use these min/max values with an offset of 1 or 2 to determine proper limits to use. For instance above, we could put .invalid_under_time to 33 and .valid_over_time to 50. The prober should then try different values until it find a valid one, which would look like below:

...
[0x0000]=0x7d? min=31 max=39 tries= 7
[0x0000]=0x02? min=40 max=56 tries= 7
[0x0001]=0x01? min=35 max=52 tries=34

We see that the last try at address 0x0000 had a maximum response time of 56 µs, which means that was the correct byte. The prober then advances to the next byte, and continues the process. Note that all 8 byte limits have to be tuned separately, but once done, it will work for the whole memory. When 8 correct bytes have been found, it will print it out in ihex format:

:0800000002002932ffffffff9f

Logging the UART output to a text file allows to grep for ^: to extract the complete memory context in ihex format.

Circuit

An example circuit for firmware extraction is shown below. Two transistors make it possible to turn off the power to the CH55x from the Blue Pill. This is recommended over using software reset only, because software reset only works when the CH55x is in bootloader mode (and the bootloader has a timeout after which it starts the application code). The resistors on the UART to the CH55x are included because I have a suspicion internal pull-ups on the CH55x might back feed power to it from UART even when the power is turned off. The 10k resistor between V33 and P3.6 is needed to put the CH55x into bootloader mode. On the Blue Pill, PA11 is tied to the UART RX pin to be able to time the response using timer 1.

CH55x firmware extractor circuit

Conclusion

By tuning and using this tool, the firmware of CH55X devices can be extracted. The extraction process is not fast, but will extract the 14 kb in one or two days. It depends a bit on the tuning and how good the frequency table used matches the actual firmware assembler. Unfortunately, the source code is a bit of a mess. I made this tool because I needed the firmware from a CH554 device, and now that I have obtained that, there is no real reason to work on the tool itself. Nevertheless, it should prove useful in case somebody needs to extract firmware from CH55x devices.

About

Firmware extractor for CH55x microprocessors

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages