はじめに (対象読者・この記事でわかること)

この記事は、Linuxカーネルモジュール(LKM)を使ったシステムコールフックに興味がある中級者以上のC言語プログラマーを対象としています。カーネルプログラミングの基礎知識はあるが、実際にシステムコールをフックして監視する方法がわからない方におすすめです。

この記事を読むことで、LKMを使ってexecveシステムコールをフックし、実行されたプロセスの情報を取得する方法がわかります。また、カーネルモジュールの基本的な作り方や、システムコールテーブルの操作、カーネルログの取得方法も習得できます。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - C言語の基本的な文法とポインタの理解 - Linuxの基本的なコマンド操作とカーネルモジュールのロード/アンロード - システムコールの基本概念と役割

LKMとシステムコールフックの基礎知識

Linuxカーネルモジュール(LKM: Loadable Kernel Module)は、Linuxカーネルに動的にロードできるプログラムです。カーネル空間で動作するため、ハードウェアドライバだけでなく、セキュリティ監視ツールやデバッグ用のツールとしても利用されています。

システムコールフックは、プロセスがカーネルにサービスを要求する際の入口を横取りする技術です。たとえば、プロセスが新規実行されるときはexecveシステムコールが呼ばれます。この呼び出しをフックすることで、どんなプログラムが実行されたかを記録したり、特定のプログラムの実行を禁止したりできます。

LKMを使ったシステムコールフックは、セキュリティ製品や監視ツールで広く使われています。ただし、カーネル空間で動作するため、プログラムミングミスがシステムクラッシュにつながる可能性がある点には十分注意が必要です。

LKMでexecveシステムコールをフックする実装手順

それでは実際に、LKMを使ってexecveシステムコールをフックする方法を解説します。なお、ここではx86_64アーキテクチャのLinux 5.x系カーネルを想定しています。

ステップ1: モジュールの基本構造とヘッダファイルの準備

まず、カーネルモジュールの基本構造から始めます。以下のコードは、最小限のLKMのテンプレートです。

C
#include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/syscalls.h> #include <linux/kallsyms.h> #include <linux/unistd.h> #include <linux/slab.h> #include <linux/uaccess.h> MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kousukei"); MODULE_DESCRIPTION("execveシステムコールフックモジュール"); static unsigned long **sys_call_table; static int __init hook_init(void) { printk(KERN_INFO "execve_hook: モジュールロード開始\n"); return 0; } static void __exit hook_exit(void) { printk(KERN_INFO "execve_hook: モジュールアンロード\n"); } module_init(hook_init); module_exit(hook_exit);

このコードをexecve_hook.cとして保存し、以下のMakefileを同じディレクトリに作成します。

Makefile
obj-m += execve_hook.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

makeコマンドを実行すると、カーネルモジュールがビルドされます。

ステップ2: システムコールテーブルの取得とフックの実装

次に、システムコールテーブルを取得し、execveをフックする実装を追加します。

C
#define __NR_execve 59 asmlinkage int (*original_execve)(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp); asmlinkage int hooked_execve(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp) { char *kernel_filename; int ret; kernel_filename = kmalloc(PATH_MAX, GFP_KERNEL); if (!kernel_filename) return -ENOMEM; if (strncpy_from_user(kernel_filename, filename, PATH_MAX) < 0) { kfree(kernel_filename); return -EFAULT; } printk(KERN_INFO "execve_hook: プロセス実行検知 -> %s\n", kernel_filename); kfree(kernel_filename); return original_execve(filename, argv, envp); } static int __init hook_init(void) { sys_call_table = (unsigned long **)kallsyms_lookup_name("sys_call_table"); if (!sys_call_table) { printk(KERN_ERR "execve_hook: システムコールテーブルが見つかりません\n"); return -ENOENT; } /* カーネルメモリの保護を一時的に無効化 */ write_cr0(read_cr0() & (~0x10000)); original_execve = (void *)sys_call_table[__NR_execve]; sys_call_table[__NR_execve] = (unsigned long *)hooked_execve; /* カーネルメモリの保護を再有効化 */ write_cr0(read_cr0() | 0x10000); printk(KERN_INFO "execve_hook: execveフック完了\n"); return 0; }

ステップ3: クリーンアップ処理の実装

モジュールアンロード時に、元のシステムコールを復元する処理が必要です。

C
static void __exit hook_exit(void) { if (sys_call_table) { /* カーネルメモリの保護を一時的に無効化 */ write_cr0(read_cr0() & (~0x10000)); sys_call_table[__NR_execve] = (unsigned long *)original_execve; /* カーネルメモリの保護を再有効化 */ write_cr0(read_cr0() | 0x10000); printk(KERN_INFO "execve_hook: execveフック解除完了\n"); } }

ハマった点やエラー解決

実装中に多くの開発者が陥るのが、カーネルメモリ保護の問題です。最近のカーネルではwrite_cr0が直接呼べない場合があります。その場合は以下の代替手段を検討してください。

  1. kprobesを使ったフック方式
  2. ftraceを使ったトレース方式
  3. khookなどのサードパーティライブラリの利用

また、モジュールをロードしてもexecveがフックされない場合は、以下の点を確認してください。

  • カーネルバージョンに応じた__NR_execveの番号が正しいか
  • kallsyms_lookup_nameが有効になっているか(カーネル設定でCONFIG_KALLSYMSが必要)
  • SELinuxやAppArmorなどのセキュリティ機構が干渉していないか

解決策

write_cr0が使えない場合は、代わりにset_memory_rw関数を使ってページテーブルの属性を変更する方法があります。

C
int make_rw(unsigned long address) { unsigned int level; pte_t *pte = lookup_address(address, &level); if (!pte) return -1; pte->pte |= _PAGE_RW; return 0; } int make_ro(unsigned long address) { unsigned int level; pte_t *pte = lookup_address(address, &level); if (!pte) return -1; pte->pte &= ~_PAGE_RW; return 0; }

まとめ

本記事では、LKMを使ってexecveシステムコールをフックし、プロセス実行を監視する方法を解説しました。

  • カーネルモジュールの基本的な作り方とビルド方法
  • システムコールテーブルの取得と書き換え
  • プロセス実行検知のためのフック関数の実装
  • モジュールアンロード時のクリーンアップ処理

この記事を通して、カーネルレベルでのシステム監視の基礎を習得できたでしょう。LKMを使ったシステムコールフックは、セキュリティ製品だけでなく、デバッグやトラブルシューティングでも有用です。

今後は、kprobesを使ったより安全なフック方法や、他のシステムコール(open, connectなど)をフックする方法についても記事にする予定です。

参考資料