linux のシステムコールをフックする
最近、とあるクローズドソースなデバイス管理ツールの挙動が気になり、その動作について解析してみることにしました。
プログラムをデバッグしたり解析したい時、どんなシステムコールが呼ばれ、どのような引数が渡されているかを、調べることができる strace は非常に有用です。
しかし、strace では ioctl で渡される複雑なデータ構造を表示することはできないため、システムコールをフックして引数を表示するという手段を取ることにしました。
そんな訳で linux でシステムコールをフックする方法について調べて見たところ、意外といろいろな方法が有ることを知りましたので、試してみた方法を幾つか紹介したいと思います。
注)今回の実験に使用した linux kernel のバージョンは 2.6.25.11 です。異なるバージョンではこの実験通りにはならない場合があります。
LD_PRELOAD を使ってフックする
まず一番手っ取り早そうなのは LD_PRELOAD を使用して hook する方法です。
参考: ウノウラボ Unoh Labs: LD_PRELOADを使って任意の関数呼び出しにフックしてみる
しかし、今回私が解析したいプログラムは libc をスタティックリンクした上に strip しているというバイナリでしたので、残念ながらこの方法では hook することが出来ませんでした。
kernel module でフックする
次に、kernel module を使用したフックに挑戦してみました。
こちらを参考にして以下のような open をフックする kernel モジュール を書いてみたのですが、うまくコンパイルできません。
#include <linux/init.h> #include <linux/module.h> #include <linux/syscalls.h> #include <linux/utsname.h> MODULE_LICENSE("GPL"); asmlinkage int (*orig_open)(const char *pathname, int flags); asmlinkage static int hook_open(const char *pathname, int flags) { printk(KERN_INFO "hook_open(\"%s\", %d)\n", pathname, flags); return orig_open(pathname, flags); } static int hook_init(void){ printk(KERN_INFO "hook_init\n"); orig_open = sys_call_table[__NR_open]; sys_call_table[__NR_open] = hook_open; return 0; } static void hook_exit(void){ printk(KERN_INFO "hook_exit\n"); sys_call_table[__NR_open] = orig_open; } module_init(hook_init); module_exit(hook_exit);
どうやら上記の方法で hook 出きるのは kernel 2.4 の頃までで、 2.6 からはセキュリティ上の理由から sys_call_table シンボルが export されなくなったようです。
kernel 変更してのフック
どうにか 2.6.x でも hook する方法は無いのかな、と調べてみると以下の方法を見つけました。
Re: using sys_mknod in init_moduleメールの内容と若干異なりますが。x86 32bit アーキテクチャの場合 arch/x86/kernel/i386_ksyms_32.c へ以下の2行を追記し、
extern void* sys_call_table[]; EXPORT_SYMBOL(sys_call_table);
kernel module に以下の宣言を追記することで無事ビルドして insmod 出来るようになりました。
extern void *sys_call_table[];
早速 insmod して
# dmesg -c # cat /etc/passwd > /dev/null # dmesg hook_open("/dev/null", 33345) hook_open("/etc/ld.so.cache", 0) hook_open("/lib/libc.so.6", 0) hook_open("/etc/passwd", 32768) hook_open("/etc/ld.so.cache", 0) hook_open("/lib/libc.so.6", 0)
ばっちり hook することが出来ました。
kernel を変更しないでフックする
rootkit の危険性は理解出来るのですが kernel を変更しなければ hook 出来ないというのも大変なので、kernel を変更を加えず、kernel module のみで hook 出きるかどうか調べてみました。
- sys_call_table をハードコーディングする
- sys_call_table を探索する
sys_call_table が export されなくともアドレスを予め調べておいてハードコードしてやればよさそうです。未圧縮の vmlinux が残っていれば sys_call_table のアドレスを簡単に調べることが出来ます。(/proc/kallsyms でも調べられるようです)
nm vmlinux|grep sys_call_table c02bc838 R sys_call_table
問題解析の用途ではこの方法で十分かもしれませんね。
ハードコードするのはちょっとなぁという場合は module の init 時に sys_call_table を探索するという手がある様です。
Finding the Linux System Call table in 2.6 series kernels | Subversity
こちらに sys_call_table を探索するコードが紹介されているのですが、unlock_kernel シンボルが見当たらないためビルドできませんでした。
unlock_kernel の代わりに、程よく sys_call_table の前方にあるシンボルを眺めていたところ strstr というシンボルがちょうど良さそうなので、これを使用した sys_call_table の探索コードを紹介します。
unsigned long **find_sys_call_table(void) { unsigned long **sctable; unsigned long ptr; sctable = NULL; for (ptr = (unsigned long)&strstr; ptr < (unsigned long)&boot_cpu_data; ptr += sizeof(void *)) { unsigned long *p; p = (unsigned long *)ptr; if (p[__NR_close] == (unsigned long) sys_close) { sctable = (unsigned long **)p; return &sctable[0]; } } return NULL; }
まとめ
今回は、問題解析のためにとにかくフックしたかったのでやや強引な方法になってしまいましたが、もっと真っ当な方法でシステムコールをフックする方法がありそうです。
また、今回いろいろな方法を調べてみてシステムコールのフックに興味が出てきたので、また別の方法を知った時には紹介してみたいと思います。
--hamano
トラックバックURL
この記事へのコメント
このページを参考にして、自分もLinuxでのシステムコールフックを試してみました。
試した中で一番のお気に入りは、ptraceです。
カーネルモジュールを使う方法もいいのですが、2.6.25以降では、sys_call_tableが
リードオンリーになっており、フックするためにはカーネルの再構築が必要となります。
僕が試した方法は、フックしたいプログラムを親プロセスとして起動するプログラムを
書きます。その中で、PTRACE_SYSCALLを使ってシステムコールに入る直前の
子プロセスをストップします。
子プロセスの汎用レジスタにシステムコールの情報(引数とかシステムコールの名前)
が格納されているので、そこをPTRACE_POKEDATAを使って書き換えてやれば
システムコールを好きなように書き換えれます。
この方法だと、カーネルの再構築やsys_call_tableのアドレスがわからなくても
フックすることができるので、お勧めです。