CTXz / stm32f1-picopwner

Dump read-out protected STM32F1's with a Pi Pico - A Pi Pico implementation of @JohannesObermaier's, Marc Schink's and Kosma Moczek's Glitch and FPB attack to bypass RDP (read-out protection) level 1 on STM32F1 chips
166 stars 23 forks source link

STM32F100 #10

Closed analogic closed 5 months ago

analogic commented 10 months ago

Hi there,

I have STM32F100 value line with rdp desoldered from original board and transferred to blue pill.

After various difficulties I am able to get through to the last stage where dumping firmware should be possible but I am not getting any data. I am trying to understand the problem here.

When I stop cpu after failed script I have following result:

Target stopped by debug request, current mode: Handler HardFault
xPSR: 0x61000003 pc: 0x200003a8 msp: 0x20004fe0

The program counter seems to be pointing to SRAM. By dumping part of the SRAM I've confirmed that there is a correct target firmware (1cc_usart1.bin). Perhaps the target fw is failing?

Should I be looking for a software problem e.g. target firmware? Or power glitching failure / reset / hw is possible issue here?

Thanks for your help, great job!

analogic commented 10 months ago

Hmm, the hardfault seems to be debug related. I've checked the uart output and it seems that after the script ran, the stm32 rebooted to the original firmware with a welcome message - even though it was set to boot from sram. I am really lost.

analogic commented 10 months ago

I have a simple question - is there any obvious way to check that the power glitch attack was successful?

CTXz commented 10 months ago

Did you test if the blue-pill allowed you to dump rdp firmware with it's original chip in place? Just trying to reduce external factors before jumping to the conclusion that it's chip related. Also, perhaps you could take a look at the following PR:

https://github.com/CTXz/stm32f1-picopwner/pull/5

As for obvious ways to confirm if the glitch attack was successful: You could try to test if the target firmware reaches stage one by ex. printing something to UART during stage 1. That'd hopefully hint further which part of the exploit fails. You could also add UART messages to the initialization code of the 2nd stage.

Another tip: If you have access to an oscilloscope, try to monitor your reset line to see how it behaves.

Also, is there a chance that the F1 chip is new? In the troubleshooting section of the README I've mentioned that there are rumours that newer F1 chips have fixed this exploit.

analogic commented 9 months ago

Firstly, thanks for the tips!

I've borrowed a friend's 2ch osciloscope and removed some caps. Put back "stock" blue pill stm32f103. (although I suspect it is a fake). And still can't get it to work

fresh start (boot1=boot0=1 => from SRAM)

> mdd 0x20000000
0x20000000: c029c306220023c0 

(strange, there are some data?)

after SRAM write
> mdd 0x20000000
0x20000000: 2000053520005000 

... power glitch ...

> mdd 0x20000000
0x20000000: 4039e304220023c0 

Scope seems to be as expected. AFAIK sram boot should end in an infinite loop (sinfl) and not overwrite itself.

NewFile0

I'll try to transfer the locked stm32f100 to the board again - at least I know its genuiene part and it should behave like the original one. And maybe even try to buy another version of the blue pill - maybe there is a problem. Both are old chips. If you have another hint please let me know

CTXz commented 9 months ago

If i'm reading the oscilloscope correctly, the reset line has a suspiciously low voltage of 1.76V (it should be 3.3V). This is likely due to a voltage divider forming between the STM and the pico's input, which has now been fixed in the most recent release here:

https://github.com/CTXz/stm32f1-picopwner/releases/tag/a1.2_t1.2

The reason why this issue didn't affect everybody is because for some odd reason the voltage drop appears to vary between devices? Mine for instance reported 2.7V, which was enough to be registered as high. I'm not entirely sure if this variation was caused by differences in Pico's or STM32F1 chips or both, either way, the new release should tackle this problem at least!

Please try the latest release and report if you're still experiencing the same issues.

analogic commented 8 months ago

Hello,

power is ok, oscilloscope was set to AC. I've now fully understood the mechanism of the exploit, using latest picopwner, but I'm still not getting a result. So I'm at the stage where I'm confirming each step of the exploit.

I am now paused after the first stage before rp2040 resets the stm32. Check this code:

ldr r0, =0xe0002000
movs r1, #3
movs r3, #0x05                                                                                  
movs r2, #0x20
stm r0!, {r1, r2, r3}

After stopping chip in loop at openocd, I try to read memory:

> mdb 0xE0002000
0xe0002000: 61 
(two first bits equal #3 in FP_CTRL)

> mdb 0xE0002004
0xe0002004: 20 
(equal to #20 in FP_REMAP)

> mdb 0xE0002008
0xE0002008: 00 
(no #0x05 in FP_COMP0, wtf?!)

Do you have any idea why I am not getting exactly what is written by the thumb code?

The device the chip comes from is from 2020. Assuming the supply chain I don't think it's a fixed revision, but still can't rule it out. Also, another "old" exploit with partial flash reading works fine (https://blog.zapb.de/stm32f1-exceptional-failure/). Do you have any info on how fixed revisions work?

analogic commented 8 months ago

OMG, you can ignore my comment. OpenOCD is just reseting all comparators when initialized. https://openocd.org/doc-release/doxygen/cortex__m_8c_source.html#l02485

until I find better way to work with debug:

$ st-flash --debug read omg 0xE0002008 1
...
 05 00 00 00
...

to be continued...

analogic commented 8 months ago

The saga continues.

It seems that the chip is running the last stage of the exploit - the C code part. But after a couple of instructions it ends up in HardFault. I am no expert but it seems there is something limiting thread resources, for example: when I call init_usart it ends in hardfault immediately, but when I copy whole content of init_usart to main() it runs just fine and fail after. Almost every time something complex is called hardfault happens.

AFAIK I can't step debug at this stage to know the exact situation in which fault happen. All hardfaults are flaged as precise mostly with BFAR 0x20004ff8 ... 8 bytes away from 0x20005000 (MSP at test.S).

Any thoughts?

analogic commented 8 months ago

Hurray, I've got a dump!

I haven't solved why the mcu crashes, but I have patched main() with this ugly code. Strangely, every attempt to make the code nicer ends with a hardfault. Even removing unused functions or changing compiler optimisations ends with the same result.

int main(void)
{
    /* Enable Clocks */
    *RCC_APB2ENR |= (1 << 2); // Input-Output Port A clock enable
    *RCC_APB2ENR |= (1 << 14); // USART1 clock enable

    /* Configure Pins */

    // Set PA9 (TX) to alternate function push-pull
    *GPIOA_CRH &= ~(0xF << 4);
    *GPIOA_CRH |= (PIN_CONFIG_ALT_PUSH_PULL << 4);

    // Set PA10 (RX) to input pull-up
    *GPIOA_CRH &= ~(0xF << 8);
    *GPIOA_CRH |= (PIN_CONFIG_INPUT_PULL_UP << 8);

    /* Configure and enable USART1 */
    USART1_CTRL[2] = 0x00000341u;
    USART1_CTRL[3] = 0x0000200Cu;

    sel_usart_ctrl = USART1_CTRL;

    // Flash size register, RM0008, page 1076:
    // https://www.st.com/resource/en/reference_manual/rm0008-stm32f101xx-stm32f102xx-stm32f103xx-stm32f10

    uint32_t flash_size = *(uint32_t*) 0x1FFFF7E0 & 0xFFFF;
    if(flash_size == 64)        // Force reading of the entire 128KB flash in 64KB devices, often used.
        flash_size = 128;

    while (!(sel_usart_ctrl[0] & 0x80u)) {}
    sel_usart_ctrl[1] = DUMP_START_MAGIC[0];

    while (!(sel_usart_ctrl[0] & 0x80u)) {}
    sel_usart_ctrl[1] = DUMP_START_MAGIC[1];

    while (!(sel_usart_ctrl[0] & 0x80u)) {}
    sel_usart_ctrl[1] = DUMP_START_MAGIC[2];

    while (!(sel_usart_ctrl[0] & 0x80u)) {}
    sel_usart_ctrl[1] = DUMP_START_MAGIC[3];
    uint32_t i = 0;
    unsigned char* p;
    while(1) {
        while (!(sel_usart_ctrl[0] & 0x80u)) {}
        p = (unsigned char*)(0x08000000+i);
        sel_usart_ctrl[1] = *p;
        i++;
    }
}

I'm not eager to find out the origin of the problem since I've got what I wanted, so close the question if you like.

CTXz commented 8 months ago

Gratuluji ti :)

I was going to suggest if you can get the code to at least partially execute, then you could perhaps write a hacked up version that doesn't trigger the hardfault, which as I see is exactly what you did.

As for the issue related to deleting unused functions: I have also encountered this issue when I modified the original attack firmware code to my needs, which is exactly why I decided to leave many of the functions used in the original code written by Johannes in there. I didn't really bother checking out what caused it as I simply took the approach of "If it ain't broke, don't fix it". That being said, if there is a correlation to the issues you were facing, then I honestly might find some time and take a deeper look into it.

Since this is more of a workaround than a fix, I'll leave the issue open until the true cause of this bug has been found.

Cheers!

CTXz commented 6 months ago

Just a quick update on this, I'm suspecting the issue was caused by missaligned branches/PC loads. The PC value must always be loaded with the LSB set to 1 on Armv7-M, as it only supports Thumb instructions.

See https://developer.arm.com/documentation/ddi0403/d/Application-Level-Architecture/The-ARMv7-M-Instruction-Set/About-the-instruction-set/ARMv7-M-and-interworking-support?lang=en

The result of not complying with this is a good ol' hardfault. This happens to coincide a lot with the fact that function calls would cause crashes. I suspect something is fishy with the whole C + ASM linking deal. I'll take a peek at this soon and see If this is in fact what's happening.

As to why this appears to be promintent on the F100? Not sure, perhaps it has to do with the difference in the Vector table size which propagates a missalignment.

CTXz commented 5 months ago

The issue appears to have been resolved with https://github.com/CTXz/stm32f1-picopwner/commit/3bdbdd64eb0c30af236b515af399e9bb58c8f19d. It seems that the too large stack size was the culprit. This also explains why function calls caused these crashes to occur: Most functions will push their registers to the stack upon call!

Altering the main.c code now no longer causes inexplicable hardfaults. I unfortunately do not have a F100 to test this on, but I am fairly confident that this issue should no longer occur. Should this turn out to be wrong, I'll gladly reopen the issue.

The fix will be merged into main with the next release.