最近、そのようなタスクがありました:Linuxカーネルをアセンブルし、そのモジュールを作成し、その助けを借りてシステムコールをインターセプトします。 最初の2つを問題なく完了した場合、3番目を実行する過程で、10年前にシステムコールの操作が時代遅れになったという印象を受けました。
定期的に、私が探していたものに近い記事をインターネットで見つけました。一部は非常によく書かれていましたが、誰もが重大な欠点がありました-それらは時代遅れでした。
初期条件
- 4コアIntel Core i7プロセッサー
- 4 GB RAM + 4 GBスワップ
- VirtualBox 5.1.10仮想マシン上のUbuntu 16.10 x64
- Linuxカーネル4.9.0
- gcc 6.2.0
擬似グラフィックモードでカーネル構成を編集するには、ncursesが必要です。
sudo apt-get update sudo apt-get install libncurses5-dev
コアアセンブリのクリーニング
モジュールの開発を開始する前に、クリーンなカーネルを構築することをお勧めします。 これには2つの理由があります。
- 最初のカーネルビルドは、かなり長いプロセスです。 ほとんどの場合、20分から3時間続きます。 事前にビルドしておけば、ほとんどのカーネルバイナリを入手できます。再コンパイルする必要はありません。 これにより、「私の最初のHello Worldが開始されますか?」という質問への回答を待つことなく、モジュールの開発に完全に集中できます。
- きれいなコアを正常に組み立てたら、この段階で問題がなく、次のステップに進むことができることがわかります。 新しくコンパイルされたカーネルを使用したブートが失敗する場合があり、モジュールを使用してそれをアセンブルした場合、システムが正確に何を設定したかを理解することは困難です。
そのため、カーネルアセンブリ:
- ソースを含むアーカイブをダウンロードします。
wget https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-xxxtar.xz
ここで、xxxはカーネルのバージョンです。
または、 kernel.orgから手動でアーカイブをダウンロードできます
- アーカイブからデータを抽出します。
tar -xpJf linux-xxxtar.xz
- 新しく解凍したフォルダーに移動します。
cd linux-xxx
- デフォルトのカーネル構成を生成します。
make defconfig
上級者向け:make menuconfig
カーネル構成の疑似グラフィックインターフェイスが開きます。 ほとんどのオプションを理解することは難しくありませんが、各可変パラメーターを理解しなければ、すべてを壊すことは非常に簡単です。 最初のビルドでは、デフォルト設定を使用することをお勧めします。
- カーネルとモジュールのアセンブリを直接開始します。
make && make modules
アセンブリは20分から3時間続きます。
ライフハック:make -jx && make modules -jx
xはプロセッサコアの数+ 1です。つまり、私の場合はx = 5です。
この値はすべてのマニュアルで設定することをお勧めしますが、実際には、任意の値を設定できます。 「コアの数を2倍にする」、つまり-j 9パラメーターを使用してアセンブリを開始することにしました。これにより、アセンブリが2倍速くなることはありませんが、システム内の他のすべてのプロセスに対するアセンブリプロセスの競争力が高まります。
さらに、システムモニター(gnome-system-monitor)で、すべてのmakeプロセスの最大優先度を設定します。 システムは文字通りハングしましたが、アセンブリには6分かかりました。 この方法は自己責任で使用してください。
ビルドが成功したら、組み立てたものをすべてインストールする必要があります。 これにはルート権限が必要です。
- ヘッダーの設定:
sudo make headers_install
- モジュールのインストール:
sudo make modules_install
- カーネルを直接インストールする:
sudo make install
- インストールコマンドは、初期RAMディスクを生成し、grubを更新する必要があります。 突然初期RAMディスクが生成されなかった場合、新しいカーネルを備えたシステムは起動しません。
これは、ファイル「/boot/initrd.img-xxx」(xxx-カーネルバージョン)の存在によって確認できます。
ファイルが見つからなかった場合は、手動で生成します。
sudo update-initramfs –c –k xxx
- GRUBブートローダーの更新:
sudo update-grub
できた! 再起動後、システムは新しいカーネルで起動します。 現在のカーネルバージョンを確認します。
uname -r
突然何かがおかしくなり、システムが新しいカーネルで起動しない場合は、コンピューターを再起動し、grubメニューの詳細オプションメニューに移動して、異なるバージョンのカーネル(以前に起動したカーネル、通常は接尾辞-generalがデフォルトバージョンに追加されます) )
モジュール作成
多くの方法でカーネルモジュールを作成することは、カーネルに関連するいくつかの違いを除いて、Cでの通常のユーザープログラムの作成に似ています。
- カーネルは標準Cライブラリにアクセスできませんが、これは実行速度とコード量が理由です。 ただし、一部の機能はカーネルソースにあります。 例えば、通常の文字列関数はファイルlib / string.cに記述されています
- メモリ保護の欠如。 通常のプログラムがメモリに誤ってアクセスしようとすると、カーネルがプロセスをクラッシュさせる可能性があります。 カーネルがメモリに誤ってアクセスしようとすると、結果の制御が低下します。 さらに、カーネルはページ置換を使用しません。カーネルで使用される各バイトは物理メモリの1バイトです。
- カーネルでは浮動小数点計算を使用できません。 浮動小数点モードをアクティブにするには、他のルーチン操作の中でも特に、浮動小数点サポートデバイスのレジスタを保存および登録する必要があります。
- 固定スタック(およびかなり小さい)。 そのため、カーネルで再帰を使用することは推奨されません。
こんにちは世界!
カーネルモジュールとして「Hello world」の具体例を見てみましょう。 都合の良い任意のフォルダーにhello.cファイルを作成します。
// hello.c #include <linux/module.h> #include <linux/kernel.h> static int __init myinit(void) { printk("%s\n","<my_tag> hello world"); return 0; } static void __exit myexit(void) {} module_init(myinit); module_exit(myexit); MODULE_LICENSE("GPL");
通常のユーザープログラムは、main()関数の呼び出しで開始され、システムに値を返すまで機能します。
実際、モジュールはコアコード自体の一部であり、一部のイベントのハンドラー関数が含まれています。 このようなイベントの最も単純な例は、モジュール自体のロードまたはアンロードです。
これらのイベントを処理する関数はそれぞれ
static int __init myinit(void)
静的void __exit myexit(void)
それらは__init、__ exitマクロでマークされ、module_initおよびmodule_exitでイベントハンドラーとして登録されます。 これらの関数の名前は何でも構いませんが、カーネル内の他の関数と競合しないようにしてください。
カーネルは標準Cライブラリを使用しないため、stdio.hは使用できません。 代わりに、printk関数を実装するkernel.hファイルを含めます。 この関数はprintfに似ていますが、端末ウィンドウではなくシステムログ(/ var / log / syslog)にメッセージを表示する点が異なります。
システム全体からの多くのメッセージがこのログに書き込まれるため、後でgrepユーティリティを使用してモジュールからのメッセージのみを選択できるように、元のtagでマークする必要があります。
別のわかりにくい行はMODULE_LICENSE( "GPL")です。
彼女は、私たちのモジュールがGPLに準拠していることを示しています。 これがないと、カーネル内の一部の機能が利用できなくなります。
組立
モジュールのソースコードがある同じフォルダーにこのモジュールをアセンブルするには、Makefileを作成します。
# KERNEL_PATH = /path-to-your-kernel/linux-xxx # , obj-m += hello.o all: # make -C , # KERNEL_PATH. , # # SUBDIRS - , , # - make -C $(KERNEL_PATH) SUBDIRS=$(PWD) modules # , make clean # , clean: rm -f *.o *.mod* Module.symvers modules.order
Makefileを作成したら、アセンブリに直接移動します。
make
数秒後、既製のコンパイル済みモジュールであるhello.koファイルがフォルダーに表示されます。
ロードとアンロード
モジュールをカーネルにロードするには、2つの方法があります。
- モジュールとカーネルのアセンブリ。 この場合、モジュールはシステム起動の一部としてロードされ、モジュール自体がカーネルコードの一部になります。
- すでに実行中のシステムでの動的ロード。 上記のモジュール作成方法には、まさにそのようなロード方法が含まれます。 この場合、モジュールのロードは、通常のユーザープログラムの起動に似ています。
ダウンロードモジュール:
sudo insmod hello.ko
insmodコマンドはモジュールをカーネル空間にロードし、初期化関数を呼び出します。
その後、モジュールはダウンロードされたもののリストに分類されます。 これは、 lsmodコマンドで確認できます。
初期化関数では、システムログにメッセージを出力するprintkの呼び出しを追加しました。
システムログを表示するには、 dmesgユーティリティが存在します。
dmesg | grep '<my_tag>'
上記のコマンドは出力します
<my_tag> hello world
モジュールをロードした後、モジュールはアンロードされるまでカーネル内でハングしたままになります。 これを行うには:
sudo rmmod hello.ko
このコマンドは__exitイベントハンドラーを呼び出しますが、空の関数があるため、カーネルからモジュールをアンロードする以外は何も起こりません。
ライフハック
デバッグ中にモジュールをロードおよびアンロードするたびに2つのコマンドを入力しないように、初期化関数で値-1が返されます。 そのようなモジュールをロードしようとすると、端末にエラーが表示され、その後動作を停止しますが、同時に初期化機能は完全かつ正確に実行され、本質的にユーザープログラムのメイン()機能のアナログに変わります。
static int __init myinit(void) { printk("%s\n","<my_tag> hello world"); return -1; }
次に、モジュールをロードする最初の方法と、この記事が最初に書かれた目的を検討します。
システムコールの傍受
安全でない方法
昔々、バージョン2.6のカーネルの前でさえ、システムコールをインターセプトするために、彼らはそれを置き換えるフック関数を書きました。
関数のような各システムコールには独自のアドレスがあり、Linuxにはこれらのアドレスが格納される特別なテーブルがあるため、タスクはシステムコールアドレスをこのテーブルの関数のアドレスに置き換えることでした。
後に、Linux開発者はそのような方法の可能性を排除しようとしましたが、この方法を実装できるようにするハッキングが依然として存在します。
ただし、これは非常に安全ではないため、説明しません。 さらに、問題を解決するために、彼らはよりエレガントで安全なソリューションを思いつきました。
LSM
LSMは、カーネルセキュリティモジュールを開発するためのフレームワークです。 標準のDACセキュリティモデルを拡張し、より柔軟にするために作成されました。 このフレームワークは、有名なSELinuxセキュリティモジュールと、カーネルに組み込まれた他のいくつかのセキュリティモジュールを使用します。
このフレームワークで私たちにとって最も価値のあることは、カーネルに事前にインストールされたフックのセットを介して実装されることです(実際、カーネルはそのようなフック用に事前に設計されているため、上記の方法は安全です)。
LSMでは、フックのコードにカスタム呼び出しを挿入できます。これにより、文字テーブルを変更せずにシステム呼び出しを安全に処理できます。
すべてが非常に簡単です。 mk_dirシステムコールをインターセプトするfoobarセキュリティモジュールを作成する例を考えてみましょう。
コード記述
- カーネルソースにセキュリティフォルダーを見つけ、その中にモジュール用のフォルダーを作成し、そのソースコードfoobar.cを作成します。
// /security/foobar/foobar.c //---INCLUDES #include <linux/module.h> #include <linux/lsm_hooks.h> //---HOOKS //mkdir hook static int foobar_inode_mkdir(struct inode *dir, struct dentry *dentry, umode_t mask) { printk("%s\n","<my_tag> mkdir hook"); return 0; } //---HOOKS REGISTERING static struct security_hook_list foobar_hooks[] = { LSM_HOOK_INIT(inode_mkdir, foobar_inode_mkdir), }; //---INIT void __init foobar_add_hooks(void) { security_add_hooks(foobar_hooks, ARRAY_SIZE(foobar_hooks)); }
lsm_hooks.hファイルにはこれらの事前定義されたフックのヘッダーが含まれ、LSM_HOOK_INITは対応するfoobar_inode_mkdir()をinode_mkdir()フックに記録し、security_add_hooks()は関数をLSMカスタムフックの一般リストに追加します。
したがって、 mkdirを呼び出すたびに 、関数foobar_inode_mkdir()が呼び出されます。
- 関数のヘッダーをファイル「/include/linux/lsm_hooks.h」に追加します。
#ifdef CONFIG_SECURITY_FOOBAR extern void __init foobar_add_hooks(void); #else static inline void __init foobar_add_hooks(void) { } #endif
すべての呼び出しはsecurity.cソースファイル(以降)で行われます。このステップでは、関数の存在を彼に通知します。
- ファイル「/security/security.c」で、「int __init security_init(void)」関数を見つけ、その本体に次の呼び出しを追加します。
foobar_add_hooks();
すべて、コード内の依存関係が正しく構成されています。 カーネル構成ファイルに、モジュールと一緒にアセンブルすることを通知するだけです。
アセンブリ構成
- モジュールのあるフォルダー(/ security / foobar /)で、Kconfigファイルを作成します。
config SECURITY_FOOBAR bool "FooBar security module" default y help Any help text here
これにより、モジュールでメニュー項目が作成されます。
- ファイル/ security / Kconfigを開き、「menu」セキュリティオプション」の行の直後に次のテキストを追加します。
source security/foobar/Kconfig
これにより、メニュー項目がグローバルカーネル設定メニューに追加されます。
- モジュールを含むフォルダーにMakefileを作成します。
obj-$(CONFIG_SECURITY_FOOBAR) += foobar.o
- セキュリティセクション全体(/ security / Makefile)のMakefileを開き、次の行を追加します(他のモジュールの同じ行と同様)。
subdir-$(CONFIG_SECURITY_FOOBAR) += foobar obj-$(CONFIG_SECURITY_FOOBAR) += foobar/
- 疑似グラフィックモードで構成を実行します。
make menuconfig
「セキュリティオプション」サブメニューに移動すると、最初の項目に「y」記号が付いたモジュールが表示されます(Kconfigファイルの作成時にこのデフォルト値を設定します)。つまり、モジュールをカーネルコードに直接統合します。
組立
この段階では、記事の冒頭で説明したように、最も一般的なカーネルアセンブリを実行します。 ただし、クリーンコアは既に事前に組み立てられているため、プロセスは少し簡略化されました。
make && make modules
makeは、数秒でモジュールを使用してカーネルを再構築するため、-jオプションを必要としません。
sudo make install
ヘッダーとモジュールのインストールは不要です。これは以前に行われました。
それだけです!
システムをリブートするために残り、その後、mkdirインターセプトを使用したモジュールがカーネルでハングします。 先ほど言ったように、次の方法で確認します。
dmesg | grep '<my_tag>'
目から隠れているシステムには多くのプロセスがあるので、多くの傍受があるのを見て驚かないでください。
このガイドが誰かに役立つことを願っています(カーネルを掘り始める前に誰かが私に代わって書いてくれたなら、2〜3週間の命を救うでしょう)。
どんな批判も歓迎します。
ご清聴ありがとうございました。