提案手法の概要
目的: データの保護
- Buffer Overflowの検知
- Use After Freeの検知
- etc
手法: Data-Flow Integrity + Field-sensitiveポインタ解析
以下の2つを組み合わせて,構造体の要素を区別したデータフローの保護を実現する:
-
Data-Flow Integrity: データフローの保護
- 静的解析: 定義と使用の関係(DefUse)を求める
- 動的解析: 実行時に各変数の定義と使用の関係が正しいのか確認する
-
Field-sensitiveポインタ解析: 構造体の要素を区別したポインタ解析
Step1: 静的解析
Field-sensitiveなDef-Useを求める.
1
2
3
4
5
6
7
8
| struct Vec {
int x, y;
};
struct Vec v;
v.x = 100;
v.y = 200;
return v.x + v.y;
|
定義と使用の関係(Def-Use)
|
DEF |
USE |
v.x |
L.6 |
L.8 |
v.y |
L.7 |
L.8 |
Step2: 動的解析
Shadow Memoryを用いて以下を行う:
- 変数の定義場所の保存
- 変数の使用時,定義場所が正しいかどうかのチェック
1
2
3
4
5
6
7
8
| struct Vec {
int x, y;
};
struct Vec v;
v.x = 100;
v.y = 200;
return v.x + v.y;
|
定義と使用の関係(Def-Use)
|
DEF |
USE |
v.x |
L.6 |
L.8 |
v.y |
L.7 |
L.8 |
Memory
Shadow Memory
Motivating Example
vs Data-Flow Integrity[OSDI'06]
- DFIは,構造体の要素を区別して保護できない(Field-insensitive)
- 提案手法は,Field-sensitiveポインタ解析を用いることで,構造体の要素を区別して保護する
1
2
3
4
5
6
7
8
9
| struct operation{
char opd[8];
void (*func)();
};
struct operation op;
op.func = func1;
Input(op.opd); // overwrite op.func
op.func();
|
Shadow Memory
提案手法によるDef-Use
|
DEF |
USE |
op.func |
L.7 |
L.9 |
op.opd |
L.8 |
- |
vs EffectiveSan[PLDI'18] (Type-Based Sanitizer)
-
EffectiveSanは,静的解析 + 動的解析を組み合わせ,非常に正確な型(Effective Type)を求める
- 構造体の要素とその要素を区別できる
- 動的にサイズが決まる型を実行時に求められる
- 提案手法も,Shadow Memoryを利用して正確に保護できる
1
2
3
4
| int n = InputLength();
char str[n]; // VLA
Input(str);
return str[100];
|
Shadow Memory
- 一方,EffectiveSanは,Use-After-Freeを正確に検知できない
- 提案手法は,定義と使用の関係から,Use-After-Freeを検知する
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| int createKey() {
int key;
key = ... // DEF key
return key; // USE key
}
void readKey() {
int illegal_key;
printf("key = %d\n", illegal_key);
}
int main() {
createKey();
readKey();
}
|
Def-Use
|
DEF |
USE |
key |
L.3 |
L.4 |
illegal_key |
- |
L.9 |
Shadow Memory
進捗
-
実装
- Def-Use解析
- データフローの実行時チェック
- 外部(標準)ライブラリへの対応
-
実験
- Motivating Exampleを含むテストコード
- 合成検体: cfi-eval
外部ライブラリへの対応
定義関数
-
read(), scanf()など,外部入力による変数の定義をShadow Memoryに反映させる
- 引数・返り値から,どこに,何バイト書き込んだかを取得する
1
2
3
4
5
6
7
8
9
| struct operation{
char opd[8];
void (*func)();
};
struct operation op;
op.func = func1;
read(0, op.opd, 0x30); // overwrite op.func
op.func();
|
Shadow Memory
提案手法によるDef-Use
|
DEF |
USE |
op.func |
L.7 |
L.9 |
op.opd |
L.8 |
- |
malloc(), free()
-
malloc()時に,確保したオブジェクトの情報を保存する
- 引数・返り値から,どこに,何バイトのオブジェクトを確保したか取得する
- Effective Typeから着想を得た
-
free()時に,malloc()で確保されたオブジェクトに"FREE"とマーク
- 既にfreeされていた場合,Double Freeとして報告
1
2
3
4
5
6
| char *secret = malloc(n); // 0x1000
...
free(secret);
char *uaf = malloc(0); // 0x1000
printf("secret = %s\n", uaf);
|
L.1: malloc(n)
L.3: free()
L.6: Use
実験結果
cfi-eval: Control-Flow Attackの保護評価
Classification |
CBench Testsuite |
提案手法 |
Indirect Call |
Code Pointer |
Overwrite |
✓ |
Reuse |
✗ |
Indirect Jump |
Tail Call |
Overwrite |
✓ |
Reuse |
✗ |
setjmp/longjmp |
✗ (対応可) |
Return Address |
Return Address Overwrite |
✓ |
Type Confusion |
Function Type Confusion |
✗ |
Assembly Support |
Indirect Call/Jump |
✗ |
Cross DSO Support |
Dynamic Shared Objects |
△ |
-
Type Confusion, Assembly Support
-
Cross DSO Support
- DSOファイルのLLVM IRと一緒に解析・コンパイルできる場合のみ有効
Reuse: Def-Useの保護では防げない攻撃
- もし,Input()が1の場合,配列の範囲外であるy[0]が読まれる
- しかし,xとyのどちらもグローバル変数であり,定義元を判別できない
1
2
3
4
5
6
7
8
9
10
| struct Vec {
int x[1];
int y[1];
};
struct Vec g = {100, 200}; // global variable
int main() {
return v.x[Input()];
}
|
Def-Use
|
DEF |
USE |
g.x[0] |
Global init (L.0) |
L.9 |
g.y[0] |
Global init (L.0) |
- |
リアル検体: gravity, GNU cflow
解析結果
リアル検体への適応・解析の報告予定だったが間に合わず,,,
リアル検体のビルド ⇒ LLVM IRファイルの取得
-
多くのC,C++プロジェクトには,ビルドシステムによるビルドの自動化がされる
- gravity: CMake
- GNU cflow: Autotools
- ビルドシステムに手を加え,LLVM IRを取得する
-
コンパイルオプション: Link Time Optimization (LTO)
- リンク時に最適化するためのオプション
- LLVM IRがコンパイルされる
-
リンカー: gold
- LLVM IRのLTOに対応しているリンカー
- プロジェクト全体を一つのLLVM IRファイルにまとめられる
C,C++を解析対象にしている & LLVMを使用する人の手助けはできるかも
今後について
-
リアル検体への対応・解析
- gravity
- GNU cflow
- その他3つの検体
-
静的解析ライブラリSVFへの理解を深める
-
最適化の検討・実装