__strcpy_chkについて実験してみた

あるアプリを実行したところ、エラーで終了してしまった。バックトレースを確認してみると__strcpy_chkという関数ではじかれている。

どうやらstrcpy関数がコンパイル時に__strcpy_chk関数へ置き換えられているらしい。

そこで__strcpy_chkについていくつか実験してみた。


1. 環境

Ubuntu 12.04 x86_64上で実験した。

ツールチェインにGCC 4.7.3(PPAリポジ トリのもの)とEGLIBC 2.15を使用。


2. EGLIBCにおける__strcpy_chkの実装

EGLIBCの__strcpy_chkの実装は以下の通り。

strcpyと異なり、第3引数にdestlenという変数が使われている。

/* Copy SRC to DEST with checking of destination buffer overflow.  */
char *
__strcpy_chk (dest, src, destlen)
     char *dest;
     const char *src;
     size_t destlen;
{

s[n]がキャッシュに乗ることを期待してるのか、アライメント例外対策なのか、while文で、destlenの残りが3Byte以下になるまで4Byteずつコピーしていく。

  char c;
  char *s = (char *) src;
  const ptrdiff_t off = dest - s;
 
  while (__builtin_expect (destlen >= 4, 0))
    {
      c = s[0];
      s[off] = c;
      if (c == '
        return dest;
      c = s[1];
      s[off + 1] = c;
      if (c == '
        return dest;
      c = s[2];
      s[off + 2] = c;
      if (c == '
        return dest;
      c = s[3];
      s[off + 3] = c;
      if (c == '
        return dest;
      destlen -= 4;
      s += 4;
    }
 

do-while文で残り(3Byte以下)をコピーしている。残りがコピーされる前にdestlenが0になると__chk_fail()経由でabort()が呼ばれる。

  do
    {
      if (__glibc_unlikely (destlen-- == 0))
        __chk_fail ();
      c = *s;
      *(s++ + off) = c;
    }
  while (c != '
 
  return dest;
}

3. strcpyが__strcpy_chkに置き換えられるか実験

コピー先を配列とmalloc領域の2種類にして実験してみる。

コピーの文字列は乱数で生成する。生成の際、コピー先の領域よりも1文字長い文字列を作るようにしている。また、それぞれのコードを-O0と-O1の2種類のオプションでコンパイルした。


3.1. コードの内容

コピー先が配列のコードは以下の通り。

$ cat __strcpy_chk_with_array.c 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <time.h>
 
#define BUFFER_SIZE 29
 
char *create_rand_string(char *buffer, size_t length)
{
  int i;
  for (i = 0; i < length - 1; i++)
    buffer[i] = (rand() % (CHAR_MAX - 1)) + 1;
  buffer[length - 1] = '¥0';
  return buffer;
}
 
int main(void)
{
  char src[BUFFER_SIZE + 1];
  char dst[BUFFER_SIZE];
 
  srand((unsigned int)time(NULL));
  create_rand_string(src, sizeof(src));
  strcpy(dst, src);
 
  return 0;
}

コピー先がmalloc領域のコードは以下の通り。

$ cat __strcpy_chk_with_malloc.c 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <time.h>
 
#define BUFFER_SIZE 29
 
char *create_rand_string(char *buffer, size_t length)
{
  int i;
  for (i = 0; i < length - 1; i++)
    buffer[i] = (rand() % (CHAR_MAX - 1)) + 1;
  buffer[length - 1] = '¥0';
  return buffer;
}
 
int main(void)
{
  char src[BUFFER_SIZE + 1];
  char *dst = (char *) malloc(BUFFER_SIZE);
 
  if (!dst) {
    perror("malloc");
    return 1;
  }
 
  srand((unsigned int)time(NULL));
  create_rand_string(src, sizeof(src));
  strcpy(dst, src);
  free(dst);
 
  return 0;
}

3.2. nmと実行結果

配列で-O0オプションのnmと実行結果は以下の通り。

$ gcc -Wall -O0 -o __strcpy_chk_with_array_O0 __strcpy_chk_with_array.c
$ nm __strcpy_chk_with_array_O0 | grep strcpy
                 U strcpy@@GLIBC_2.2.5
$ ./__strcpy_chk_with_array_O0 
$

配列で-O1オプションの実行結果は以下の通り。

$ gcc -Wall -O1 -o __strcpy_chk_with_array_O1 __strcpy_chk_with_array.c
$ nm __strcpy_chk_with_array_O1 | grep strcpy
                 U __strcpy_chk@@GLIBC_2.3.4
$ ./__strcpy_chk_with_array_O1
*** buffer overflow detected ***: ./__strcpy_chk_with_array_O1 terminated
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(__fortify_fail+0x37)[0x7f547fe24f47]
/lib/x86_64-linux-gnu/libc.so.6(+0x109e40)[0x7f547fe23e40]
./__strcpy_chk_with_array_O1[0x400736]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed)[0x7f547fd3b76d]
./__strcpy_chk_with_array_O1[0x400599]
======= Memory map: ========
<snip>
$

malloc領域で-O0オプションの実行結果は以下の通り。

$ gcc -Wall -O0 -o __strcpy_chk_with_malloc_O0 __strcpy_chk_with_malloc.c
$ nm __strcpy_chk_with_malloc_O0 | grep strcpy
                 U strcpy@@GLIBC_2.2.5
$ ./__strcpy_chk_with_malloc_O0 
$

malloc領域で-O1オプションの実行結果は以下の通り。

$ gcc -Wall -O1 -o __strcpy_chk_with_malloc_O1 __strcpy_chk_with_malloc.c
$ nm __strcpy_chk_with_malloc_O1 | grep strcpy
                 U __strcpy_chk@@GLIBC_2.3.4
$ ./__strcpy_chk_with_malloc_O1
*** buffer overflow detected ***: ./__strcpy_chk_with_malloc_O1 terminated
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(__fortify_fail+0x37)[0x7f5541b81f47]
/lib/x86_64-linux-gnu/libc.so.6(+0x109e40)[0x7f5541b80e40]
./__strcpy_chk_with_malloc_O1[0x400826]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed)[0x7f5541a9876d]
./__strcpy_chk_with_malloc_O1[0x400669]
======= Memory map: ========
<snip>
$

どれもバッファオーバフローを起こしていることに違いはないが、どうやら最適化オプションが有効な場合にstrcpyが__strcpy_chkに置き換わるようだ。


4. 最適化オプションによるヘッダファイルの変化

EGLIBCに__strcpy_chkが定義されている。


4.1. string.h

/usr/include/string.hに__USE_FORTIFY_LEVELと__extern_always_inlineというマクロが使われている。

# if __USE_FORTIFY_LEVEL > 0 && defined __extern_always_inline
/* Functions with security checks.  */
#  include <bits/string3.h>
# endif

4.2. bits/string3.h

/usr/include/bits/string3.hに__USE_BSDというマクロが使われており、 strcpy関数から__buildin__strcpy_chk関数が呼ばれるようになっている。

#ifdef __USE_BSD
<snip>
__NTH (strcpy (char *__restrict __dest, __const char *__restrict __src))
{
  return __builtin___strcpy_chk (__dest, __src, __bos (__dest));
}
<snip>
#endif

4.3. マクロの確認

以下のコードでマクロを確認してみた。

$ cat macro.c 
#include <stdio.h>
#include <string.h>
 
int main(void)
{
#ifdef __USE_BSD
  printf("__USE_BSD is defined\n");
#endif
 
#ifdef __USE_FORTIFY_LEVEL
  printf("__USE_FORTIFY_LEVEL = %d\n", (int) __USE_FORTIFY_LEVEL);
#endif
 
#ifdef __extern_always_inline
  printf("__extern_always_inline is defined\n");
#endif
 
  return 0;
}

-O0の実行結果は以下の通り。

$ gcc -O0 -Wall -o macro_O0 macro.c
$ ./macro_O0
__USE_BSD is defined
__USE_FORTIFY_LEVEL = 0
__extern_always_inline is defined

-O1の実行結果は以下の通り。

$ gcc -O1 -Wall -o macro_O1 macro.c
$ ./macro_O1 
__USE_BSD is defined
__USE_FORTIFY_LEVEL = 2
__extern_always_inline is defined

-O2の実行結果は以下の通り。

$ gcc -O2 -Wall -o macro_O2 macro.c
$ ./macro_O2
__USE_BSD is defined
__USE_FORTIFY_LEVEL = 2
__extern_always_inline is defined

-O3の実行結果は以下の通り。

$ gcc -O3 -Wall -o macro_O3 macro.c
$ ./macro_O3 
__USE_BSD is defined
__USE_FORTIFY_LEVEL = 2
__extern_always_inline is defined

-O1, -O2, -O3の場合に__USE_FORTIFY_LEVELが2となり、-O0の場合は0となる

__strcpy_chkが定義されるどうかの違いは__USE_FORTIFY_LEVELの値によるもの


よって最適化オプションが有効な場合に、externなstrcpyとinlineなstrcpyの二つが存在することになる。

特にinlineなstrcpyは__strcpy_chkを呼び出している。


4.4. __bos

なお、__bosの定義は/usr/include/sys/cdefs.hでされている。

#define __bos(ptr) __builtin_object_size (ptr, __USE_FORTIFY_LEVEL > 1)

この__buildin_object_sizeはGCCで定義されている関数である。

__strcpy_chkの第3引数を決定しているのはGCC。


5. __builtin_object_size

5.1. 挙動の確認

以下のコードで__builtin_object_sizeを挙動を確かめる。

#include <stdio.h>
#include <stdlib.h>
 
#define BUFFER_SIZE 29
 
int main(void)
{
  char buffer_with_array[BUFFER_SIZE];
  char *buffer_with_malloc = (char *) malloc(BUFFER_SIZE);
 
  if (!buffer_with_malloc) {
    perror("malloc");
    return 1;
  }
 
  printf("(int) (__USE_FORTIFY_LEVEL > 1) = %d\n",
         (int) (__USE_FORTIFY_LEVEL > 1));
  printf("__builtin_object_size(buffer_with_array, "
         "__USE_FORTIFY_LEVEL > 1) = %lu\n",
         __builtin_object_size(buffer_with_array,
                               __USE_FORTIFY_LEVEL > 1));
  printf("__builtin_object_size(buffer_with_malloc, "
         "__USE_FORTIFY_LEVEL > 1) = %lu\n",
         __builtin_object_size(buffer_with_malloc,
                               __USE_FORTIFY_LEVEL > 1));
 
  free(buffer_with_malloc);
  return 0;
}

-O0でコンパイルした場合の実行結果は以下の通り。

$ gcc -Wall -O0 -o __builtin_object_size __builtin_object_size.c 
$ ./__builtin_object_size 
(int) (__USE_FORTIFY_LEVEL > 1) = 0
__builtin_object_size(buffer_with_array, __USE_FORTIFY_LEVEL > 1) = 29
__builtin_object_size(buffer_with_malloc, __USE_FORTIFY_LEVEL > 1) =
18446744073709551615

-O1でコンパイルした場合の実行結果は以下の通り。

$ gcc -Wall -O1 -o __builtin_object_size
__builtin_object_size.c 
$ ./__builtin_object_size 
(int) (__USE_FORTIFY_LEVEL > 1) = 1
__builtin_object_size(buffer_with_array, __USE_FORTIFY_LEVEL > 1) = 29
__builtin_object_size(buffer_with_malloc, __USE_FORTIFY_LEVEL > 1) = 29

-O1でないとmalloc領域のサイズが割り出されない


5.2. pass_object_sizes

ではGCCのどこで__builtin_object_sizeを処理しているのかを調べてみた・・・ が全然分からない・・・。gcc/tree-object-size.cにpass_object_sizesとい ういかにもな名前のstruct gimple_opt_passがあったので、無効にしてみる。

struct gimple_opt_pass pass_object_sizes =
{
 {
  GIMPLE_PASS,
  "objsz",              /* name */
  NULL,                 /* gate */
  compute_object_sizes,         /* execute */
  NULL,                 /* sub */
  NULL,                 /* next */
  0,                    /* static_pass_number */
  TV_NONE,              /* tv_id */
  PROP_cfg | PROP_ssa,          /* properties_required */
  0,                    /* properties_provided */
  0,                    /* properties_destroyed */
  0,                    /* todo_flags_start */
  TODO_verify_ssa                   /* todo_flags_finish */
 }
};

GCC pluginを用いてpass_object_sizesを別のものに置き換える。下記の通り、 "objsz"という名前を参照してPASS_POS_REPLACEで置き換えた。

#include <gcc-plugin.h>
#include <tree-pass.h>
 
int plugin_is_GPL_compatible;
 
static bool disable_pass_object_sizes_gate(void)
{
  return true;
}
 
static unsigned disable_pass_object_sizes_exec(void)
{
  /** Do nothing */
  return 0;
}
 
static struct gimple_opt_pass disable_pass_object_sizes =
  {
    {
      .type = GIMPLE_PASS,
      .name = "disable_pass_object_sizes",
      .gate = disable_pass_object_sizes_gate,
      .execute = disable_pass_object_sizes_exec,
    }
  };
 
int plugin_init(struct plugin_name_args *args,
                struct plugin_gcc_version *version)
{
  struct register_pass_info pass =
    {
      .pass = &disable_pass_object_sizes.pass,
      .reference_pass_name = "objsz",
      .ref_pass_instance_number = 1,
      .pos_op = PASS_POS_REPLACE,
    };
  register_callback(args->base_name, PLUGIN_PASS_MANAGER_SETUP,
                    NULL, &pass);
  return 0;
}

-O1オプションでプラグインを使わなかった場合は以下の通り。

g++ -O1 target/__builtin_object_size.c && ./a.out
(int) (__USE_FORTIFY_LEVEL > 1) = 1
__builtin_object_size(buffer_with_array, __USE_FORTIFY_LEVEL > 1) = 29
__builtin_object_size(buffer_with_malloc, __USE_FORTIFY_LEVEL > 1) = 29

-O1オプションでプラグインを使った場合は以下の通り。

g++ -O1 -fplugin=./disable_pass_object_sizes
target/__builtin_object_size.c && ./a.out
(int) (__USE_FORTIFY_LEVEL > 1) = 1
__builtin_object_size(buffer_with_array, __USE_FORTIFY_LEVEL > 1) = 29
__builtin_object_size(buffer_with_malloc, __USE_FORTIFY_LEVEL > 1) = 18446744073709551615

pass_object_sizesが動かないとmalloc領域のサイズが割り出せていない。

mallocのサイズを割り出しているのはpass_object_sizesのようだ

配列のサイズを割り出すのは別のところらしい。


5.3. externなstrcpyとinlineなstrcpyの決まり方

配列を使った場合、mallocを直接使った場合、関数経由でmallocを使った場合の3種類のコピー先を使ってどちらのstrcpyが使われるかを確認する。

関数経由でmallocを使った場合についてはALLOW_INLINEというマクロでinline展開するかしないかをコントロールした。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <time.h>
 
#define BUFFER_SIZE 29
 
char *create_rand_string(char *buffer, size_t length)
{
  int i;
  for (i = 0; i < length - 1; i++)
    buffer[i] = (rand() % (CHAR_MAX - 1)) + 1;
  buffer[length - 1] = '
  return buffer;
}
 
static void *malloc_via_func(size_t size)
{
  return malloc(size);
}
 
#ifdef ALLOW_INLINE
#define prevent_inline() /* Do nothing */
#else
static void prevent_inline(void)
{
#define try_free(stmt)                          \
  do {                                          \
    void *p = stmt;                             \
    if (!p) {                                   \
      perror(#stmt);                            \
      exit(1);                                  \
    }                                           \
    free(p);                                    \
  } while (0)
  try_free(malloc_via_func(1));
  try_free(malloc_via_func(1));
  try_free(malloc_via_func(1));
  try_free(malloc_via_func(1));
#undef try_free
}
#endif
 
int main(void)
{
  char src[BUFFER_SIZE + 1];
  char dst_with_array[BUFFER_SIZE];
  char *dst_with_malloc;
  char *dst_with_malloc_via_func;
 
  dst_with_malloc = (char *) malloc(BUFFER_SIZE);
  if (!dst_with_malloc) {
    perror("malloc");
    return 1;
  }
 
  dst_with_malloc_via_func = (char *) malloc_via_func(BUFFER_SIZE);
  if (!dst_with_malloc_via_func) {
    perror("malloc_via_func");
    free(dst_with_malloc);
    return 1;
  }
 
#define __print(stmt) printf("%s = %lu\n", #stmt, stmt)
#define print(dst) \
  __print(__builtin_object_size(dst, __USE_FORTIFY_LEVEL > 1))
  print(dst_with_array);
  print(dst_with_malloc);
  print(dst_with_malloc_via_func);
#undef print
#undef __print
  srand((unsigned int) time(NULL));
  create_rand_string(src, sizeof(src));
  strcpy(dst_with_array, src);
  strcpy(dst_with_malloc, src);
  strcpy(dst_with_malloc_via_func, src);
 
  free(dst_with_malloc_via_func);
  free(dst_with_malloc);
  prevent_inline();
  return 0;
}

mallocを使った関数がインライン展開されない場合。

cc -Wall -O1 malloc_via_func.c
objdump -D a.out | grep strcpy
0000000000400670 <strcpy@plt>:
00000000004006d0 <__strcpy_chk@plt>:
  4009a1:       e8 2a fd ff ff          callq  4006d0 <__strcpy_chk@plt>
  4009b3:       e8 18 fd ff ff          callq  4006d0 <__strcpy_chk@plt>
  4009c0:       e8 ab fc ff ff          callq  400670 <strcpy@plt>
./a.out || true
__builtin_object_size(dst_with_array, __USE_FORTIFY_LEVEL > 1) = 29
__builtin_object_size(dst_with_malloc, __USE_FORTIFY_LEVEL > 1) = 29
__builtin_object_size(dst_with_malloc_via_func, __USE_FORTIFY_LEVEL > 1) = 18446744073709551615

mallocを使った関数がインライン展開される場合。

cc -Wall -O1 -DALLOW_INLINE malloc_via_func.c
objdump -D a.out | grep strcpy
0000000000400650 <__strcpy_chk@plt>:
  400901:       e8 4a fd ff ff          callq  400650 <__strcpy_chk@plt>
  400913:       e8 38 fd ff ff          callq  400650 <__strcpy_chk@plt>
  400925:       e8 26 fd ff ff          callq  400650 <__strcpy_chk@plt>
./a.out || true
__builtin_object_size(dst_with_array, __USE_FORTIFY_LEVEL > 1) = 29
__builtin_object_size(dst_with_malloc, __USE_FORTIFY_LEVEL > 1) = 29
__builtin_object_size(dst_with_malloc_via_func, __USE_FORTIFY_LEVEL > 1) = 29

pass_object_sizesはinline展開されない関数経由のmalloc領域サイズを把握できない。

サイズが確定しているものは__strcpy_chkになっている。


先ほど作成したpluginを使ってmalloc領域のサイズを分からないままにした場合はどうなるだろうか。

pluginを使って、mallocを使った関数がインライン展開されない場合。

cc -O1 -fplugin=../gcc-plugin/disable_pass_object_sizes \
                   malloc_via_func.c
objdump -D a.out | grep strcpy
0000000000400690 <__strcpy_chk@plt>:
  400964:       e8 27 fd ff ff          callq  400690 <__strcpy_chk@plt>
  400974:       e8 17 fd ff ff          callq  400690 <__strcpy_chk@plt>
  400984:       e8 07 fd ff ff          callq  400690 <__strcpy_chk@plt>
./a.out || true
__builtin_object_size(dst_with_array, __USE_FORTIFY_LEVEL > 1) = 29
__builtin_object_size(dst_with_malloc, __USE_FORTIFY_LEVEL > 1) = 18446744073709551615
__builtin_object_size(dst_with_malloc_via_func, __USE_FORTIFY_LEVEL > 1) = 184467440737095

pluginを使って、mallocを使った関数がインライン展開される場合。

cc -O1 -fplugin=../gcc-plugin/disable_pass_object_sizes \
                   -DALLOW_INLINE malloc_via_func.c
objdump -D a.out | grep strcpy
0000000000400650 <__strcpy_chk@plt>:
  400906:       e8 45 fd ff ff          callq  400650 <__strcpy_chk@plt>
  400916:       e8 35 fd ff ff          callq  400650 <__strcpy_chk@plt>
  400926:       e8 25 fd ff ff          callq  400650 <__strcpy_chk@plt>
./a.out || true
__builtin_object_size(dst_with_array, __USE_FORTIFY_LEVEL > 1) = 29
__builtin_object_size(dst_with_malloc, __USE_FORTIFY_LEVEL > 1) = 18446744073709551615
__builtin_object_size(dst_with_malloc_via_func, __USE_FORTIFY_LEVEL > 1) = 18446744073709551615

pass_object_sizesが動作しない場合、サイズが確定しない領域も__strcpy_chkのままである。

strcpy呼び出しは一度__strcpy_chkに置き換えられ、pass_object_sizesによって領域が確定しないmalloc領域についてはstrcpyに戻す動きをしているらしい


6. まとめ

  • 最適化オプションが有効な場合にEGLIBCではstrcpy関数はexternなstrcpyとinlineなstrcpyの二つが存在する。
  • inlineなstrcpyは__strcpy_chkを呼び出す。
  • __strcpy_chkの第3引数を決めているのはGCCである。
  • 特にmalloc領域についてはGCCのpass_object_sizesが担っており、malloc領域サイズが確定しないものについてはexternなstrcpyを呼び出す。


__strcpy_chk以外にも__printf_chk等もある。

最適化オプションを有効にしたバイナリに対してnmで調べてみるのも一興。


ダウンロード
ソースコード
__strcpy_chkの実験で使ったコード
__strcpy_chk.tar.gz
GNU tar 2.3 KB