libFuzzer によるファジング

ファジングは、プログラムへの入力として潜在的に無効なデータ、予期しないデータ、またはランダムなデータを提供するだけの手法ですが、大規模なソフトウェア システムのバグを見つけるには非常に効果的であり、ソフトウェア開発ライフサイクルの重要な構成要素になっています。

Android のビルドシステムは、LLVM コンパイラ インフラストラクチャ プロジェクトからの libFuzzer を組み込むことで、ファジングをサポートします。LibFuzzer は、テスト対象のライブラリにリンクされ、ファジング セッション中に発生するすべての入力選択、変換、クラッシュ レポートを処理します。LLVM のサニタイザーは、メモリ破損の検出とコード カバレッジ指標に役立てられます。

この記事では、Android での libFuzzer の使用方法とインストゥルメント化ビルドの実行方法を紹介します。また、ファザーの作成、実行、カスタマイズの手順についても説明します。

設定とビルド

デバイス上で正常なイメージが実行されていることを確認するには、ファクトリー イメージをダウンロードしてフラッシュします。または、AOSP ソースコードをダウンロードして、以下のセットアップとビルドの例に従うこともできます。

設定例

この例では、ターゲット デバイスが Pixel(taimen)であり、すでに USB デバッグ(aosp_taimen-userdebug)用に準備されていることを前提としています。他の Pixel バイナリは、ドライバ バイナリからダウンロードできます。

mkdir ~/bin
export PATH=~/bin:$PATH
curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
chmod a+x ~/bin/repo
repo init -u https://android.googlesource.com/platform/manifest -b main
repo sync -c -j8
wget https://dl.google.com/dl/android/aosp/google_devices-taimen-qq1a.191205.008-f4537f93.tgz
tar xvf google_devices-taimen-qq1a.191205.008-f4537f93.tgz
./extract-google_devices-taimen.sh
wget https://dl.google.com/dl/android/aosp/qcom-taimen-qq1a.191205.008-760afa6e.tgz
tar xvf qcom-taimen-qq1a.191205.008-760afa6e.tgz
./extract-qcom-taimen.sh
. build/envsetup.sh
lunch aosp_taimen-userdebug

ビルド例

ファズ ターゲットを実行する最初のステップは、最新のシステム イメージを取得することです。Android の最新の開発版を使用することが推奨されます。

  1. 次のコマンドを実行して、最初のビルドを実行します。
    m
  2. デバイスにフラッシュするには、適切なキーの組み合わせを使用して、デバイスを fastboot モードで起動します。
  3. 次のコマンドにより、ブートローダーのロックを解除して、新しくコンパイルされたイメージをフラッシュします。
    fastboot oem unlock
    fastboot flashall
    

これで、ターゲット デバイスで libFuzzer ファジングを行う準備が整いました。

ファザーを作成する

Android で libFuzzer を使用してエンドツーエンドのファザーを作成する方法を説明するため、テストケースとして次の脆弱なコードを使用します。これにより、ファザーをテストして、すべてが正常に機能していることを確認し、クラッシュ データがどのように表示されるかを示します。

テスト関数は次のとおりです。

#include <stdint.h>
#include <stddef.h>
bool FuzzMe(const char *data, size_t dataSize) {
    return dataSize >= 3  &&
           data[0] == 'F' &&
           data[1] == 'U' &&
           data[2] == 'Z' &&
           data[3] == 'Z';  // ← Out of bounds access
}

このテスト用ファザーをビルドして実行するには:

  1. ファズ ターゲットは、ビルド ファイルとファジング ターゲット ソースコードの 2 つのファイルで構成されます。 ファジングするライブラリの横の場所にファイルを作成します。ファザーには、ファザーが何をするのかを説明する名前を付けます。
  2. libFuzzer を使用してファズ ターゲットを作成します。ファズ ターゲットは、指定されたサイズのデータの blob を受け取って、ファジング対象の関数に渡す関数です。脆弱なテスト関数の基本的なファザーは以下のようになります。
    #include <stddef.h>
    #include <stdint.h>
    
    extern "C" int LLVMFuzzerTestOneInput(const char *data, size_t size) {
      // ...
      // Use the data to call the library you are fuzzing.
      // ...
      return FuzzMe(data, size);
    }
    
  3. Android のビルドシステムに、ファザー バイナリの作成を指示します。 ファザーをビルドするには、次のコードを Android.bp ファイルに追加します。
    cc_fuzz {
      name: "fuzz_me_fuzzer",
      srcs: [
        "fuzz_me_fuzzer.cpp",
      ],
      // If the fuzzer has a dependent library, uncomment the following section and
      // include it.
      // static_libs: [
      //   "libfoo", // Dependent library
      // ],
      //
      // The advanced features below allow you to package your corpus and
      // dictionary files during building. You can find more information about
      // these features at:
      //  - Corpus: https://llvm.org/docs/LibFuzzer.html#corpus
      //  - Dictionaries: https://llvm.org/docs/LibFuzzer.html#dictionaries
      // These features are not required for fuzzing, but are highly recommended
      // to gain extra coverage.
      // To include a corpus folder, uncomment the following line.
      // corpus: ["corpus/*"],
      // To include a dictionary, uncomment the following line.
      // dictionary: "fuzz_me_fuzzer.dict",
    }
    
  4. ターゲット(デバイス)で実行するためのファザーを作成するには、次のコードを追加します。
    SANITIZE_TARGET=hwaddress m fuzz_me_fuzzer
    
  5. ホスト上でファザーを実行するには、次のコードを追加します。
    SANITIZE_HOST=address m fuzz_me_fuzzer
    

便宜上、ファズ ターゲットへのパスとバイナリ名(先ほど作成したビルドファイルから)を含むシェル変数を定義します。

export FUZZER_NAME=your_fuzz_target

以上のステップを完了すると、ファザーがビルドされます。ファザーのデフォルトの場所は、(この例の Pixel ビルドでは)以下のようになります。

  • $ANDROID_PRODUCT_OUT/data/fuzz/$TARGET_ARCH/$FUZZER_NAME/$FUZZER_NAME(デバイス)
  • $ANDROID_HOST_OUT/fuzz/$TARGET_ARCH/$FUZZER_NAME/$FUZZER_NAME(ホスト)
  • ホストでファザーを実行する

  • Android.bp ビルドファイルに次のコードを追加します。
    host_supported: true,
    なお、これはファジングしたいライブラリがホストでサポートされている場合にのみ適用できます。
  • ビルドされたファザー バイナリを単に実行することで、ファザーをホストで実行します。
    $ANDROID_HOST_OUT/fuzz/x86_64/$FUZZER_NAME/$FUZZER_NAME
  • デバイスでファザーを実行する

    adb を使って以下をデバイスにコピーします。

    1. これらのファイルをデバイスのディレクトリにアップロードするには、次のコマンドを実行します。
      adb root
      adb sync data
      
    2. 次のコマンドを使用して、デバイスでテストファザーを実行します。
      adb shell /data/fuzz/$(get_build_var TARGET_ARCH)/$FUZZER_NAME/$FUZZER_NAME \
        /data/fuzz/$(get_build_var TARGET_ARCH)/$FUZZER_NAME/corpus

    これにより、次のような出力が得られます。

    INFO: Seed: 913963180
    INFO: Loaded 2 modules   (16039 inline 8-bit counters): 16033 [0x7041769b88, 0x704176da29), 6 [0x60e00f4df0, 0x60e00f4df6),
    INFO: Loaded 2 PC tables (16039 PCs): 16033 [0x704176da30,0x70417ac440), 6 [0x60e00f4df8,0x60e00f4e58),
    INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
    INFO: A corpus is not provided, starting from an empty corpus
    #2	INITED cov: 5 ft: 5 corp: 1/1b exec/s: 0 rss: 24Mb
    #10	NEW    cov: 6 ft: 6 corp: 2/4b lim: 4 exec/s: 0 rss: 24Mb L: 3/3 MS: 3 CopyPart-ChangeByte-InsertByte-
    #712	NEW    cov: 7 ft: 7 corp: 3/9b lim: 8 exec/s: 0 rss: 24Mb L: 5/5 MS: 2 InsertByte-InsertByte-
    #744	REDUCE cov: 7 ft: 7 corp: 3/7b lim: 8 exec/s: 0 rss: 25Mb L: 3/3 MS: 2 ShuffleBytes-EraseBytes-
    #990	REDUCE cov: 8 ft: 8 corp: 4/10b lim: 8 exec/s: 0 rss: 25Mb L: 3/3 MS: 1 ChangeByte-
    ==18631==ERROR: HWAddressSanitizer: tag-mismatch on address 0x0041e00b4183 at pc 0x0060e00c5144
    READ of size 1 at 0x0041e00b4183 tags: f8/03 (ptr/mem) in thread T0
        #0 0x60e00c5140  (/data/fuzz/arm64/example_fuzzer/example_fuzzer+0xf140)
        #1 0x60e00ca130  (/data/fuzz/arm64/example_fuzzer/example_fuzzer+0x14130)
        #2 0x60e00c9b8c  (/data/fuzz/arm64/example_fuzzer/example_fuzzer+0x13b8c)
        #3 0x60e00cb188  (/data/fuzz/arm64/example_fuzzer/example_fuzzer+0x15188)
        #4 0x60e00cbdec  (/data/fuzz/arm64/example_fuzzer/example_fuzzer+0x15dec)
        #5 0x60e00d8fbc  (/data/fuzz/arm64/example_fuzzer/example_fuzzer+0x22fbc)
        #6 0x60e00f0a98  (/data/fuzz/arm64/example_fuzzer/example_fuzzer+0x3aa98)
        #7 0x7041b75d34  (/data/fuzz/arm64/lib/libc.so+0xa9d34)
    
    [0x0041e00b4180,0x0041e00b41a0) is a small allocated heap chunk; size: 32 offset: 3
    0x0041e00b4183 is located 0 bytes to the right of 3-byte region [0x0041e00b4180,0x0041e00b4183)
    allocated here:
        #0 0x70418392bc  (/data/fuzz/arm64/lib/libclang_rt.hwasan-aarch64-android.so+0x212bc)
        #1 0x60e00ca040  (/data/fuzz/arm64/example_fuzzer/example_fuzzer+0x14040)
        #2 0x60e00c9b8c  (/data/fuzz/arm64/example_fuzzer/example_fuzzer+0x13b8c)
        #3 0x60e00cb188  (/data/fuzz/arm64/example_fuzzer/example_fuzzer+0x15188)
        #4 0x60e00cbdec  (/data/fuzz/arm64/example_fuzzer/example_fuzzer+0x15dec)
        #5 0x60e00d8fbc  (/data/fuzz/arm64/example_fuzzer/example_fuzzer+0x22fbc)
        #6 0x60e00f0a98  (/data/fuzz/arm64/example_fuzzer/example_fuzzer+0x3aa98)
        #7 0x7041b75d34  (/data/fuzz/arm64/lib/libc.so+0xa9d34)
        #8 0x60e00c504c  (/data/fuzz/arm64/example_fuzzer/example_fuzzer+0xf04c)
        #9 0x70431aa9c4  (/data/fuzz/arm64/example_fuzzer/example_fuzzer+0x519c4)
    
    Thread: T1 0x006700006000 stack: [0x007040c55000,0x007040d4ecc0) sz: 1023168 tls: [0x000000000000,0x000000000000)
    Thread: T0 0x006700002000 stack: [0x007fe51f3000,0x007fe59f3000) sz: 8388608 tls: [0x000000000000,0x000000000000)
    Memory tags around the buggy address (one tag corresponds to 16 bytes):
       00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
       00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
       00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
       00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
       00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
       00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
       00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
       08  00  cf  08  dc  08  cd  08  b9  08  1a  1a  0b  00  04  3f
    => 27  00  08  00  bd  bd  2d  07 [03] 73  66  66  27  27  20  f6 <=
       5b  5b  87  87  03  00  01  00  4f  04  24  24  03  39  2c  2c
       05  00  04  00  be  be  85  85  04  00  4a  4a  05  05  5f  5f
       00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
       00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
       00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
       00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
       00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
       00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
    Tags for short granules around the buggy address (one tag corresponds to 16 bytes):
       04  ..  ..  cf  ..  dc  ..  cd  ..  b9  ..  ..  3f  ..  57  ..
    => ..  ..  21  ..  ..  ..  ..  2d [f8] ..  ..  ..  ..  ..  ..  .. <=
       ..  ..  ..  ..  9c  ..  e2  ..  ..  4f  ..  ..  99  ..  ..  ..
    See https://clang.llvm.org/docs/HardwareAssistedAddressSanitizerDesign.html#short-granules for a description of short granule tags
    Registers where the failure occurred (pc 0x0060e00c5144):
        x0  f8000041e00b4183  x1  000000000000005a  x2  0000000000000006  x3  000000704176d9c0
        x4  00000060e00f4df6  x5  0000000000000004  x6  0000000000000046  x7  000000000000005a
        x8  00000060e00f4df0  x9  0000006800000000  x10 0000000000000001  x11 00000060e0126a00
        x12 0000000000000001  x13 0000000000000231  x14 0000000000000000  x15 000e81434c909ede
        x16 0000007041838b14  x17 0000000000000003  x18 0000007042b80000  x19 f8000041e00b4180
        x20 0000006800000000  x21 000000000000005a  x22 24000056e00b4000  x23 00000060e00f5200
        x24 00000060e0128c88  x25 00000060e0128c20  x26 00000060e0128000  x27 00000060e0128000
        x28 0000007fe59f16e0  x29 0000007fe59f1400  x30 00000060e00c5144
    SUMMARY: HWAddressSanitizer: tag-mismatch (/data/fuzz/arm64/example_fuzzer/example_fuzzer+0xf140)
    MS: 1 ChangeByte-; base unit: e09f9c158989c56012ccd88111b82f778a816eae
    0x46,0x55,0x5a,
    FUZ
    artifact_prefix='./'; Test unit written to ./crash-0eb8e4ed029b774d80f2b66408203801cb982a60
    Base64: RlVa
    

    この出力例では、クラッシュの原因は 10 行目の fuzz_me_fuzzer.cpp です。

          data[3] == 'Z';  // :(
    

    data の長さが 3 であれば、これは単純な境界外の読み取りです。

    ファザーを実行すると、多くの場合出力がクラッシュし、不適切な入力がコーパスに保存されて、それに ID が与えられます。この出力例では、ID は crash-0eb8e4ed029b774d80f2b66408203801cb982a60 です。

    デバイスでファジング中にクラッシュ情報を取得するには、クラッシュ ID を指定して次のコマンドを発行します。

    adb pull /data/fuzz/arm64/fuzz_me_fuzzer/corpus/CRASH_ID
    テストケースを適切なディレクトリに保存するには、上記の例のようにコーパス フォルダを使用するか、artifact_prefix 引数(例: `-artifact_prefix=/data/fuzz/where/my/crashes/go`)を使用します。

    ホストでファザーを使用する場合、ファザーが実行されるローカル フォルダのクラッシュ フォルダにクラッシュ情報が表示されます。

    ライン カバレッジを生成する

    ライン カバレッジを利用すると、デベロッパーは、コード内のカバーされていない範囲を特定し、今後のファジングの実行でその範囲をヒットできるように適宜ファザーを更新することができるため、非常に便利です。

    1. ファザー カバレッジ レポートを生成する手順は次のとおりです。
      CLANG_COVERAGE=true NATIVE_COVERAGE_PATHS='*' make ${FUZZER_NAME}
      
    2. ファザーとその依存関係をデバイスにプッシュした後、次のように LLVM_PROFILE_FILE でファズ ターゲットを実行します。
      DEVICE_TRACE_PATH=/data/fuzz/$(get_build_var TARGET_ARCH)/${FUZZER_NAME}/data.profraw
      adb shell LLVM_PROFILE_FILE=${DEVICE_TRACE_PATH} /data/fuzz/$(get_build_var TARGET_ARCH)/${FUZZER_NAME}/${FUZZER_NAME} -runs=1000
      
    3. カバレッジ レポートを作成するには、以下に示すように、まずデバイスから profraw ファイルをプルし、coverage-html という名前のフォルダに html レポートを生成します。
      adb pull ${DEVICE_TRACE_PATH} data.profraw
      llvm-profdata merge --sparse data.profraw --output data.profdata
      llvm-cov show --format=html --instr-profile=data.profdata \
        symbols/data/fuzz/$(get_build_var TARGET_ARCH)/${FUZZER_NAME}/${FUZZER_NAME} \
        --output-dir=coverage-html --path-equivalence=/proc/self/cwd/,$ANDROID_BUILD_TOP
      

    libFuzzer の詳細については、アップストリーム ドキュメントをご覧ください。