LSMを使用したLinuxシステムコールのインターセプト





最近、そのようなタスクがありました:Linuxカーネルをアセンブルし、そのモジュールを作成し、その助けを借りてシステムコールをインターセプトします。 最初の2つを問題なく完了した場合、3番目を実行する過程で、10年前にシステムコールの操作が時代遅れになったという印象を受けました。



定期的に、私が探していたものに近い記事をインターネットで見つけました。一部は非常によく書かれていましたが、誰もが重大な欠点がありました-それらは時代遅れでした。



初期条件





擬似グラフィックモードでカーネル構成を編集するには、ncursesが必要です。



sudo apt-get update sudo apt-get install libncurses5-dev
      
      





コアアセンブリのクリーニング



モジュールの開発を開始する前に、クリーンなカーネルを構築することをお勧めします。 これには2つの理由があります。



  1. 最初のカーネルビルドは、かなり長いプロセスです。 ほとんどの場合、20分から3時間続きます。 事前にビルドしておけば、ほとんどのカーネルバイナリを入手できます。再コンパイルする必要はありません。 これにより、「私の最初のHello Worldが開始されますか?」という質問への回答を待つことなく、モジュールの開発に完全に集中できます。



  2. きれいなコアを正常に組み立てたら、この段階で問題がなく、次のステップに進むことができることがわかります。 新しくコンパイルされたカーネルを使用したブートが失敗する場合があり、モジュールを使用してそれをアセンブルした場合、システムが正確に何を設定したかを理解することは困難です。


そのため、カーネルアセンブリ:



  1. ソースを含むアーカイブをダウンロードします。



     wget https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-xxxtar.xz
          
          





    ここで、xxxはカーネルのバージョンです。



    または、 kernel.orgから手動でアーカイブをダウンロードできます



  2. アーカイブからデータを抽出します。



     tar -xpJf linux-xxxtar.xz
          
          





  3. 新しく解凍したフォルダーに移動します。



     cd linux-xxx
          
          





  4. デフォルトのカーネル構成を生成します。



     make defconfig
          
          





    上級者向け:
     make menuconfig
          
          





    カーネル構成の疑似グラフィックインターフェイスが開きます。 ほとんどのオプションを理解することは難しくありませんが、各可変パラメーターを理解しなければ、すべてを壊すことは非常に簡単です。 最初のビルドでは、デフォルト設定を使用することをお勧めします。



  5. カーネルとモジュールのアセンブリを直接開始します。



     make && make modules
          
          





    アセンブリは20分から3時間続きます。



    ライフハック:
     make -jx && make modules -jx
          
          





    xはプロセッサコアの数+ 1です。つまり、私の場合はx = 5です。

    この値はすべてのマニュアルで設定することをお勧めしますが、実際には、任意の値を設定できます。 「コアの数を2倍にする」、つまり-j 9パラメーターを使用してアセンブリを開始することにしました。これにより、アセンブリが2倍速くなることはありませんが、システム内の他のすべてのプロセスに対するアセンブリプロセスの競争力が高まります。



    さらに、システムモニター(gnome-system-monitor)で、すべてのmakeプロセスの最大優先度を設定します。 システムは文字通りハングしましたが、アセンブリには6分かかりました。 この方法は自己責任で使用してください。



    ビルドが成功したら、組み立てたものをすべてインストールする必要があります。 これにはルート権限が必要です。



  6. ヘッダーの設定:



     sudo make headers_install
          
          





  7. モジュールのインストール:



     sudo make modules_install
          
          





  8. カーネルを直接インストールする:



     sudo make install
          
          





  9. インストールコマンドは、初期RAMディスクを生成し、grubを更新する必要があります。 突然初期RAMディスクが生成されなかった場合、新しいカーネルを備えたシステムは起動しません。



    これは、ファイル「/boot/initrd.img-xxx」(xxx-カーネルバージョン)の存在によって確認できます。

    ファイルが見つからなかった場合は、手動で生成します。



     sudo update-initramfs –c –k xxx
          
          





  10. GRUBブートローダーの更新:



     sudo update-grub
          
          





できた! 再起動後、システムは新しいカーネルで起動します。 現在のカーネルバージョンを確認します。



 uname -r
      
      





突然何かがおかしくなり、システムが新しいカーネルで起動しない場合は、コンピューターを再起動し、grubメニューの詳細オプションメニューに移動して、異なるバージョンのカーネル(以前に起動したカーネル、通常は接尾辞-generalがデフォルトバージョンに追加されます) )



モジュール作成



多くの方法でカーネルモジュールを作成することは、カーネルに関連するいくつかの違いを除いて、Cでの通常のユーザープログラムの作成に似ています。





こんにちは世界!



カーネルモジュールとして「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つの方法があります。



  1. モジュールとカーネルのアセンブリ。 この場合、モジュールはシステム起動の一部としてロードされ、モジュール自体がカーネルコードの一部になります。



  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システムコールをインターセプトするfoob​​arセキュリティモジュールを作成する例を考えてみましょう。



コード記述



  1. カーネルソースにセキュリティフォルダーを見つけ、その中にモジュール用のフォルダーを作成し、そのソースコード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を呼び出すたび 、関数foob​​ar_inode_mkdir()が呼び出されます。



  2. 関数ヘッダーをファイル「/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ソースファイル(以降)で行われます。このステップでは、関数の存在を彼に通知します。



  3. ファイル「/security/security.c」で、「int __init security_init(void)」関数を見つけ、その本体に次の呼び出しを追加します。



     foobar_add_hooks();
          
          





すべて、コード内の依存関係が正しく構成されています。 カーネル構成ファイルに、モジュールと一緒にアセンブルすることを通知するだけです。



アセンブリ構成



  1. モジュールのあるフォルダー(/ security / foobar /)で、Kconfigファイルを作成します。



     config SECURITY_FOOBAR bool "FooBar security module" default y help Any help text here
          
          





    これにより、モジュールでメニュー項目が作成されます。



  2. ファイル/ security / Kconfigを開き、「menu」セキュリティオプション」の行の直​​後に次のテキストを追加します。



     source security/foobar/Kconfig
          
          





    これにより、メニュー項目がグローバルカーネル設定メニューに追加されます。



  3. モジュールを含むフォルダーにMakefileを作成します。



     obj-$(CONFIG_SECURITY_FOOBAR) += foobar.o
          
          





  4. セキュリティセクション全体(/ security / Makefile)のMakefileを開き、次の行を追加します(他のモジュールの同じ行と同様)。



     subdir-$(CONFIG_SECURITY_FOOBAR) += foobar obj-$(CONFIG_SECURITY_FOOBAR) += foobar/
          
          





  5. 疑似グラフィックモードで構成を実行します。



     make menuconfig
          
          





    「セキュリティオプション」サブメニューに移動すると、最初の項目に「y」記号が付いたモジュールが表示されます(Kconfigファイルの作成時にこのデフォルト値を設定します)。つまり、モジュールをカーネルコードに直接統合します。


組立



この段階では、記事の冒頭で説明したように、最も一般的なカーネルアセンブリを実行します。 ただし、クリーンコアは既に事前に組み立てられているため、プロセスは少し簡略化されました。



 make && make modules
      
      





makeは、数秒でモジュールを使用してカーネルを再構築するため、-jオプションを必要としません。



 sudo make install
      
      





ヘッダーとモジュールのインストールは不要です。これは以前に行われました。



それだけです!



システムをリブートするために残り、その後、mkdirインターセプトを使用したモジュールがカーネルでハングします。 先ほど言ったように、次の方法で確認します。



 dmesg | grep '<my_tag>'
      
      





目から隠れているシステムには多くのプロセスがあるので、多くの傍受があるのを見て驚かないでください。



このガイドが誰かに役立つことを願っています(カーネルを掘り始める前に誰かが私に代わって書いてくれたなら、2〜3週間の命を救うでしょう)。



どんな批判も歓迎します。

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



All Articles