2008年07月25日

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 を使用したフックに挑戦してみました。

参考: Linux Kernel Hacking

こちらを参考にして以下のような 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 出きるかどうか調べてみました。

  1. sys_call_table をハードコーディングする
  2. sys_call_table が export されなくともアドレスを予め調べておいてハードコードしてやればよさそうです。未圧縮の vmlinux が残っていれば sys_call_table のアドレスを簡単に調べることが出来ます。(/proc/kallsyms でも調べられるようです)

    nm vmlinux|grep sys_call_table
    c02bc838 R sys_call_table
    

    問題解析の用途ではこの方法で十分かもしれませんね。

  3. sys_call_table を探索する
  4. ハードコードするのはちょっとなぁという場合は 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
klab_gijutsu2 at 09:00│Comments(3)TrackBack(0)kernel 

トラックバックURL

この記事へのコメント

1. Posted by とおりがかり   2008年07月26日 07:18
つ http://kstrax.sourceforge.net/

2. Posted by ぺお   2008年10月15日 19:20
kstraxはlinux-2.6.27ではすでに使えなかった...linux内部API変わりすぎ;<
3. Posted by キム   2010年08月06日 12:52
こんにちわ。

このページを参考にして、自分もLinuxでのシステムコールフックを試してみました。

試した中で一番のお気に入りは、ptraceです。

カーネルモジュールを使う方法もいいのですが、2.6.25以降では、sys_call_tableが
リードオンリーになっており、フックするためにはカーネルの再構築が必要となります。

僕が試した方法は、フックしたいプログラムを親プロセスとして起動するプログラムを
書きます。その中で、PTRACE_SYSCALLを使ってシステムコールに入る直前の
子プロセスをストップします。

子プロセスの汎用レジスタにシステムコールの情報(引数とかシステムコールの名前)
が格納されているので、そこをPTRACE_POKEDATAを使って書き換えてやれば
システムコールを好きなように書き換えれます。

この方法だと、カーネルの再構築やsys_call_tableのアドレスがわからなくても
フックすることができるので、お勧めです。

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント