提案手法の概要

  • 目的: データの保護
    • Buffer overflowの検知
    • 未初期化変数の使用
    • Use after freeの検知
    • etc
  • 特徴(目標):
    • 構造体のメンバ変数を保護
      → 高精度な保護
    • 選択的データ保護
      → 高効率な保護
保護手法: Data-Flow Integrity
  • 保護のアイデア: プログラム実行中に以下を監視
    • データが正しく書き込まれる
    • データが正しく読み込まれる
  • 保護の流れ
    1. Use-Def解析: 正しいデータの読み書きの関係を求める
    2. チェック関数の挿入
    3. 実行時チェック
例)
1
2
3
4
5
6
7
8
struct Vec2D { int x, y; };

struct Vec2D v;
v.x = 100;
v.y = 200;

v.x;
v.y;

Step.1 Use-Def解析

正しい定義と使用の関係を求める
1
2
3
4
5
6
7
8
struct Vec2D { int x, y; };

struct Vec2D v;
v.x = 100;
v.y = 200;

v.x;
v.y;
Use-Def解析の結果
Use Def
v.x L.4 L.7
v.y L.5 L.8

Step.2 チェック関数の挿入

Step.1の結果に沿ったデータの読み書きが行われるかをチェックする関数を挿入
  • set_id(): データの書き込み情報を取得 + メタデータに保存
  • check_id(): メタデータから正しい読み込みかどうか確認
1
2
3
4
5
6
7
8
9
10
11
12
struct Vec2D { int x, y; };

struct Vec2D v;
v.x = 100;
set_id(&v.x, ID1);
v.y = 200;
set_id(&v.y, ID2);

check_id(&v.x, ID1);
v.x;
check_id(&v.y, ID2);
v.y;

Step.3 実行時チェック

プログラムを実行し,正しく読み書きされているかどうかをチェック
1
2
3
4
5
6
7
8
9
10
11
12
struct Vec2D { int x, y; };

struct Vec2D v;
v.x = 100;
set_id(&v.x, ID1);
v.y = 200;
set_id(&v.y, ID2);

check_id(&v.x, ID1);
v.x;
check_id(&v.y, ID2);
v.y;

上記の例でのポイント: メンバ変数に対して,別々のIDを付与している
→ メンバ変数を区別して保護可能

Field-sensitive DFIの問題点

問題点: メタデータの粒度を細かくしなければならない

Field-insensitive DFIの場合

"複数バイト ↔ 1 定義ID" の粒度で定義IDを付与
例) DFI: 4 byte ↔ 1 定義ID

struct Aligned {
  int x, y;
};
struct Unaligned {
  char c1, c2, c3, c4;
};
  • メリット: 1つの定義IDで,複数バイトの安全性チェックが可能 → 効率的な保護
  • デメリット: 複数のデータに対して,一つの定義IDを付与する → 保護精度が低い
Field-sensitive DFIの場合

"1 byte ↔ 1 定義ID" の粒度で定義IDを付与

  • メリット: 1つのデータに対して,少なくとも1つのメタデータを付与 → 保護精度が高い
  • デメリット: 1つのデータの安全性チェックに,複数の定義IDチェックが必要 → 非効率な保護
    • 例) aligned.x ↔ 4 定義ID が付与されてしまう

解決案

解決案1: 4-byte Alignment計装 (中間発表以前)

全データを4 byte境界に配置するように計装

結果: 外部ライブラリとのリンク時にエラー多発 → Compatibilityが低い
∵ 構造体のメモリレイアウトを変更してしまうため

解決案2: 2つの領域への割り振り (現在取り組み中)

データを

  • 4-byte境界に配置されているデータ
  • それ以外
に分類し,それぞれを
  • 4-byte Aligned Region: "4-byte ↔ 1 定義ID" の粒度でメタデータを持つ領域
  • Unaligned Region: "1-byte ↔ 1 定義ID" の粒度でメタデータを持つ領域
に割り当てる

結果予想:

  • 4-byte境界に配置されているデータ: "4-byte ↔ 1 定義ID" → 効率的な保護
  • それ以外: "1-byte ↔ 1 定義ID" → 高精度な保護

手法の流れ + 実装

手法概要
  1. 保護データの指定
  2. 2領域への割り振り
  3. 各領域毎のデータフロー保護

Step.1 保護データの指定

保護データを以下で指定:

  • グローバル変数, ローカル変数: アノテーションを追加
  • ヒープ変数: mallocをsafe_mallocに書き換え

例)
1
2
3
4
5
6
7
8
9
10
// global
int g __attribute__((annotate("dfi_protection")));

int main(void) {
  // local
  int l __attribute__((annotate("dfi_protection")));

  // heap
  int *p = (int *)safe_malloc(sizeof(int));
}

Step.2 2領域への割り振り

保護データを4-byte境界に配置されているかどうかで,以下の2領域に割り振る:

  • 4-byte Aligned Region
  • Unaligned Region

ローカル変数の割り振り

ローカル変数のメモリ確保を領域毎のメモリ確保に置き換え

例)
割り振り前
1
2
3
4
5
int main(void) {
  struct Aligned aligned     __attribute__((annotate("dfi_protection")));
  struct Unaligned unaligned __attribute__((annotate("dfi_protection")));
  ...
}
割り振り後
1
2
3
4
5
6
7
8
9
int main(void) {
  struct Aligned *aligned
    = (struct Aligned *)aligned_malloc(sizeof(struct Aligned));
  struct Unaligned *unaligned
    = (struct Unaligned *)unaligned_malloc(sizeof(struct Unaligned));
  ...
  free(aligned);
  free(unaligned);
}
ヒープ変数の割り振り

safe_malloc()を領域毎のメモリ確保に置き換え

例)
割り振り前
1
2
3
4
5
6
7
int main(void) {
  struct Aligned *aligned
    = (struct Aligned *)safe_malloc(sizeof(struct Aligned));
  struct Unaligned *unaligned
    = (struct Unaligned *)safe_malloc(sizeof(struct Unaligned));
  ...
}
割り振り後
1
2
3
4
5
6
7
int main(void) {
  struct Aligned *aligned
    = (struct Aligned *)aligned_malloc(sizeof(struct Aligned));
  struct Unaligned *unaligned
    = (struct Unaligned *)unaligned_malloc(sizeof(struct Unaligned));
  ...
}
グローバル変数の割り振り

リンカースクリプトによるセクションの追加 + グローバル変数のセクション指定

例)
割り振り前
1
2
3
// global
struct Aligned aligned     __attribute__((annotate("dfi_protection")));
struct Unaligned unaligned __attribute__((annotate("dfi_protection")));
割り振り後
1
2
3
// global
struct Aligned aligned,     section "aligned_global";
struct Unaligned unaligned, section "unaligned_global";

Step.3 各領域毎のデータフロー保護

各領域に用意されたメタデータを用いてデータフロー保護

4-byte Aligned Region

4-byte ↔ 1 定義ID の粒度で保護

例)
1
2
3
4
5
aligned->x = 100;
aligned_set_id(&aligned->x, ID1);   // inserted
...
aligned_check_id(&aligned->x, ID1); // inserted
aligned->x;
Unaligned Region

1-byte ↔ 1 定義ID の粒度で保護

例)
1
2
3
4
5
unaligned->c1 = 'a';
unaligned_set_id(&unaligned->c1, ID2);    // inserted
...
unaligned_check_id(&unaligned->c1, ID2);  // inserted
unaligned->c1;
非保護領域

保護データ領域にアクセスしないようにチェック

例) l.5: arr[i];にて,保護データを不正に読み込まないようチェック
1
2
3
4
5
int i, arr[100];
...
if (AddrIsInSafeRegion(&arr[i]))  // inserted
  report_error(&arr[i]);          // inserted
arr[i];

実装進捗

  • Step.1 保護データの指定 ✓
  • Step.2 2領域への割り振り ✓
  • Step.3 各領域毎のデータフロー保護
    • 4-byte Aligned Region
    • Unaligned Region
    • 非保護領域

今後の予定

  • 領域毎のデータフロー保護 実装
    • 4-byte Aligned Region
    • Unaligned Region
    • 非保護領域
  • リアル検体解析
    • オーバーヘッド計測: SPEC CPU2006
    • 攻撃検知:
      • mbed TLS: CVE-2015-5291
        • cf. DataShield[CCS'17]
      • Sub-object overflow bugを含む検体
      • Use-After-Freeを含む検体
  • 実験結果より,今後の方針を決定
    1. Sandboxingによるデータ隔離
      • 目的: 非保護データが保護データを破壊しないよう保証
      • メリット: より効率的な保護が可能
      • cf. DataShield[CCS'17]
    2. スレッドインターリーブを考慮したフロー保護
      • 目的: Inter-thread value flow bugの検知
      • メリット: スレッド安全性も保証できそう
      • cf. Canary[PLDI'21]