C言語 Advent Calendar 2016 16日目です。
clang 3.1, gcc 4.9以降にメモリ関連の不正な操作を検出するAddressSanitizerという仕組みが入りました。 二重freeやバッファオーバーフローなどCプログラミングにありがちなメモリ操作を検出できるので、ソフトウェアの品質向上だけでなく、セキュリティ対策としても有用です。
以下に思いつく限りのメモリの不正操作を実際に試してみました。
- (1) スタックオーバーフロー
- (1.1) 正方向の書き込み [stack_overwrite.c] (https://github.com/hamano/santest/blob/master/tests/stack_overwrite.c)
- (1.2) 正方向の参照 [stack_overread.c] (https://github.com/hamano/santest/blob/master/tests/stack_overread.c)
- (1.3) 負方向の書き込み [stack_underwrite.c] (https://github.com/hamano/santest/blob/master/tests/stack_underwrite.c)
- (1.4) 負方向の参照 [stack_underread.c] (https://github.com/hamano/santest/blob/master/tests/stack_underread.c)
- (2) ヒープバッファオーバーフロー
- (2.1) 正方向の書き込み [heap_overwrite.c] (https://github.com/hamano/santest/blob/master/tests/heap_overwrite.c)
- (2.2) 正方向の参照 [heap_overread.c] (https://github.com/hamano/santest/blob/master/tests/heap_overread.c)
- (2.3) 負方向の書き込み [heap_underwrite.c] (https://github.com/hamano/santest/blob/master/tests/heap_underwrite.c)
- (2.4) 負方向の参照 [heap_underread.c] (https://github.com/hamano/santest/blob/master/tests/heap_underread.c)
- (3) 2重free [double_free.c] (https://github.com/hamano/santest/blob/master/tests/double_free.c)
- (4.1) free後領域の書き込み [after_free_write.c] (https://github.com/hamano/santest/blob/master/tests/after_free_write.c)
- (4.2) free後領域の参照 [after_free_read.c] (https://github.com/hamano/santest/blob/master/tests/after_free_read.c)
- (5) メモリリーク [memleak.c] (https://github.com/hamano/santest/blob/master/tests/memleak.c)
結論としては上記全ての不正操作を検出できるという良い感じの結果となりました。 AddressSanitizerはメモリリークも検出できるので実行環境にvalgrindを入れる必要も無くなりそうですね。
あと、初期化していない領域の参照 (stack_uninitialized_read.c, heap_uninitialized_read.c)はAddressSanitizer(-fsanitize=address) では検出できなかったけどMemorySanitizer(-fsanitize=memory)で検出できました。
Sanitizer API
それからなんとなくllvmのソースを眺めていると、Sanitizer APIというのを見つけてしまった。
malloc/freeをhookしたりメモリの統計情報を取得できたりなど有用な関数がたくさんあるが、なかでもASAN_POISON_MEMORY_REGION()
マクロ(__asan_poison_memory_region()
関数)がおもしろい。
このマクロで触ってはいけない毒領域をマークして、その領域にアクセスすると死ぬことができる。 これを使えば自作のメモリアロケーターにもサニタイザを組込む事ができそうだ。
セキュリティ的な用途として以下の様なバッファオーバーフローを想定してみる。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct {
char title[16];
char username[16];
} song_t;
int main(){
song_t *song = malloc(sizeof(song_t));
strcpy(song->username, "taro");
strcpy(song->title, "pen_pineapple___apple_pen");
printf("username: %s\n", song->username);
printf("title: %s\n", song->title);
free(song);
return 0;
}
実行結果:
% clang -g -fsanitize=address -fno-omit-frame-pointer pico1.c -o pico1
% ./pico1
username: apple_pen
title: pen_pineapple___apple_pen
上記は典型的なバッファオーバーフローであるが、構造体メンバの範囲外アクセスはAddressSanitizerで検出できない。
もちろんこの例だとまずバッファオーバーフローを直せよって話しになるが、メモリアロケーターなどの下位ライブラリで上位の操作ミスを検出したいこともあるだろう。
そこで少しメモリを無駄遣いして毒領域を設定してみる。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sanitizer/asan_interface.h>
typedef struct {
char title[16];
char poison[16]; /* 毒領域 */
char username[16];
} song_t;
int main(){
song_t *song = malloc(sizeof(song_t));
/* 毒領域の設定 */
printf("poisoning: %p\n", song->poison);
ASAN_POISON_MEMORY_REGION(song->poison,
sizeof(song->poison));
strcpy(song->username, "taro");
strcpy(song->title, "pen_pineapple___apple_pen");
printf("username: %s\n", song->username);
printf("title: %s\n", song->title);
free(song);
return 0;
}
これで毒領域への書き込みを検知し、異常終了させることでバッファオーバーフローを防止できる。
実行結果
% clang -g -fsanitize=address -fno-omit-frame-pointer pico2.c -o pico2
% ./pico2
poisoning: 0x60400000dfe0
=================================================================
==15422==ERROR: AddressSanitizer: use-after-poison on address 0x60400000dfe0 atpc 0x000000489f4c bp 0x7ffd03a33a50 sp 0x7ffd03a33208
WRITE of size 26 at 0x60400000dfe0 thread T0
#0 0x489f4b (/home/hamano/git/santest/pico2+0x489f4b)
#1 0x4bd1b5 (/home/hamano/git/santest/pico2+0x4bd1b5)
#2 0x7f71843e0b44 (/lib/x86_64-linux-gnu/libc.so.6+0x21b44)
#3 0x4bceac (/home/hamano/git/santest/pico2+0x4bceac)
0x60400000dfe0 is located 16 bytes inside of 48-byte region [0x60400000dfd0,0x60400000e000)
allocated by thread T0 here:
#0 0x49f82b (/home/hamano/git/santest/pico2+0x49f82b)
#1 0x4bd0a9 (/home/hamano/git/santest/pico2+0x4bd0a9)
#2 0x7f71843e0b44 (/lib/x86_64-linux-gnu/libc.so.6+0x21b44)
SUMMARY: AddressSanitizer: use-after-poison ??:0 ??
Shadow bytes around the buggy address:
0x0c087fff9ba0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c087fff9bb0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c087fff9bc0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c087fff9bd0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c087fff9be0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0c087fff9bf0: fa fa fa fa fa fa fa fa fa fa 00 00[f7]f7 00 00
0x0c087fff9c00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c087fff9c10: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c087fff9c20: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c087fff9c30: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c087fff9c40: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Heap right redzone: fb
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack partial redzone: f4
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
ASan internal: fe
==15422==ABORTING
zsh: exit 1 ./pico2