はじめに
これは、Linux ルートキットに関する 2 部構成のシリーズの第 1 部です。この最初の記事では、ルートキットの仕組みの理論、つまりルートキットの分類、進化、カーネルを破壊するために使用するフック手法に焦点を当てます。パート 2 では、防御側に移り、検出エンジニアリングについて詳しく取り上げ、実稼働環境でこれらの脅威を識別して対応するための実践的なアプローチについて説明します。
ルートキットとは何ですか?
ルートキットは、ファイル、プロセス、ネットワーク接続、カーネル モジュール、アカウントなどの悪意のあるアクティビティを隠すように設計されたステルス性の高いマルウェアです。その主な目的は永続性と回避であり、攻撃者がサーバー、インフラストラクチャ、エンタープライズ システムなどの価値の高いターゲットへの長期的なアクセスを維持できるようにします。他の形式のマルウェアとは異なり、ルートキットはすぐに目的を達成するのではなく、検出されないままでいることに重点を置いています。
ルートキットはどのように動作するのでしょうか?
ルートキットはオペレーティング システムを操作して、ユーザーやセキュリティ ツールに情報を表示する方法を変更します。これらはユーザー空間またはカーネル内で動作します。ユーザー空間ルートキットは、 LD_PRELOADやライブラリハイジャックなどの手法を使用して、ユーザーレベルのプロセスを変更します。カーネル空間ルートキットは最高の権限で実行され、カーネル構造を変更したり、システムコールを傍受したり、悪意のあるモジュールをロードしたりします。この緊密な統合により、強力な回避能力が得られますが、運用上のリスクも増大します。
ルートキットの検出が難しいのはなぜですか?
カーネル空間ルートキットはコア OS 機能を操作し、セキュリティ ツールを破壊し、ユーザーランドの可視性からアーティファクトを隠すことができます。多くの場合、マルウェアはシステム内に最小限の痕跡しか残さず、新しいプロセスやファイルなどの明らかな兆候を回避し、従来の検出を困難にします。ルートキットを識別するには、多くの場合、メモリフォレンジック、カーネル整合性チェック、または OS レベル以下のテレメトリが必要です。
ルートキットが攻撃者にとって諸刃の剣である理由
ルートキットはステルス性と制御性を提供しますが、運用上のリスクを伴います。カーネル ルートキットは、カーネルのバージョンと環境に合わせて正確に調整する必要があります。メモリの取り扱いを誤ったり、システムコールを誤ってフックしたりするなどのミスにより、システムクラッシュ (カーネルパニック) が発生し、攻撃者が直ちに危険にさらされる可能性があります。少なくとも、これらの障害はシステムに不要な注目を集めます。これは、攻撃者が足場を維持するために積極的に回避しようとしているシナリオです。
カーネルの更新にも課題があります。API、メモリ構造、またはシステムコールの変更により、ルートキットの機能が破壊され、永続性が脆弱になる可能性があります。ルートキットは標的を絞った高度なスキルによる攻撃を強く示唆するため、疑わしいモジュールやフックが検出されると、通常は詳細なフォレンジック調査が開始されます。攻撃者にとって、ルートキットはリスクが高く、利益も大きいツールです。防御者にとって、この脆弱性は低レベルの監視による検出の機会を提供します。
WindowsとLinuxのルートキット
Windows ルートキット エコシステム
ルートキット開発の主な焦点は Windows です。攻撃者は、カーネルフック、ドライバー、文書化されていないシステムコールを悪用して、マルウェアを隠したり、資格情報を盗んだり、永続化したりします。成熟した研究コミュニティと企業環境での広範な使用により、DKOM、PatchGuard バイパス、ブートキットなどの技術を含む継続的なイノベーションが推進されています。
強力なセキュリティ ツールと Microsoft の強化努力により、攻撃者はますます高度な手法を用いるようになります。Windows は、企業のエンドポイントと消費者向けデバイスにおける優位性により、依然として魅力的です。
Linuxルートキットエコシステム
Linux ルートキットは歴史的にはあまり注目されてきませんでした。ディストリビューションやカーネル バージョン間の断片化により、検出と開発が複雑になります。学術的な研究は存在するものの、多くのツールは時代遅れであり、実稼働の Linux 環境では専門的な監視が不足していることがよくあります。
しかし、クラウド、コンテナ、IoT、ハイパフォーマンスコンピューティングにおける Linux の役割により、Linux はますます標的になりつつあります。実際の Linux ルートキットは、クラウド プロバイダー、通信会社、政府機関に対する攻撃で確認されています。攻撃者にとっての主な課題は次のとおりです。
- 多様なカーネルはディストリビューション間の互換性を妨げます。
- 稼働時間が長くなると、カーネルの不一致も長引きます。
- SELinux、AppArmor、モジュール署名などのセキュリティ機能により難易度が増します。
Linux 特有の脅威には次のようなものがあります:
- コンテナーと Kubernetes : コンテナー エスケープによる新しい永続性ベクトル。
- IoT デバイス: 監視が最小限に抑えられた古いカーネル。
- 実稼働サーバー: ユーザーの操作が欠け、可視性が低下するヘッドレス システム。
Linux が現代のインフラストラクチャを支配しているため、ルートキットは監視が不十分でありながら、拡大しつつある脅威となっています。Linux 固有の技術の検出、ツール、および調査の改善がますます急務となっています。
Linuxルートキット実装モデルの進化
過去 20 年間で、 Linux ルートキットは基本的なユーザーランド技術から、 eBPFやio_uringなどの最新のカーネル インターフェースを活用した高度なカーネル常駐インプラントへと進化してきました。この進化の各段階は、攻撃者の革新と防御者の対応の両方を反映し、ルートキットの設計をより高いステルス性、柔軟性、および回復力へと推し進めています。
このセクションでは、主要な特徴、歴史的背景、実際の例など、その進歩について概説します。
2000年代初頭: 共有オブジェクト (SO) ユーザーランド ルートキット
最も初期の Linux ルートキットは、カーネルの変更を必要とせず、完全にユーザー空間で動作し、 LD_PRELOADなどの手法やシェル プロファイルの操作を利用して、悪意のある共有オブジェクトを正当なバイナリに挿入していました。これらのルートキットは、 opendir 、 readdir 、 fopenなどの標準libc関数を傍受することで、 ps 、 ls 、 netstatなどの診断ツールの出力を操作する可能性があります。このアプローチにより導入が容易になりましたが、ユーザーランドフックに依存していたため、カーネルレベルのインプラントに比べてステルス性と範囲が制限されていました。つまり、単純な再起動や構成のリセットで簡単に中断されてしまいました。著名な例としては、 libc関数をフックしてファイルと接続を非表示にするJynx ルートキット (2009)や、共有オブジェクト インジェクションとオプションのカーネル モード機能を組み合わせたAzazel (2013)などがあります。このダイナミック リンカーの悪用に関する基本的な手法は、2003 年のPhrack Magazine #61で詳しく説明されました。
2000年代半ば~2010年代: ローダブルカーネルモジュール(LKM)ルートキット
防御側がユーザーランドの操作を見抜く能力を身につけるにつれ、攻撃者はロード可能カーネルモジュール (LKM) を介してカーネル空間に移行しました。LKM は正当な拡張機能ですが、悪意のある攻撃者はそれを利用して完全な権限で操作し、 sys_call_tableをフックしたり、 ftraceを操作したり、内部リンク リストを変更してプロセス、ファイル、ソケット、さらにはルートキット自体を隠したりします。LKM は高度な制御と強力な隠蔽機能を提供しますが、強化された環境では厳しい監視に直面します。これらは、汚染されたカーネル状態、 /proc/modulesリスト、または特殊な LKM スキャナーを介して検出可能であり、セキュア ブート、モジュール署名、Linux セキュリティ モジュール (LSM) などの最新の防御によってますます妨害されています。この時代の典型的な例としては、システムコールをフックして自身を隠蔽できる LKM であるAdore-ng (2004+) 、多くのディストリビューションで機能し続けている人気のフッカーであるDiamorphine (2016) 、バックドア機能を備えた最新の亜種であるReptile (2020)などがあります。
2010年代後半: eBPFベースのルートキット
攻撃者は、LKM ベースの脅威の検出の増加を回避するために、安全なパケット フィルタリングとカーネル トレースのために元々構築されたサブシステムである eBPF を悪用し始めました。Linux 4.8 以降、eBPF は、syscall フック、kprobe、トレースポイント、または Linux セキュリティ モジュール イベントにコードをアタッチできる、プログラム可能なカーネル内仮想マシンへと進化しました。これらのインプラントはカーネル空間で実行されますが、従来のモジュールの読み込みを回避するため、 rkhunterやchkrootkitなどの標準の LKM スキャナーやセキュア ブートの制限を回避できます。これらは/proc/modulesには表示されず、一般的なモジュール監査メカニズムでは基本的に表示されないため、展開するにはCAP_BPFまたはCAP_SYS_ADMIN (またはまれに特権のない BPF アクセス) が必要です。この時代は、eBPF プログラムを挿入してexecveなどのシステムコールをフックする概念実証であるTriple Cross (2022)や、完全に eBPF を介して秘密の C2 チャネルを実装するBoopkit (2022)などのツール、およびこのトピックを探る多数の Defcon プレゼンテーションによって定義されます。
2025年以降:io_uringベースのルートキット(出現)
最新の進化は、Linux 5.1 (2019) で導入された高性能な非同期 I/O インターフェースであるio_uringを活用しており、これによりプロセスは共有メモリ リングを介してシステム操作をバッチ処理できるようになります。パフォーマンス向上のためにシステム コールのオーバーヘッドを削減するように設計されていますが、レッド チームは、 io_uring悪用して、システム コール ベースの EDR を回避するステルス性の高いユーザーランド エージェントやカーネル コンテキスト ルートキットを作成できることを実証しました。これらのルートキットは、 io_uring_enterを使用してファイル、ネットワーク、およびプロセス操作をバッチ処理することで、観測可能なシステムコール イベントをはるかに少なく生成し、従来の検出メカニズムを妨害し、LKM および eBPF に課せられた制限を回避します。まだ実験的ではありますが、 io_uringを使用してread 、 write 、 connect 、 unlinkなどの一般的なシステム コールを密かに置き換えるRingReaper (2025)などの例や、 ARMO による研究では、これがカスタム インストルメンテーションなしでは追跡が困難な将来のルートキット開発にとって非常に有望なベクトルであることが強調されています。
Linux ルートキットの設計は、防御力の強化に応じて継続的に適応してきました。LKM の読み込みが困難になり、システムコールの監査がより高度になるにつれて、攻撃者は eBPF やio_uringなどの代替インターフェースに目を向けるようになりました。この進化により、戦いはもはや検出だけではなく、フック戦略や内部アーキテクチャから始めて、ルートキットがシステムの中核に溶け込むために使用するメカニズムを理解することになりました。
Rootkit Internals and Hooking Techniques
Linux ルートキットのアーキテクチャを理解することは、検出と防御に不可欠です。ほとんどのルートキットは、次の 2 つの主要コンポーネントを持つモジュール設計に従います。
- ローダー: ルートキットをインストールまたは挿入し、永続性を確立する可能性があります。厳密には必要ではありませんが、ルートキットを展開するマルウェア感染チェーンでは、別のローダー コンポーネントがよく見られます。
- ペイロード: ファイルの隠蔽、システムコールの傍受、秘密通信などの悪意のあるアクションを実行します。
ペイロードは、実行フローを変更し、ステルス性を実現するためにフック技術に大きく依存しています。
ルートキットローダーコンポーネント
ローダーは、ルートキットをメモリに転送し、その実行を初期化し、多くの場合、永続性を確立したり権限を昇格したりする役割を担うコンポーネントです。その役割は、最初のアクセス (エクスプロイト、フィッシング、または誤った構成など) と完全なルートキットの展開との間のギャップを埋めることです。
ルートキットのモデルに応じて、ローダーは完全にユーザー空間で動作したり、標準のシステム インターフェイスを介してカーネルと対話したり、オペレーティング システムの保護を完全にバイパスしたりする場合があります。大まかに言えば、ローダーは、マルウェアベースのドロッパーと、ユーザーランド ルートキット初期化子、カスタム カーネル空間ローダーの 3 つのクラスに分類できます。さらに、攻撃者はinsmodなどのユーザー空間ツールを使用してルートキットを手動でロードする可能性もあります。
マルウェアベースのドロッパー
マルウェア ドロッパーは軽量のプログラムであり、多くの場合、最初のアクセス後に展開され、その唯一の目的はルートキット ペイロードをダウンロードまたは解凍して実行することです。これらのドロッパーは通常、ユーザー空間で動作しますが、権限を昇格し、カーネルレベルの機能と対話します。
一般的な手法は次のとおりです。
- モジュール インジェクション: 悪意のある
.koファイルをディスクに書き込み、insmodまたはmodprobeを呼び出してカーネル モジュールとしてロードします。 - Syscall ラッパー:
init_module()またはfinit_module()ラッパーを使用して、Syscall を通じて LKM を直接ロードします。 - メモリ内インジェクション:
ptraceやmemfd_createなどのインターフェースを活用し、多くの場合ディスク アーティファクトを回避します。 - BPF ベースのロード:
bpftool、tc、または直接のbpf()システムコールなどのユーティリティを使用して、eBPF プログラムをロードし、カーネル トレースポイントまたは LSM フックにアタッチします。
ユーザーランドローダー
共有オブジェクト ルートキットの場合、ローダーはユーザー構成または環境設定の変更に限定される可能性があります。
- 動的リンカーの悪用:
LD_PRELOAD=/path/to/rootkit.soを設定すると、ターゲット バイナリの実行時に悪意のある共有オブジェクトが libc 関数をオーバーライドできるようになります。 - プロファイルの変更による永続性: プリロード構成を
.bashrc、.profile、または/etc/profileなどのグローバル ファイルに挿入すると、セッション間での実行が継続されます。
これらのローダーは実装が簡単ですが、防御が弱い環境や多段階の感染チェーンの一部として使用すると効果を発揮します。
カスタムカーネルローダー
高度なルートキットには、標準のモジュール読み込みパスを完全にバイパスするように設計されたカスタム カーネル ローダーが含まれている場合があります。これらのローダーは、低レベルのカーネル インターフェイスまたはメモリ デバイスと直接対話してルートキットをメモリに書き込み、多くの場合、カーネル監査ログやモジュール署名の検証を回避します。
たとえば、Reptile にはユーザー空間バイナリがローダーとして含まれており、 insmodまたはmodprobeを呼び出さずにルートキットをロードできます。ただし、モジュールをメモリにロードするには、依然としてinit_modシステムコールに依存します。
追加のローダー機能
マルウェア ローダーは、単純な初期化を超えた拡張された役割を担うことが多く、攻撃チェーンの多機能コンポーネントになります。これらの高度なローダーにとって重要なステップは、権限の昇格です。権限の昇格では、多くの場合、ローカルカーネルの脆弱性を悪用して、主要なペイロードをロードする前にルートアクセスを求めます。これは、「Dirty Pipe」脆弱性 (CVE-2022-0847) に代表される一般的な戦術です。権限が確保されると、ローダーはトラックをカバーする任務を負います。これには、 bash_history 、カーネル ログ、監査ログ、システムのメインsyslogなどの重要なファイルからエントリをクリアすることによって実行の証拠を消去するプロセスが含まれます。最後に、システムの再起動時に再実行を保証するために、ローダーは、 systemdユニット、 cronジョブ、 udevルール、初期化スクリプトの変更などのメカニズムをインストールすることで永続性を確保します。これらの多機能な動作により、特に複雑な多段階の感染では、単なる「ローダー」と本格的なマルウェアとの区別が曖昧になることがよくあります。
ペイロードコンポーネント
ペイロードは、ステルス、制御、永続性といったコア機能を提供します。攻撃者が使用する可能性のある主な方法はいくつかあります。ユーザー空間ペイロード (SO ルートキットとも呼ばれる) は、ダイナミック リンカーを介してreaddirやfopenなどの標準 C ライブラリ関数を乗っ取ることで動作します。これにより、 ls 、 netstat 、 psなどの一般的なシステムツールの出力を操作できるようになります。一般的に導入は容易ですが、運用範囲は限られています。
対照的に、カーネル空間のペイロードは完全なシステム権限で動作します。ファイルやプロセスを/procから直接隠したり、ネットワーク スタックを操作したり、カーネル構造を変更したりできます。より現代的なアプローチには、システムコール トレースポイントまたは Linux セキュリティ モジュール (LSM) フックに添付されたカーネル内バイトコードを活用する eBPF ベースのルートキットが含まれます。これらのキットは、ツリー外のモジュールを必要とせずにステルス性を提供するため、セキュア ブートやモジュール署名ポリシーを備えた環境で特に効果的です。bpftoolのようなツールは読み込みを簡素化するため、検出が複雑になります。最後に、 io_uringベースのペイロードは、 io_uring_enter (Linux 5.1 以降で使用可能) を介して非同期 I/O バッチ処理を活用し、従来のシステム コール監視をバイパスします。これにより、テレメトリの露出を最小限に抑えながら、ステルス性の高いファイル、ネットワーク、およびプロセス操作が可能になります。
Linuxルートキット - フック技術
この基本的な基盤を基にして、ほとんどのルートキット機能の中核であるフックについて説明します。本質的に、フックとは、関数またはシステム コールの実行を傍受して変更し、悪意のあるアクティビティを隠したり、新しい動作を挿入したりすることです。ルートキットは、通常のコードフローを迂回することで、ファイルやプロセスを隠したり、セキュリティイベントを除外したり、システムを密かに監視したりすることができ、多くの場合、明白な手がかりを残さずに済みます。フッキングはユーザーランドとカーネル空間の両方で実装できます。長年にわたり、攻撃者は従来の方法から最新の回避策まで、さまざまなフッキング手法を考案してきました。このパートでは、Linux ルートキットで使用される一般的なフック手法について詳しく説明し、各手法を例と実際のルートキット サンプル ( Reptile 、 Diamorphine 、 PUMAKIT 、最近ではFlipSwitchなど) とともに説明して、その仕組みとカーネルの進化がそれらの手法にどのような課題を与えているのかを理解します。
フックの概念
大まかに言えば、フックとは、関数またはシステム コールの呼び出しを傍受し、悪意のあるコードにリダイレクトする手法です。そうすることで、ルートキットは返されたデータや動作を変更して、その存在を隠したり、システム操作を改ざんしたりすることができます。たとえば、ルートキットはディレクトリ ( getdents ) 内のファイルを一覧表示するシステム コールをフックし、ルートキット自身のファイルに一致するファイル名をスキップさせて、それらのファイルをlsなどのユーザー コマンドに対して「見えない」ようにする可能性があります。
フックはカーネル内部に限定されず、ユーザー空間でも発生する可能性があります。初期の Linux ルートキットは、悪意のある共有オブジェクトをプロセスに挿入することで、完全にユーザーランドで動作していました。ダイナミック リンカーのLD_PRELOAD環境変数を使用するなどの手法により、ルートキットはユーザー プログラム内の標準 C ライブラリ関数 (例: getdents 、 readdir 、 fopen ) をオーバーライドできます。つまり、ユーザーがpsやnetstatなどのツールを実行すると、ルートキットの挿入されたコードがプロセスまたはネットワーク接続をリストするための呼び出しを傍受し、悪意のあるものを除外します。これらのユーザーランドフックはカーネル権限を必要とせず、実装も比較的簡単です。
注目すべき例としては、数十のlibc関数をフックしてプロセス、ファイル、ネットワーク ポートを隠し、バックドアを有効にするユーザー モード ルートキットであるJynxKit (2012)やAzazel (2014)などがあります。ただし、ユーザーランドフックには重大な制限があります。検出と削除が容易であり、カーネルレベルのフックのような詳細な制御ができません。その結果、最近の Linux ルートキットのほとんどは、複雑さとリスクが増すにもかかわらず、カーネル フックに移行しました。これは、カーネル フックによって、低レベルでオペレーティング システムとセキュリティ ツールを包括的に騙すことができるためです。
カーネルにおけるフックとは通常、カーネルが特定の操作 (ファイルを開く、システム コールを行うなど) を実行しようとするときに、正当なコードの代わりに (または正当なコードに加えて) ルートキットのコードを呼び出すようにカーネルのデータ構造またはコードを変更することを意味します。長年にわたり、Linux カーネルの開発者は不正な変更を防ぐために強力な保護を導入してきましたが、攻撃者はますます洗練されたフック手法で対応してきました。以下では、古い方法 (現在ではほとんど廃止されています) から始めて、現在のカーネル防御を回避しようとする最新の手法まで、カーネル空間での主要なフック手法について説明します。各サブセクションでは、この手法について説明し、簡略化されたコード例を示し、既知のルートキットでの使用法と、現在の Linux の安全対策を考慮した制限について説明します。
カーネルのフック技術
割り込み記述子テーブル(IDT)フック
Linux における最も初期のカーネル フック トリックの 1 つは、割り込み記述子テーブル (IDT) をターゲットにすることでした。32 ビット x86 Linux では、システム コールはソフトウェア割り込み ( int 0x80 ) を介して呼び出されていました。IDT は、割り込み番号をハンドラー アドレスにマッピングするテーブルです。0x80の IDT エントリを変更すると、カーネル自身のシステム コール ディスパッチャが制御を取得する前に、ルートキットがシステム コール エントリ ポイントをハイジャックする可能性があります。つまり、いずれかのプログラムがint 0x80を介してシステムコールをトリガーすると、CPU はまずルートキットのカスタム ハンドラーにジャンプし、ルートキットが最も低いレベルで呼び出しをフィルターまたはリダイレクトできるようになります。以下は、IDT フックの簡略化されたコード例です (説明目的)。
// Install the IDT hook
static int install_idt_hook(void) {
// Get pointer to IDT table
idt_table = get_idt_table();
// Save original syscall handler (int 0x80 = entry 128)
original_syscall_entry = idt_table[0x80];
// Calculate original handler address
original_syscall_handler = (void*)(
(original_syscall_entry.offset_high << 16) |
original_syscall_entry.offset_low
);
// Install our hook
idt_table[0x80].offset_low = (unsigned long)custom_int80_handler & 0xFFFF;
idt_table[0x80].offset_high =
((unsigned long)custom_int80_handler >> 16)
& 0xFFFF;
// Keep same selector and attributes as original
// idt_table[0x80].selector and type_attr remain unchanged
printk(KERN_INFO "IDT hook installed at 0x80\n");
return 0;
}
上記のコードは、割り込み0x80の新しいハンドラーを設定し、システムコール処理が発生する前に実行フローをルートキットのハンドラーにリダイレクトします。これにより、ルートキットは syscall テーブルのレベルより下のレベルで syscall の動作を傍受または変更できるようになります。IDT フッキングは、 SuckITなどの教育用および古いルートキットで使用されます。
IDT フッキングは現在ではほとんど歴史的な手法です。これは、 int 0x80メカニズム (Linux 2.6 より前の 32 ビット x86 カーネル) を使用する古い Linux システムでのみ動作しました。最新の 64 ビット Linux では、ソフトウェア割り込みの代わりにsysenter / syscall命令が使用されるため、 0x80の IDT エントリはシステム コールに使用されなくなりました。さらに、IDT フッキングはアーキテクチャに非常に特化しており (x86 のみ)、x86_64 やその他のアーキテクチャの最新のカーネルでは効果がありません。
システムコールテーブルのフック
Syscall テーブル フッキングは、 sys_call_tableと呼ばれるカーネルのシステム コール ディスパッチ テーブルを変更する古典的なルートキット手法です。このテーブルは関数ポインターの配列であり、各エントリは特定のシステムコール番号に対応します。このテーブル内のポインタを上書きすると、攻撃者はgetdents64 、 kill 、 readなどの正当なシステムコールを悪意のあるハンドラーにリダイレクトできます。以下に例を示します。
asmlinkage int (*original_getdents64)(
unsigned int,
struct linux_dirent64 __user *,
unsigned int);
asmlinkage int hacked_getdents64(
unsigned int fd,
struct linux_dirent64 __user *dirp,
unsigned int count)
{
int ret = original_getdents64(fd, dirp, count);
// Filter hidden entries from dirp
return ret;
}
write_cr0(read_cr0() & ~0x10000); // Disable write protection
sys_call_table[__NR_getdents64] = hacked_getdents64;
write_cr0(read_cr0() | 0x10000); // Re-enable write protection
この例では、テーブルを変更するには、カーネル モジュールがまずテーブルが存在するメモリ ページの書き込み保護を無効にする必要があります。次のアセンブリ コード (Diamorphine で表示) は、 write_cr0関数がモジュールにエクスポートされなくなった場合でも、 CR0制御レジスタの 20 番目のビット (書き込み保護) をクリアする方法を示しています。
static inline void
write_cr0_forced(unsigned long val)
{
unsigned long __force_order;
asm volatile(
"mov %0, %%cr0"
: "+r"(val), "+m"(__force_order));
}
書き込み保護が無効になると、テーブル内のシステムコールのアドレスが悪意のある関数のアドレスに置き換えられる可能性があります。変更後、書き込み保護が再度有効になります。この手法を使用したルートキットの代表的な例としては、Diamorphine、Knark、Reveng_rtkit などがあります。Syscall テーブル フックにはいくつかの制限があります。
- カーネルの強化(2.6.25以降)
sys_call_tableを非表示にします。 - カーネル メモリ ページは読み取り専用になりました (
CONFIG_STRICT_KERNEL_RWX)。 - セキュア ブートやカーネル ロックダウン メカニズムなどのセキュリティ機能により、CR0 への変更が妨げられる可能性があります。
最も決定的な緩和策は Linux カーネル 6.9 で実現され、x86-64 アーキテクチャ上でシステムコールがディスパッチされる方法が根本的に変更されました。バージョン 6.9 より前では、カーネルはsys_call_table配列内のハンドラーを直接検索してシステムコールを実行していました。
// Pre-v6.9 Syscall Dispatch
asmlinkage const sys_call_ptr_t sys_call_table[] = {
#include <asm/syscalls_64.h>
};
カーネル 6.9 以降では、適切なハンドラーを見つけて実行するために、switch ステートメントで syscall 番号が使用されます。sys_call_tableはまだ存在しますが、トレース ツールとの互換性のためにのみ設定されており、syscall 実行パスでは使用されなくなりました。
// Kernel v6.9+ Syscall Dispatch
long x64_sys_call(const struct pt_regs *regs, unsigned int nr)
{
switch (nr) {
#include <asm/syscalls_64.h>
default: return __x64_sys_ni_syscall(regs);
}
};
このアーキテクチャの変更の結果、カーネル 6.9 以降のsys_call_tableで関数ポインターを上書きしても syscall の実行には影響せず、この手法は完全に無効になります。これにより、システム コール テーブルのパッチ適用はもはや実行可能ではないと想定されましたが、最近、このベクトルが決して消滅していないことを示すFlipSwitchテクニックを公開しました。この方法は、特定のレジスタ操作ガジェットを活用してカーネルの書き込み保護メカニズムを一時的に無効にし、攻撃者が最新のシステムコール パスの「不変性」を回避し、強化された環境内でもフックを再導入できるようにします。
FlipSwitch は、データベースのsys_call_tableをターゲットにするのではなく、カーネルの新しい syscall ディスパッチャ関数x64_sys_callのコンパイルされたマシン コードに焦点を当てます。カーネルは現在、システム コールを実行するために大規模な switch-case ステートメントを使用しているため、各システム コールにはディスパッチャのバイナリ内にハードコードされたcall命令があります。FlipSwitch は、 x64_sys_call関数のメモリをスキャンして、対象のシステム コールの特定の「シグネチャ」を見つけます。これは通常、 0xe8オペコード ( CALL命令) と、それに続く元の正当なハンドラーを指す 4 バイトの相対オフセットです。
ディスパッチャ内でこの呼び出しサイトが識別されると、ルートキットはガジェットを使用して CR0 制御レジスタの書き込み保護 (WP) ビットをクリアし、カーネルの実行可能コード セグメントへの一時的な書き込みアクセスを許可します。元の相対オフセットは、悪意のある、敵対者が制御する関数を指す新しいオフセットで上書きされます。これにより、ディスパッチの時点で実質的に「スイッチが切り替わり」、カーネルが最新のスイッチ ステートメント パスを通じて対象の syscall を実行しようとするたびに、代わりにルートキットにリダイレクトされるようになります。これにより、6.9 カーネルのアーキテクチャ強化にもかかわらず、信頼性が高く正確な syscall インターセプションが継続されます。
インラインフック / 関数プロローグパッチ
インライン フックは、ポインター テーブル経由のフックの代替手段です。インライン フックは、テーブル内のポインターを変更する代わりに、ターゲット関数自体のコードにパッチを適用します。ルートキットはカーネル関数の開始部分 (プロローグ) にジャンプ命令を書き込み、ルートキット自身のコードに実行を誘導します。この手法は、関数のホットパッチや Windows のユーザー モード フックの動作 (たとえば、関数の最初のバイトを変更して迂回路にジャンプする) に似ています。
たとえば、ルートキットはdo_sys_openのようなカーネル関数 (オープンファイルのシステムコール処理の一部) をターゲットにする可能性があります。ルートキットは、 do_sys_openの最初の数バイトを悪意のあるコードへのx86 JMP命令で上書きすることで、 do_sys_openが呼び出されるたびに、代わりにルートキットのルーチンにジャンプするようになります。その後、悪意のあるルーチンは、任意の操作を実行できます (たとえば、開くファイル名が隠しファイルリストに含まれているかどうかを確認し、アクセスを拒否するなど)。また、オプションで元のdo_sys_openを呼び出して、隠しファイル以外のファイルに対して通常の動作を続行することもできます。
unsigned char *target = (unsigned char *)kallsyms_lookup_name("do_sys_open");
unsigned long hook = (unsigned long)&malicious_function;
int offset = (int)(hook - ((unsigned long)target + 5));
unsigned char jmp[5] = {0xE9};
memcpy(&jmp[1], &offset, 4);
// Memory protection omitted for brevity
memcpy(target, jmp, 5);
asmlinkage long malicious_function(
const char __user *filename,
int flags, umode_t mode) {
printk(KERN_INFO "do_sys_open hooked!\n");
return -EPERM;
}
このコードは、実行を悪意のあるコードにリダイレクトするJMP命令でdo_sys_open()の先頭を上書きします。オープンソースのルートキット Reptile は、KHOOK (後ほど説明します) と呼ばれるカスタム フレームワークを介してインライン関数パッチを広範に使用します。
Reptile のインライン フックは、 sys_killなどの関数をターゲットにして、バックドア コマンドを有効にします (たとえば、プロセスに特定のシグナルを送信すると、ルートキットがトリガーされて権限が昇格したり、プロセスが非表示になったりします)。もう 1 つの例は Suterusu です。これもフックの一部にインライン パッチを適用しました。
インライン フックは脆弱でリスクが高くなります。関数のプロローグを上書きすると、カーネル バージョンとコンパイラの違いに左右されます (そのため、フックではビルドごとのパッチやランタイム逆アセンブリが必要になることが多い)。また、命令や同時実行が正しく処理されないとシステムが簡単にクラッシュする可能性があり、最新のメモリ保護 ( W^X 、 CR0 WP 、モジュール署名/ロックダウン) をバイパスするか、脆弱性を悪用してカーネル テキストを書き込み可能にする必要があります。
仮想ファイルシステムフック
Linux の仮想ファイルシステム (VFS) レイヤーは、ファイル操作の抽象化を提供します。たとえば、ディレクトリ ( ls /procなど) を読み取る場合、カーネルは最終的にディレクトリ エントリを反復処理する関数を呼び出します。ファイル システムは、 iterate_shared (ディレクトリの内容を一覧表示する) やファイル I/O の読み取り/書き込みなどのアクション用の関数ポインターを使用して独自の file_operations を定義します。VFS フッキングでは、これらの関数ポインターをルートキットが提供する関数に置き換えて、ファイルシステムがデータを表示する方法を操作します。
本質的には、ルートキットは VFS にフックして、ファイルやディレクトリをディレクトリ リストから除外することで、それらを隠すことができます。一般的なトリック: ディレクトリ エントリを反復する関数をフックし、特定のパターンに一致するファイル名をスキップするようにします。ディレクトリのfile_operations構造 (特に/procまたは/sys内) は、悪意のあるプロセスを隠すには/proc/<pid>の下のエントリを隠す必要があることが多いため、頻繁にターゲットになります。
ディレクトリ一覧関数の次のフックの例を考えてみましょう。
static iterate_dir_t original_iterate;
static int malicious_filldir(
struct dir_context *ctx,
const char *name, int namelen,
loff_t offset, u64 ino,
unsigned int d_type)
{
if (!strcmp(name, "hidden_file"))
return 0; // Skip hidden_file
return ctx->actor(ctx, name, namelen, offset, ino, d_type);
}
static int malicious_iterate(struct file *file, struct dir_context *ctx)
{
struct dir_context new_ctx = *ctx;
new_ctx.actor = malicious_filldir;
return original_iterate(file, &new_ctx);
}
// Hook installation
file->f_op->iterate = malicious_iterate;
この置換関数は、ディレクトリ一覧操作中に隠しファイルを除外します。VFS レベルでフックすることにより、ルートキットはシステム コール テーブルや低レベルのアセンブリを改ざんする必要がなくなり、単にファイル システム インターフェイスを利用するだけになります。かつて人気を博した Linux ルートキットであるAdore-NG は、ファイルとプロセスを隠蔽するために VFS フックを採用していました。ディレクトリ反復の関数ポインタにパッチを適用して、特定の PID とファイル名のエントリを非表示にしました。他の多くのカーネル ルートキットにも、VFS フックを介して自分自身やそのアーティファクトを隠すための同様のコードがあります。
VFS フックは依然として広く使用されていますが、バージョン間でのカーネル構造オフセットの変更による制限があり、フックが壊れる可能性があります。
Ftraceベースのフッキング
最新の Linux カーネルには、ftrace (関数トレーサー) と呼ばれる強力なトレース フレームワークが含まれています。Ftrace はデバッグとパフォーマンス分析を目的としており、カーネル コードを直接変更せずに、ほぼすべてのカーネル関数のエントリまたは終了にフック (コールバック) を接続できます。これは、制御された方法で実行時にカーネル コードを動的に変更することによって機能します (多くの場合、トレース ハンドラーを呼び出す軽量トランポリンにパッチを適用することによって行われます)。重要なのは、特定の条件 (カーネルが ftrace サポート付きでビルドされ、debugfs インターフェイスが利用可能であるなど) が満たされている限り、ftrace はカーネル モジュールがトレース ハンドラーを登録するための API を提供するという点です。
ルートキットは、ftrace を悪用して、あまり目立たない方法でフックを実装し始めています。ルートキットは、関数にJMP手動で書き込む代わりに、カーネルの ftrace 機構に代わってそれを実行するように要求できます。つまり、フックを「正当化」することになります。つまり、ルートキットは関数のアドレスを見つけたり、ページ保護を変更したりする必要がなく、インターセプトしたい関数名のコールバックを登録するだけで、カーネルがフックをインストールします。
以下は、ftrace を使用してmkdirシステム コール ハンドラーをフックする簡略化された例です。
static int __init hook_init(void) {
target_addr = kallsyms_lookup_name(SYSCALL_NAME("sys_mkdir"));
if (!target_addr) return -ENOENT;
real_mkdir = (void *)target_addr;
ops.func = ftrace_thunk;
ops.flags = FTRACE_OPS_FL_SAVE_REGS
| FTRACE_OPS_FL_RECURSION_SAFE
| FTRACE_OPS_FL_IPMODIFY;
if (ftrace_set_filter_ip(&ops, target_addr, 0, 0)) return -EINVAL;
return register_ftrace_function(&ops);
}
このフックはsys_mkdir関数をインターセプトし、悪意のあるハンドラーを介して再ルーティングします。KoviD 、 Singularity 、 Umbraなどの最近のルートキットは、ftrace ベースのフックを利用しています。これらのルートキットは、さまざまなカーネル関数 (システムコールを含む) に ftrace コールバックを登録して、それらを監視または操作します。
ftrace フックの主な利点は、グローバル テーブルやパッチされたコードに明らかな痕跡を残さないことです。フッキングは正当なカーネル インターフェイスを介して行われます。訓練を受けていない目には、すべて正常に見えます。 sys_call_tableはそのままで、関数のプロローグはルートキットによって手動で上書きされていません (ftrace メカニズムによって上書きされますが、これはトレースが有効になっているカーネルでは一般的かつ許容される現象です)。また、ftrace フックはオンザフライで有効化/無効化できることが多く、手動でパッチを適用するよりも煩わしさが少なくなります。
ftrace フックは強力ですが、環境と権限の境界によって制約されます (カーネル外部から使用する場合)。トレース インターフェース (debugfs) へのアクセスとCAP_SYS_ADMIN権限が必要ですが、UID 0 でさえ名前空間、LSM、またはセキュア ブート ロックダウン ポリシーによって制限されている強化されたシステムまたはコンテナー化されたシステムでは、これらの権限が利用できない可能性があります。セキュリティ上の理由から、Debugfs は実稼働環境ではマウント解除されたり、読み取り専用になったりする場合もあります。したがって、完全な権限を持つルート ユーザーは通常 ftrace を使用できますが、最近の防御機能によりこれらの機能が無効化または制限されることが多く、高度に強化された環境では ftrace ベースのフックの実用性が低下します。
Kprobes フッキング
Kprobes はデバッグと計測を目的とした別のカーネル機能であり、攻撃者はこれをルートキットフックに再利用しています。Kprobes を使用すると、プローブ ハンドラーを登録することで、実行時にほぼすべてのカーネル ルーチンに動的に侵入できます。指定された命令が実行されるとき、kprobe インフラストラクチャは状態を保存し、制御をカスタム ハンドラーに渡します。ハンドラーが実行されると (レジスタや命令ポインターを変更することもできます)、カーネルは元のコードの通常の実行を再開します。簡単に言えば、kprobes を使用すると、ハンドラー付きのブレークポイントのように、カーネル コード内の任意のポイント (関数のエントリ、特定の命令など) にカスタム コールバックをアタッチできます。
悪意のあるフックに kprobes を使用する場合、通常は関数をインターセプトして、関数が何かを実行するのを阻止するか、何らかの情報を取得します。最近のルートキットでよく使用される方法: 多くの重要なシンボル ( sys_call_tableやkallsyms_lookup_nameなど) はエクスポートされなくなったため、ルートキットはそのシンボルにアクセスできる関数に kprobe を展開してシンボルを盗むことができます。kprobe の構造と登録を以下に示します。
// Declare a kprobe targeting the symbol "kallsyms_lookup_name"
static struct kprobe kp = {
.symbol_name = "kallsyms_lookup_name"
};
// Function pointer type matching kallsyms_lookup_name
typedef unsigned long
(*kallsyms_lookup_name_t)(const char *name);
// Global pointer to the resolved kallsyms_lookup_name
kallsyms_lookup_name_t kallsyms_lookup_name;
// Register the kprobe; kernel resolves kp.addr
// to the address of the symbol
register_kprobe(&kp);
// Assign resolved address to our function pointer
kallsyms_lookup_name =
(kallsyms_lookup_name_t) kp.addr;
// Unregister the kprobe (only needed it once)
unregister_kprobe(&kp);
このプローブは、通常は syscall テーブル フッキングの前身となるkallsyms_lookup_nameシンボル名を取得するために使用されます。最初のコミットには存在しませんでしたが、Diamorphine の最近のアップデートではこの手法が使用されました。kprobe を配置してkallsyms_lookup_name自体のポインタを取得します (または既知の関数で kprobe を使用して、必要なものを間接的に取得します)。同様に、他のルートキットは一時的な kprobe を使用してシンボルを検索し、完了したら登録を解除して、他の手段でフックを実行します。Kprobes は、アドレスを見つけるだけでなく、動作を直接フックするためにも使用できます。または、jprobe (特殊な kprobe) は関数全体をリダイレクトできます。ただし、kprobes を使用して機能を完全に置き換えるのは難しいため、一般的には行われません。これは、関数を一貫してハイジャックしたい場合は、パッチを適用するか ftrace を使用する方が簡単だからです。Kprobes は、断続的または補助的なフッキングによく使用されます。
Kprobe は便利ですが、制限があります。実行時のオーバーヘッドが追加され、非常にホットな、または制限された低レベル関数に配置するとシステムが不安定になる可能性がある (再帰プローブは抑制される) ため、攻撃者はプローブ ポイントを慎重に選択する必要があります。また、Kprobe は監査可能であり、カーネル警告をトリガーしたり、システム監査によってログに記録されたりする可能性があります。また、アクティブなプローブは/sys/kernel/debug/kprobes/listで表示できます (したがって、予期しないエントリは疑わしい)。一部のカーネルは、kprobe/debug サポートなしでビルドされる可能性があります。
カーネルフックフレームワーク
前述のように、Reptile ルートキットでは、攻撃者はフックを管理するために、より高レベルのフレームワークを作成することがあります。Kernel Hook (KHOOK) は、インライン パッチの面倒な作業を抽象化し、ルートキット開発者にクリーンなインターフェイスを提供するフレームワークの 1 つです (Reptile の作者によって開発されました)。本質的に、KHOOK はフックする関数と置換を指定できるライブラリであり、元の関数を安全に呼び出すためのトランポリンを提供しながらカーネル コードの変更を処理します。説明のために、KHOOK のようなマクロ (Reptile の使用法に基づく) を使用して kill システムコールをフックする方法の例を次に示します。
// Creates a replacement for sys_kill:
// long sys_kill(long pid, long sig)
KHOOK_EXT(long, sys_kill, long, long);
static long khook_sys_kill(long pid, long sig) {
// Signal 0 is used to check if a process
// exists (without sending a signal)
if (sig == 0) {
// If the target is invisible (hidden by
// a rootkit), pretend it doesn't exist
if (is_proc_invisible(pid)) {
return -ESRCH; // No such process
}
}
// Otherwise, forward the call to the original sys_kill syscall
return KHOOK_ORIGIN(sys_kill, pid, sig);
}
KHOOK はインライン関数パッチを介して動作し、攻撃者が制御するハンドラーへのジャンプで関数のプロローグを上書きします。上記の例は、kill シグナルが 0 の場合にsys_kill()悪意のあるハンドラーにリダイレクトされる方法を示しています。
KHOOK はインライン パッチを簡素化しますが、それでもすべての欠点を継承しています。つまり、カーネル テキストを変更してジャンプ スタブを挿入するため、カーネル ロックダウン、セキュア ブート、 W^Xなどの保護によってブロックされる可能性があります。また、これらはアーキテクチャとバージョンに依存しており (通常は x86 に限定され、カーネル 5.x 以降では失敗します)、ビルド間で脆弱になります。
ユーザー空間でのフックテクニック
ユーザー空間フックは、libc レイヤー、またはダイナミック リンカーを介してアクセスされるその他の共有ライブラリを対象として、ユーザー ツールで使用される一般的な API 呼び出しをインターセプトする手法です。これらの呼び出しの例としてはreaddir 、 getdents 、 open 、 fopen 、 fgets 、 connectなどがあります。置換関数を挿入することで、攻撃者はps 、 ls 、 lsof 、 netstatなどの通常のユーザーランドツールを操作して、変更されたビューまたは「サニタイズされた」ビューを返すことができます。これは、プロセス、ファイル、ソケットを隠したり、悪意のあるコードの証拠を隠したりするために使用されます。
これを実装するための一般的な方法は、動的リンカーがシンボルを解決する方法やプロセス メモリの変更を伴う方法を反映しています。これらの方法には、 LD_PRELOAD環境変数またはLD_AUDITを使用して悪意のある共有オブジェクト (.so) ファイルの早期ロードを強制する、ELF DT_* エントリまたはライブラリ検索パスを変更して敵対的なライブラリを優先する、プロセス内でランタイム GOT/PLT 上書きを実行するなどが含まれます。GOT/PLT を上書きするには、通常、メモリ保護設定 ( mprotect ) の変更、新しいコードの書き込み ( write )、そして挿入後の元の設定の復元 ( restore ) が必要です。
フックされた関数は通常、通常の操作のためにdlsym(RTLD_NEXT, ...)を使用して実際の libc シンボルを呼び出します。次に、隠したいターゲットに対してのみ結果をフィルタリングまたは変更します。readdir()関数のLD_PRELOADフィルターの基本的な例を以下に示します。
#define _GNU_SOURCE // GNU extensions (RTLD_NEXT)
#include <dlfcn.h> // dlsym(), RTLD_NEXT
#include <dirent.h> // DIR, struct dirent, readdir()
#include <string.h> // strstr()
// Pointer to the original readdir()
static struct dirent *(*real_readdir)(DIR *d);
struct dirent *readdir(DIR *d) {
if (!real_readdir) // resolve original once
real_readdir =
dlsym(RTLD_NEXT, "readdir");
struct dirent *ent;
// Fetch next dir entry from real readdir
while ((ent = real_readdir(d)) != NULL) {
// If name contains the secret marker,
// skip this entry (hide it)
if (strstr(ent->d_name, ".secret"))
continue;
return ent; // return visible entry
}
return NULL; // no more entries
}
この例では、実際のlibc前に解決されるライブラリを提供することで、プロセス内のreaddir()を置き換え、フィルターに一致するファイル名を効果的に非表示にします。従来のユーザー モードの非表示ツールや軽量の「ルートキット」では、 LD_PRELOADまたは GOT/PLT パッチを使用して、プロセス、ファイル、ソケットを非表示にしてきました。攻撃者は、カーネル モジュールを必要とせずに標的のステルスを実現するために、特定のサービスに共有オブジェクトを挿入することもあります。
ユーザー空間介入は、悪意のあるライブラリをロードする(または注入される)プロセスにのみ影響します。システム全体の永続性には脆弱です (サービス/ユニット ファイル、サニタイズされた環境、setuid/静的バイナリによって複雑になります)。検出はカーネルフックに比べて簡単です。疑わしいLD_PRELOAD / LD_AUDITエントリ、 /proc/<pid>/maps内の予期しないマップされた共有オブジェクト、ディスク上のライブラリとメモリ内のインポート間の不一致、または変更された GOT エントリをチェックします。整合性ツール、サービス スーパーバイザー (systemd)、および単純なプロセス メモリ検査によって、通常、この手法が公開されます。
eBPF を使用したフック手法
最近のルートキット実装モデルでは、eBPF (拡張 Berkeley Packet Filter) が悪用されています。eBPF は、特権ユーザーがバイトコード プログラムをカーネルにロードできるようにする Linux のサブシステムです。多くの場合、「サンドボックス化された VM」と説明されていますが、そのセキュリティは実際には、ほぼゼロのレイテンシで実行できるようにネイティブ マシン コードに JIT コンパイルされる前に、バイトコードが安全であること (無限ループや不正なメモリ アクセスがない) を確認する静的検証に依存しています。
攻撃者は、LKM を挿入してカーネルの動作を変更する代わりに、機密性の高いカーネル イベントに接続する 1 つ以上の eBPF プログラムをロードできます。たとえば、 execveシステム コール エントリに (kprobe またはトレースポイント経由で) 接続する eBPF プログラムを作成して、プロセスの実行を監視または操作することができます。同様に、eBPF は LSM レイヤー (プログラム実行通知など) をフックして、特定のアクションを防止したり非表示にしたりできます。以下に例を示します。
// Attach this eBPF program to the tracepoint for sys_enter_execve
SEC("tp/syscalls/sys_enter_execve")
int tp_sys_enter_execve(struct sys_execve_enter_ctx *ctx) {
// Get the current process's PID and TID as a 64-bit value
// Upper 32 bits = PID, Lower 32 bits = TID
__u64 pid_tgid = bpf_get_current_pid_tgid();
// Delegate handling logic to a helper function
return handle_tp_sys_enter_execve(ctx, pid_tgid);
}
2 つの著名な公開例としては、TripleCross と Boopkit があります。TripleCross は、永続性と隠蔽性のために、eBPF を使用して execve などのシステムコールをフックするルートキットを実演しました。Boopkit は、ソケット バッファを操作できる eBPF プログラムを添付することで、eBPF を秘密の通信チャネルおよびバックドアとして使用しました (これにより、リモート側が細工されたパケットを通じてルートキットと通信できるようになります)。これらは概念実証プロジェクトですが、ルートキット開発における eBPF の実現可能性が証明されました。
主な利点は、eBPF フックでは LKM をロードする必要がなく、最新のカーネル保護と互換性があることです。eBPF をサポートするカーネルの場合、これは強力な手法です。しかし、強力ではあるが、制約も存在します。これらをロードするには昇格した権限が必要であり、検証者の安全性チェックによって制限され、再起動すると無効になる (別の永続性が必要) ため、監査/フォレンジック ツールによって検出される可能性が高まっています。eBPF の使用は、通常 eBPF ツールを使用しないシステムで特に顕著になります。
io_uringを使用した回避テクニック
While io_uring is not used for hooking, it deserves a honorable mention as a recent addition to the EDR evasion techniques used by rootkits. io_uring is an asynchronous, ring-buffer-based I/O API that lets processes submit batches of I/O requests (SQEs) and reap completions (CQEs) with minimal syscall overhead. It is not a hooking framework, but its design changes the syscall/visibility surface and exposes powerful kernel-facing primitives (registered buffers, fixed files, mapped rings) that attackers can abuse for stealthy I/O, syscall-evading workflows, or, when combined with a vulnerability, as an exploit primitive that leads to installing hooks at a lower layer.
攻撃パターンは 2 つのクラスに分類されます: (1)回避/パフォーマンスの悪用: 悪意のあるプロセスがio_uringを使用して大量の読み取り/書き込み/メタデータ操作を大量にバッチで実行するため、従来のシステム コールごとの検出器ではイベントや非定型パターンが少なくなります。(2)エクスプロイトの有効化: io_uringサーフェス (リング マッピング、登録されたリソース) のバグは、これまで権限昇格のベクトルとなってきました。その後、攻撃者はより従来の手段でカーネル フックをインストールできます。io_uring 、コードが直接操作を送信する場合、一部の libc ラッパーもバイパスするため、libc 呼び出しをインターセプトするユーザーランドのフックが回避される可能性があります。単純な送信/取得フローを以下に示します。
// Minimal io_uring usage (error handling omitted)
// io_uring context (SQ/CQ rings shared with kernel)
struct io_uring ring;
// Initialize ring with space for 16 SQEs
io_uring_queue_init(16, &ring, 0);
// Grab a free submission entry (or NULL if full)
struct io_uring_sqe *sqe =
io_uring_get_sqe(&ring);
// Prepare SQE as a read(fd, buf, len, offset=0)
io_uring_prep_read(sqe, fd, buf, len, 0);
// Submit pending SQEs to the kernel (non-blocking)
io_uring_submit(&ring);
struct io_uring_cqe *cqe;
// Block until a completion is available
io_uring_wait_cqe(&ring, &cqe);
// Mark the completion as handled (free slot)
io_uring_cqe_seen(&ring, cqe);
上記の例は、1 つまたは少数のio_uring_enterシステム コールを使用して多数のファイル操作をカーネルに送り、操作ごとのシステム コール テレメトリを削減する送信キューを示しています。
ステルス的なデータ収集や高スループットの窃取に関心のある攻撃者は、システムコールのノイズを減らすためにio_uringに切り替える可能性があります。io_uringは本質的にグローバル フックをインストールしたり、他のプロセスの動作を変更したりしません。権限昇格と組み合わせない限り、プロセス ローカルです。検出は、 io_uringシステムコール ( io_uring_enter 、 io_uring_register ) をインストルメント化し、異常なパターン (異常に大きなバッチ、多数の登録済みファイル/バッファ、大量のバッチ メタデータ操作を実行するプロセス) を監視することによって可能です。カーネル バージョンの違いも重要です。 io_uring機能は急速に進化するため、攻撃者の手法はバージョンに依存する可能性があります。最後に、 io_uring悪意のあるプロセスの実行が必要であるため、防御側は多くの場合それを中断し、そのリング、登録されたファイル、およびメモリ マッピングを検査して、悪用を発見できます。
まとめ
Linux のフック技術は、テーブル内のポインタを単純に上書きするだけから大きく進歩しました。現在、攻撃者が正当なカーネル インストルメンテーション フレームワーク (ftrace、kprobes、eBPF) を悪用して、検出が困難なフックを埋め込んでいるのが確認されています。IDT や syscall テーブル パッチからインライン フックや動的プローブまで、各方法にはステルス性と安定性の点で独自のトレードオフがあります。防御側は、これらすべての可能性のあるベクトルに注意する必要があります。実際には、現代のルートキットは目的を達成するために複数のフック手法を組み合わせることがよくあります。たとえば、PUMAKIT は直接的な syscall テーブル フックと ftrace フックを使用し、Diamorphine は syscall フックと kprobe を使用してシンボルの非表示を回避します。この階層化アプローチは、検出ツールがシステムの多くの側面をチェックする必要があることを意味します: IDT エントリ、システム コール テーブル、モデル固有のレジスタ (sysenter フック用)、関数プロローグの整合性、構造体 (VFS など) 内の重要な関数ポインタの内容、アクティブな ftrace オペレーション、登録された kprobe、およびロードされた eBPF プログラム。
このシリーズの第 2 部では、理論から実践に移ります。ここで説明するルートキットの分類とフック手法を理解した上で、実際の Linux 環境でこれらの脅威を識別するための検出エンジニアリング、実用的な検出戦略の構築と適用に焦点を当てます。
