Stack exploitation based on buffer overflow has been one of the well-known security exploits. Refer here for the basic understanding of buffer overflow based vulnerabilities on x86 architecture. Embedded devices are an integral part of the IoT ecosystem. These devices generally have ARM/MIPS architecture based controllers/processors that control various sensors and actuators.
In this blog, we would understand buffer overflow based attacks on these embedded devices. We would also see the step that should be taken on ARM architecture-based devices to mitigate stack exploitation. For demonstration purposes, I have prepared an attack case scenario using DIVA as the hardware platform.
Case Scenario Description
In this system, there are 8 virtual doors. The hardware board DIVA uses the STM32F411x microcontroller (ARM Cortex-M4). The controller controls the corresponding GPIOs that controls the access to the doors. The access to the virtual door is demonstrated by controlling LEDs connected to corresponding GPOIs. The system gives user, the permission to access door 5 to 8. The user has to enter the choice of door 5 to 8 using a host machine that communicates with the device serially via virtual COM Port. The LEDs corresponding to respective doors are shown below.
5 as user input, the LED corresponding to door 5 is ON indicating Access Granted.
8 as user input, the LED corresponding to door 8 is ON indicating Access Granted.
The access to door 1 to 4 is restricted. The LEDs corresponding to respective doors 1 to 4 remain OFF.
Our target is to get access to the restricted doors i.e. turn ON the restricted GPIOs of the controller.
Look for attack surface
It is found that the JTAG port of the hardware is accessible. The firmware of the device is then extracted using ExpliotNano via JTAG Port. The extracted firmware is then opened using Ghidra for analysis.
The JTAG access on the hardware gives the opportunity to remotely debug the device. The communication between the device and the host is setup using the Openocd tool and GDB via FTDI based Exploit Nano interface.
In one terminal open openocd using –
openocd -f /path/to/jtagConfigFile.cfg
In the next terminal, open gdb using –
gdb-multiarch and pass the following commands :
set arch arm
target remote localhost:3333
monitor reset halt
In Ghidra, under defined string column, look for strings coming on the console. Check for the reference address of the string. Here, the reference address corresponding to string “Enter any door number 5 to 8 ” at is found at 0x08002554. It gives the hint to the address of the function at 0x08002550 where the user input is being handled. For remote debugging, inside GDB, put breakpoints to analyze the value of registers and the stack data.
- We put a breakpoint at 0x08002554 i.e. the address of function when entrance string is loaded; using
- By observing the decompiled function, we find that on the user console, the input from the user is expected after the print of two statements “Enter any door number 5 to 8 that you want to open. n Which door do you want to get in?“. We put another breakpoint at 0x08002566 i.e. the address where the user input function is called; using
- To check for the stack data after accepting user input, we put another breakpoint after returning from the function at address 0x0800256c; using
Continue the system operation using command
c on GDB. We encounter the first breakpoint at the entry of the first statement on the console at 0x08002554. Check for register values using the command
i r It is found that the value of link register lr is 0x0800252b
Again, Continue the system operation using command
c on GDB. We encounter another breakpoint just before receiving user input at 0x08002566. Check for the stack data using command
It is found that the stack pointer value is 0x2001ffe0 and the value of lr 0x0800252b is at address 0x2001ffec on the stack i.e at the offset of 12 bytes from the stack pointer. Continue the system operation using command
c on GDB. On the user interface side, let’s try by giving input like: “aaaaaaaaaa”
After entering, the system halts at another breakpoint at address 0x0800256c. On gdb terminal, check the values inside stack after this breakpoint using the command
x/8x $sp. You can observe 10 times 0x61 (ascii value of ‘a’, i.e. the input that we passed). The values in stacks show that the buffer offset value is 0x2001ffe0.
You can observe that since we entered only 10 bytes in user input, the lr value is not overwritten as it is at the offset of 12 bytes from stack pointer (mentioned above). If we now continue on gdb using
c command, you can observe that the system runs normally stating invalid input and waiting for another input from the user.
It means if the user enters more than 12 bytes, the lr value will be overwritten.
Yayyyy !! We found that we can exploit this input buffer and hence perform RCE by exploiting the stack.
Perform Stack Exploitation Attack.
Create a shellcode to be executed after stack smash. Next, create a python script to pass user input to the system such that the lr value in the stack gets overwritten leading to the execution of vulnerable shellcode.
It is found that the virtual doors (represented by LEDs) are controlled by GPIOC of the microcontroller. We write an assembly code (here named as led.s) for direct access of GPIOC register. As per the datasheet GPIOC is at address 0x4002 0800 – 0x4002 0BFF.
So, in order to access door 1 to 4 directly, we populate the GPIOC register with value 0xff. The shellcode will write value 0xff at memory address 0x40020814 leading to direct access of restricted doors.
Compile the arm assembly code using
arm-none-eabi-as -mlittle-endian -mcpu=cortex-m4 led.s -o led.o For shellcode, do
arm-none-eabi-objdump -d led.o
We can now observe the 12 bytes of machine instructions as :
x25ffxf640x0614xf2c4x0602x6035. Since it’s little-endian , we get shell code as :
xffx25x40xf6x14x06xc4xf2x02x06x35x60. We now write a python script to send the user input to the system serially via host machine. As observed above, the value of lr is at offset 0x2001ffec i.e. 12 bytes from the stack pointer. We send 12bytesOfShellcode + OffsetAddress(start address of shellcode)
xFFx25x40xF6x14x06xC4xF2x02x06x35x60xE1xFFx01x20. As observed, the buffer offset address as 0x2001ffe0, since it’s in thumb mode, so the last four bytes of the above string is 0x2001ffe1
Smash the stack
Now, we will see how buffer overflow based exploit can be performed to divert the system from the main firmware logic and execute the vulnerable code. In continuation with the above mentioned setup in GDB, after running the python script; we encounter the breakpoint at address 0x0800256c. At this breakpoint, the python script had sent 16 bytes of data containing shellcode and the offset address. Observe that the stack contains 12 bytes of shellcode starting at offset address 0x2001ffe0 . The lr value 0x0800252b at 0x2001ffec is overwritten by the buffer offset address 0x2001ffe1 (thumb mode).
On gdb, pass the continue command
c, observe that the system gets stuck and GPIOC has now been populated with value 0xff. That is, despite of invalid input, we got access to the restricted GPIOs.
Hence, the stack got smashed leading to the execution of vulnerable shellcode instead of actual firmware logic. Great, we can now see even the LEDs 1 to 4 i.e. corresponding to restricted doors is ON. Yayyy!! we got access to the restricted doors now.
Mitigate the Attack
In this section, we will discuss the measure that developers can take to stop the execution of vulnerable codes on the system.
Here we will show protection steps for ARM-based devices.
-fstack-protector enables the stack protection. If the stack does not encounter any type of smash, then the value of *__stack_chk_guard remains unchanged. When it is detected that the guard variable on the stack has been modified, the run-time environment is notified by calling the function __stack_chk_fail(void). At this point, the __stack_chk_fail(void) function is executed. Depending on the implementation of this function, it can exit the system operation, stopping the vulnerable code to get executed.
We can now see the the demonstration of it. Here we would stop the above vulnerable shellcode from being executed. Inside the main source code of the system, in Makefile we add -fstack-protector. Then, initialize the guard variable *__stack_chk_guard = 0xabcdabcd and define the function __stack_chk_fail(void).
As mentioned previously, open the firmware using Ghidra, perform remote debugging using openocd and GDB. In Ghidra, find the reference point to the entry of the function by searching for string “Enter any door number 5 to 8 that you want to open” under defined strings colomn.
Observing the function address, we give breakpoint in GDB at address 0x08002d26. Then continue the system operation using the command
c on GDB. At entrance of the function, the system halts at this breakpoint. Observe the value of registers using command
i r. It is found that the lr value is 0x8002cd7
We put 2nd breakpoint at address 0x08002d38, just before the function that accepts input from the user. Continue using using the command
c on GDB. At 2nd breakpoint the system halts, observe the value of stack at this point.
It is found that the lr value 0x8002cd7 is at the offset of 20 bytes from the stack pointer 0x2001ffc8. Another most important thing to observe is that the __stack_chk_guard variable 0xabcdabcd is present on the stack at the offset of 12 bytes from the stack pointer. We put another breakpoint at the address 0x08002d3e after the return from the receiving function. Continue using the command
c on GDB. For testing, we pass “ccccccccc” as the user input.
The breakpoint is encountered at the address 0x08002d3e. Then the data on stack is observed using
x/8x $sp on GDB.
It is observed that the stack contains 9 bytes of 0x63 (ascii of ‘c’) from the offset of 4 bytes from the stack pointer. It is found that in this case eventhough the value of lr register 0x08002cd7 is not overwritten but the value of __stack_chk_guard variable 0xabcdabcd is modified, this leads to the detection of stack smash by the sytem that executes the __stack_chk_fail(void) function and the system halts.
Now, we would test to execute the above vulnerable shellcode. We write a python script (as above) to send the user input to the system. As observed above, the value of lr is at the offset of 20 bytes from the stack pointer and 16 bytes from the buffer. We send 12bytesOfShellcode + 4bytesOfDummyData + OffsetAddress(start address of shellcode)
xFFx25x40xF6x14x06xC4xF2x02x06x35x60x41x41x41x41xCDxFFx01x20. As observed, the buffer offset address is at 0x2001ffcc, since it’s in thumb mode, so the last four bytes of the above string is 0x2001ffcd
This time, we send the user input using the above python script. As set previously, the breakpoint is encountered at the address 0x08002d3e. Then the data on stack is observed using
x/8x $sp on GDB.
At this breakpoint, the python script had sent 20 bytes of data containing shellcode, dummy data and the offset address. Observe that the stack contains 12 bytes of shellcode starting at offset address 0x2001ffcc and the lr value 0x8002cd7 is overwritten by the buffer offset address 0x2001ffcd (thumb mode). But the shellcode is now unable to control the restricted GPIOs.
Hence we observe that eventhough , the lr value was overwritten by the address of vulnerable shellcode but the detection of modified value of stack_chk_guard variable, executed stack_chk_fail(void) function. This didn’t allow the shellcode to execute. It can be seen in above images that the system halted after smash detection without any LEDs being set ON. Thus enabling
-fstack-protector stopped the remote code execution of vulnerable shellcode.