システムコールハンドラのスプーフィング

すべての人に良い一日を! 私は専門大学の2年生です。 数か月前、コースプロジェクトのトピックを選択する時が来ました。 電卓のようなテーマは私に合わなかった。 そこで、もっと面白いものはないかと尋ねると、肯定的な答えが返ってきました。 「システムコールハンドラの代替」が私のトピックです。



はじめに


割り込みハンドラー(または割り込みサービスルーチン)は、処理を実行するために割り込みで呼び出される特別なプロシージャです。 これらのハンドラーは、ハードウェア割り込み、またはプログラム内の対応する命令のいずれかによって呼び出され、通常はデバイスと対話したり、オペレーティングシステム機能(wiki)を呼び出したりするように設計されています。



なんで?


おそらく、作業システムの視覚的な主な目標は、ドライ理論を「噛む」ことではなく、その仕組みを視覚的に確認することです。 まあ、または以前と同様に、プログラマーはDOSでタイマーイベントハンドラーをオーバーライドして「マルチタスクを実行」しようとしました。



32ビットハンドラー



アプローチ方法

インターネットで少し調べて( この投稿は特に役に立ちました)、Linuxアーキテクチャのマニュアルを「s製」しました。32ビット割り込みをCに置き換える実装を見つけました。すべてが思ったより簡単であることがわかりました。



順番に分析します。

システムコールは、アプリケーションからオペレーティングシステムのカーネルへの呼び出しであり、操作を実行します(wiki)。 システムコールハンドラのアドレスは、カーネルによってシステムコールテーブル(sys_call_table)に保存されます。 これらのアドレスのいずれかにあるハンドラーは、プログラムがeaxレジスターのシステムコールの番号で割り込み80hを呼び出すたびに呼び出されます(たとえば、ファイルまたは出力デバイスに書き込む書き込みシステムコールのeax = 4) 。 このテーブルのアドレスと必要な呼び出しの数がわかっているので、そのハンドラーを独自のコードに置き換えることができます。



だから、32ビットの割り込みで考え出した。



割り込み置換アルゴリズムは非常に簡単です。

  1. システムコールテーブル(sys_call_table)のアドレスを探します
  2. 必要なシステムコールのアドレスを探します
  3. このアドレスの代わりに、ハンドラーのアドレスを書き込みます


このような操作の後、置き換えた割り込みを呼び出すと、ハンドラーが呼び出されます。

これを実装するために、カーネルモジュールを作成します。 なぜモジュールなのか? はい、すべてが簡単です。モジュールは、必要に応じてメモリからロードまたはアンロードできるプログラムコードです。 したがって、システムを再起動する必要なく、カーネルの機能を拡張します。 モジュールはCで作成されます。



カーネルモジュールのスケルトン:



static int init(void) { } static void exit(void) { } module_init(init); module_exit(exit);
      
      







上記からわかるように、モジュールには少なくとも2つの関数が含まれている必要があります-メモリ内のモジュール初期化関数(モジュールがメモリにロードされたときに呼び出されます)とシャットダウン関数(モジュールがアンロードされたときに呼び出されます)。

ハンドラーを置き換えるために解決する必要がある主なタスクは、RAM内のシステムコールテーブルの場所を見つけることです。 テーブルのアドレスは、「System.map-version_kernel」ファイルにあります。 見つかったアドレスをコンパイル済みモジュールに追加します。 アドレスを見つけるには、次のコマンドを使用します。



grep sys_call_table /boot/System.map-$(uname -r) |awk '{print $1}'







コマンドは、見つかったアドレスを表示します。例:



c05d3180







システムコールのテーブルを検索するプロセスを自動化するには、実際に行った小さなスクリプトを作成します。

システムコールをコードで完全に置き換えるには、その機能を完全に実装する必要があります。 したがって、不必要な頭痛を避けるために、システムコールテーブルのアドレスを変更するときに、以前の値を変数に保存し、アクションを実行するたびにこのアドレスに制御を移します。 このアプローチにより、既存の機能を損なうことなく独自のアクションを追加できるため、OSを破壊することはありません。

何も損なわないために、最初のカーネルモジュールは、書き込み関数が呼び出されたときにメッセージをシステムカーネルログに出力するだけです。



重要な詳細! ハンドラーを置き換える場合、割り込みベクターの領域の書き込み保護をバイパスする必要があります。 これを行うには、CR0システムレジスタのWPビットをリセットします。 このビットはハードウェアレベルで動作し、メモリページへの書き込みが許可されているかどうかに関係なく、メモリページへの変更を許可します(十分な特権を持つコードの場合)。 CR0レジスタは、write_cr0()およびread_cr0()マクロによってアクセスされます。



モジュール概要コード
 #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/errno.h> #include <linux/types.h> #include <linux/unistd.h> #include <asm/cacheflush.h> #include <asm/page.h> #include <asm/current.h> #include <linux/sched.h> #include <linux/kallsyms.h> unsigned long *syscall_table = (unsigned long *)0xTABLE; // TABLE -     (    c05d3180) asmlinkage int (*original_write)(unsigned int, const char __user *, size_t); asmlinkage int new_write(unsigned int fd, const char __user *buf, size_t count) { //   write printk(KERN_ALERT "It works!\n"); return (*original_write)(fd, buf, count); } static int init(void) { printk(KERN_ALERT "Module init\n"); write_cr0 (read_cr0 () & (~ 0x10000)); //  WP  original_write = (void *)syscall_table[__NR_write];//     syscall_table[__NR_write] = new_write; //    write_cr0 (read_cr0 () | 0x10000); //  WP   return 0; } static void exit(void) { write_cr0 (read_cr0 () & (~ 0x10000)); //  WP  syscall_table[__NR_write] = original_write; //      write_cr0 (read_cr0 () | 0x10000); //  WP   printk(KERN_ALERT "Module exit\n"); return; } module_init(init); module_exit(exit);
      
      







64ビットハンドラー



私が気づいた最初の違いは、コマンドを入力したときでした



grep sys_call_table /boot/System.map-$(uname -r) |awk '{print $1}'







2つのアドレスが推定されました。



ffffffff81801300





ffffffff81805260







チームを少し変えて、私はそのような結果を得ました



grep sys_call_table /boot/System.map-$(uname -r)







ffffffff81801300 R sys_call_table





ffffffff81805260 R ia32_sys_call_table







すべてがすぐに明らかになりました。 64ビットアーキテクチャでは、32ビットアーキテクチャとの互換性のために、2つのシステムコールテーブルがあります。 ご覧のとおり、1つは64ビット呼び出し用で、2つ目は32ビット呼び出し用です。

32ビットアーキテクチャでは、__ NR_writeは4(わかりやすい、書き込みシステムコールは4)で、x64では1です。以前64ビットアセンブラーで作業したことがないため、何が起こっているのかすぐにはわかりませんでした。しかし、彼女は64ビットアーキテクチャのsys_writeが1番であることを発見しました。

実際、64ビット書き込み割り込みハンドラーと32ビット書き込み割り込みハンドラーの間に私が興味を持っているすべての違いは、私にとっては終わりました。

TKはアセンブラーの使用を伴うため、Cでカーネルモジュールを記述し、そのすべての機能をアセンブラーで記述します。



シェルスクリプト
 #!/bin/bash TABLE=$(grep ' sys_call_table' /boot/System.map-$(uname -r) |awk '{print $1}') echo $TABLE sed -is/TABLE/$TABLE/g module.c
      
      





カーネルモジュール
 #include <linux/init.h> #include <linux/module.h> unsigned long *syscall_table = (unsigned long *)0xTABLE; extern void change(unsigned long *temp); extern void unchange(unsigned long *temp); static int init(void) { printk(KERN_ALERT "\nModule init\n"); change(syscall_table); return 0; } static void cleanup(void) { unchange(syscall_table); printk(KERN_ALERT "Module exit\n"); } module_init(init); module_exit(cleanup);
      
      





補助モジュール
 global unlockWP global lockWP global change global unchange extern printk SECTION .text newwrite: mov rax, original ; original_write mov rax, QWORD[rax] ;  rdi - 4  fd call far rax ;  rsi - 8  buf ;  rdx - 8  count ;    push rax ;     xor rax, rax ;  rax mov rdi, work ;   "It works" call printk ;   printk pop rax ;   rax ,     ret change: call unlockWP ;   ; rdi -  add rdi, 8 ; rdi - syscall_table + __NR_write mov rax, QWORD [rdi] ; rax - syscall_table[__NR_write] mov rbx , original mov QWORD [rbx], rax ;     mov rax, newwrite ;       mov QWORD [rdi], rax ;    call lockWP ;   ret unchange: call unlockWP ;   ; rdi -  add rdi, 8 ; rdi - syscall_table + __NR_write mov rbx, original ;  rbx    mov rax, QWORD [rbx] ; rax - syscall_table[__NR_write] mov QWORD [rdi],rax ;      call lockWP ;   ret unlockWP: mov rax, cr0 and rax, 0xfffffffffffeffff mov cr0, rax ret lockWP: mov rax, cr0 xor rax, 0x0000000000001000 mov cr0, rax ret SECTION .data original: DQ 0,0 work: DB "It works!",10,0
      
      





メイクファイル
 obj-m += kmod.o kmod-objs := module.o main.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) module: nasm -f elf64 -o main.o main.asm make -C $(KDIR) SUBDIRS=$(PWD) modules make clean clean: rm -f *.o *mod.c *.symvers *.order
      
      







すべてがうまくいった場合、ソースディレクトリでkmod.koファイルを取得します。 これがカーネルモジュールです。 その動作を確認するには、メモリにロードする必要があります。 これは、 insmod _.



を使用して行われinsmod _.



モジュールをアンロードするには、コマンドrmmod _.



実行しrmmod _.



モジュールの動作を確認するには、 dmesg



実行して、カーネルメッセージバッファーを標準出力ストリームに出力します。



ご清聴ありがとうございました。



PS:
いくつかのスクリーンショット。
スクリプト実行



ヘルパーモジュールとカーネルモジュールのコンパイル



モジュールをメモリにロードする



カーネルメッセージバッファを標準出力に出力します



メモリからモジュールをアンロードする



カーネルメッセージバッファーを再出力する









さて、ソースを持つアーカイバ



All Articles