全てのプログラムに gcc の -D_FORTIFY_SOURCE と -fstack-protector オプションが効くわけではない話

GCCを使ってプログラムを書くとき, 外部からの攻撃に備えたスタック保護のために

  • -D_FORTIFY_SOURCE オプション
  •  -fstack-protector オプション

をつけるのは半ば常識となっています.

本記事はその保護機能を否定するものでない、ということをまず申し上げておきます.

しかしながら、このオプションをつければすべてのプログラム、すべての関数のスタックがいついかなる時も保護されるわけではない、という話をしたいと思います.

最初に参考文献を

まず最初に本件の参考文献を紹介しておきます.

Binary Hacks ―ハッカー秘伝のテクニック100選、最初にこの本をちゃんと読んでおけば今回苦労しなくてもよかった、という話です.

ことの発端は?

Linuxを使ったシステムを開発してますが, プロジェクトの方針として

OSSを含むすべてのC/C++で書かれたプログラムに -D_FORTIFY_SOURCE オプション, -fstack-protector オプションを設定せよ

という指示がありました.

これは至極まっとうな指示だと理解してます.

そして,

ビルドされたバイナリに対し、追加したコンパイルオプションが効いていることを checksec.sh で確認せよ,

という指示もありました.

ところが、我々が担当しているプログラム (いくつかのOSSを含む) 一様に -D_FORTIFY_SOURCE オプション, -fstack-protector オプションを追加しているにも関わらず, checksec.sh の結果でオプションが効いているもの、効いていないものがある結果となり、

なぜだろう?

というのがコトの発端です.

バイナリのセキュリティ・プロパティを調べる checksec.sh

ビルド済みのバイナリに対し、セキュリティプロパティの有効・無効を確認する checksec.sh.を /bin/ls に対して実行した結果です.

STACK CANARY     FORTIFY     FILE
Canary found       Yes       /bin/ls

STACK CANARYCanary found となっていれば -fstack-protector によるスタック保護機能が, FORTIFYYes となっていれば -D_FORTIFY_SOURCE によるバッファオーバーフロー検出機能が組み込まれていることを意味します.

-fstack-protector および -D_FORTIFY_SOURCE オプションを指定すれば, どんなプログラムでも checksec.shの結果が STACK CANARY, および FORTIFY が有効であることを期待していました.

これがそもそもの誤りです.

オプション付けたが checksec.sh の結果が STACK CANARY, FORTIFY とも無効

ためしに以下のシンプルなプログラムをビルドして checksec.sh を実行してみました.

もちろんコンパイルオプション -fstack-protector および -D_FORTIFY_SOURCE ともつけてビルドしています.

#include <stdio.h>
#include <string.h>

int main (int argc, char ** argv)
{
  int i;

  for (i = 0; i < argc; i++)
    {
      printf ("argv[%d]:%s\n", i, argv[i]);
    }
  
  return 0;
}

checksec.sh の実行結果は以下の通り.

STACK CANARY      FORTIFY      FILE
No canary found     No        ./sample_simple

コンパイルオプション -fstack-protector および -D_FORTIFY_SOURCE をつけてビルドしたにも関わらず、結果は STACK CANARY, FORTIFY とも検出されません…なぜだ!?

そもそも -fstack-protector オプションとは?

そもそも -fstack-protector オプションをつけたらどうなるか、を知らずに言われるまま対応しているのが悪いんです. 何か問題があれば man コマンドに立ち戻ります.

man gcc(1) の -fstack-protector によると

-fstack-protector
    Emit extra code to check for buffer overflows, such as stack
    smashing attacks.  This is done by adding a guard variable to
    functions with vulnerable objects.  This includes functions
    that call "alloca", and functions with buffers larger than 8
    bytes.  The guards are initialized when a function is entered
    and then checked when the function exits.  If a guard check
    fails, an error message is printed and the program exits.

8バイト以上のローカルなバッファを持つ関数にガード関数を追加し, 関数の入り口でガード処理が初期化され、関数の出口でバッファ破壊がチェックされ、破壊が分かればエラーを表示し、プログラムが終了する、とあります.

以下のプログラムを -fstack-protector および -D_FORTIFY_SOURCE をつけてビルドし,

#include <stdio.h>

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

  if (argc >= 1)
    {
      for (sp = argv[1], dp = buf;*sp != '\0'; sp++, dp++)
      {
	       *dp = *sp;
	     }
    }
  return 0;
}

そのビルド結果のchecksec.sh の実行結果は以下の通り.

STACK CANARY     FORTIFY     FILE
Canary found       No       ./sample_stack_protector

STACK CANARY が Canary found、すなわち -fstack-protector が有効であることがわかります.

$ ./sample_stack_protector 0123456789012345
 *** stack smashing detected ***:  terminated
 Aborted (core dumped)

そして、8 byte のバッファをオーバーフローするように文字列を書きこんだ場合, 上記の様なエラーメッセージを表示し、プログラムが終了します.

何をもって checksec.sh は STACK CANARY が Canary found と判断している?

先の STACK CANARY が No canary found となったプログラム sample_simple.c はローカル変数がオーバーフローするようなコードではありません. そのため, バッファのガード関数がリンクされません.

$ readelf -s ./sample_stack_protector |grep _chk
__stack_chk_fail@GLIBC_2.17
__stack_chk_guard@GLIBC_2.17
__stack_chk_fail@@GLIBC_2
__stack_chk_guard@@GLIBC

checksec.sh はビルドしたバイナリのシンボルに __stack_chk_fail を探し, あれば STACK CANARY が Canary found と表示します.

これが,バッファのガード関数がリンクされない先のプログラム sample_simple に対する checksec.sh の実行結果が STACK CANARY が No canary found となる理由です.

参考にしている文献 Binary Hacks ―ハッカー秘伝のテクニック100選 によると, バッファ溢れチェックは char, sigined char, unsigned char の配列が対象と書かれています. それ以外の型の配列はバッファ溢れチェックの対象外となることに注意です.

-D_FORTIFY_SOURCE オプションが有効となるとき

先ほどのプログラムの評価結果は FORTIFYNo です.

-D_FORTIFY_SOURCE オプションとなるのはどんな場合でしょうか?

以下のプログラム sample_fortify_source.c は, strcpy() でローカル変数の char の配列 buf にコピーするプログラムです.

このプログラムを -fstack-protector および -D_FORTIFY_SOURCE をつけてビルドし,

#include <stdio.h>
#include <string.h>

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

  if (argc >= 1)
    {
      strcpy(buf, argv[1]);
    }
  return 0;
}

そのビルド結果のchecksec.sh の実行結果は以下の通り.

STACK CANARY     FORTIFY     FILE
Canary found       Yes       ./sample_fortify_source

FORTIFY が Yes, すなわち -D_FORTIFY_SOURCE オプションが有効であることがわかります.

さらに, STACK CANARY が Canary found、すなわち -fstack-protector も有効であることがわかります.

ビルドした結果のシンボルを見ると,

$ readelf -s ./sample_fortify_source |grep _chk
__strcpy_chk@GLIBC_2.17 
__stack_chk_fail@GLIBC_2.17 
__stack_chk_guard@GLIBC_2.17 
__strcpy_chk@@GLIBC_2.17 
__stack_chk_fail@@GLIBC_2 
__stack_chk_guard@@GLIBC

バッファのガード関数 _stack_chk_fail とともに, _strcpy_chk というシンボルが見えます.

-D_FORTIFY_SOURCE オプションでは strcpy(), memcpy(), read() などのランタイムライブラリでバッファオーバーフローを監視する機能です.

_strcpy_chk は strcpy() を使用する際に実行されるバッファオーバーフロー検知関数です.

監視対象となるランタイムライブラリの数は Binary Hacks ―ハッカー秘伝のテクニック100選 の著書の環境だと 65個, とのこと. 対象の関数の調査方法はこの本に書かれてましたので参照ください.

-D_FORTIFY_SOURCE オプションは有効だが、-fstack-protector が無効となる場合

先のプログラム例は FORTIFY が Yes, STACK CANARY が Canary found という結果で, すなわち両方のオプションが効いていました.

以下のプログラム例は -D_FORTIFY_SOURCE オプションが有効だが, -fstack-protector が無効となる例です.

#include <stdio.h>
#include <string.h>

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

  if (argc >= 1)
    {
      memcpy (buf, argv[1], sizeof (buf));
    }
  return 0;
}

ローカル変数の配列の型が int 型であるため, -fstack-protector のスタックチェック機能の対象から外れます.

また、以下のプログラム例は -D_FORTIFY_SOURCE オプション-fstack-protector が両方とも無効となります.

#include <stdio.h>
#include <string.h>

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

  if (argc >= 1)
    {
      memcpy (buf, argv[1], sizeof (buf));
    }
  return 0;
}

ローカル変数の配列を有するものの、書きこむサイズはコンパイル時に決定するため、ランタイムのバッファオーバーフローの監視の必要が無いため、チェック関数がリンクされません.

そのため, -D_FORTIFY_SOURCE オプションも無効となります.

このようにスタック保護のオプションはセキュリティ対策のためには必要なものですが、いついかなる場合にも有効である、ということではなく、その働きを理解して利用する必要があります.

サンプルコードはここにあります.

本記事で紹介した簡単なサンプルコードは GitHub 上で公開しています. ご参考になれば幸いです.

追申:

今回はこの本にずいぶん助けられました. 仕事上でも欠かせない一冊です.
その記事はこちらにも書きました.

Cプログラマであれば是非読んでおいて損の無い一冊かと.

追申: 2022.7.31

最近の GCC (9.4.0) で動作検証してみましたが, -D_FORTIFY_SOURCE,および -fstack-protectorオプションはデフォルトでONになっています.

そのため明示的にコンパイル時に -D_FORTIFY_SOURCE を指定すると Redefined の警告メッセージが表示されます.

ただし, -Wstack-protector による警告出力を期待する場合には, 明示的にコンパイル時-fstack-protectorオプションをつける必要がある点に注意です.

では