🛡️

AFLでファジングやってみた

2021/12/04に公開

はじめに

IPAのファジング実践資料(AFL編)を参考に、AFLでファジングしてみました。

AFLとは

AFL (American Fuzzy Lop) は、最も著名なファジングツールの1つです。

やってみた

環境構築

仮想サーバ起動

仮想サーバ上でファジングをやりましょう。
Ubuntuの仮想サーバを起動します。

git clone git@github.com:kannkyo/boilerplate-vagrant.git
cd vagrant-sandbox/ubuntu-focal64-20201117.0.0/
vagrant up
vagrant ssh

AFL インストール

Ubuntuのオフィシャルレポジトリは、AFL派生版のafl++を提供しています。

apt-getで簡単にインストールできるのでafl++をインストールしましょう。

sudo apt update
sudo apt install afl++ afl++-clang afl++-doc

RAMディスク作成

aflの開発コミュニティは、RAMディスク上でaflを実行することを推奨しています。
aflはファイルI/Oが多く、HDDの寿命を縮める恐れが有ります。

RAMディスク /tmp/afl-ramdisk/ を作成しましょう。

mkdir -p /tmp/afl-ramdisk && chmod 777 /tmp/afl-ramdisk/
sudo mount -t tmpfs -o size=512M tmpfs /tmp/afl-ramdisk/
cd /tmp/afl-ramdisk/

ソースコードの準備

まず、ファジングの対象とするソースコードを作成します。

$ cat <<EOF >example.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char** argv)
{
  char buf[8];

  if(read(0, buf, 8) < 1)
  {
     exit(1);
  }

  printf(buf);
  exit(0);
}
EOF

標準入力から文字列を読み取るプログラムです。

コンパイル

afl-gccを使って、ソースコードをコンパイルします。
afl-gccgccのラッパーです。

コード内に測定用コードを埋め込んだあとにコンパイルします。
測定コードは、ファジングにより発見された不具合の発生箇所の特定、カバレッジの測定などを可能にします。

afl-gccのマニュアルを確認します。

$ afl-gcc 
afl-cc++2.59d by Michal Zalewski
[!] NOTE: afl-gcc is deprecated, llvm_mode is much faster and has more options

This is a helper application for afl-fuzz. It serves as a drop-in replacement
for gcc or clang, letting you recompile third-party code with the required
runtime instrumentation. A common use pattern would be one of the following:

  CC=/usr/bin/afl-gcc ./configure
  CXX=/usr/bin/afl-g++ ./configure

You can specify custom next-stage toolchain via AFL_CC, AFL_CXX, and AFL_AS.
Setting AFL_HARDEN enables hardening optimizations in the compiled code.

afl-gccは廃止deprecatedされていて、中身はafl+cc++に書き換わっているようです。

コンパイルを実行します。

$ afl-gcc -o example example.c
afl-cc++2.59d by Michal Zalewski
[!] NOTE: afl-gcc is deprecated, llvm_mode is much faster and has more options
example.c: In function ‘main’:
example.c:14:10: warning: format not a string literal and no format arguments [-Wformat-security]
   14 |   printf(buf);
      |          ^~~
afl-as++2.59d by Michal Zalewski
[+] Instrumented 2 locations (64-bit, non-hardened mode, ratio 100%).

ファジング

afl-fuzzコマンドを使って、ファジングを実行します。

afl-fuzzのマニュアルを確認します。

$ afl-fuzz 
afl-fuzz++2.59d based on afl by Michal Zalewski and a big online community

afl-fuzz [ options ] -- /path/to/fuzzed_app [ ... ]

Required parameters:
  -i dir        - input directory with test cases
  -o dir        - output directory for fuzzer findings

Execution control settings:
  -p schedule   - power schedules recompute a seed's performance score.
                  <explore (default), fast, coe, lin, quad, or exploit>
                  see docs/power_schedules.txt
  -f file       - location read by the fuzzed program (stdin)
  -t msec       - timeout for each run (auto-scaled, 50-1000 ms)
  -m megs       - memory limit for child process (50 MB)
  -Q            - use binary-only instrumentation (QEMU mode)
  -U            - use unicorn-based instrumentation (Unicorn mode)
  -W            - use qemu-based instrumentation with Wine (Wine mode)

Mutator settings:
  -R[R]         - add Radamsa as mutator, add another -R to exclusivly run it
  -L minutes    - use MOpt(imize) mode and set the limit time for entering the
                  pacemaker mode (minutes of no new paths, 0 = immediately).
                  a recommended value is 10-60. see docs/README.MOpt

Fuzzing behavior settings:
  -N            - do not unlink the fuzzing input file
  -d            - quick & dirty mode (skips deterministic steps)
  -n            - fuzz without instrumentation (dumb mode)
  -x dir        - optional fuzzer dictionary (see README, its really good!)

Testing settings:
  -s seed       - use a fixed seed for the RNG
  -V seconds    - fuzz for a maximum total time of seconds then terminate
  -E execs      - fuzz for a maximum number of total executions then terminate
  Note: -V/-E are not precise, they are checked after a queue entry is done
  which can be many minutes/execs later

Other stuff:
  -T text       - text banner to show on the screen
  -M / -S id    - distributed mode (see parallel_fuzzing.txt)
  -I command    - execute this command/script when a new crash is found
  -B bitmap.txt - mutate a specific test case, use the out/fuzz_bitmap file
  -C            - crash exploration mode (the peruvian rabbit thing)
  -e ext        - File extension for the temporarily generated test case

For additional tips, please consult /usr/share/doc/afl++-doc/README

次に、ファジング用のサンプル・テストケースをダウンロードして、ファジングを実行します。

$ wget -O - https://lcamtuf.coredump.cx/afl/releases/afl-latest.tgz | tar zxvf -
$ afl-fuzz -i afl-2.52b/testcases/others/text/ -o out/ ./example
afl-fuzz++2.59d based on afl by Michal Zalewski and a big online community
[+] afl++ is maintained by Marc "van Hauser" Heuse, Heiko "hexcoder" Eissfeldt and Andrea Fioraldi
[+] afl++ is open source, get it at https://github.com/vanhauser-thc/AFLplusplus
[+] Power schedules from github.com/mboehme/aflfast
[+] Python Mutator and llvm_mode whitelisting from github.com/choller/afl
[+] afl-tmin fork server patch from github.com/nccgroup/TriforceAFL
[+] MOpt Mutator from github.com/puppet-meteor/MOpt-AFL
[*] Getting to work...
[+] Using exploration-based constant power schedule (EXPLORE)
[+] You have 1 CPU core and 1 runnable tasks (utilization: 100%).
[*] Checking core_pattern...
[!] WARNING: Could not check CPU scaling governor
[*] Setting up output directories...
[+] Output directory exists but deemed OK to reuse.
[*] Deleting old session data...
[+] Output dir cleanup successful.
[*] Scanning 'afl-2.52b/testcases/others/text/'...
[+] No auto-generated dictionary tokens to reuse.
[*] Creating hard links for all input files...
[*] Validating target binary...
[*] Attempting dry run with 'id:000000,time:0,orig:hello_world.txt'...
[*] Spinning up the fork server...
[+] All right - fork server is up.
    len = 6, map size = 2, exec speed = 242 us
[+] All test cases processed.

[+] Here are some useful stats:

    Test case count : 1 favored, 0 variable, 1 total
       Bitmap range : 2 to 2 bits (average: 2.00 bits)
        Exec timing : 242 to 242 us (average: 242 us)

[*] No -t option specified, so I'll use exec timeout of 20 ms.
[+] All set and ready to roll!

              american fuzzy lop ++2.59d (example) [explore] {-1}
┌─ process timing ────────────────────────────────────┬─ overall results ────┐
│        run time : 0 days, 0 hrs, 4 min, 5 sec       │  cycles done : 4789  │
│   last new path : none yet (odd, check syntax!)     │  total paths : 1     │
│ last uniq crash : 0 days, 0 hrs, 4 min, 3 sec       │ uniq crashes : 1     │
│  last uniq hang : none seen yet                     │   uniq hangs : 0     │
├─ cycle progress ───────────────────┬─ map coverage ─┴──────────────────────┤
│  now processing : 0.4789 (0.0%)    │    map density : 0.00% / 0.00%        │
│ paths timed out : 0 (0.00%)        │ count coverage : 1.00 bits/tuple      │
├─ stage progress ───────────────────┼─ findings in depth ───────────────────┤
│  now trying : havoc                │ favored paths : 1 (100.00%)           │
│ stage execs : 255/256 (99.61%)     │  new edges on : 1 (100.00%)           │
│ total execs : 1.23M                │ total crashes : 31 (1 unique)         │
│  exec speed : 5054/sec             │  total tmouts : 0 (0 unique)          │
├─ fuzzing strategy yields ──────────┴───────────────┬─ path geometry ───────┤
│   bit flips : 0/32, 0/31, 0/29                     │    levels : 1         │
│  byte flips : 0/4, 0/3, 0/1                        │   pending : 0         │
│ arithmetics : 0/224, 0/0, 0/0                      │  pend fav : 0         │
│  known ints : 0/23, 0/84, 0/44                     │ own finds : 0         │
│  dictionary : 0/0, 0/0, 0/0                        │  imported : n/a       │
│   havoc/rad : 1/1.23M, 0/0, 0/0                    │ stability : 100.00%   │
│   py/custom : 0/0, 0/0                             ├───────────────────────┘
│        trim : 33.33%/1, 0.00%                      │             [cpu:199%]
└────────────────────────────────────────────────────┘^C

+++ Testing aborted by user +++
[+] We're done here. Have a nice day!

ファジング実行中は実行状況のサマリーがカラフルに表示されます。

afl-sample.png

結果の確認

ファジングの結果を確認します。

まず、出力ファイルを確認します。

$ ll out/
total 80
drwx------ 5 root root   200 Aug 30 15:30 ./
drwxrwxrwt 4 root root   140 Aug 30 15:29 ../
-rw------- 1 root root     2 Aug 30 15:34 .cur_input
-rw------- 1 root root    10 Aug 30 15:30 cmdline
drwx------ 2 root root    80 Aug 30 15:30 crashes/
-rw------- 1 root root 65536 Aug 30 15:30 fuzz_bitmap
-rw------- 1 root root   802 Aug 30 15:34 fuzzer_stats
drwx------ 2 root root    40 Aug 30 15:30 hangs/
-rw------- 1 root root  2835 Aug 30 15:34 plot_data
drwx------ 3 root root    80 Aug 30 15:30 queue/

$ ll out/crashes/
total 8
drwx------ 2 root root  80 Aug 30 15:30 ./
drwx------ 5 root root 200 Aug 30 15:30 ../
-rw------- 1 root root 586 Aug 30 15:30 README.txt
-rw------- 1 root root   6 Aug 30 15:30 id:000000,sig:11,src:000000,time:2487,op:havoc,rep:32

$ hexdump -C out/crashes/id\:000000\,sig\:11\,src\:000000\,time\:2487\,op\:havoc\,rep\:32 
00000000  40 c0 72 40 25 53                                 |@.r@%S|
00000006

out/フォルダに出力結果のファイルが格納されているようですが、バイナリファイルなどもあってよくわかりません。

afl-plotコマンドで出力結果を可視化してみます。

まず、afl-plotに必要なgnuplotをインストールします。

sudo apt install gnuplot -y

次に、コマンドを実行します。

afl-plot out/ graph/

graph/ファルダには下図のようなグラフの画像が格納されています。

afl-plot.png

1つ目のグラフは、時系列で実行パス数などをプロットしたグラフです。
プロットされる項目は以下の5つです。

  • total paths:トータルパス
  • current path:現在のパス
  • pending paths:ペンディングされているパス
  • pending favs:ペンディングされているfavs
  • cycles done:サイクル終了数

2つ目のグラフは、時系列で発見された不具合を種類別にプロットしたグラフです。
以下の3つの項目がプロットされます。

  • uniq crashes:一意なクラッシュ数
  • uniq hangs:一意なハング数
  • levels:レベル

3つ目のグラフは、時系列で実行速度が表示されるグラフです。
以下の1つの項目がプロットされます。

  • execs/sec:1秒あたりの実行数

Discussion