FortiGuard Labs 脅威リサーチ

Linuxルートキットマルウェアの深層

投稿者 Xiaopeng Zhang, Faisal Abdul Malik Qureshi および John Simmons | 2025年2月14日
  • Article Contents
投稿者 Xiaopeng Zhang, Faisal Abdul Malik Qureshi および John Simmons | 2025年2月14日

影響を受けるプラットフォーム: CentOS Linux
影響を受けるユーザー:     CentOSのユーザー
影響:             被害者のデバイスを完全にリモート制御
深刻度:            クリティカル

背景

これは、以前投稿したゼロデイ脆弱性の悪用に関するブログの続編となる分析です。この件では、FortiGuardインシデントレスポンス(FGIR)チームが、リモートの攻撃者がどのようにしてアプライアンスの複数の脆弱性を悪用してお客様のシステムの制御を奪ったかを精査しました。

前ブログでは最後に、リモートの攻撃者がシェルスクリプト(Install.sh)を使ってルートキット(ローダブルカーネルモジュールsysinitd.ko)とユーザースペースバイナリファイル(sysinitd)を標的のシステムに展開したことを明らかにしました。また、ルートキットの永続化のために、ルートキットマルウェア用の入口が/etc/rc.localファイルと/etc/rc.d/rc.localファイルに追加され、システム起動時にルートキットマルウェアが読み込まれるようになっていました。

概要

侵害されたデバイスのイメージを分析する中で、そのデバイスのLinux監査ログに、脅威アクターがアプライアンスに対して実行したユーザースペースシェルコマンドが含まれていることが判明しました。実行されたシェルコマンドは、16進BLOBとして監査ログ内に保存されていました。見つかったログエントリの1つは、以下のようなものでした。

 

2024-09-07 03:26:18

type=USER_CMD msg=audit 1725679577.835  pid=25212 uid=1001 auid=4294967295 ses=4294967295 subj=system_u:system_r:unconfined_service_t:s0 msg='cwd="/opt/landesk/broker/webroot/gsb" cmd=6563686F2048347349414A2B…<SNIPPED>…43793542576D692F312B terminal=? res=success'   

この16進数をデコードしたところ、Base64でエンコードされたBLOBであることがわかりました。

このBase64 BLOBをデコードするとtgzファイルになり、ファイルを解凍すると次の2つのファイルになりました。

インジェクタースクリプトであるinstall.shを分析すると、このスクリプトがデフォルトで共有可能なオブジェクトsysinitd.so/usr/share/empty/にインストールすることがわかりました。FGIRチームはこれを念頭に置いて、侵害されたIvantiボックスのイメージを分析し、脅威アクターがディスク上の/usr/share/empty/に作成した2つの悪意のあるファイルを取得しました。その2つのうち1つはルートキットで、悪意のあるカーネルモジュールsysinitd.soですが、本分析ではsysinitd.koと呼ばれています。

FortiGuardは、悪意のあるルートキットマルウェアの詳細な分析を行いました。分析により、カーネルモジュールが侵害されたIvantiシステムへのインバウンドネットワークトラフィックをどのようにハイジャックし、悪意のあるユーザースペースファイルがどのように起動されてルートキットモジュールとやり取りするかが明らかになりました。また、この分析でルートキットマルウェアの全体的な目的も明らかになりました。

カーネルモジュール:初期化

図1:sysinitd.koのヘッダー情報

コマンド「readelf –h sysinitd.ko」によって、図1のようなカーネルモジュールのELF(Executable and Linkable Format)ファイルヘッダー情報が表示されます。ELFタイプは「REL (Relocatable file)」ですが、これはカーネルモジュールに対して予期されるとおりのELFタイプです。

読み込まれたカーネルモジュールは、図2のようにコマンド「insmod /usr/share/empty/init/sysinitd.ko」を実行した後、動作を始めます。

図2:「insmod」コマンドがルートキットマルウェアを読み込みます。

カーネルモジュールがカーネルに挿入されると、カーネルモジュールのinit_module()関数が呼び出されます。

この関数は、グローバル変数の値を設定する、文字列を復号化する、ネットワークフック関数を登録する、「/proc」フォルダ内にファイル記述子を作成するなど、初期化タスクを実行します。

図3:文字列「abrtinfo」の復号化

図3は、関数を呼び出して文字列をどのように復号化するかを示しています。この例では、文字列「abrtinfo」が復号化されています。この文字列はその後、procfs(プロセスファイルシステム)エントリの作成時に使用されます。

Netfilterフックの登録

カーネルモジュールは次に、Netfilterフックを登録するためにカーネルAPIのnf_register_hook()を呼び出します。図4は、nf_register_hook()を呼び出すためのコンテキスト依存型のASM命令を示しています。

図4:Netfilterフックの登録

APIが引数として取るのは構造体「nf_hook_ops」だけです。この構造体は、フック関数、フック番号、優先度、およびプロトコルファミリーを指定します。

フックコールバック関数は「network_hook_func_15()です(図4を参照)。IPv4プロトコルのプロトコルファミリーは2です。フック番号は0で、NF_INET_PRE_ROUTINGを示します。これは、LinuxカーネルのNetfilterフレームワーク内のフックポイントであり、そこで受信パケットがルーティング決定前に傍受されます。通常、これはパケットがシステム到着後に傍受される最初のポイントになります。

2つのファイルのうち1つはルートキットで、フック番号オプションに使用可能なすべての値とその説明をリストします。

 

マクロ

説明

0

NF_INET_PRE_ROUTING

パケットがルーティング決定前に受信されます。

1

NF_INET_LOCAL_IN

パケットがルーティング後にローカルマシンに送られます。

2

NF_INET_FORWARD

パケットが別のマシンに転送されます。

3

NF_INET_LOCAL_OUT

パケットがルーティング前にローカルマシンから送信されます。

4

NF_INET_POST_ROUTING

パケットがルーティング後に送信可能な状態になっています。

3つのprocfsエントリの作成

その後、カーネルモジュール(sysinitd.ko)のinit_module()関数は、カーネルAPIのproc_create_data()を使って、3つのprocfsエントリを「/proc/」フォルダに作成します。procfsエントリの名前は、事前に復号化された文字列「abrtinfo」から取られます。

次に示すASM命令のスニペットは、3つのprocfsエントリの作成を示しています。

 

[…]

.text:023D6     xor     r8d, r8d

.text:023D9     mov     rcx, offset as_STDIN_FD

.text:023E0     xor     edx, edx

.text:023E2     mov     esi, 1B6h       ; "RW-RW-RW-"

.text:023E7     mov     rdi, offset byte_3275 ; "abrtinfo"

.text:023EE     call    proc_create_data

.text:023F3     xor     r8d, r8d

.text:023F6     mov     rcx, offset as_STDOUT_FD

.text:023FD     xor     edx, edx

.text:023FF     mov     esi, 1B6h       ; "RW-RW-RW-"

.text:02404     mov     rdi, offset byte_3276 ; "brtinfo"

.text:0240B     mov     cs:gv_26, rax

.text:02412     call    proc_create_data

.text:02417     xor     r8d, r8d

.text:0241A    mov     rcx, offset as_control_STDIN_FD

.text:02421     xor     edx, edx

.text:02423     mov     esi, 1B6h       ; "RW-RW-RW-"

.text:02428     mov     rdi, offset byte_3277 ; "rtinfo"

.text:0242F     mov     cs:gv_261, rax

.text:02436     call    proc_create_data

[…]

すべてのprocfsエントリの権限が1B6hとして設定されていますが、これは「rw-rw-rw-」を表し、すべてのユーザーに読み取り / 書き込みアクセスを許可します。procfsエントリの名前はすべて文字列「abrtinfo」に由来しますが、それぞれオフセットが異なり、「abrtinfo」のオフセットは0x3275、「brtinfo」のオフセットは0x3276、「rtinfo」のオフセットは0x3277です。

作成された3つのファイルを図5に示します。

図5:作成されたprocfsエントリ

Install.shは、「/proc/abrtinfo」が作成されたかどうかをチェックすることで、カーネルモジュールが正常に読み込まれたかどうかを判断します。

この3つのprocfsエントリは、ユーザースペースプロセス(sysinitd)の実行時にファイル記述子として振る舞うことがわかりました。「/proc/abrtinfo」はsysinitdのstdinで、「/proc/brtinfo」はsysinitdのstdoutです。「/proc/rtinfo」は制御コマンドをsysinitdに渡します。

カーネルモジュール内には、これらのprocfsエントリにバインドされた多くのコールバック関数があります。

  • /proc/abrtinfo:sysinitdプロセスによって読み取られたときにコールバック関数が呼び出されます。
  • /proc/brtinfo:sysinitdプロセスによって書き込まれたときにコールバック関数が呼び出されます。
  • /proc/rtinfo:sysinitdプロセスによって読み取られたときにコールバック関数が呼び出されます。

カーネルモジュール:Netfilterフック関数

Linuxは、IPv4受信パケット(UDPおよびTCP)が到着すると、Netfilterフック関数を呼び出します。マルウェアはTCPパケット(プロトコル値は6)のみに焦点を合わせ、受信されたパケットのプロトコル値を比較して、TCP以外のパケットは無視します。

TCPセッションは、三方向ハンドシェイクによって確立されます。したがって、攻撃者は侵害されたシステム上で実行されているHTTP(ポート80)、HTTPS(443)、SSH(22)、FTP(21)などのサービスを使用して、TCPセッションを確立する必要があります。

攻撃開始パケット

フック関数が攻撃者からのパケットを認識できるようにするために、攻撃者は侵害したシステムに特別なパケットを送信する必要があります。この分析では、そのパケットを攻撃開始パケット(最初のパケット)と呼んでいます。

攻撃開始パケットは、以下の形式に従った長さ0xdバイトのパケットでなければなりません。

 

オフセット

長さ

説明

0x00

1

「\x31」または「\x30」。トラフィックの暗号化を有効化 / 無効化するためのフラグ。

0x01

4

検証データ。  「Dw1」

0x05

4

検証データ。  「Dw5」

0x09

4

検証データ。  「Dw9」


目的を果たすためには、パケットが以下の条件を満たしている必要があります。
 

1>  Dw1 == Dw9 ^ 0x32C21F0A
2> Dw5 == Dw9 ^ 0xED22AF9E or Dw5 == Dw9 ^ 0x4B1EF486
 

作成された攻撃開始パケットの例を以下に示します。
 

“\x30”+“\x3E\x2B\xF6\x06”+”\xAA\x9Bx16\xD9”+”\x34\x34\x34\x34”

攻撃開始パケットが検証されると、カーネルモジュールが送信元のIPとポート、いくつかのグローバル変数、および送信先のIPとポートを記録します。これにより、条件を満たすその後のトラフィックが攻撃者からのものとして認識され、Netfilterフック関数内でのみ処理されるようになります。

その一方で、一連のカーネルAPIが呼び出されますが、これにはqueue_work_on()、kthread_create_on_node()、wake_up_process()、call_usermodehelper()が含まれます。分析によると、これらのAPIはユーザースペースファイル(sysinitd)を起動します。

call_usermodehelper() APIは、カーネルスペースからユーザースペースプログラムを実行するために、Linuxカーネル内で使用されます。関数定義は以下のとおりです。

 

int call_usermodehelper(const char *path, char **argv, char **envp, int wait);

  • path:ユーザースペースプロセスのフルパス。
  • argv:プロセスに渡される引数リスト。
  • envp: envp:ユーザースペースプロセスの環境変数リスト。
  • wait: カーネルがユーザースペースプロセスの完了を待つかどうかを制御します。
図6:call_usermodehelper() APIの引数の表示

図6内のASM命令に従い、ユーザースペースプロセスがコマンドライン引数「abrtinfo:0」を使って起動されます。このプロセスへのパス「/usr/share/empty/init/sysinitd」は、カーネルモジュール内にハードコーディングされています。このユーザースペースプロセスがどのように動作するかについて、次のセクションで説明します。

応答パケット

カーネルモジュールからの応答パケットはすべて、次の表に示すように同じ形式になっています。
 

オフセット

長さ

説明

0x00

4

ペイロードデータのサイズ。

0x04

変数

ペイロードデータ。


ペイロードデータは、攻撃開始パケットの最初のバイトが「\x31」であれば暗号化されます。暗号キーは、攻撃開始パケット内の検証データから算出されて、攻撃開始パケットの処理中に攻撃者に戻されます。

図7:攻撃開始パケットと暗号キーパケット

図7は、攻撃者(クライアント)が侵害したシステム(サーバー)に攻撃開始パケットを送り付ける攻撃シナリオのシミュレーションを示しています。ルートキットマルウェアは、パケットを検証し、最初のパケットと他の関連データから暗号キー(4バイト)を生成して、応答パケットでその暗号キーを攻撃者に送り返します。

データ交換と制御コマンド

攻撃開始パケットが暗号化関数を有効にした場合、クライアントとサーバーの両方で、ペイロードデータの暗号化 / 復号化に暗号キーが使用されるようになります。有効にしなかった場合は、どちらの側でも暗号キーパケットが破棄されます。

その時点から、攻撃者が感染したシステムと通信できるようになります。攻撃者から送られてきたパケットはユーザースペースプロセスに渡され、プロセスは読み取りコールバック関数を通じて「/proc/abrtinfo」からデータを読み取ります。カーネルモジュールも、「/proc/brtinfo」にデータを書き込んだときに、ユーザースペースプロセスの出力を送り返します。

図8は、「/proc/abrtinfo」に割り当てられた読み取りコールバック関数の擬似コードの例です。この関数は、攻撃者のデータをユーザースペースプロセス(sysinitd)にコピーするために、カーネルAPIのcopy_to_user()を呼び出します。

図8:「/proc/abrtinfo」に割り当てられた読み取りコールバック関数の擬似コード

攻撃者が侵害したシステムに4バイトの制御コマンドを送信するときには、ユーザースペースプロセスにコマンドを渡して制御するために、「/proc/rtinfo」が使用されます。
 

コマンド

アクション

0xB3FEB404

0xE1を「/proc/rinfo」に渡します。

0x80CDD03C

0xE2を「/proc/rinfo」に渡します。

0x44724774

0xE4を「/proc/rinfo」に渡します。

ユーザースペースプロセス

ユーザースペースファイル「sysinitd」は、Install.shで「/usr/share/empty/init」にコピーされました。図9に示されているsysinitdのELFヘッダー情報を見ると、ELFタイプがEXEC(ユーザースペース実行ファイル)であることがわかります。

図9:sysinitdのELFヘッダー情報


sysinitdプロセスは、カーネルモジュールによって起動されます。このプロセスは、自身のプロセス名を復号化された文字列「bash」で置き換えることで、bashプログラムを装います。その結果、システム管理者がこのプロセスをマルウェアとして特定しにくくなります。その後、次のCコードのように、これをアーカイブするためにAPIを呼び出します。
 

memset(argv[0], 0, sizeof(argv[0]));

strcpy(argv[0], “bash”);

さらに、図10のようにAPIのstrcmp()を使用して、コマンドライン引数が「abrtinfo」であるかどうかを検証します。コマンドライン引数が一致しなかった場合、このプロセスは終了します。

図10:コマンドライン引数の検証

このプロセスは次に、Linuxシステムコール「fork()」を呼び出して、子プロセスを作成します。この時点から、親プロセスと子プロセスはそれぞれ異なるワークフローに従うことになります。

子プロセス

起動されたfork()は、3つのdup2()システムコールを次々に呼び出して、現在のプロセスの標準入力を「/proc/abrtinfo」に設定し、標準出力と標準エラーを「/proc/brtinfo」に設定します。このアクションを実行するASM命令を図11に示します。

図11:/bin/shプログラムを使って現在のプロセスを置き換えます

これらのprocfsエントリは、カーネルモジュールで作成されます。各procfsにコールバック関数が割り当てられ、読み取り / 書き込み操作が実行されると呼び出されます。

図11の下部で、もう1つのシステムコールexecv()が呼び出されています。その目的は、現在のプロセスを、指定されたプロセス(この場合は「/bin/sh」)で置き換えることです。

これにより、攻撃者は侵害したシステム上で悪意のあるカーネルモジュール(sysinitd.ko)と子プロセス(sysinitd -> /bin/sh)を通じてリモートから任意のコマンドをルート権限で実行できるようになります。

親プロセス

親プロセスはタスク(通常はデーモンプロセスと関連付けられている)の実行を開始し、例えば子プロセスの管理(子プロセスの起動、再起動、強制終了など)を行います。

ファイル記述子「/proc/rtinfo」から制御コマンドを読み取り、コマンドの値に応じて異なる分岐をたどります。図12に示すように、親プロセスは制御コマンド0xE1を読み取っています。これをアーカイブするために、攻撃者はこの4バイトパケット「\x04\xb4\xfe\xb3」を送信する必要があります。

図12:親プロセスが/proc/rtinfoから制御コマンドを読み取っています。

サポートされている制御コマンドを次の表に示します。
 

コマンド

アクション

0xE1

子プロセスを再起動します。

0xE2

子プロセスを強制終了します。

0xE3

子プロセスにCtrl+Cを送信します。

0xE4

子プロセスと親プロセスを両方とも強制終了します。

図13は、コマンド「ps aux | grep –e " sh" –e "bash"」の出力のスクリーンショットです。"sh"は子プロセスで、"bash"は親プロセスです。

図13:実行中の親プロセスと子プロセス

図14は、攻撃者が侵害したシステムをルートキットマルウェアとユーザースペースプロセスを使ってどのように制御するかを示しています。

図14:このルートキットマルウェアのワークフロー図

デモ

FGIRチームは、攻撃者が侵害したLinuxシステムを制御する様子をシミュレートするために、Pythonスクリプトを開発しました。このPythonスクリプトを活用して多数のLinuxコマンドを侵害されたシステムに送信することで、図15(トラフィックデータのWiresharkスクリーンショット)のように、通信とコマンド実行の結果を見ることができます。

Linuxシステムに送信されたコマンドは、「whoami」、「pwd」、「wget -O Fortinet.html -o summary www.fortinet.com」、「ls -l fortinet.html」です。

図15:攻撃者と侵害されたシステムの間でのネットワークトラフィック

結論

本分析では、ルートキットマルウェアに焦点を当てました。最初に、カーネルモジュールがどのようにNetfilterフック関数をNF_INET_PRE_ROUTINGに設定して、侵害したシステムへの受信TCPトラフィックをハイジャックするかを説明しました。

次に、Netfilterフック関数がどのような関連タスクを実行するかを詳述しました。これには、攻撃開始パケットと応答パケットの形式の取り扱い、ユーザースペースファイルの起動、ユーザースペースプロセス / カーネルモジュール間でのデータ交換などが含まれます。

また、ユーザースペースプロセスがどのように起動され、どのようにして自身を「bash」であるかのように偽装し、fork()システムコールを使ってどのように子プロセスを作成し、最終的に攻撃者からのLinuxコマンドを処理してシステムを制御するためにどのように「/bin/sh」で置き換えられるかについても学びました。

最後に、攻撃者がどのようにして侵害したシステムとの接続を確立し、Linuxコマンドを送信し、トラフィックを傍受して結果を取得するかをデモで示しました。

フォーティネットのソリューション

フォーティネットのお客様は、すでにこの攻撃から保護されています。FortiGuardアンチウイルスサービスは、悪意のあるファイルinstall.sh、sysinitd.ko、およびsysinitdを以下のように検知します。

BASH/Injector.CSA!tr
ELF64/Injector.CSA!tr
ELF64/Injector.CSA!tr

FortiGate、FortiMail、FortiClient、およびFortiEDRは、FortiGuardアンチウイルスサービスをサポートしています。FortiGuardアンチウイルスエンジンは、各ソリューションの一部です。したがって、最新の保護機能を備えたこれらの製品を使用しているお客様は、すでに保護されています。

また、読者の皆様には、フォーティネットが無償で提供するNSEトレーニング: NSE 1 – 情報セキュリティ意識向上を受講することをお勧めします。このトレーニングにはインターネットの脅威に関するモジュールが含まれ、エンドユーザーは各種のフィッシング攻撃を識別して自らを保護する方法を学習できます。

お客様の組織が上記の攻撃や他のサイバーセキュリティ攻撃を受けていると思われる場合は、フォーティネットのグローバルFortiGuardインシデントレスポンスチームまでご連絡ください。

IOC(Indicators of Compromise:侵害指標)

関連サンプルSHA-256:

[install.sh]
8D016D02F8FBE25DCE76481A90DD0B48630CE9E74E8C31BA007CF133E48B8526

[sysinitd.ko]
6EDD7B3123DE985846A805931CA8EE5F6F7ED7B160144AA0E066967BC7C0423A

[sysinitd]
D57A2CAC394A778E19CE9B926F2E0A71936510798F30D20F207F2A49B49CE7B1
 


関連資料:危険なゼロデイ脆弱性:国家的関与が疑われる攻撃者がIvanti CSAを標的に