
Fuzzing de Sistemas Embarcados usando Breakpoints de Hardware
Este é o código complementar para o artigo: 'Fuzzing Embedded Systems using Debugger Interfaces'. Uma pré-impressão do artigo pode ser encontrada aqui https://publications.cispa.saarland/3950/. O código permite que os usuários reproduzam e estendam os resultados relatados no artigo. Por favor, cite o artigo acima ao reportar, reproduzir ou estender os resultados.
.
├── benchmark # Scripts para construir o conjunto de teste de fuzzer do Google e executar experimentos
├── dependencies # Contém um Makefile para instalar dependências para o GDBFuzz
├── evaluation # Dados brutos dos experimentos, apresentados no artigo
├── example_firmware # Aplicações de exemplo embarcadas, usadas para a avaliação
├── example_programs # Contém um programa de exemplo compilado e configurações para testar o GDBFuzz
├── src # Contém a implementação do GDBFuzz
├── Dockerfile # Para criar uma imagem Docker com todas as dependências do GDBFuzz instaladas
├── LICENSE # Licença
├── Makefile # Makefile para criar a imagem Docker ou instalar o GDBFuzz localmente
└── README.md # Este arquivo README
A ideia do GDBFuzz é aproveitar pontos de interrupção de hardware de microcontroladores como feedback para fuzzing guiado por cobertura. Para isso, o GDB é usado como uma interface genérica para permitir ampla aplicabilidade. Para análise binária do firmware, o Ghidra é usado. O código contém uma configuração de benchmark para avaliar o método. Além disso, arquivos de firmware de exemplo estão incluídos.
O GDBFuzz permite fuzzing guiado por cobertura para sistemas embarcados, mas - para fins de avaliação - também pode fuzzar aplicações de usuário arbitrárias. Para fuzzing em microcontroladores, recomendamos uma instalação local do GDBFuzz para poder enviar dados de fuzz para o dispositivo sob teste sem problemas.
O GDBFuzz foi testado no Ubuntu 20.04 LTS e no Raspberry Pi OS 32 bits. Pré-requisitos são java e python3. Primeiro, crie um novo ambiente virtual e instale todas as dependências.
virtualenv .venv
source .venv/bin/activate
make
chmod a+x ./src/GDBFuzz/main.py
O GDBFuzz lê configurações de um arquivo de configuração com as seguintes chaves.
[SUT]
# Caminho para o arquivo binário do SUT.
# Isso pode ser, por exemplo, um arquivo .elf ou um arquivo .bin.
binary_file_path = <caminho>
# Endereço do nó raiz do CFG.
# Pontos de interrupção são colocados em nós deste CFG.
# ex.: 'LLVMFuzzerTestOneInput' ou 'main'
entrypoint = <ponto_de_entrada>
# Número de entradas que devem ser executadas sem um ponto de interrupção ser atingido até
# que os pontos de interrupção sejam rotacionados.
until_rotate_breakpoints = <número>
# Número máximo de pontos de interrupção que podem ser colocados a qualquer momento.
max_breakpoints = <número>
# Lista de funções que devem ser ignoradas.
# ignore_functions é uma lista de nomes de funções separados por espaço, ex.: 'malloc free'.
ignore_functions = <lista separada por espaço>
# Um de {Hardware, QEMU, SUTRunsOnHost}
# Hardware: Um componente externo inicia um servidor gdb e o GDBFuzz pode se conectar a esse servidor gdb.
# QEMU: O GDBFuzz inicia o QEMU. O QEMU emula binary_file_path e inicia o gdbserver.
# SUTRunsOnHost: O GDBFuzz inicia o programa alvo dentro do GDB.
target_mode = <modo>
# Defina como False se você quiser iniciar o ghidra, analisar o SUT
# e iniciar o servidor bridge do ghidra manualmente.
start_ghidra = True
# Lista separada por espaços de endereços onde pontos de interrupção de software (para código
# de tratamento de erros) são definidos. A execução destes é considerada uma falha.
# Exemplo: software_breakpoint_addresses = 0x123 0x432
software_breakpoint_addresses =
# Se todos os pontos de interrupção de software acionados são considerados como falha
consider_sw_breakpoint_as_error = False
[SUTConnection]
# A classe 'SUT_connection_class' no arquivo 'SUT_connection_path' implementa
# como as entradas são enviadas para o SUT.
# As entradas podem, por exemplo, ser enviadas via Wi-Fi, Serial, Bluetooth, ...
# Esta classe deve herdar de ./connections/SUTConnection.py.
# Veja ./connections/SUTConnection.py para mais informações.
SUT_connection_file = FIFOConnection.py
[GDB]
path_to_gdb = gdb-multiarch
#Escrito como endereço:porta
gdb_server_address = localhost:4242
[Fuzzer]
# Em bytes
maximum_input_length = 100000
# Em segundos
single_run_timeout = 20
# Em segundos
total_runtime = 3600
# Opcional
# Caminho para um diretório onde cada arquivo contém uma semente. Se você não quiser
# usar sementes, deixe o valor vazio.
seeds_directory =
[BreakpointStrategy]
# As estratégias para escolher blocos básicos estão localizadas em
# 'src/GDBFuzz/breakpoint_strategies/'
# Para o artigo, usamos as seguintes estratégias
# 'RandomBasicBlockStrategy.py' - Escolhe aleatoriamente blocos básicos não alcançados
# 'RandomBasicBlockNoDomStrategy.py' - Como o anterior, mas não usa relações de dominância para derivar nós transitivamente alcançados.
# 'RandomBasicBlockNoCorpusStrategy.py' - Como o primeiro, mas impede o crescimento do corpus de entrada e, portanto, se comporta como fuzzing de caixa preta com medição de cobertura.
# 'BlackboxStrategy.py' - Não define nenhum ponto de interrupção
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]
# Um de {DEBUG, INFO, WARNING, ERROR, CRITICAL}
loglevel = INFO
# Caminho para um diretório onde os arquivos de saída (ex.: gráficos, arquivos de log) são armazenados.
output_directory = ./output
# Se definido como True, um cliente MQTT envia elementos de UI (ex.: gráficos)
enable_UI = False
Um exemplo de arquivo de configuração está localizado em ./example_programs/ junto com um programa de exemplo que foi compilado usando nosso harness de fuzzing em benchmark/benchSUTs/GDBFuzz_wrapper/common/.
Inicie o fuzzing por uma hora com o seguinte comando.
chmod a+x ./example_programs/json-2017-02-12
./src/GDBFuzz/main.py --config ./example_programs/fuzz_json.cfg
Primeiro vemos a saída do Ghidra analisando o executável binário e, em seguida, mensagens quando os pontos de interrupção são realocados ou atingidos.
Dependendo do output_directory especificado no arquivo de configuração, agora deve haver uma pasta trial-0 com a seguinte estrutura
.
├── corpus # Uma pasta que contém o corpus de entrada.
├── crashes # Uma pasta que contém entradas que causam falha - se houver.
├── cfg # O grafo de fluxo de controle como lista de adjacência.
├── fuzzer_stats # Estatísticas da campanha de fuzzing.
├── plot_data # Tabela mostrando em qual tempo relativo na campanha de fuzzing qual bloco básico foi alcançado.
├── reverse_cfg # O grafo de fluxo de controle reverso.
Definindo start_ghidra = False no arquivo de configuração, o GDBFuzz se conecta a uma instância do Ghidra executando no modo GUI. Portanto, o plugin ghidra_bridge precisa ser iniciado manualmente a partir do gerenciador de scripts. Durante o fuzzing, os blocos de programa alcançados são destacados em verde.
Para fuzzing em aplicações de usuário Linux, o GDBFuzz aproveita o ponto de entrada padrão LLVMFuzzOneInput que é usado por quase todos os fuzzers como AFL, AFL++, libFuzzer,....
Em benchmark/benchSUTs/GDBFuzz_wrapper/common há um wrapper que pode ser usado para compilar qualquer harness de fuzzing compatível em um programa independente que obtém entrada via um pipe nomeado em /tmp/fromGDBFuzz.
Isso permite simular um dispositivo embarcado que consome dados através de uma interface de entrada bem definida e, portanto, executar o GDBFuzz em qualquer aplicação. Para conveniência, criamos um script em benchmark/benchSUTs que compila todos os programas de nossa avaliação com nosso wrapper, conforme explicado posteriormente.
NOTA: O GDBFuzz não se destina a fuzzar aplicações de usuário Linux. Use AFL++ ou outros fuzzers para isso. O wrapper existe apenas para fins de avaliação para permitir a execução de benchmarks e comparações em escala!
A eficácia geral de nossa abordagem é demonstrada em um benchmark de grande escala implantado como contêineres Docker.
make dockerimage
Para executar o experimento acima no contêiner Docker (por uma hora, conforme especificado no arquivo de configuração), mapeie as pastas example_programs e output como volumes e inicie o GDBFuzz da seguinte forma.
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
Uma pasta de saída deve aparecer no diretório de trabalho atual com a estrutura explicada acima.
Nossa avaliação é dividida em duas partes.
O GDBFuzz pode funcionar com qualquer servidor GDB e, portanto, com a maioria das sondas de depuração para microcontroladores.
Em relação à RQ1 do artigo, executamos o GDBFuzz em diferentes microcontroladores com diferentes firmwares localizados em example_firmware.
Para cada experimento, executamos o GDBFuzz com a estratégia RandomBasicBlock e com a estratégia RandomBasicBlockNoCorpus. A segunda se comporta como fuzzing sem feedback, mas ainda podemos medir a cobertura alcançada.
Para responder à RQ1, comparamos a cobertura alcançada da estratégia RandomBasicBlock e RandomBasicBlockNoCorpus.
Os arquivos de configuração respectivos estão nas subpastas correspondentes e agora explicamos como configurar o fuzzing nas quatro placas de desenvolvimento.
O GDBFuzz requer acesso a um Servidor GDB. Neste caso, a placa B-L4S5I-IOT01A e seu depurador integrado são usados. Este depurador integrado configura um servidor GDB através do programa 'st-util' e permite acesso a este servidor GDB via localhost:4242.
sudo apt-get install stlink-tools gdb-multiarch
Compile e grave um firmware para o STM32 B-L4S5I-IOT01A, por exemplo o projeto arduinojson.
Pré-requisito: Instale platformio (pio)
cd ./example_firmware/stm32_disco_arduinojson/
pio run --target upload
Para sua informação: o platformio armazena um arquivo .elf do SUT aqui: ./example_firmware/stm32_disco_arduinojson/.pio/build/disco_l4s5i_iot01a/firmware.elf Este arquivo .elf também é usado posteriormente na configuração do usuário para o Ghidra.
Inicie um novo terminal e execute o seguinte para iniciar o Servidor GDB:
st-util
Execute o GDBFuzz com uma configuração de usuário para arduinojson. Podemos enviar dados pela porta USB para o microcontrolador. O microcontrolador encaminha esses dados via serial para o SUT. No nosso caso, /dev/ttyACM0 é o dispositivo USB para a placa microcontroladora. Se seu sistema atribuiu outro dispositivo à placa microcontroladora, altere /dev/ttyACM0 no arquivo de configuração para o seu dispositivo.
./src/GDBFuzz/main.py --config ./example_firmware/stm32_disco_arduinojson/fuzz_serial_json.cfg
Estatísticas e logs do fuzzer estão no diretório ./output/...
Instale pyocd:
pip install --upgrade pip 'mbed-ls>=1.7.1' 'pyocd>=0.16'
Certifique-se de que 'KitProg v3' esteja no dispositivo e coloque a placa no modo 'Arm DAPLink' pressionando o botão apropriado. Inicie o servidor GDB:
pyocd gdbserver --persist
Grave um firmware e inicie o fuzzing, por exemplo, com
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
Compile e grave um firmware para o ESP32, por exemplo o exemplo arduinojson com platformio.
cd ./example_firmware/esp32_arduinojson/
pio run --target upload
Adicione a seguinte linha ao arquivo de configuração do openocd para o depurador J-Link: jlink.cfg
adapter speed 10000
Inicie um novo terminal e execute o seguinte para iniciar o Servidor GDB:
get_idf
openocd -f interface/jlink.cfg -f target/esp32.cfg -c "telnet_port 7777" -c "gdb_port 8888"
Execute o GDBFuzz com uma configuração de usuário para arduinojson. Podemos enviar dados pela porta USB para o microcontrolador. O microcontrolador encaminha esses dados via serial para o SUT. No nosso caso, /dev/ttyUSB0 é o dispositivo USB para a placa microcontroladora. Se seu sistema atribuiu outro dispositivo à placa microcontroladora, altere /dev/ttyUSB0 no arquivo de configuração para o seu dispositivo.
./src/GDBFuzz/main.py --config ./example_firmware/esp32_arduinojson/fuzz_serial.cfg
Estatísticas e logs do fuzzer estão no diretório ./output/...
Instale o TI MSP430 GCC em https://www.ti.com/tool/MSP430-GCC-OPENSOURCE
Inicie o Servidor GDB
./gdb_agent_console libmsp430.so
ou (mais estável). Compile o mspdebug de https://github.com/dlbeer/mspdebug/ e use:
until mspdebug --fet-skip-close --force-reset tilib "opt gdb_loop True" gdb ; do sleep 1 ; done
O Ghidra falha ao analisar binários para o controlador TI MSP430 imediatamente. Para corrigir isso, importamos o arquivo na GUI do Ghidra, escolhemos MSP430X como arquitetura e pulamos a análise automática. Em seguida, abrimos a 'Tabela de Símbolos', classificamos por nome e excluímos todos os símbolos com nomes como $C$L*. Agora a análise automática pode ser executada. Após a análise, inicie o bridge do ghidra manualmente a partir da GUI do Ghidra e então inicie o GDBFuzz.
./src/GDBFuzz/main.py --config ./example_firmware/msp430_arduinojson/fuzz_serial.cfg
Para acessar dispositivos USB como usuário não root com pyusb, adicionamos regras apropriadas ao udev. Cole as seguintes linhas em /etc/udev/rules.d/50-myusb.rules:
SUBSYSTEM=="usb", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678" GROUP="usbusers", MODE="666"
Recarregue o udev:
sudo udevadm control --reload
sudo udevadm trigger
Na RQ2 do artigo, comparamos o GDBFuzz com a abordagem baseada em emulação Fuzzware. Primeiro executamos o GDBFuzz e o Fuzzware conforme descrito anteriormente nos arquivos de firmware fornecidos. Para cada experimento do GDBFuzz, criamos um arquivo com blocos básicos válidos a partir dos arquivos do grafo de fluxo de controle da seguinte forma:
cut -d " " -f1 ./cfg > valid_bbs.txt
Agora podemos reproduzir a cobertura em relação ao resultado do fuzzware: fuzzware genstats --valid-bb-file valid_bbs.txt
Quando entradas que causam falha ou travamento são encontradas, elas são armazenadas na pasta crashes. Durante a avaliação, encontramos os três bugs a seguir:
O GDBFuzz também pode ser executado em um host Raspberry Pi com pequenas modificações:
No arquivo ./dependencies/ghidra/support/launch.sh:125 A variável JAVA_HOME deve ser codificada, por exemplo, para JAVA_HOME="/usr/lib/jvm/default-java"
Para fuzzar software em outras placas, o GDBFuzz requer:
src/GDBFuzz/connections) que aciona a execução do código no ponto de entrada, por exemplo, conexão serialTodas essas propriedades precisam ser especificadas no arquivo de configuração.
Para as RQs 4 a 8, executamos um benchmark de grande escala.
Primeiro, construa a imagem Docker conforme descrito anteriormente e compile aplicações do Fuzzer Test Suite do Google com nosso harness de fuzzing em benchmark/benchSUTs/GDBFuzz_wrapper/common.
cd ./benchmark/benchSUTs
chmod a+x setup_benchmark_SUTs.py
make dockerbenchmarkimage
Em seguida, adapte as configurações do benchmark em benchmark/scripts/benchmark.py e benchmark/scripts/benchmark_aflpp.py às suas necessidades (especialmente number_of_cores, trials e seconds_per_trial) e inicie o benchmark com:
cd ./benchmark/scripts
./benchmark.py $(pwd)/../benchSUTs/SUTs/ SUTs.json
./benchmark_aflpp.py $(pwd)/../benchSUTs/SUTs/ SUTs.json
Uma pasta aparece em ./benchmark/scripts que contém arquivos de plotagem (cobertura ao longo do tempo), arquivos de estatísticas do fuzzer e arquivos de grafo de fluxo de controle para cada experimento, como em evaluation/fuzzer_test_suite_qemu_runs.
O GDBFuzz possui um recurso opcional onde ele plota o grafo de fluxo de controle dos nós cobertos. Isso está desabilitado por padrão. Você pode habilitá-lo seguindo as instruções desta seção e definindo 'enable_UI' como 'True' na configuração do usuário.
No host:
Instale
sudo apt-get install graphviz
Instale uma versão recente de node, por exemplo Opção 2 de aqui. Use a Opção 2 e não a opção 1. Isso deve instalar tanto node quanto npm. Para referência, nossos números de versão são (mas versões mais novas também devem funcionar):
➜ node --version
v16.9.1
➜ npm --version
7.21.1
Instale as dependências da interface web:
cd ./src/webui
npm install
Instale o broker MQTT mosquitto, por exemplo, veja aqui
Atualize a configuração do broker mosquitto: Substitua o arquivo /etc/mosquitto/conf.d/mosquitto.conf pelo seguinte conteúdo:
listener 1883
allow_anonymous true
listener 9001
protocol websockets
Reinicie o broker mosquitto:
sudo service mosquitto restart
Verifique se o broker mosquitto está em execução:
sudo service mosquitto status
A saída deve incluir o texto 'Active: active (running)'
Inicie a interface web:
cd ./src/webui
npm start
Seu navegador web deve abrir automaticamente em 'http://localhost:3000/'.
Inicie o GDBFuzz e use um arquivo de configuração de usuário onde enable_UI está definido como True. Você pode usar o contêiner Docker e o SUT arduinojson acima. Mas certifique-se de definir 'enable_UI' como 'True'.
Os nós cobertos em 'azul' estão cobertos. Nós brancos não estão cobertos. Mostramos apenas nós não cobertos se seu pai estiver coberto (desenhar o grafo de fluxo de controle completo leva muito tempo se o grafo for grande).
O GDBFuzz é licenciado sob a licença AGPL-3.0. Consulte o arquivo LICENSE para obter detalhes.
Para uma lista de outros componentes de código aberto incluídos no GDBFuzz, consulte o arquivo 3rd-party-licenses.txt.