Level 9 - HWisntThatHardv2 (hardware pwn)

This challenge requires us to find a vulnerability in STM32 firmware, and exploit it to read the flag in memory. We are given an stm32 emulator to emulate the firmware since we do not have access to the physical hardware. To better understand the challenge structure, we need to look at the config file config.yaml:

The firmware logic is in csit_iot.bin which is loaded at address 0x08000000, and an external SPI flash on SPI3.

If we look at ext-flash.bin we see the following:

The first 32 bytes are the real flag we're interested in, and the rest of the data are fake flags. Including the credits there are 16 entries.

Let's look at the dockerfile:

The stm32 emulator is run with the provided config file. When we try to see all options the stm32 emulator has we see the following:

However, when I tried using the different options they didn't seem to actually work. I later managed to find the stm32-emulator repo on GitHub, but it had not been configured to allow the user to enter input from stdin. In other words, both provided and sourced emulators didn't work perfectly. I decided to try reversing the binary first.

Using my IDA Pro MCP + GitHub Copilot, it gave me some understanding as to how the program worked, and I learnt that there were 2 main types of input it accepted, both in json:

  1. View the 32 bytes at a certain slot from 1 to 15 inclusive. The request is of the format

The response looks something like

  1. Find how many matches your provided array has with the slot. The request is something like:

And the response is something like:

Where 1 is the number of similarities we have with the slot data

On reversing the binary we identify a function main at address 0x807900, which calls a function parse_command at address 0x8007260.

main then has the following pseudocode:

As you can see, it ensures that the provided index is โ‰ฅ 1 and <= 15. So we can't directly read the flag at slot 0.

If there is user data we enter a function check_data_and_format_result at 0x800023c, otherwise we enter an else block which simply prints the 32 bytes of slot data.

Now let's look at check_data_and_format_result :

Let's look at the comparison functions:

At this point I tried fuzzing the program, and the first thing I tried was to provide a data array that was longer than 32 bytes. That actually caused the program to crash, which made me suspect that providing a long array actually resulted in a segfault somewhere in the program. I then asked ChatGPT to identify potential buffer overflow vulnerabilities in the program, and I learnt that the memcpy_ call in the comparison function was vulnerable to a buffer overflow. This is because if the array has >32 bytes, memcpy_ copies more bytes into v18 in the comparison function. We can see from the pseudocode that v18 is positioned at sp+1. That is 1 byte after the stack pointer. Thus we are able to overflow into the saved pc pointer / return address of comparison, and control the program execution flow.

Let's look at the function cleanup in comparison:

Although the pseudocode shows that v18 is at sp+1, I found out by trial and error that the data is actually copied to address sp. Since the function calls adds 0x24 to SP then pops r4 then r5 when closing, this means that the padding between the memcpy target and PC is 0x24 + 2 * 4 = 44 bytes (2 * 4 because there are 2 registers being popped and each it 32 bits / 4 bytes long).

Therefore after writing 44 bytes into the array, the remaining content will overwrite saved PC.

Now, to get the flag we simply need to read slot 0. Let's see the relevant part of main for reading slot data again:

So our objective is to have slot_index = 0 and has_user_data = 0.

How do we achieve slot_index = 0? We can simply jump to the middle of the main function after the <= 0xE check is done, and set the value of the register containing slot_index to 0 by performing Return Oriented Programming (ROP) to set register values. Let's look at the corresponding asm:

So we need to execute the instruction at address 0x8007b14 with R3 = 0. We can do this by looking for a pop {R3 , , ... PC} gadget. Note that in ARMTHUMB architecture we also need to perform a | 1 operation to every instruction address we specify.

I found a pop {R3, PC} gadget at address 0x0801058e .

The next problem: How do we set has_user_data = 0? Well, has_user_data is stored in the stack of the main function at SP+0x5C, so we can actually overwrite it with our array payload.

So the final ROP chain outline will be something like:

  1. Return to 0x0801058f to pop r3 and pc

  2. Return to the middle of the main function at address 0x8007b15, and it now thinks our slot is 0 since r3 = 0.

  3. Overwrite the byte at address sp+0x5C with \x00 to set has_user_data = 0.

Below is my full exploit:

Last updated