あるアプリを実行したところ、エラーで終了してしまった。バックトレースを確認してみると__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で調べてみるのも一興。