boschresearch / gdbfuzz

Fuzzing Embedded Systems using Hardware Breakpoints
GNU Affero General Public License v3.0
169 stars 15 forks source link

Struggles Fuzzing an STM32 Device #5

Open SethGen opened 3 months ago

SethGen commented 3 months ago

Hello Max and The Bosch Research team

I am currently trying to Fuzz an Embedded Application on the STM32G474RE Nucleo Board - NUCLEO G474RE. I am going to quickly walk you through the steps I am currently taking while trying to Fuzz, along with my output on execution. I have used your "GDBFuzz on STM32 B-L4S5I-IOT01A board" detailed instructions as my initial tutorial. Any support/ walking through the steps taken for you to test your STM32 B-L4S5I-IOT01A firmware would be greatly appreciated.

1. In Ubuntu 22.04.3, head to gdfuzz/example_firmware and create a new folder

In my case, I copied the "stm32_disco_arduinojson" folder, pasted it and renamed it to "stm32g4_sample". From here, I made the appropriate changes to the platform.ini file, allowing PlatformIO to configure itself for the NUCLEO-G474RE.

[env:nucleo_g474re]
platform = ststm32
board = nucleo_g474re
framework = arduino
upload_protocol = stlink
lib_archive = no
lib_deps = bblanchon/ArduinoJson@^6.19.0

I want to believe that this part is okay due to my ability to work with PlatformIO. I will show this in a moment.

2. Get a binary(.elf) file from STM32Cube after Building the Project

After building in STM32CubeIDE, the .elf file is conveniently placed within the 'Binaries' folder. From here, I simply copy and paste that file into "gdbfuzz/example_firmware/stm32g4_sample", where the binary file is renamed to "firmware.elf". In this compiled project I have written a fuzz wrapper and test function. The symbols(FuzzMe & LLVMFuzzerTestOneInput) are both found by gdbfuzz and can be found within the .elf file.

int FuzzMe(const uint8_t *Data, size_t DataSize) {
  return DataSize >= 3 &&
      Data[0] == 'F' &&
      Data[1] == 'U' &&
      Data[2] == 'Z' &&
      Data[3] == 'Z';  // Error occurs when DataSize == 3 -->
                       // Data = {F,U,Z}
}

int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  if(FuzzMe(Data, Size))
      return 0;
  return 1;
}

This leads me into my first question.

How are you implementing your fuzz wrapper in STM32's framework?

In the configuration file, you state that I can also specify a symbol name when configuring the entry point. The thing is, how do I implement that entry point in C code? In traditional software fuzzers(LLVM for example), it is implemented as shown above and then compiled with clang(-fsanitize=fuzzer) to produce a fuzz executable. I guess I am just struggling to understand how the entrypoint function must be implemented in order to function properly.

In the output files shown later, I had used LLVMFuzzerTestOneInput as my entrypoint, set up exactly as shown in the code snippet above.

3. Ensure STLINK can be communicated with and has appropriate privileges

To do this, I modified my udev rules to contain the necessary information such that my STM32G474RE's STLINK-V3 communicates as intended. The following code is used to reload and start a udev instance

sudo /lib/systemd/systemd-udevd --daemon
sudo udevadm control --reload-rules
sudo udevadm trigger

I can now check the STLINKs permissions with

lsusb
ls -l ~/../../dev/bus/usb/001

Output:
Bus 001 Device 003: ID 0483:374e STMicroelectronics STLINK-V3
total 0
crw-rw-rw- 1 root plugdev 189, 2 Jun 17 14:05 003

Thus, I have shown that I have proper read-write permissions for the STLINK

4. Use PlatformIO to Flash Firmware to Device

:~/gdbfuzz/example_firmware/stm32g4_sample$ pio run --target upload
Processing nucleo_g474re (platform: ststm32; board: nucleo_g474re; framework: arduino)
------------------------------------------------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/ststm32/nucleo_g474re.html
PLATFORM: ST STM32 (17.4.0+sha.1464ee1) > Nucleo G474RE
HARDWARE: STM32G474RET6 170MHz, 128KB RAM, 512KB Flash
DEBUG: Current (stlink) On-board (stlink) External (blackmagic, cmsis-dap, jlink)
PACKAGES:
 - framework-arduinoststm32 @ 4.20701.0 (2.7.1)
 - framework-cmsis @ 2.50900.0 (5.9.0)
 - tool-dfuutil @ 1.11.0
 - tool-dfuutil-arduino @ 1.11.0
 - tool-openocd @ 3.1200.0 (12.0)
 - tool-stm32duino @ 1.0.1
 - toolchain-gccarmnoneeabi @ 1.120301.0 (12.3.1)
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 13 compatible libraries
Scanning dependencies...
Dependency Graph
|-- ArduinoJson @ 6.21.5
Building in release mode
Checking size .pio/build/nucleo_g474re/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM:   [          ]   1.0% (used 1300 bytes from 131072 bytes)
Flash: [=         ]   5.0% (used 26376 bytes from 524288 bytes)
Configuring upload protocol...
AVAILABLE: blackmagic, cmsis-dap, jlink, mbed, stlink
CURRENT: upload_protocol = stlink
Uploading .pio/build/nucleo_g474re/firmware.elf
xPack Open On-Chip Debugger 0.12.0-01004-g9ea7f3d64-dirty (2023-01-30-15:03)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
debug_level: 1

hla_swd
[stm32g4x.cpu] halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x0800410c msp: 0x20020000
** Programming Started **
Warn : Adding extra erase range, 0x08006908 .. 0x08006fff
** Programming Finished **
** Verify Started **
** Verified OK **
** Resetting Target **
shutdown command invoked

As you can see, I am able to program to the STM32G474RE with PlatformIO and see that a program has been flashed to my MCU. However, the binary file "gdbfuzz/example_firmware/stm32g4_sample/.pio/build/nucleo_g474re/firmware.elf" is not the same as "gdbfuzz/example_firmware/stm32g4_sample/firmware.elf". One thing that I have noticed about this process is that if I now replace "gdbfuzz/example_firmware/stm32g4_sample/.pio/build/nucleo_g474re/firmware.elf" with "gdbfuzz/example_firmware/stm32g4_sample/firmware.elf", the program that I wrote in STM32Cube will be flashed.

Essentially, when I initially upload, the binary file in .pio/build/nucleo_g474re is not the same as the firmware.elf file that I want to fuzz. This means that my initial flashing of the device does not flash the device with the firmware to be fuzzed. It isn't until I replace that "gdbfuzz/example_firmware/stm32g4_sample/.pio/build/nucleo_g474re/firmware.elf" file with the file to be fuzzed that I am able to flash the file to be fuzzed onto my MCU. This leads me to my second question.

Is the binary file that populates .pio/build/nucleo_g474re after a successful flash supposed to be the same as the binary file that I took from STM32Cube which contains my program to be fuzzed? Am I supposed to change this file to the file to be fuzzed?

I have tried both not changing the .pio/build binary file after initial flash and changing that file to the binary file to be fuzzed. Neither have solved my issue.

So, I have shown that I am able to successfully communicate with my STM32G474RE via PlatformIO, however I am unsure if everything is set up appropriately.

5. Configuring The Fuzzer

Now this is where I am having the most troubles. I am using the integrated STLINK on my Nucleo Board, so I likely ignorantly assume that I should be using a fuzz_serial.cfg or fuzz_serial_json.cfg configuration file. I simply copied the fuzz_serial_json.cfg file from your stm32_disco_arduinojson example and made the necessary changes.

# This config file is used to test GDBFuzz on the arduinojson example firmware.
# Copyright (c) 2022 Robert Bosch GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

[SUT]
# Path to the binary file of the SUT.
# This can, for example, be an .elf file or a .bin file.
binary_file_path = ./example_firmware/stm32g4_sample/firmware.elf

# Address of the root node of the CFG.
# If 'binary_file_path' is an elf, you can also specify a symbol name here.
# Breakpoints are placed at nodes of this CFG.
entrypoint = LLVMFuzzerTestOneInput

# Number of inputs that must be executed without a breakpoint hit until
# breakpoints are rotated.
until_rotate_breakpoints = 1000

# Maximum number of breakpoints that can be placed at any given time.
max_breakpoints = 6

# ignore_functions is a space separated list of function names.
# Example: ignore_functions = malloc free
# These functions will not be included in the CFG.
# This setting is optional, leave it empty if you dont want to ignore any function.
ignore_functions = 

# One of {Hardware, QEMU, SUTRunsOnHost}
# Hardware: An external component starts a gdb server and GDBFuzz can connect
#     to this gdb server
# QEMU: GDBFuzz starts QEMU. QEMU emulates binary_file_path and starts gdbserver.
# SUTRunsOnHost: GDBFuzz start the target program within GDB.
target_mode = Hardware

# Set this to False if you want to start ghidra, analyze the SUT,
# and start the ghidra bridge server manually.
start_ghidra = True

# Space separated list of addresses where software breakpoints (for error
# handling code) are set.
# Example: software_breakpoint_addresses = 0x123 0x432
software_breakpoint_addresses =

# Whether all triggered software breakpoints are considered as crash
consider_sw_breakpoint_as_error = True

[SUTConnection]
# The class 'SerialConnection.py' in file 'connections/SerialConnection.py' implements
# how inputs are sent to the SUT.
# Inputs can, for example, be sent over Wi-Fi, Serial, Bluetooth, ...
# This class must inherit from connections/SUTConnection.py.
# See connections/SUTConnection.py for more information.
SUT_connection_file = SerialConnection.py
port = /dev/ttyACM0
baud_rate = 38400

[GDB]
path_to_gdb = gdb-multiarch
#Written in address:port
gdb_server_address = localhost:4242

[Fuzzer]
# In Bytes
maximum_input_length = 1000
# In seconds
single_run_timeout = 20
# In seconds
total_runtime = 1800

# Optional
# Path to a directory where each file contains one seed. If you don't want to
# use seeds, leave the value empty, like so:
#seeds_directory =
seeds_directory = 

[BreakpointStrategy]
# Filename of the Breakpoint Strategy. This file must be in the
# ./src/GDBFuzz/breakpoint_strategies directory.
breakpoint_strategy_file = RandomBasicBlockStrategy.py

[Dependencies]
# Path to dependencies. You you do not use the docker containers,
# you may need to set these.
path_to_qemu = dependencies/qemu/build/qemu-x86_64
path_to_ghidra = dependencies/ghidra/

[LogsAndVisualizations]
# Verbosity of logging output.
# One of {DEBUG, INFO, WARNING, ERROR, CRITICAL}
loglevel = DEBUG

# Path to a directory where output files (e.g. graphs, logfiles) are stored.
output_directory = output/stm32g4_sample

# If set to True, an MQTT client sends UI elements (e.g. graphs)
enable_UI = False
What configuration do you suggest for simple testing of firmware via STLINK? I am pretty sure it is serial, but I am not understanding why you used JSON for some examples but the non-JSON version for others. Additionally, how can I tell whether or not my entrypoint is being entered as intended?

I have enabled DEBUG logging for the fuzzer. I am not able to discern any helpful information that it relays to me, as I am unsure if I am even entering my program and fuzzing the entrypoint. As you are about to see from the Fuzzer's output, the Fuzzer is able to connect to GDB, ghidra_bridge and the SUT, however there is something still going wrong.

6.Running The Fuzzer

I should note that I have ran the following initialization code prior to this process

virtualenv .venv
source .venv/bin/activate
make
chmod a+x ./src/GDBFuzz/main.py
sudo apt-get install stlink-tools gdb-multiarch

As stated in your demonstration, I must first start a GDB Server using st-util

$ st-util
st-util 1.8.0-32-g32ce4bf
2024-06-17T16:21:05 INFO common.c: STM32G47x_G48x: 128 KiB SRAM, 512 KiB flash in at least 2 KiB pages.
2024-06-17T16:21:05 INFO gdb-server.c: Listening at *:4242...

We can see that the STLINK is found and a GDB Server has started listening to Port 4242

To run the GDBFuzz, I input the following:

cd ~/gdbfuzz
./src/GDBFuzz/main.py --config ./example_firmware/stm32g4_sample/fuzz_serial.cfg

Here is the GDB Server Output after fuzzing for a minute or two:

2024-06-17T13:35:05 INFO common.c: STM32G47x_G48x: 128 KiB SRAM, 512 KiB flash in at least 2 KiB pages.
2024-06-17T13:35:05 INFO gdb-server.c: Listening at *:4242...
2024-06-17T13:35:24 INFO common.c: STM32G47x_G48x: 128 KiB SRAM, 512 KiB flash in at least 2 KiB pages.
2024-06-17T13:35:24 INFO gdb-server.c: Found 6 hw breakpoint registers
2024-06-17T13:35:24 INFO gdb-server.c: GDB connected.
2024-06-17T13:35:24 INFO gdb-server.c: Found 6 hw breakpoint registers
2024-06-17T13:35:46 ERROR gdb-server.c: cannot recv: -2
2024-06-17T13:35:46 INFO gdb-server.c: Listening at *:4242...
2024-06-17T13:36:01 INFO common.c: STM32G47x_G48x: 128 KiB SRAM, 512 KiB flash in at least 2 KiB pages.
2024-06-17T13:36:01 INFO gdb-server.c: Found 6 hw breakpoint registers
2024-06-17T13:36:01 INFO gdb-server.c: GDB connected.
2024-06-17T13:36:01 INFO gdb-server.c: Found 6 hw breakpoint registers
2024-06-17T13:36:23 ERROR gdb-server.c: cannot recv: -2
2024-06-17T13:36:23 INFO gdb-server.c: Listening at *:4242...

We can see that the crashing -> reprogramming/rerun is functioning. The program is crashing(?) and we can see it starts up on its own and runs it again. This is the only output I see on the GDB Server side. What you see above repeats throughout the entire fuzzing process. I feel like the "cannot recv: -2" is a hint at what might be going wrong, but my searches on the internet have been fruitless.

Now this is where I could really use your expertise. Here is the Fuzzer output on the Fuzzer side (loglevel = DEBUG). out.log

It appears that the same input is being sent repeatedly, and not much else is happening. In the "gdbfuzz/output/stm32g4_sample/trial-0/crashes" folder, there is one crash file titled: "0x080057200xfffffff90x081a3f320x080045ba" with the inner content:

9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999hii

I hope there is something in my process which is flawed such that this is an easy fix.

As I said before, any help and guidance would be much appreciated. Another resource that I would love to take a look at is Max's Black Hat Europe Presentation: https://www.blackhat.com/eu-23/arsenal/schedule/index.html#gdbfuzz-embedded-fuzzing-with-hardware-breakpoints-35796 I am sure there is some good information in there related to what I am asking, however have not been able to find a recording of his presentation and am wondering if you have and are willing to share this with me.

Best,

Seth G

maxeisele commented 3 weeks ago

Hi Seth,

I think the misunderstanding lies in the transmission of test cases. As in one of our examples here, we constantly fetch inputs via the serial connection and pass them to our program under test. You might just copy this fuzz loop here