
Fuzzing de Sistemas Embebidos usando Puntos de Interrupción de Hardware
Este es el código complementario del artículo: 'Fuzzing de Sistemas Embebidos usando Interfaces de Depurador'. Una preimpresión del artículo se puede encontrar aquí https://publications.cispa.saarland/3950/. El código permite a los usuarios reproducir y extender los resultados reportados en el artículo. Por favor, cite el artículo anterior al reportar, reproducir o extender los resultados.
.
├── benchmark # Scripts para construir el conjunto de pruebas de fuzzer de Google y ejecutar experimentos
├── dependencies # Contiene un Makefile para instalar dependencias de GDBFuzz
├── evaluation # Datos de experimentos en bruto, presentados en el artículo
├── example_firmware # Aplicaciones de ejemplo embebidas, utilizadas para la evaluación
├── example_programs # Contiene un programa de ejemplo compilado y configuraciones para probar GDBFuzz
├── src # Contiene la implementación de GDBFuzz
├── Dockerfile # Para crear una imagen Docker con todas las dependencias de GDBFuzz instaladas
├── LICENSE # Licencia
├── Makefile # Makefile para crear la imagen docker o instalar GDBFuzz localmente
└── README.md # Este archivo README
La idea de GDBFuzz es aprovechar los puntos de interrupción por hardware de los microcontroladores como retroalimentación para el fuzzing guiado por cobertura. Por lo tanto, GDB se utiliza como una interfaz genérica para permitir una amplia aplicabilidad. Para el análisis binario del firmware, se utiliza Ghidra. El código contiene una configuración de evaluación comparativa para evaluar el método. Además, se incluyen archivos de firmware de ejemplo.
GDBFuzz permite el fuzzing guiado por cobertura para sistemas embebidos, pero, con fines de evaluación, también puede fuzzing de aplicaciones de usuario arbitrarias. Para el fuzzing en microcontroladores, recomendamos una instalación local de GDBFuzz para poder enviar datos de fuzzing al dispositivo bajo prueba sin problemas.
GDBFuzz ha sido probado en Ubuntu 20.04 LTS y Raspberry Pi OS de 32 bits. Los requisitos previos son java y python3. Primero, cree un nuevo entorno virtual e instale todas las dependencias.
virtualenv .venv
source .venv/bin/activate
make
chmod a+x ./src/GDBFuzz/main.py
GDBFuzz lee la configuración de un archivo de configuración con las siguientes claves.
[SUT]
# Ruta al archivo binario del SUT.
# Esto puede ser, por ejemplo, un archivo .elf o un archivo .bin.
binary_file_path = <path>
# Dirección del nodo raíz del CFG.
# Los puntos de interrupción se colocan en los nodos de este CFG.
# ej. 'LLVMFuzzerTestOneInput' o 'main'
entrypoint = <entrypoint>
# Número de entradas que deben ejecutarse sin alcanzar un punto de interrupción hasta
# que se rotan los puntos de interrupción.
until_rotate_breakpoints = <number>
# Número máximo de puntos de interrupción que se pueden colocar en un momento dado.
max_breakpoints = <number>
# Lista negra de funciones que deben ignorarse.
# ignore_functions es una lista separada por espacios de nombres de funciones, ej. 'malloc free'.
ignore_functions = <space separated list>
# Uno de {Hardware, QEMU, SUTRunsOnHost}
# Hardware: Un componente externo inicia un servidor gdb y GDBFuzz puede conectarse a este servidor gdb.
# QEMU: GDBFuzz inicia QEMU. QEMU emula binary_file_path e inicia gdbserver.
# SUTRunsOnHost: GDBFuzz inicia el programa objetivo dentro de GDB.
target_mode = <mode>
# Establezca esto en False si desea iniciar ghidra, analizar el SUT,
# e iniciar el puente ghidra bridge manualmente.
start_ghidra = True
# Lista separada por espacios de direcciones donde se colocan puntos de interrupción por software (para código de manejo de errores). La ejecución de estos se considera un fallo.
# Ejemplo: software_breakpoint_addresses = 0x123 0x432
software_breakpoint_addresses =
# Si todos los puntos de interrupción por software activados se consideran como fallo
consider_sw_breakpoint_as_error = False
[SUTConnection]
# La clase 'SUT_connection_class' en el archivo 'SUT_connection_path' implementa
# cómo se envían las entradas al SUT.
# Las entradas pueden, por ejemplo, enviarse a través de Wi-Fi, Serial, Bluetooth, ...
# Esta clase debe heredar de ./connections/SUTConnection.py.
# Consulte ./connections/SUTConnection.py para obtener más información.
SUT_connection_file = FIFOConnection.py
[GDB]
path_to_gdb = gdb-multiarch
# Escrito en address:port
gdb_server_address = localhost:4242
[Fuzzer]
# En Bytes
maximum_input_length = 100000
# En segundos
single_run_timeout = 20
# En segundos
total_runtime = 3600
# Opcional
# Ruta a un directorio donde cada archivo contiene una semilla. Si no desea usar semillas, deje el valor vacío.
seeds_directory =
[BreakpointStrategy]
# Las estrategias para elegir bloques básicos se encuentran en
# 'src/GDBFuzz/breakpoint_strategies/'
# Para el artículo utilizamos las siguientes estrategias
# 'RandomBasicBlockStrategy.py' - Elegir aleatoriamente bloques básicos no alcanzados
# 'RandomBasicBlockNoDomStrategy.py' - Como el anterior, pero no usa relaciones de dominancia para derivar nodos transitivamente alcanzados.
# 'RandomBasicBlockNoCorpusStrategy.py' - Como el primero, pero evita hacer crecer el corpus de entrada y por lo tanto se comporta como fuzzing de caja negra con medición de cobertura.
# 'BlackboxStrategy.py', - No coloca ningún punto de interrupción
breakpoint_strategy_file = RandomBasicBlockStrategy.py
[Dependencies]
path_to_qemu = dependencies/qemu/build/x86_64-linux-user/qemu-x86_64
path_to_ghidra = dependencies/ghidra
[LogsAndVisualizations]
# Uno de {DEBUG, INFO, WARNING, ERROR, CRITICAL}
loglevel = INFO
# Ruta a un directorio donde se almacenan los archivos de salida (ej. gráficos, archivos de registro).
output_directory = ./output
# Si se establece en True, un cliente MQTT envía elementos de la interfaz de usuario (ej. gráficos)
enable_UI = False
Un archivo de configuración de ejemplo se encuentra en ./example_programs/ junto con un programa de ejemplo que fue compilado usando nuestro harness de fuzzing en benchmark/benchSUTs/GDBFuzz_wrapper/common/. Inicie el fuzzing durante una hora con el siguiente comando.
chmod a+x ./example_programs/json-2017-02-12
./src/GDBFuzz/main.py --config ./example_programs/fuzz_json.cfg
Primero vemos la salida de Ghidra analizando el ejecutable binario y posteriormente mensajes cuando los puntos de interrupción son reubicados o alcanzados.
Dependiendo del output_directory especificado en el archivo de configuración, ahora debería haber una carpeta trial-0 con la siguiente estructura
.
├── corpus # Una carpeta que contiene el corpus de entrada.
├── crashes # Una carpeta que contiene entradas que causan fallos, si las hay.
├── cfg # El grafo de flujo de control como lista de adyacencia.
├── fuzzer_stats # Estadísticas de la campaña de fuzzing.
├── plot_data # Tabla que muestra en qué tiempo relativo de la campaña de fuzzing se alcanzó cada bloque básico.
├── reverse_cfg # El grafo de flujo de control inverso.
Al establecer start_ghidra = False en el archivo de configuración, GDBFuzz se conecta a una instancia de Ghidra ejecutándose en modo GUI. Por lo tanto, el plugin ghidra_bridge debe iniciarse manualmente desde el administrador de scripts. Durante el fuzzing, los bloques de programa alcanzados se resaltan en verde.
Para el fuzzing en aplicaciones de usuario de Linux, GDBFuzz aprovecha el punto de entrada estándar LLVMFuzzOneInput que es utilizado por casi todos los fuzzers como AFL, AFL++, libFuzzer,...
En benchmark/benchSUTs/GDBFuzz_wrapper/common hay un wrapper que se puede usar para compilar cualquier harness de fuzzing compatible en un programa independiente que obtiene entrada a través de un pipe con nombre en /tmp/fromGDBFuzz.
Esto permite simular un dispositivo embebido que consume datos a través de una interfaz de entrada bien definida y, por lo tanto, ejecutar GDBFuzz en cualquier aplicación. Por conveniencia, creamos un script en benchmark/benchSUTs que compila todos los programas de nuestra evaluación con nuestro wrapper, como se explica más adelante.
NOTA: GDBFuzz no está diseñado para fuzzing de aplicaciones de usuario de Linux. Utilice AFL++ u otros fuzzers para eso. El wrapper solo existe con fines de evaluación para permitir la ejecución de evaluaciones comparativas y comparaciones a escala.
La efectividad general de nuestro enfoque se muestra en una evaluación comparativa a gran escala desplegada como contenedores docker.
make dockerimage
Para ejecutar el experimento anterior en el contenedor docker (durante una hora como se especifica en el archivo de configuración), mapee las carpetas example_programs y output como volúmenes e inicie GDBFuzz de la siguiente manera.
chmod a+x ./example_programs/json-2017-02-12
docker run -it --env CONFIG_FILE=/example_programs/fuzz_json_docker_qemu.cfg -v $(pwd)/example_programs:/example_programs -v $(pwd)/output:/output gdbfuzz:1.0
Debería aparecer una carpeta de salida en el directorio de trabajo actual con la estructura explicada anteriormente.
Nuestra evaluación se divide en dos partes.
GDBFuzz puede funcionar con cualquier servidor GDB y, por lo tanto, con la mayoría de las sondas de depuración para microcontroladores.
Con respecto a RQ1 del artículo, ejecutamos GDBFuzz en diferentes microcontroladores con diferentes firmwares ubicados en example_firmware. Para cada experimento ejecutamos GDBFuzz con la estrategia RandomBasicBlock y con la estrategia RandomBasicBlockNoCorpus. La última se comporta como fuzzing sin retroalimentación, pero aún podemos medir la cobertura alcanzada. Para responder a RQ1, comparamos la cobertura alcanzada de la estrategia RandomBasicBlock y la estrategia RandomBasicBlockNoCorpus. Los archivos de configuración respectivos están en las subcarpetas correspondientes y ahora explicamos cómo configurar el fuzzing en las cuatro placas de desarrollo.
GDBFuzz requiere acceso a un Servidor GDB. En este caso se utilizan la B-L4S5I-IOT01A y su depurador integrado. Este depurador integrado configura un servidor GDB a través del programa 'st-util', y permite el acceso a este servidor GDB mediante localhost:4242.
sudo apt-get install stlink-tools gdb-multiarch
Construya y cargue un firmware para la STM32 B-L4S5I-IOT01A, por ejemplo el proyecto arduinojson.
Requisito: Instalar platformio (pio)
cd ./example_firmware/stm32_disco_arduinojson/
pio run --target upload
Para su información: platformio almacena un archivo .elf del SUT aquí: ./example_firmware/stm32_disco_arduinojson/.pio/build/disco_l4s5i_iot01a/firmware.elf Este archivo .elf también se usa posteriormente en la configuración de usuario para Ghidra.
Inicie una nueva terminal y ejecute lo siguiente para iniciar un Servidor GDB:
st-util
Ejecute GDBFuzz con una configuración de usuario para arduinojson. Podemos enviar datos a través del puerto USB al microcontrolador. El microcontrolador reenvía estos datos a través de serie al SUT. En nuestro caso, /dev/ttyACM0 es el dispositivo USB hacia la placa del microcontrolador. Si su sistema asignó otro dispositivo a la placa del microcontrolador, cambie /dev/ttyACM0 en el archivo de configuración por su dispositivo.
./src/GDBFuzz/main.py --config ./example_firmware/stm32_disco_arduinojson/fuzz_serial_json.cfg
Las estadísticas y registros del fuzzer están en el directorio ./output/...
Instale pyocd:
pip install --upgrade pip 'mbed-ls>=1.7.1' 'pyocd>=0.16'
Asegúrese de que 'KitProg v3' esté en el dispositivo y ponga la placa en modo 'Arm DAPLink' presionando el botón correspondiente. Inicie el servidor GDB:
pyocd gdbserver --persist
Cargue un firmware e inicie el fuzzing, por ejemplo con
gdb-multiarch
target remote :3333
load ./example_firmware/CY8CKIT_json/mtb-example-psoc6-uart-transmit-receive.elf
monitor reset
./src/GDBFuzz/main.py --config ./example_firmware/CY8CKIT_json/fuzz_serial_json.cfg
Construya y cargue un firmware para el ESP32, por ejemplo el ejemplo arduinojson con platformio.
cd ./example_firmware/esp32_arduinojson/
pio run --target upload
Agregue la siguiente línea al archivo de configuración de openocd para el depurador J-Link: jlink.cfg
adapter speed 10000
Inicie una nueva terminal y ejecute lo siguiente para iniciar el Servidor GDB:
get_idf
openocd -f interface/jlink.cfg -f target/esp32.cfg -c "telnet_port 7777" -c "gdb_port 8888"
Ejecute GDBFuzz con una configuración de usuario para arduinojson. Podemos enviar datos a través del puerto USB al microcontrolador. El microcontrolador reenvía estos datos a través de serie al SUT. En nuestro caso, /dev/ttyUSB0 es el dispositivo USB hacia la placa del microcontrolador. Si su sistema asignó otro dispositivo a la placa del microcontrolador, cambie /dev/ttyUSB0 en el archivo de configuración por su dispositivo.
./src/GDBFuzz/main.py --config ./example_firmware/esp32_arduinojson/fuzz_serial.cfg
Las estadísticas y registros del fuzzer están en el directorio ./output/...
Instale TI MSP430 GCC desde https://www.ti.com/tool/MSP430-GCC-OPENSOURCE
Inicie el Servidor GDB
./gdb_agent_console libmsp430.so
o (más estable). Construya mspdebug desde https://github.com/dlbeer/mspdebug/ y use:
until mspdebug --fet-skip-close --force-reset tilib "opt gdb_loop True" gdb ; do sleep 1 ; done
Ghidra no puede analizar binarios para el controlador TI MSP430 de forma predeterminada. Para solucionarlo, importe el archivo en la GUI de Ghidra, elija MSP430X como arquitectura y omita el análisis automático. A continuación, abra la 'Tabla de Símbolos', ordénelos por nombre y elimine todos los símbolos con nombres como $C$L*. Ahora se puede ejecutar el análisis automático. Después del análisis, inicie el puente ghidra desde la GUI de Ghidra manualmente y luego inicie GDBFuzz.
./src/GDBFuzz/main.py --config ./example_firmware/msp430_arduinojson/fuzz_serial.cfg
Para acceder a dispositivos USB como usuario no root con pyusb, agregamos reglas apropiadas a udev. Pegue las siguientes líneas en /etc/udev/rules.d/50-myusb.rules:
SUBSYSTEM=="usb", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678" GROUP="usbusers", MODE="666"
Recargue udev:
sudo udevadm control --reload
sudo udevadm trigger
En RQ2 del artículo, comparamos GDBFuzz con el enfoque basado en emulación Fuzzware. Primero ejecutamos GDBFuzz y Fuzzware como se describió anteriormente en los archivos de firmware incluidos. Para cada experimento de GDBFuzz, creamos un archivo con bloques básicos válidos a partir de los archivos del grafo de flujo de control de la siguiente manera:
cut -d " " -f1 ./cfg > valid_bbs.txt
Ahora podemos reproducir la cobertura contra el resultado de fuzzware: fuzzware genstats --valid-bb-file valid_bbs.txt
Cuando se encuentran entradas que causan fallos o cuelgues, se almacenan en la carpeta crashes. Durante la evaluación, encontramos los siguientes tres bugs:
GDBFuzz también puede ejecutarse en un host Raspberry Pi con ligeras modificaciones:
En el archivo ./dependencies/ghidra/support/launch.sh:125 La variable JAVA_HOME debe estar codificada, por ejemplo, a JAVA_HOME="/usr/lib/jvm/default-java"
Para fuzzing de software en otras placas, GDBFuzz requiere
src/GDBFuzz/connections) que active la ejecución del código en el punto de entrada, por ejemplo, conexión serieTodas estas propiedades deben especificarse en el archivo de configuración.
Para las RQ 4 - 8 ejecutamos un benchmark a gran escala.
Primero, construya la imagen Docker como se describió anteriormente y compile las aplicaciones del Fuzzer Test Suite de Google con nuestro harness de fuzzing en benchmark/benchSUTs/GDBFuzz_wrapper/common.
cd ./benchmark/benchSUTs
chmod a+x setup_benchmark_SUTs.py
make dockerbenchmarkimage
A continuación, adapte la configuración del benchmark en benchmark/scripts/benchmark.py y benchmark/scripts/benchmark_aflpp.py a sus necesidades (especialmente number_of_cores, trials y seconds_per_trial) e inicie el benchmark con:
cd ./benchmark/scripts
./benchmark.py $(pwd)/../benchSUTs/SUTs/ SUTs.json
./benchmark_aflpp.py $(pwd)/../benchSUTs/SUTs/ SUTs.json
Aparece una carpeta en ./benchmark/scripts que contiene archivos de trazado (cobertura a lo largo del tiempo), archivos de estadísticas del fuzzer y archivos de grafo de flujo de control para cada experimento, como en evaluation/fuzzer_test_suite_qemu_runs.
GDBFuzz tiene una función opcional donde dibuja el grafo de flujo de control de los nodos cubiertos. Esto está deshabilitado por defecto. Puede habilitarlo siguiendo las instrucciones de esta sección y estableciendo 'enable_UI' a 'True' en la configuración de usuario.
En el host:
Instale
sudo apt-get install graphviz
Instale una versión reciente de node, por ejemplo Opción 2 desde aquí. Use la Opción 2 y no la opción 1. Esto debería instalar tanto node como npm. Como referencia, nuestros números de versión son (pero las versiones más nuevas también deberían funcionar):
➜ node --version
v16.9.1
➜ npm --version
7.21.1
Instale las dependencias de la interfaz web:
cd ./src/webui
npm install
Instale el broker MQTT mosquitto, por ejemplo, consulte aquí
Actualice la configuración del broker mosquitto: Reemplace el archivo /etc/mosquitto/conf.d/mosquitto.conf con el siguiente contenido:
listener 1883
allow_anonymous true
listener 9001
protocol websockets
Reinicie el broker mosquitto:
sudo service mosquitto restart
Verifique que el broker mosquitto esté ejecutándose:
sudo service mosquitto status
La salida debe incluir el texto 'Active: active (running)'
Inicie la interfaz web:
cd ./src/webui
npm start
Su navegador web debería abrirse automáticamente en 'http://localhost:3000/'.
Inicie GDBFuzz y use un archivo de configuración de usuario donde enable_UI esté establecido en True. Puede usar el contenedor Docker y el SUT arduinojson de arriba. Pero asegúrese de establecer 'enable_UI' en 'True'.
Los nodos cubiertos en 'azul' están cubiertos. Los nodos blancos no están cubiertos. Solo mostramos nodos no cubiertos si su padre está cubierto (dibujar el grafo de flujo de control completo lleva demasiado tiempo si el grafo de flujo de control es grande).
GDBFuzz es de código abierto bajo la licencia AGPL-3.0. Consulte el archivo LICENSE para obtener más detalles.
Para obtener una lista de otros componentes de código abierto incluidos en GDBFuzz, consulte el archivo 3rd-party-licenses.txt.