Linuxカーネルでsystem callのhook

この記事はLinux Advent Calendar 2014の5日目の記事です。

Linuxカーネルシステムコールをhookしたい場合にどうやるかという話です。試したカーネルは3.18.0-rc6です。

まずシステムコールですが、これはテーブルで管理されていて、各要素はNR_システムコール名(NR_closeとか)という形でインデックスを指定してアクセスできます。
そしてこのテーブルはsys_call_tableという名前です。System.mapを見ると以下のような感じで見つけられると思います。

masami@kerntest:~$ grep sys_call_table /boot/System.map-3.18.0-rc6-ktest
ffffffff81601600 R sys_call_table
ffffffff8160cf80 R ia32_sys_call_table

ちなみに、Rが付いているのでリードオンリーです。sys_call_tableのアドレスはSystem.mapを見ることでわかるのですが、R/Oは解除しないとテーブルの更新ができないのでそこを解決してあげる必要があります。
ぐぐったところページの属性変更方法はlinux-syscall-hookerを参考にしました。
やり方はlookup_address()を使ってsys_call_tableが存在するページのpteを取得し、そのpteの属性をR/Wに変えてあげるという方法です。

既存のsys_reboot()をhookしてリブートする代わりにpanic()するようにしてrebootした結果がこれですw

f:id:masami256:20141204002543p:plain

ということで、↓がカーネルモジュールです。

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <asm/uaccess.h>
#include <asm/cacheflush.h>
#include <linux/syscalls.h>
#include <linux/mm.h>

MODULE_DESCRIPTION("system call replace test module");
MODULE_AUTHOR("masami256");
MODULE_LICENSE("GPL");

/* following string SYSCALL_TABLE_ADDRESS will be replaced by set_syscall_table_address.sh */
static void **syscall_table = (void *) __SYSCALL_TABLE_ADDRESS__;

asmlinkage long (*orig_sys_reboot)(int magic1, int magic2, unsigned int cmd, void __user *arg);

asmlinkage long syscall_replace_sys_reboot(int magic1, int magic2, unsigned int cmd, void __user *arg)
{
    panic("call original reboot system call\n");
    return orig_sys_reboot(magic1, magic2, cmd, arg);
}

static void save_original_syscall_address(void)
{
    orig_sys_reboot = syscall_table[__NR_reboot];
}

static void change_page_attr_to_rw(pte_t *pte)
{
    set_pte_atomic(pte, pte_mkwrite(*pte));
}

static void change_page_attr_to_ro(pte_t *pte)
{
    set_pte_atomic(pte, pte_clear_flags(*pte, _PAGE_RW));
}

static void replace_system_call(void *new)
{
    unsigned int level = 0;
    pte_t *pte;

    pte = lookup_address((unsigned long) syscall_table, &level);
    /* Need to set r/w to a page which syscall_table is in. */
    change_page_attr_to_rw(pte);

    syscall_table[__NR_reboot] = new;
    /* set back to read only */
    change_page_attr_to_ro(pte);
}

static int syscall_replace_init(void)
{
    pr_info("sys_call_table address is 0x%p\n", syscall_table);
    
    save_original_syscall_address();
    pr_info("original sys_reboot's address is %p\n", orig_sys_reboot);

    replace_system_call(syscall_replace_sys_reboot);

    pr_info("system call replaced\n");
    return 0;
}

static void syscall_replace_cleanup(void)
{
    pr_info("cleanup");
    if (orig_sys_reboot)
        replace_system_call(orig_sys_reboot);
}

module_init(syscall_replace_init);
module_exit(syscall_replace_cleanup);

Makefileはこちら。

KERNDIR := /lib/modules/`uname -r`/build
BUILD_DIR := $(shell pwd)
VERBOSE = 0

obj-m := syscall_replace.o
smallmod-objs := syscall_replace.o

all:
  bash set_syscall_table_address.sh
  make -C $(KERNDIR) SUBDIRS=$(BUILD_DIR) KBUILD_VERBOSE=$(VERBOSE) modules

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

set_syscall_table_address.shはこれです。

#!/bin/bash

system_map="System.map-`uname -r`"
address=`grep "R sys_call_table" /boot/${system_map} | cut -f1 -d' '`
if [ $? -ne 0 ]; then
  echo "Cannot fount ${system_map} on your /boot"
  exit -1
fi

sed -i "s/__SYSCALL_TABLE_ADDRESS__/0x${address}/" syscall_replace.c

コードは masami256/syscall_replace · GitHub に置いておきました。