
ハードウェアブレークポイントを使用した組み込みシステムのファジング
これは論文「Fuzzing Embedded Systems using Debugger Interfaces」の付属コードです。 論文のプレプリントはこちら 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 = <path>
# CFGのルートノードのアドレス。
# ブレークポイントはこのCFGのノードに配置されます。
# 例: 'LLVMFuzzerTestOneInput' または 'main'
entrypoint = <entrypoint>
# ブレークポイントがローテーションされるまでに、ブレークポイントがヒットせずに実行される入力数。
until_rotate_breakpoints = <number>
# 同時に配置できるブレークポイントの最大数。
max_breakpoints = <number>
# 無視する関数のブラックリスト。
# 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 = <mode>
# ghidraを起動し、SUTを解析し、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' は、
# 入力がSUTにどのように送信されるかを実装します。
# 入力は、例えばWi-Fi、シリアル、Bluetoothなどを介して送信できます。
# このクラスは ./connections/SUTConnection.py を継承する必要があります。
# 詳細については ./connections/SUTConnection.py を参照してください。
SUT_connection_file = FIFOConnection.py
[GDB]
path_to_gdb = gdb-multiarch
#アドレス:ポート形式で記述
gdb_server_address = localhost:4242
[Fuzzer]
# バイト単位
maximum_input_length = 100000
# 秒単位
single_run_timeout = 20
# 秒単位
total_runtime = 3600
# オプション
# 各ファイルに1つのシードが含まれるディレクトリへのパス。シードを使用しない場合は、値を空のままにします。
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/ にあるファジングハーネスを使用してコンパイルされたサンプルプログラムも同梱されています。
以下のコマンドで1時間ファジングを開始します。
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コンテナで上記の実験を実行するには(設定ファイルで指定された1時間)、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
現在の作業ディレクトリに、上記で説明した構造の出力フォルダが表示されるはずです。
私たちの評価は2つの部分に分かれています。
GDBFuzzは任意のGDBサーバーと、したがってほとんどのマイクロコントローラ用デバッグプローブで動作します。
論文のRQ1に関して、example_firmware にある異なるファームウェアを持つ異なるマイクロコントローラ上でGDBFuzzを実行します。
各実験で、GDBFuzzを RandomBasicBlock 戦略と RandomBasicBlockNoCorpus 戦略で実行します。後者はフィードバックなしのファジングのように動作しますが、達成されたカバレッジを測定することはできます。
RQ1に答えるために、RandomBasicBlock 戦略と RandomBasicBlockNoCorpus 戦略の達成されたカバレッジを比較します。
対応する設定ファイルは各サブフォルダにあり、4つの開発ボードでのファジング設定方法を以下で説明します。
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はSUTの .elf ファイルをここに保存します:./example_firmware/stm32_disco_arduinojson/.pio/build/disco_l4s5i_iot01a/firmware.elf この .elf ファイルは後でGhidraのユーザー設定でも使用されます。
新しいターミナルを開き、以下を実行してGDBサーバーを起動します:
st-util
arduinojsonのユーザー設定でGDBFuzzを実行します。USBポートを介してマイクロコントローラにデータを送信できます。マイクロコントローラはこのデータをシリアル経由でSUTに転送します。この場合、/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ポートを介してマイクロコントローラにデータを送信できます。マイクロコントローラはこのデータをシリアル経由でSUTに転送します。この場合、/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
pyusb を使用して非rootユーザーとして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
論文のRQ2では、GDBFuzzをエミュレーションベースのアプローチ Fuzzware と比較します。最初に、前述のように同梱のファームウェアファイルに対してGDBFuzzとFuzzwareを実行します。 各GDBFuzz実験について、制御フローグラフファイルから有効な基本ブロックを含むファイルを次のように作成します:
cut -d " " -f1 ./cfg > valid_bbs.txt
次に、fuzzwareの結果に対してカバレッジを再生できます: fuzzware genstats --valid-bb-file valid_bbs.txt
クラッシュまたはハングする入力が見つかると、それらは crashes フォルダに保存されます。評価中に、次の3つのバグを発見しました:
GDBFuzzは、若干の変更を加えることでRaspberry Piホスト上でも実行できます:
ファイル ./dependencies/ghidra/support/launch.sh:125 で、JAVA_HOME 変数をハードコードする必要があります。例:JAVA_HOME="/usr/lib/jvm/default-java"
他のボードのソフトウェアをファジングするために、GDBFuzzは以下を必要とします:
src/GDBFuzz/connections を参照)これらのプロパティはすべて設定ファイルで指定する必要があります。
RQ 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/' で開くはずです。
enable_UI が True に設定されたユーザー設定ファイルを使用してGDBFuzzを起動します。上記のDockerコンテナとarduinojson SUTを使用できます。ただし、'enable_UI' を 'True' に設定してください。
'blue' でカバーされたノードがカバーされています。白いノードはカバーされていません。親ノードがカバーされている場合にのみ、カバーされていないノードを表示します(完全な制御フローグラフを描画すると、制御フローグラフが大きい場合に時間がかかりすぎます)。
GDBFuzzはAGPL-3.0ライセンスの下でオープンソース化されています。詳細については、 LICENSE ファイルを参照してください。
GDBFuzzに含まれるその他のオープンソースコンポーネントのリストについては、 ファイル 3rd-party-licenses.txt を参照してください。