monoの開発ブログ

Linuxのシステムコールをフック

Linuxのシステムコールをフックしてみます。

システムコールはユーザ空間のアプリケーションがカーネルの機能を利用するために必ず通る必要のある道で、ここに手を加えることですべてのアプリケーションを制御することができます。

しかし、その強力さからrootkitなどにも利用できるため、最近のカーネルでは以下の2つの壁によってシステムコールのフックができないようになっています。

  • sys_call_tableをエクスポートしない
  • sys_call_tableを読み取り専用にする

今回は、この2つを無効化したカーネルを作成し、自由にシステムコールをフックできる環境を構築します。当然ですが、rootkitの攻撃も受け入れる状態になってしまいますので、本番環境では絶対に使用しないでください。

実験環境

今回はScientific Linux 6.1 (32bit)をベースにカーネルを3.1.0に置き換えて実験しました。

カーネルをビルド

まず、改造前のカーネルをビルドして動作を確認しておきます。

# cd /usr/src
# wget http://www.kernel.org/pub/linux/kernel/v3.0/linux-3.1.tar.bz2
# tar xf linux-3.1.tar.bz2
# cd linux-3.1
# cp /boot/config-2.6.32-131.0.15.el6.i686 .config
# make menuconfig
# make oldconfig
# make

makeはかなり時間がかかりますが、気長に待ちましょう。

# cp arch/x86/boot/bzImage /boot/vmlinuz-3.1.0
# make modules_install
# cd /boot
# mkinitrd initramfs-3.1.0.img 3.1.0
# vi grub/grub.conf

GRUBのメニューに項目を追加します。また、hiddenmenuをコメントアウトして起動時にメニューが表示されるようにします。

default=0
timeout=10
splashimage=(hd0,0)/grub/splash.xpm.gz
#hiddenmenu
title Scientific Linux (2.6.32-131.0.15.el6.i686)
    root (hd0,0)
    kernel /vmlinuz-2.6.32-131.0.15.el6.i686 ro root=/dev/mapper/vg_scientificvbox-lv_root rd_LVM_LV=vg_scientificvbox/lv_root rd_LVM_LV=vg_scientificvbox/lv_swap rd_NO_LUKS_ rd_NO_MD rd_NO_DM LANG=ja_JP.UTF-8 KEYBOARDTYPE=pc KEYTABLE=jp106 crashkernel=auto rhgb quiet
    initrd /initramfs-2.6.32-131.0.15.el6.i686.img

title Scientific Linux (3.1.0)
    root (hd0,0)
    kernel /vmlinuz-3.1.0 ro root=/dev/mapper/vg_scientificvbox-lv_root rhgb quiet
    initrd /initramfs-3.1.0.img

再起動してScientific Linux (3.1.0)を選択し、正しく起動すれば成功です。

sys_call_tableをエクスポート

システムコールの実体のアドレス一覧は、sys_call_tableという配列に保持されていますが、2.6以降のカーネルではエクスポートされなくなってしまったため、対策が必要です。実行時に強引にアドレスを探り当てるという方法もありますが、もう1点の問題を解決するためにどちらにしろカーネルへの変更が必要になるため、sys_call_tableをエクスポートするようにカーネルを改造します。

# cd /usr/src/linux-3.1
# vi arch/x86/kernel/i386_ksyms_32.c

ファイルの末尾に以下の2行を追加します。

extern void* sys_call_table[];
EXPORT_SYMBOL(sys_call_table);

sys_call_tableの読み取り専用属性を解除

いつのバージョンからかは未確認ですが、sys_call_tableが読み取り専用になり、仮にアドレスを探り当てたとしても書き込めないようになってしまいました。ここでは読み取り専用属性を解除し、自由に書き換えられるようにします。

# cd /usr/src/linux-3.1
# vi arch/x86/kernel/entry_32.S

.section部分の記述を修正します。

/* .section .rodata,"a" */
.section .data,"aw"
#include "syscall_table.S"

syscall_table_size=(.-sys_call_table)

改造したカーネルをビルド

2回目のmakeですがそれなりに時間が掛かります。やはり気長に待ちましょう。

# cd /usr/src/linux-3.1
# make
# cp arch/x86/boot/bzImage /boot/vmlinuz-3.1.0
# make modules_install
# cd /boot
# mkinitrd --force initramfs-3.1.0.img 3.1.0

無事に再起動できればそこは自由な世界、になっていると思われます。

動作確認

openシステムコールをフックするLKMを作って動作させてみます。今回作成したプログラムは、システムコールに渡された引数を表示してオリジナルのopenシステムコールにそのまま流すだけのものです。

なお、モジュールをつくるところまではrootでなくてもできるので普通のユーザで作業しています。

$ mkdir /path/to/syscall-hook
$ vi syscall-hook.c
$ vi Makefile
$ make

syscall-hook.c

#include <linux/kernel.h>
#include <linux/module.h>
#include <asm/unistd.h>

MODULE_LICENSE("GPL");

extern void* sys_call_table[];

asmlinkage static int (*original_open)(const char* pathname, int flags);

asmlinkage static int my_open(const char* pathname, int flags) {
    printk(KERN_INFO "my_open(\"%s\", %d)\n", pathname, flags);
    return original_open(pathname, flags);
}

static int on_init(void) {
    printk(KERN_INFO "on_init\n");
    original_open = sys_call_table[__NR_open];
    sys_call_table[__NR_open] = my_open;
    return 0;
}

static void on_exit(void) {
    printk(KERN_INFO "on_exit\n");
    sys_call_table[__NR_open] = original_open;
}

module_init(on_init);
module_exit(on_exit);

Makefile

obj-m := syscall-hook.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
    $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules

clean:
    rm -rf *.o *.ko *.mod.c *~

結果

rootになってモジュールをinsmod/rmmodしてみます。フックできていれば、/var/log/messagesにopenシステムコール呼び出しの内容が出力されます。

# cd /path/to/syscall-hook
# insmod syscall-hook.ko
# rmmod syscall-hook.ko
# less /var/log/messages

/var/log/messages

おわりに

こういうのを見てると、安全と自由を両立させることの難しさを感じますね。なんとか解決できればとは思うのですが……。

参考