C/C++開発における重大なリスク
CおよびC++はハードウェアを直接制御できる自由を与えてくれますが、その自由は諸刃の剣でもあります。手動でのメモリ管理は、正しく行うのが非常に難しいことで知られています。MicrosoftやGoogleのようなテック大手でさえ、全セキュリティ脆弱性の約70%がメモリ安全性に関連する問題であるという調査結果が出ています。バッファオーバーフローやUse-after-free(解放後メモリ使用)バグが1つあるだけで、単なるクラッシュが壊滅的なリモートコード実行(RCE)エクスプロイトに繋がる可能性があります。
標準的なユニットテストでは、こうしたエッジケースを見逃しがちです。私たちは通常、想定される入力に対してテストを書きますが、ハッカーは私たちが思いもしなかった入力を探します(これはWebセキュリティにおける脆弱性診断とも共通する考え方です)。ここで「ファジング」の出番です。コードがどこで壊れるかを推測する代わりに、ファジングは自動化されたツールを使用して、数百万ものランダムに変化させた入力をプログラムに叩き込み、何が原因で破綻するかを突き止めます。
AFL++が魔法のように機能する仕組み
AFL++(American Fuzzy Lop Plus Plus)は、現代のセキュリティ研究者にとって不可欠なツールです。これはオリジナルのAFLを高性能にフォークしたもので、より優れたミューテーション(変異)エンジンと高速なアルゴリズムで最適化されています。しかし、AFL++は単にバイナリにランダムなゴミデータを投げつけるだけの「愚かな」ファザーではありません。
AFL++は、カバレッジガイド型ファジングを採用しています。コードをコンパイルする際、AFL++はすべての分岐や関数に「インストルメンテーション(小さなセンサーのようなもの)」を注入します。ファザーの実行中、どの実行パスが通ったかを追跡します。特定の入力が新しい分岐に到達すると、AFL++はそれを「シード(種)」として保存します。その後、そのシードを変異させて、ロジックをさらに深く掘り下げます。これは本質的に、バグを見つけるために設計された進化アルゴリズムです。
ファジング環境の構築
ファジングはリソースを大量に消費するタスクです。CPUを限界まで酷使するため、Ubuntu 22.04などの専用のLinuxインスタンスを使用することをお勧めします。その際、Systemdサービスの要塞化などの手法を併用することで、より安全な実行環境を構築できます。
新しいサーバーをセットアップする際、セキュリティは最優先事項です。長期間のテストを行う場合は、toolcraft.app/ja/tools/security/password-generatorのパスワードジェネレーターを使用して強力な認証情報を作成しています。これはブラウザ内でローカルに動作するため、キーがネットワークに流出する心配がありません。
まず、主要な依存関係をインストールし、ソースからAFL++をビルドします:
sudo apt update
sudo apt install -y build-essential python3-dev automake flex bison libglib2.0-dev libpixman-1-dev clang lld llvm llvm-dev
git clone https://github.com/AFLplusplus/AFLplusplus
cd AFLplusplus
make distrib
sudo make install
最初の実行を開始する前に、Linuxカーネルセキュリティの観点から、いくつかの調整が必要です。AFL++はクラッシュを直接処理するため、OSがコアダンプをディスクに書き込もうとするのを停止させる必要があります。また、スループットを最大化するためにCPUをパフォーマンスモードに設定する必要があります:
# デフォルトのクラッシュリポーターを無効化
echo core | sudo tee /proc/sys/kernel/core_pattern
# 速度向上のためにCPU周波数を最適化
cd /sys/devices/system/cpu
echo performance | sudo tee cpu*/cpufreq/scaling_governor
実践:数秒でバグを見つける
AFL++がいかに早く特定の失敗パスを見つけられるかを確認するために、バグのある小さなCプログラムを作成してみましょう。このプログラムは、非常に特定の文字シーケンスを受け取った場合にのみクラッシュします。
1. ターゲットコード
これをvulnerable.cとして保存します:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void process_data(char *buf) {
if (buf[0] == 'F') {
if (buf[1] == 'U') {
if (buf[2] == 'Z') {
if (buf[3] == 'Z') {
// これがターゲット:シミュレートされた致命的な障害
abort();
}
}
}
}
}
int main(int argc, char *argv[]) {
if (argc < 2) return 1;
FILE *f = fopen(argv[1], "r");
if (!f) return 1;
char buffer[100];
if (fgets(buffer, sizeof(buffer), f)) {
process_data(buffer);
}
fclose(f);
return 0;
}
2. インストルメンテーションを伴うコンパイル
一時的に標準のgccのことは忘れてください。バイナリに必要な「センサー」を追加するために、AFL++のコンパイララッパーを使用する必要があります。最高のパフォーマンスを得るには、afl-clang-fastを使用します:
afl-clang-fast vulnerable.c -o vulnerable_target
3. シードの作成
AFL++には開始点が必要です。複雑である必要はありません。エンジンを動かし始めるための単純なファイルで十分です:
mkdir in
echo "テスト" > in/seed.txt
4. 攻撃の開始
以下のコマンドを使用してファザーを開始します。@@記号は、生成されたテストファイルをコマンドライン引数として挿入するようAFL++に指示するものです。
afl-fuzz -i in -o out -- ./vulnerable_target @@
ステータス画面がターミナルを占有します。「unique crashes」カウンターに注目してください。この特定のプログラムでは、AFL++は通常10秒以内に「FUZZ」という文字列を見つけ出し、クラッシュを誘発します。
結果の分析
クラッシュが発生したら、Ctrl+Cを押します。AFL++は、失敗を引き起こしたすべての入力をout/default/crashesに保存します。これらのファイルには、プログラムを壊した正確なデータが含まれています。
調査するには、ファイルをリストして内容を確認します:
ls out/default/crashes
cat out/default/crashes/id:000000... # 実際のファイル名を使用してください
このファイルをGDBやValgrindに読み込ませて、メモリ破損がどこで発生したかを正確に確認できます。この「ファジング、クラッシュ、デバッグ、修正」というループこそが、現代の安全な開発の中核です。
複雑なソフトウェアのための高度な戦術
画像パーサーやネットワークプロトコルのような実世界のターゲットは、今回のサンプルコードよりもはるかに破壊するのが困難です。スケールアップする方法は以下の通りです:
- 辞書の使用: JSONやSQLのような構造化データをファジングする場合、
-xフラグを使用してキーワードの辞書を指定します。これにより、ファザーが構文チェックを即座にバイパスできるようになります。 - パーシステントモード: プロセスを毎秒数千回再起動すると、大きなオーバーヘッドが発生します。Cコード内で
__AFL_LOOPを使用して単一プロセス内でファジングを行うと、多くの場合、速度が5倍から10倍向上します。 - 並列ファジング: 16コアのCPUを使用しているなら、それを活用しましょう。1つのマスターインスタンス(
-M master)と15のスレーブインスタンス(-S slaveN)を実行して、コードパス空間を同時に探索します。
結論:ファジングを習慣にする
ファジングは製品リリース前の単発のイベントであってはなりません。CI/CDパイプラインのセキュリティ強化の一環として組み込むべきものです。新機能に対してAFL++を一晩中実行させることで、脆弱性が本番サーバーに到達する前に発見できます。
まずはローカルのユーティリティをファジングすることから始めてみましょう。慣れてきたら、opensslやlibxml2のような大規模なライブラリに進んでください。「安定している」と言われるソフトウェアであっても、たった1つの奇妙な入力で完全に崩壊してしまうことがすぐに分かるはずです。

