本文是论文《使用调试器接口模糊测试嵌入式系统》的配套代码。 论文预印本可在此处获取:https://publications.cispa.saarland/3950/。代码允许用户 复现并扩展论文中报告的结果。在报告、复现或扩展结果时,请引用上述论文。
.
├── benchmark # 用于构建 Google 模糊测试套件并运行实验的脚本
├── dependencies # 包含安装 GDBFuzz 依赖项的 Makefile
├── evaluation # 论文中呈现的原始实验数据
├── example_firmware # 用于评估的嵌入式示例应用程序
├── example_programs # 包含一个编译好的示例程序及配置,用于测试 GDBFuzz
├── src # 包含 GDBFuzz 的实现代码
├── Dockerfile # 用于创建安装了全部 GDBFuzz 依赖项的 Docker 镜像
├── LICENSE # 许可证
├── Makefile # 用于创建 Docker 镜像或在本地安装 GDBFuzz 的 Makefile
└── README.md # 本 README 文件
GDBFuzz 的思想是利用微控制器的硬件断点作为覆盖引导模糊测试的反馈。因此,使用 GDB 作为通用接口以实现广泛的适用性。对于固件的二进制分析,使用 Ghidra。代码包含一个用于评估该方法的基准测试设置。此外,还附带了示例固件文件。
GDBFuzz 能够对嵌入式系统进行覆盖引导模糊测试,但出于评估目的,也可以模糊测试任意的用户应用程序。对于微控制器上的模糊测试,我们建议在本地安装 GDBFuzz,以便能够顺畅地将模糊数据发送到被测设备。
GDBFuzz 已在 Ubuntu 20.04 LTS 和 Raspberry Pi OS 32 位版本上测试通过。 前提条件是 java 和 python3。首先,创建一个新的虚拟环境并安装所有依赖项。
virtualenv .venv
source .venv/bin/activate
make
chmod a+x ./src/GDBFuzz/main.py
GDBFuzz 从一个包含以下键的配置文件中读取设置。
[SUT]
# 被测系统二进制文件的路径。
# 例如,可以是 .elf 文件或 .bin 文件。
binary_file_path = <路径>
# 控制流图根节点的地址。
# 断点将放置在该 CFG 的节点上。
# 例如 'LLVMFuzzerTestOneInput' 或 'main'
entrypoint = <入口点>
# 在没有命中断点的情况下,必须执行的输入数量,之后将轮换断点。
until_rotate_breakpoints = <数字>
# 任何时候最多可以放置的断点数量。
max_breakpoints = <数字>
# 要忽略的黑名单函数。
# ignore_functions 是一个空格分隔的函数名列表,例如 'malloc free'。
ignore_functions = <空格分隔的列表>
# 枚举值之一:{Hardware, QEMU, SUTRunsOnHost}
# Hardware: 外部组件启动 gdb 服务器,GDBFuzz 可以连接到该 gdb 服务器。
# QEMU: GDBFuzz 启动 QEMU。QEMU 模拟 binary_file_path 并启动 gdbserver。
# SUTRunsOnHost: GDBFuzz 在 GDB 内部启动目标程序。
target_mode = <模式>
# 如果希望手动启动 Ghidra、分析被测系统并启动 Ghidra 桥接服务器,请设置为 False。
start_ghidra = True
# 设置软件断点(用于错误处理代码)的地址列表,以空格分隔。
# 执行到这些地址被视为崩溃。
# 示例:software_breakpoint_addresses = 0x123 0x432
software_breakpoint_addresses =
# 是否将所有触发的软件断点视为错误
consider_sw_breakpoint_as_error = False
[SUTConnection]
# 文件 'SUT_connection_path' 中的类 'SUT_connection_class' 实现了
# 如何将输入发送到被测系统。
# 例如,可以通过 Wi-Fi、串口、蓝牙等方式发送输入。
# 该类必须继承自 ./connections/SUTConnection.py。
# 更多信息请参阅 ./connections/SUTConnection.py。
SUT_connection_file = FIFOConnection.py
[GDB]
path_to_gdb = gdb-multiarch
# 格式为 address:port
gdb_server_address = localhost:4242
[Fuzzer]
# 以字节为单位
maximum_input_length = 100000
# 以秒为单位
single_run_timeout = 20
# 以秒为单位
total_runtime = 3600
# 可选
# 一个目录的路径,其中每个文件包含一个种子。如果不希望使用种子,请将值留空。
seeds_directory =
[BreakpointStrategy]
# 选择基本块的策略位于 'src/GDBFuzz/breakpoint_strategies/' 中
# 在论文中,我们使用了以下策略:
# 'RandomBasicBlockStrategy.py' - 随机选择未到达的基本块
# 'RandomBasicBlockNoDomStrategy.py' - 与上一策略类似,但不使用支配关系导出传递到达的节点。
# 'RandomBasicBlockNoCorpusStrategy.py' - 与第一策略类似,但阻止输入语料库增长,因此表现为带有覆盖率测量的黑盒模糊测试。
# 'BlackboxStrategy.py', - 不设置任何断点
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]
# 枚举值之一:{DEBUG, INFO, WARNING, ERROR, CRITICAL}
loglevel = INFO
# 存储输出文件(如图表、日志文件)的目录路径。
output_directory = ./output
# 如果设置为 True,MQTT 客户端将发送 UI 元素(如图表)
enable_UI = False
示例配置文件位于 ./example_programs/ 中,同时还有一个示例程序,该程序是使用 benchmark/benchSUTs/GDBFuzz_wrapper/common/ 中的模糊测试工具链编译的。
使用以下命令启动一小时的模糊测试:
chmod a+x ./example_programs/json-2017-02-12
./src/GDBFuzz/main.py --config ./example_programs/fuzz_json.cfg
我们首先看到 Ghidra 分析二进制可执行文件的输出,随后是断点重定位或命中时的消息。
根据配置文件中指定的 output_directory,现在应该有一个 trial-0 文件夹,其结构如下:
.
├── corpus # 包含输入语料库的文件夹。
├── crashes # 包含崩溃输入的文件夹(如果有)。
├── cfg # 以邻接表形式表示的控制流图。
├── fuzzer_stats # 模糊测试活动的统计信息。
├── plot_data # 显示在模糊测试活动中哪个基本块在哪个相对时间被到达的表。
├── reverse_cfg # 反向控制流图。
通过在配置文件中设置 start_ghidra = False,GDBFuzz 会连接到以 GUI 模式运行的 Ghidra 实例。因此,需要从脚本管理器手动启动 ghidra_bridge 插件。在模糊测试期间,到达的程序块会以绿色高亮显示。
对于 Linux 用户应用程序的模糊测试,GDBFuzz 利用几乎所有模糊器(如 AFL、AFL++、libFuzzer 等)都使用的标准 LLVMFuzzOneInput 入口点。
在 benchmark/benchSUTs/GDBFuzz_wrapper/common 中有一个包装器,可以将任何兼容的模糊测试工具链编译成一个独立的程序,该程序通过位于 /tmp/fromGDBFuzz 的命名管道获取输入。
这允许模拟一个通过明确定义的输入接口消费数据的嵌入式设备,从而在任意应用程序上运行 GDBFuzz。为了方便起见,我们在 benchmark/benchSUTs 中创建了一个脚本,该脚本可以按照稍后说明的方式,使用我们的包装器编译来自我们评估的所有程序。
注意: GDBFuzz 并非用于模糊测试 Linux 用户应用程序。请使用 AFL++ 或其他模糊器。该包装器仅用于评估目的,以便在规模上运行基准测试和比较!
我们方法的总体效果在部署为 Docker 容器的大规模基准测试中得到了展示。
make dockerimage
要在 Docker 容器中运行上述实验(按配置文件指定的一小时),请将 example_programs 和 output 文件夹映射为卷,并按如下方式启动 GDBFuzz:
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
在当前工作目录中应该会出现一个 output 文件夹,其结构如上所述。
我们的评估分为两部分。
GDBFuzz 可以配合任何 GDB 服务器工作,因此也适用于大多数微控制器的调试探针。
关于论文中的研究问题1,我们在不同的微控制器上使用位于 example_firmware 中的不同固件来执行 GDBFuzz。
对于每个实验,我们分别使用 RandomBasicBlock 策略和 RandomBasicBlockNoCorpus 策略运行 GDBFuzz。后者的行为类似于无反馈的模糊测试,但我们仍然可以测量达到的覆盖率。
为了回答研究问题1,我们比较了 RandomBasicBlock 和 RandomBasicBlockNoCorpus 策略所达到的覆盖率。
相应的配置文件位于相应的子文件夹中,下面我们解释如何在四个开发板上设置模糊测试。
GDBFuzz 需要访问 GDB 服务器。本例中使用 B-L4S5I-IOT01A 及其板载调试器。该板载调试器通过 'st-util' 程序设置 GDB 服务器,并允许通过 localhost:4242 访问该 GDB 服务器。
sudo apt-get install stlink-tools gdb-multiarch
为 STM32 B-L4S5I-IOT01A 构建并烧录一个固件,例如 arduinojson 项目。
前提条件:安装 platformio (pio)
cd ./example_firmware/stm32_disco_arduinojson/
pio run --target upload
提示:platformio 将被测系统的一个 .elf 文件存储在此处:./example_firmware/stm32_disco_arduinojson/.pio/build/disco_l4s5i_iot01a/firmware.elf 该 .elf 文件稍后也会在 Ghidra 的用户配置中使用。
打开一个新的终端,并运行以下命令启动 GDB 服务器:
st-util
使用 arduinojson 的用户配置运行 GDBFuzz。我们可以通过 USB 端口将数据发送到微控制器。微控制器通过串口将这些数据转发给被测系统。在我们的例子中,/dev/ttyACM0 是连接微控制器板的 USB 设备。如果您的系统将其他设备分配给微控制器板,请在配置文件中将 /dev/ttyACM0 更改为您的设备。
./src/GDBFuzz/main.py --config ./example_firmware/stm32_disco_arduinojson/fuzz_serial_json.cfg
模糊测试统计信息和日志位于 ./output/... 目录中。
安装 pyocd:
pip install --upgrade pip 'mbed-ls>=1.7.1' 'pyocd>=0.16'
确保设备上装有 'KitProg v3',并通过按相应按钮将板子切换到 'Arm DAPLink' 模式。 启动 GDB 服务器:
pyocd gdbserver --persist
烧录一个固件并开始模糊测试,例如:
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
为 ESP32 构建并烧录一个固件,例如使用 platformio 的 arduinojson 示例。
cd ./example_firmware/esp32_arduinojson/
pio run --target upload
向 J-Link 调试器的 openocd 配置文件中添加以下行:jlink.cfg
adapter speed 10000
打开一个新的终端,并运行以下命令启动 GDB 服务器:
get_idf
openocd -f interface/jlink.cfg -f target/esp32.cfg -c "telnet_port 7777" -c "gdb_port 8888"
使用 arduinojson 的用户配置运行 GDBFuzz。我们可以通过 USB 端口将数据发送到微控制器。微控制器通过串口将这些数据转发给被测系统。在我们的例子中,/dev/ttyUSB0 是连接微控制器板的 USB 设备。如果您的系统将其他设备分配给微控制器板,请在配置文件中将 /dev/ttyUSB0 更改为您的设备。
./src/GDBFuzz/main.py --config ./example_firmware/esp32_arduinojson/fuzz_serial.cfg
模糊测试统计信息和日志位于 ./output/... 目录中。
从 https://www.ti.com/tool/MSP430-GCC-OPENSOURCE 安装 TI MSP430 GCC
启动 GDB 服务器:
./gdb_agent_console libmsp430.so
或者(更稳定)。从 https://github.com/dlbeer/mspdebug/ 构建 mspdebug,并使用:
until mspdebug --fet-skip-close --force-reset tilib "opt gdb_loop True" gdb ; do sleep 1 ; done
Ghidra 无法直接分析 TI MSP430 控制器的二进制文件。为了解决这个问题,我们在 Ghidra GUI 中导入文件,选择 MSP430X 作为架构,并跳过自动分析。接下来,打开“符号表”,按名称排序,并删除所有名称类似 $C$L* 的符号。现在可以执行自动分析。分析完成后,从 Ghidra GUI 手动启动 ghidra 桥,然后启动 GDBFuzz。
./src/GDBFuzz/main.py --config ./example_firmware/msp430_arduinojson/fuzz_serial.cfg
为了以非 root 用户身份使用 pyusb 访问 USB 设备,我们在 udev 中添加适当的规则。将以下行粘贴到 /etc/udev/rules.d/50-myusb.rules 中:
SUBSYSTEM=="usb", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678" GROUP="usbusers", MODE="666"
重新加载 udev:
sudo udevadm control --reload
sudo udevadm trigger
在论文的研究问题2中,我们将 GDBFuzz 与基于模拟的方法 Fuzzware 进行比较。首先,我们按照之前所述,在提供的固件文件上分别运行 GDBFuzz 和 Fuzzware。 对于每个 GDBFuzz 实验,我们通过以下方式从控制流图文件中创建一个包含有效基本块的文件:
cut -d " " -f1 ./cfg > valid_bbs.txt
现在,我们可以根据 fuzzware 结果回放覆盖率: fuzzware genstats --valid-bb-file valid_bbs.txt
当发现崩溃或挂起的输入时,它们会被存储在 crashes 文件夹中。在评估过程中,我们发现了以下三个错误:
GDBFuzz 也可以在 Raspberry Pi 主机上运行,只需稍作修改:
在文件 ./dependencies/ghidra/support/launch.sh:125 中,JAVA_HOME 变量必须硬编码,例如设置为 JAVA_HOME="/usr/lib/jvm/default-java"
要在其他板子上模糊测试软件,GDBFuzz 需要:
src/GDBFuzz/connections),用于触发入口点处代码的执行,例如串口连接所有这些属性都需要在配置文件中指定。
对于研究问题4至8,我们运行一个大规模的基准测试。
首先,如前所述构建 Docker 镜像,并使用 benchmark/benchSUTs/GDBFuzz_wrapper/common 中的模糊测试工具链编译来自 Google Fuzzer Test Suite 的应用程序。
cd ./benchmark/benchSUTs
chmod a+x setup_benchmark_SUTs.py
make dockerbenchmarkimage
接下来,根据您的需求调整 benchmark/scripts/benchmark.py 和 benchmark/scripts/benchmark_aflpp.py 中的基准测试设置(尤其是 number_of_cores、trials 和 seconds_per_trial),然后使用以下命令启动基准测试:
cd ./benchmark/scripts
./benchmark.py $(pwd)/../benchSUTs/SUTs/ SUTs.json
./benchmark_aflpp.py $(pwd)/../benchSUTs/SUTs/ SUTs.json
在 ./benchmark/scripts 中出现一个文件夹,其中包含每个实验的绘图文件(覆盖率随时间变化)、模糊器统计文件和控制流图文件,与 evaluation/fuzzer_test_suite_qemu_runs 中的类似。
GDBFuzz 有一个可选功能,可以绘制已覆盖节点的控制流图。此功能默认禁用。您可以按照本节中的说明启用它,并在用户配置中将 'enable_UI' 设置为 'True'。
在主机上:
安装:
sudo apt-get install graphviz
安装 较新版本的 node,例如来自此处的 选项 2。使用选项 2,不要使用选项 1。这应该同时安装 node 和 npm。作为参考,我们的版本号如下(但更新的版本也应该可以):
➜ node --version
v16.9.1
➜ npm --version
7.21.1
安装 Web UI 依赖项:
cd ./src/webui
npm install
安装 mosquitto MQTT 代理,例如参见此处
更新 mosquitto 代理配置:将文件 /etc/mosquitto/conf.d/mosquitto.conf 替换为以下内容:
listener 1883
allow_anonymous true
listener 9001
protocol websockets
重启 mosquitto 代理:
sudo service mosquitto restart
检查 mosquitto 代理是否正在运行:
sudo service mosquitto status
输出应包含文本 'Active: active (running)'
启动 Web UI:
cd ./src/webui
npm start
您的 Web 浏览器应自动打开 'http://localhost:3000/'。
启动 GDBFuzz,并使用一个将 enable_UI 设置为 True 的用户配置文件。您可以使用上面的 Docker 容器和 arduinojson SUT。但请确保将 'enable_UI' 设置为 'True'。
以 'blue' 颜色覆盖的节点是已覆盖的。白色节点是未覆盖的。我们只显示其父节点已覆盖的未覆盖节点(如果控制流图很大,绘制完整的控制流图会花费太多时间)。
GDBFuzz 在 AGPL-3.0 许可下开源。详情请参阅 LICENSE 文件。
有关 GDBFuzz 中包含的其他开源组件列表,请参阅 文件 3rd-party-licenses.txt。