はじめに

 今年のセキュリティ・キャンプでは,うっかり「なぜマルウェア解析は自動化できないのか」という題の講義を行ってしまったが,それだけセキュリティの世界には自動化の波が来ている.本稿では,脆弱性分析の自動化をめざして開発されているangr, AFL, Drillerをざっくり紹介する.

angr

 angrはUCSBの研究チームにしてCTFチームShellphishを中心に開発されているバイナリ解析フレームワーク.論文[PDF]はIEEE S&P 2016に採択されている.手法の新規性というよりは実装力でゴリ押しするタイプ.評価には,アメリカ国防高等研究計画局が5,500万ドル(約56億円)の資金を投じてまで開催した脆弱性分析・修正の自動化コンペ,DARPA Cyber Grand Challenge (CGC) のデータセットが用いられている.CGCの決勝戦に進出したチームには75万ドル(約7,600万円),優勝したチームは200万ドル(約2億円)が与えられる.angr開発の目的のひとつが,CGCでの勝利にあることは疑いようもない——最終的な戦績は,CMUのツールMAYHEMに優勝を譲って3位だったが.
 さて,angrはシンボリック実行やプログラムスライシングを通して,プログラムの特定位置に到達するための入力値を抽出することができる.次のコードで雰囲気をつかめるだろうか:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import sys
import angr
import simuvex
# 解析対象を指定
p = angr.Project(sys.argv[1])
# 制御フローグラフの生成
cfg = p.analyses.CFG()
print [x for x in cfg.functions.iteritems()]
# シンボルを取得
target_addr = p.loader.main_bin.get_symbol("main").addr
# パス分析クラスのインスタンス
pg = p.factory.path_group()
# シンボルへのパスを分析
pg.explore(find = target_addr)
# avoidを避け,findに到達する入力値を探索してくれる
a = p.surveyors.Explorer(find = FIND_ADDR, avoid = AVOID_ADDR).run()
# フック
p.hook_symbol('strlen', simuvex.SimProcedures['stubs']['ReturnUnconstrained'])
# 実行結果のダンプ
a.found[0].state.posix.dumps(1)

 解析対象の規模が大きい場合,エントリポイントを起点とした解析に時間を要するあるいは失敗することがあるが,プログラムの途中から解析を始めることも可能だ.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
p = angr.Project(sys.argv[1])
# 解析の起点となるアドレス
state = p.factory.blank_state(addr = START_ADDR)
# その地点までプログラムを実行したときのスタックの状態
state.regs.ebp = BASE_ADDR
state.regs.esp = STACK_ADDR
# 入力値の設定
for i in range(INPUT_LENGTH):
s = state.se.BVS('Var[{}]'.format(i), 32, explicit_name = True)
state.memory.store(INPUT_ADDR + i * 4, s)
path = p.factory.path(state)
a = p.surveyors.Explorer(start = path, find= FIND_ADDR, avoid= AVOID_ADDR)
a.found[0].state.posix.dumps(1)

 シンボリック実行はSSA形式の中間表現を前提とするが,angrはValgrindのVEX IRを用いている.バックエンドのSMTソルバはZ3だが,claripyという自前のラッパが噛ませてある.
 これ以上の説明は公式ドキュメントに譲ろう.

AFL

 AFL (American Fuzzy Lop) はGoogleのエンジニアlcamtufを中心に開発されているファジングツール.遺伝的アルゴリズムによって正常な入力を次々と変異させていき,自動的にプログラムのバグを検出する.AFLにはbashやtcpdump, OpenSSHといったさまざまなソフトウェアのバグを検出した実績があり,いまや脆弱性分析の研究になくてはならない存在だ.一般的なファジングはプログラムの浅い箇所にしか到達できない.AFLは,大量の正常な入力をトリミングしてシードとするコーパス蒸留 (corpus distillation) と,遺伝的アルゴリズムを用いてシードを変異させるGAFuzzingのいいとこ取りを図ったものだ.その実行フローは次のようになる:

  1. ユーザーから与えられた初期テストケースをキューに入れる
  2. キューから次の入力テストケースをひとつ取り出し,
  3. プログラムの振る舞いに影響を与えない最小サイズにトリミングする
  4. バランスのよい探索となるよう,さまざまな変異戦略を用いて入力を繰り返し変異させる
  5. 新たな状態遷移が計測されたら,出力をキューに入れる
  6. 2に戻る

 ここでAFLはカバレッジ計測のため,解析対象のプログラムに計測用のコードを埋め込む.これには,解析対象のソースコードが手元にある場合gccやclangの独自フロントエンドが,解析対象のソースコードが手元にない場合QEMUが用いられる.
 ファジング機能の中核は,afl-fuzz.cmain()から呼び出されるfuzz_one()にある.実装されている変異戦略は次の通り:

  • SIMPLE BITFLIP
  • ARITHMETIC INC/DEC
  • INTERESTING VALUES
  • DICTIONARY STUFF
  • RANDOM HAVOC
  • SPLICING

 CGCで優勝したCMUのMAYHEMは,このAFLにシンボリック実行機能を追加していたようだ.MAYHEMといえば同名のシンボリック実行エンジンの論文[PDF]がIEEE S&P 2012で発表されているが,当時からの変更点がどれほどなのかはわからない.
 また,AFLの拡張に,解析対象のパスを枝刈りすることで高速化を図ったAFLFastがある.これもCGC決勝進出チームによるもので,論文[PDF]はACM CCS 2016に採択されている.600行程度のパッチでトップカンファレンス.ちょっと妬ましい.

Driller

 Drillerはangrの開発陣によるAFLの拡張.論文[PDF]はNDSS 2016に採択されている.AFLのREADMEには次のようにある:

Other, more sophisticated research has focused on techniques such as program flow analysis (“concolic execution”), symbolic execution, or static analysis. All these methods are extremely promising in experimental settings, but tend to suffer from reliability and performance problems in practical uses - and currently do not offer a viable alternative to “dumb” fuzzing techniques.

 シンボリック(コンコリック)実行は実用的ではないと切って捨てている.DrillerはangrとAFLを組み合わせることで,この批判を克服し,ファジングとシンボリック実行のいいとこ取りを図ったものだ:

 たとえば,次のコードの解析には,ファジングよりシンボリック実行が適している:

1
2
3
4
5
6
7
int main(void)
{
int x;
read(0, &x, sizeof(x));
if(x == 0x123ABCD)
vulnerable();
}

 なぜなら,ファジングによって0x123ABCDという値を生成するには大量の試行を必要とするからだ.一方で,次のコードの解析には,シンボリック実行よりファジングが適している:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int check(char *x, int depth)
{
if(depth >= 100){
return 0;
}else{
int count = (*x == 'B') ? 1 : 0;
count += check(x+1, depth+1);
return count;
}
}
int main(void)
{
char x[100];
read(0, x, 100);
if(check(x, 0) == 25)
vulnerable();
}

 なぜなら,単純なシンボリック実行ではパス爆発を解決できないからだ.
 Drillerは両手法の長所と短所を踏まえた上で,それらを堅実に組み合わせている.やはりハッシュ関数の充足はDrillerをもってしても難しいようだが,現行MAYHEMもAFLとシンボリック実行を併用しているようだし,このアプローチが現在の着地点なのだろう.

おわりに

 CGCとそれにともなう研究によって,脆弱性分析の自動化手法は大きな進歩を遂げつつあることが伝わっただろうか.学会のOSS重視傾向も相まって,さまざまなツールを手元で試せるようになってきている.大変ありがたいことだ.