はじめに
ソフトウェアを開発していると、終盤になって解析に時間のかかる障害に出くわします。
その一つが「メモリ破壊によるクラッシュ」。
メモリ破壊の解析は非常に時間がかかるため、細心の注意をはらってコーディングしているものの、
誰しも1度はやらかしてしまうものです。
今回は、破壊されているメモリが「malloc/free」で確保された領域である場合を想定し、
その解析の一助となる「malloc/freeのフック方法」について紹介します。
参考
malloc/free をあまり知らない方は、「malloc:メモリ管理構造」をご参照ください。
なぜフックするのか?
メモリ破壊は発生条件や処理タイミングに依存して起きる場合が多く、
メモリ破壊が発生していたソフトウェアからなるべく変更したくないためです。
malloc/free を使用しているシステムでは、動的メモリ確保が常態化していると考えられるため、
何か仕掛けを入れようとすると標準Cライブラリを改造する必要が出てきます。
または、my_malloc などを作成して、呼び出し元すべてを置き換える必要が出てきます。
どのようにフックするのか?
ずばり、LD_PRELOAD を使用します。
プログラムを実行する際、必要な共有ライブラリ群をロードする ld というものがあります。
その ld を制御する環境変数 LD_PRELOAD を使用します。
Pre-Load という名前の通り、最初に読込んでほしい共有ライブラリを指定できます。
大半のプログラムは、早い段階で libc.so.6 がロードされるため、
それよりも先に malloc/free のシンボルを勝ち取っておく必要があります。
LD_PRELOAD を使うことで、読込む順番を強制的に変更することができるため、
簡単にフックすることができます。
malloc/free をフックする際のポイント
フックするため、用意したフック関数から標準Cライブラリの malloc/free を呼び出す必要があります。
自身も標準Cライブラリを使用するため、malloc/free のシンボルを探す方法がポイントとなります。
|
1 2 3 4 5 6 7 8 9 |
static void set_sys_funcs(void) { void *_rtld_next = ((void*)-1L); if(!sys_funcs.free) { sys_funcs.free = (void *)dlsym(_rtld_next, "free"); } } |
dlsym() の第1引数であるハンドルに「-1」をセットすると、
自分とは異なる次の共有ライブラリを探してくれます。
この動きを利用して、標準Cライブラリの malloc/free のアドレスを格納します。
サンプルプログラム
サンプルでは free が呼び出された場合に、
0 で初期化してから本来の free を呼び出すようにしています。
これにより、本来の所有者以外がアクセスした際、SIGSEGV などで異常を起こすことになり、
メモリを破壊している人が見つかる可能性があります。(運が良ければ・・・w)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
#include <string.h> #include <dlfcn.h> #include <stdbool.h> #define SIZE_SZ (sizeof(size_t)) #define chunk2mem(p) ((void*)((char*)(p) + 2*SIZE_SZ)) #define mem2chunk(mem) ((mchunkptr)((char*)(mem) - 2*SIZE_SZ)) #define PREV_INUSE 0x1 #define IS_MMAPPED 0x2 #define NON_MAIN_ARENA 0x4 #define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) #define chunksize(p) ((p)->size & ~(SIZE_BITS)) struct malloc_chunk { INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; }; typedef struct malloc_chunk* mchunkptr; struct malloc_funcs { //void *(*malloc) (size_t); //void *(*calloc) (size_t, size_t); void (*free) (void *); //void *(*realloc)(void *, size_t); }; static int hook_initialized = 0; static struct malloc_funcs sys_funcs; static void init_hook(void) { if(!hook_initialized) { memset(&sys_funcs, 0, sizeof(struct malloc_funcs)); hook_initialized = 1; } } static void set_sys_funcs(void) { void *_rtld_next = ((void*)-1L); if(!sys_funcs.free) { sys_funcs.free = (void *)dlsym(_rtld_next, "free"); } } void free(void *p_) { mchunkptr chunk; size_t sz; if (!hook_initialized) init_hook(); if (!sys_funcs.free ) set_sys_funcs(); if (p_ == NULL) return; chunk = mem2chunk(p_); sz = chunksize(chunk); memset(p_, 0, sz); sys_funcs.free(p_); } |
コンパイル
gcc malloc_hook.c -fPIC -shared -Wall -Wextra -Werror -o libmalloc_hook.so -lc -ldl
動作確認
動作確認用のプログラムはこちら。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include <unistd.h> #include <stdio.h> #include <string.h> #include <stdlib.h> static void show_mem(void *p, size_t sz) { unsigned long *ptr = (unsigned long *)p; int loop = sz / 8; int i; for (i = 0; i < loop; i++, ptr++) { printf("addr=0x%016lx, value=0x%016lx\n", (unsigned long)ptr, *ptr); } } #define TEST_SIZE 64 int main(void) { void *mem = malloc(TEST_SIZE); memset(mem, 0xFF, TEST_SIZE); printf("======== before\n"); show_mem(mem, TEST_SIZE); free(mem); printf("======== after\n"); show_mem(mem, TEST_SIZE); return 0; } |
コンパイル
gcc main.c -o test -Wall -Wextra -Werror
実行結果
LD_PRELOAD を指定しなかったとき
command
$ ./test
======== before
addr=0x0000555b0e3ba2a0, value=0xffffffffffffffff
addr=0x0000555b0e3ba2a8, value=0xffffffffffffffff
addr=0x0000555b0e3ba2b0, value=0xffffffffffffffff
addr=0x0000555b0e3ba2b8, value=0xffffffffffffffff
addr=0x0000555b0e3ba2c0, value=0xffffffffffffffff
addr=0x0000555b0e3ba2c8, value=0xffffffffffffffff
addr=0x0000555b0e3ba2d0, value=0xffffffffffffffff
addr=0x0000555b0e3ba2d8, value=0xffffffffffffffff
======== after
addr=0x0000555b0e3ba2a0, value=0x0000000000000000
addr=0x0000555b0e3ba2a8, value=0x0000555b0e3ba010
addr=0x0000555b0e3ba2b0, value=0xffffffffffffffff
addr=0x0000555b0e3ba2b8, value=0xffffffffffffffff
addr=0x0000555b0e3ba2c0, value=0xffffffffffffffff
addr=0x0000555b0e3ba2c8, value=0xffffffffffffffff
addr=0x0000555b0e3ba2d0, value=0xffffffffffffffff
addr=0x0000555b0e3ba2d8, value=0xffffffffffffffff
free 後も 0xFF で汚れたままになっています。
LD_PRELOAD を指定した時
command
$ export LD_PRELOAD=./libmalloc_hook.so
$ ./test
======== before
addr=0x00005594800252a0, value=0xffffffffffffffff
addr=0x00005594800252a8, value=0xffffffffffffffff
addr=0x00005594800252b0, value=0xffffffffffffffff
addr=0x00005594800252b8, value=0xffffffffffffffff
addr=0x00005594800252c0, value=0xffffffffffffffff
addr=0x00005594800252c8, value=0xffffffffffffffff
addr=0x00005594800252d0, value=0xffffffffffffffff
addr=0x00005594800252d8, value=0xffffffffffffffff
======== after
addr=0x00005594800252a0, value=0x0000000000000000
addr=0x00005594800252a8, value=0x0000559480025010
addr=0x00005594800252b0, value=0x0000000000000000
addr=0x00005594800252b8, value=0x0000000000000000
addr=0x00005594800252c0, value=0x0000000000000000
addr=0x00005594800252c8, value=0x0000000000000000
addr=0x00005594800252d0, value=0x0000000000000000
addr=0x00005594800252d8, value=0x0000000000000000
正しくフックできていることが分かります。