Remco SprootenRuben Groenewoud

プマキットの爪を抜く

PUMAKITは、高度なステルス機構を用いてその存在を隠し、コマンド&コントロールサーバーとの通信を保持する、洗練されたローダブルカーネルモジュール(LKM)ルートキットです。

30分で読めますマルウェア分析
PUMAKITの爪を抜く

PUMAKITの概要

PUMAKITは洗練されたマルウェアで、VirusTotalの日常的な脅威ハンティング中に最初に発見され、バイナリ内に見つかった開発者が埋め込んだ文字列にちなんで名付けられました。そのマルチステージアーキテクチャは、ドロッパー(cron)、メモリ常駐の2つの実行可能ファイル(/memfd:tgt/memfd:wpn)、LKMルートキットモジュール、および共有オブジェクト(SO)ユーザーランドルートキットで構成されています。

マルウェアの作成者が「PUMA」と呼んでいるルートキットコンポーネントは、内部のLinux関数トレーサ(ftrace)を使用して、さまざまなシステムコールといくつかのカーネル関数 18 フックし、コアシステムの動作を操作できるようにします。PUMA との対話には、権限昇格のための rmdir() システムコールの使用や、設定情報とランタイム情報の抽出のための特別なコマンドの使用など、独自の方法が使用されます。LKM ルートキットは、段階的なデプロイにより、セキュア ブート チェックやカーネル シンボルの可用性など、特定の条件が満たされた場合にのみアクティブ化されます。これらの条件は、Linuxカーネルをスキャンすることで検証され、必要なすべてのファイルはドロッパー内にELFバイナリとして埋め込まれます。

カーネルモジュールの主な機能には、権限昇格、ファイルとディレクトリの非表示、システムツールからの隠蔽、デバッグ対策、およびコマンドアンドコントロール(C2)サーバーとの通信の確立が含まれます。

重要なポイント

  • マルチステージアーキテクチャ:このマルウェアは、ドロッパー、メモリ常駐型の2つの実行可能ファイル、LKMルートキット、およびSOユーザーランドルートキットを組み合わせて、特定の条件下でのみアクティブ化します。
  • 高度なステルスメカニズム:ftrace()を使用して 18 システムコールといくつかのカーネル関数をフックし、デバッグの試みを回避しながら、ファイル、ディレクトリ、およびルートキット自体を隠します。
  • Unique Privilege Escalation: rmdir() syscall のような型破りなフッキング方法を利用して、権限をエスカレーションし、ルートキットと対話します。
  • 重要な機能:権限昇格、C2通信、アンチデバッグ、および永続性と制御を維持するためのシステム操作が含まれます。

PUMAKITディスカバリー

VirusTotalでの定期的な脅威ハンティング中に、 cronという名前の興味深いバイナリに出くわしました。バイナリは最初にアップロードされました 9月 4日 、 2024、 0 検出され、その潜在的なステルス性についての疑いが提起されました。さらに調査したところ、同じ日にアップロードされた別の関連アーティファクト、/memfd:wpn (deleted)71cc6a6547b5afda1844792ace7d5437d7e8d6db1ba995e1b2fb760699693f24、これも 0 検出されました。

私たちの注意を引いたのは、これらのバイナリに埋め込まれた明確な文字列であり、/boot/vmlinuz カーネル パッケージの操作の可能性を示唆していました。これにより、サンプルのより詳細な分析が促され、サンプルの挙動と目的に関する興味深い発見につながりました。

PUMAKITコード解析

PUMAKITは、組み込みLKMルートキットモジュール(マルウェア作成者は「PUMA」と名付けました)とSOユーザーランドルートキットであるKitsuneにちなんで名付けられ、実行チェーンを開始するドロッパーから始まるマルチステージアーキテクチャを採用しています。このプロセスは、 cron バイナリから始まり、メモリに常駐する実行可能ファイル ( /memfd:tgt (deleted)/memfd:wpn (deleted)) が作成されます。/memfd:tgt は良性の Cron バイナリとして機能しますが、/memfd:wpn はルートキットローダーとして機能します。ローダーは、システム条件の評価、一時スクリプト (/tmp/script.sh) の実行、および最終的な LKM ルートキットのデプロイを担当します。LKM ルートキットには、ユーザー空間からルートキットと対話するための埋め込み SO ファイル (Kitsune) が含まれています。この実行チェーンを以下に示します。

この構造化された設計により、PUMAKITは特定の基準が満たされた場合にのみペイロードを実行できるため、ステルス性を確保し、検出の可能性を減らすことができます。プロセスの各段階は、メモリに常駐するファイルとターゲット環境の正確なチェックを活用して、その存在を隠すように細心の注意を払って作成されています。

このセクションでは、さまざまなステージのコード分析について深く掘り下げ、そのコンポーネントと、この洗練されたマルチステージマルウェアを可能にする役割について説明します。

ステージ 1: Cron の概要

cronバイナリはドロッパーとして機能します。以下の関数は、PUMAKITマルウェアサンプルのメインロジックハンドラとして機能します。その主な目標は次のとおりです。

  1. 特定のキーワード ("Huinder") のコマンドライン引数を確認します。
  2. 見つからない場合は、隠しペイロードをファイルシステムにドロップせずに、メモリから完全に埋め込んで実行します。
  3. 見つかった場合は、特定の「抽出」引数を処理して、埋め込まれたコンポーネントをディスクにダンプし、正常に終了します。

要するに、マルウェアはステルス性を保とうとします。通常 (特定の引数なし) を実行すると、ディスクにトレースを残さずに非表示の ELF バイナリが実行され、正当なプロセス ( cronなど) になりすます可能性があります。

文字列 Huinder が引数の中に見つからない場合、 if (!argv_) 内のコードは次のように実行されます。

writeToMemfd(...): これは、ファイルレス実行の特徴です。memfd_create により、バイナリは完全にメモリ内に存在できます。このマルウェアは、埋め込まれたペイロード(tgtElfp および wpnElfp)をディスクにドロップするのではなく、匿名のファイル記述子に書き込みます。

fork() and execveat(): マルウェアは子プロセスと親プロセスに分岐します。子は、ログを残さないように標準出力とエラーを /dev/null にリダイレクトし、execveat()を使用して「武器」ペイロード (wpnElfp) を実行します。親は子を待ってから、「ターゲット」ペイロード(tgtElfp)を実行します。どちらのペイロードも、ディスク上のファイルからではなくメモリから実行されるため、検出とフォレンジック分析がより困難になります。

execveat()の選択は興味深いもので、ファイル記述子によって参照されるプログラムの実行を可能にする新しいシステムコールです。これは、このマルウェアの実行のファイルレスの性質をさらにサポートします。

tgtファイルが正当なcronバイナリであることを確認しました。これはメモリにロードされ、ルートキットローダー(wpn)が実行された後に実行されます。

実行後、バイナリはホスト上でアクティブなままになります。

> ps aux
root 2138 ./30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f

以下は、このプロセスのファイル記述子のリストです。これらのファイル記述子は、ドロッパーによって作成されたメモリ常駐ファイルを示します。

root@debian11-rg:/tmp# ls -lah /proc/2138/fd
total 0
dr-x------ 2 root root  0 Dec  6 09:57 .
dr-xr-xr-x 9 root root  0 Dec  6 09:57 ..
lr-x------ 1 root root 64 Dec  6 09:57 0 -> /dev/null
l-wx------ 1 root root 64 Dec  6 09:57 1 -> /dev/null
l-wx------ 1 root root 64 Dec  6 09:57 2 -> /dev/null
lrwx------ 1 root root 64 Dec  6 09:57 3 -> '/memfd:tgt (deleted)'
lrwx------ 1 root root 64 Dec  6 09:57 4 -> '/memfd:wpn (deleted)'
lrwx------ 1 root root 64 Dec  6 09:57 5 -> /run/crond.pid
lrwx------ 1 root root 64 Dec  6 09:57 6 -> 'socket:[20433]'

参照に従って、サンプルにロードされているバイナリを確認できます。バイトを新しいファイルにコピーするだけで、オフセットとサイズを使用してさらに分析できます。

抽出すると、次の 2 つの新しいファイルが見つかります。

  • Wpn: cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfe
  • Tgt: 934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136

これで、2つのメモリファイルのダンプができました。

ステージ 2: メモリ常駐実行可能ファイルの概要

/memfd:tgt ELFファイルを調べると、これがデフォルトのUbuntu Linux Cronバイナリであることは明らかです。バイナリに変更はないようです。

/memfd:wpn ファイルは、LKM ルートキットのロードを担当するバイナリであるため、より興味深いものです。このルートキットローダーは、 /usr/sbin/sshd 実行可能ファイルとして模倣することで、自身を隠そうとします。セキュアブートが有効になっているかどうか、必要なシンボルが使用可能かどうかなど、特定の前提条件をチェックし、すべての条件が満たされている場合は、カーネルモジュールのルートキットをロードします。

Kibana での実行を見ると、プログラムが dmesgをクエリしてセキュア ブートが有効になっているかどうかを確認できることがわかります。正しい条件が満たされると、 script.sh というシェルスクリプトが /tmp ディレクトリにドロップされて実行されます。

このスクリプトには、圧縮形式に基づいてファイルを検査および処理するためのロジックが含まれています。

その機能は次のとおりです。

  • この関数 c() は、 file コマンドを使用してファイルを検査し、ファイルがELFバイナリであるかどうかを確認します。そうでない場合、関数はエラーを返します。
  • この関数 d() 、サポートされている圧縮形式の署名に基づいて、 gunzipunxzbunzip2などのさまざまなユーティリティを使用して、特定のファイルを解凍しようとします。greptailを使用して、特定の圧縮セグメントを特定して抽出します。
  • このスクリプトは、ファイル ($i) を見つけて /tmp/vmlinuxに処理しようとします。

/tmp/script.shの実行後、ファイル /boot/vmlinuz-5.10.0-33-cloud-amd64 が入力として使用されます。tr コマンドは、gzip のマジックナンバー (\037\213\010) を見つけるために使用されます。その後、バイト オフセット +10957311 から始まるファイルの一部が tailを使用して抽出され、 gunzipで解凍され、 /tmp/vmlinuxとして保存されます。次に、結果のファイルが検証され、有効なELFバイナリであるかどうかが判断されます。

このシーケンスは、スクリプト内のすべてのエントリが関数 . d()に渡されるまで、複数回繰り返されます。

d '\037\213\010' xy gunzip
d '\3757zXZ\000' abcde unxz
d 'BZh' xy bunzip2
d '\135\0\0\0' xxx unlzma
d '\211\114\132' xy 'lzop -d'
d '\002!L\030' xxx 'lz4 -d'
d '(\265/\375' xxx unzstd

このプロセスを以下に示します。

スクリプト内のすべての項目を実行すると、 /tmp/vmlinux ファイルと /tmp/script.sh ファイルが削除されます。

このスクリプトの主な目的は、特定の条件が満たされているかどうかを確認し、満たされている場合は、カーネル オブジェクト ファイルを使用してルートキットをデプロイするための環境を設定することです。

上の画像に示すように、ローダーは Linux カーネル ファイルで __ksymtab シンボルと __kcrctab シンボルを検索し、オフセットを格納します。

いくつかの文字列は、ルートキットの開発者がドロッパー内でルートキットを「PUMA」と呼んでいることを示しています。条件に基づいて、プログラムは次のようなメッセージを出力します。

PUMA %s
[+] PUMA is compatible
[+] PUMA already loaded

さらに、カーネルオブジェクトファイルには、ルートキットとの関連付けを強化する .puma-configという名前のセクションが含まれています。

ステージ 3: LKM ルートキットの概要

このセクションでは、カーネルモジュールを詳しく見て、その基本的な機能を理解します。具体的には、そのシンボルルックアップ機能、フッキングメカニズム、および目標を達成するために変更する主要なシステムコールについて調べます。

LKM ルートキットの概要: シンボル検索とフックメカニズム

LKM ルートキットがシステムの動作を操作する機能は、syscall テーブルの使用と、シンボル解決のための kallsyms_lookup_name() への依存から始まります。カーネルバージョン 5.7 以降を対象とする最新のルートキットとは異なり、ルートキットは kprobesを使用しません。これは、古いカーネル用に設計されていることを示しています。

カーネル バージョン 5.7 より前のバージョンでは、 kallsyms_lookup_name() はエクスポートされ、適切なライセンスを持たないモジュールでも簡単に活用できたため、この選択は重要です。

2020年2月、カーネル開発者は、不正または悪意のあるモジュールによる誤用を防ぐために、 kallsyms_lookup_name() の輸出解除について議論しました。一般的な戦術は、ライセンスチェックを回避するために偽の MODULE_LICENSE("GPL") 宣言を追加することで、これらのモジュールがエクスポートされていないカーネル機能にアクセスできるようにすることでした。LKM ルートキットは、その文字列から明らかなように、この動作を示しています。

name=audit
license=GPL

このGPLライセンスの不正使用により、ルートキットは kallsyms_lookup_name() を呼び出して関数アドレスを解決し、カーネルの内部を操作できるようになります。

そのシンボル解決戦略に加えて、カーネルモジュールは ftrace() フックメカニズムを使用してフックを確立します。ftrace()を活用することで、ルートキットはシステムコールを効果的にインターセプトし、そのハンドラをカスタムフックに置き換えます。

これの証拠は、上記のコードスニペットに示されているように、 unregister_ftrace_functionftrace_set_filter_ip の使用例です。

LKM ルートキットの概要: フックされたシステムコールの概要

ルートキットの syscall フック メカニズムを分析して、PUMA がシステム機能に干渉する範囲を理解しました。次の表は、ルートキットによってフックされるシステムコール、対応するフックされる関数、およびそれらの潜在的な目的をまとめたものです。

cleanup_module()関数を見ると、unregister_ftrace_function()関数を使用してftrace()フッキングメカニズムが元に戻されていることがわかります。これにより、コールバックが呼び出されなくなったことが保証されます。その後、すべてのシステムコールは、フックされたシステムコールではなく、元のシステムコールを指すように返されます。これにより、フックされたすべてのシステムコールの概要が明確になります。

次のセクションでは、フックされたシステムコールのいくつかを詳しく見ていきます。

LKM ルートキットの概要: rmdir_hook()

カーネルモジュールの rmdir_hook() は、ルートキットの機能において重要な役割を果たし、隠蔽と制御のためのディレクトリ削除操作を操作できるようにします。このフックは、単に rmdir() システムコールをインターセプトするだけでなく、その機能を拡張して、権限昇格を強制し、特定のディレクトリ内に保存されている設定の詳細を取得します。

このフックには、いくつかのチェックがあります。フックは、 rmdir() システムコールの最初の文字が zaryaであると想定しています。この条件が満たされると、フックされた関数は、実行されるコマンドである 6 番目の文字をチェックします。最後に、実行中のコマンドのプロセス引数を含むことができる 8 番目の文字がチェックされます。構造は次のようになります: zarya[char][command][char][argument]。任意の特殊文字 (またはなし) は、 zarya とコマンドおよび引数の間に配置できます。

公開日現在、次のコマンドを確認しています。

COMMAND目的
zarya.c.0Retrieve the config
zarya.t.0動作をテストします
zarya.k.<pid>PID を非表示にする
zarya.v.0実行中のバージョンを取得する

ルートキットの初期化時に、 rmdir() syscall フックを使用して、ルートキットが正常にロードされたかどうかを確認します。これを行うには、 t コマンドを呼び出します。

ubuntu-rk:~$ rmdir test
rmdir: failed to remove 'test': No such file or directory
ubuntu-rk:~$ rmdir zarya.t
ubuntu-rk:~$

存在しないディレクトリで rmdir コマンドを使用すると、「No such file or directory」というエラー・メッセージが返されます。zarya.trmdir を使用すると、カーネル モジュールが正常に読み込まれたことを示す出力は返されません。

2 番目のコマンドは vで、実行中のルートキットのバージョンを取得するために使用されます。

ubuntu-rk:~$ rmdir zarya.v
rmdir: failed to remove '240513': No such file or directory

"failed to remove 'directory'" エラーに zarya.v が追加される代わりに、ルートキット バージョン 240513 が返されます。

3 番目のコマンドは cで、ルートキットの設定を出力します。

ubuntu-rk:~/testing$ ./dump_config "zarya.c"
rmdir: failed to remove '': No such file or directory
Buffer contents (hex dump):
7ffe9ae3a270  00 01 00 00 10 70 69 6e 67 5f 69 6e 74 65 72 76  .....ping_interv
7ffe9ae3a280  61 6c 5f 73 00 2c 01 00 00 10 73 65 73 73 69 6f  al_s.,....sessio
7ffe9ae3a290  6e 5f 74 69 6d 65 6f 75 74 5f 73 00 04 00 00 00  n_timeout_s.....
7ffe9ae3a2a0  10 63 32 5f 74 69 6d 65 6f 75 74 5f 73 00 c0 a8  .c2_timeout_s...
7ffe9ae3a2b0  00 00 02 74 61 67 00 08 00 00 00 67 65 6e 65 72  ...tag.....gener
7ffe9ae3a2c0  69 63 00 02 73 5f 61 30 00 15 00 00 00 72 68 65  ic..s_a0.....rhe
7ffe9ae3a2d0  6c 2e 6f 70 73 65 63 75 72 69 74 79 31 2e 61 72  l.opsecurity1.ar
7ffe9ae3a2e0  74 00 02 73 5f 70 30 00 05 00 00 00 38 34 34 33  t..s_p0.....8443
7ffe9ae3a2f0  00 02 73 5f 63 30 00 04 00 00 00 74 6c 73 00 02  ..s_c0.....tls..
7ffe9ae3a300  73 5f 61 31 00 14 00 00 00 73 65 63 2e 6f 70 73  s_a1.....sec.ops
7ffe9ae3a310  65 63 75 72 69 74 79 31 2e 61 72 74 00 02 73 5f  ecurity1.art..s_
7ffe9ae3a320  70 31 00 05 00 00 00 38 34 34 33 00 02 73 5f 63  p1.....8443..s_c
7ffe9ae3a330  31 00 04 00 00 00 74 6c 73 00 02 73 5f 61 32 00  1.....tls..s_a2.
7ffe9ae3a340  0e 00 00 00 38 39 2e 32 33 2e 31 31 33 2e 32 30  ....89.23.113.20
7ffe9ae3a350  34 00 02 73 5f 70 32 00 05 00 00 00 38 34 34 33  4..s_p2.....8443
7ffe9ae3a360  00 02 73 5f 63 32 00 04 00 00 00 74 6c 73 00 00  ..s_c2.....tls..

ペイロードは null バイトで始まるため、rmdir シェル コマンドで zarya.c を実行しても出力は返されません。システムコールをラップし、hex/ASCII 表現を出力する小さな C プログラムを書くことで、返されるルートキットの設定を確認できます。

ルートキットは、ほとんどのルートキットのように kill() システムコールを使用してルート権限を取得する代わりに、この目的のために rmdir() システムコールも利用します。ルートキットは、 prepare_creds 関数を使用して資格情報関連の ID を 0 (ルート) に変更し、この変更された構造体で commit_creds を呼び出して、現在のプロセス内でルート権限を取得します。

この機能をトリガーするには、6番目の文字をに設定する必要があります 0。このフックの注意点は、呼び出し元のプロセスにルート権限を与えるが、それらを維持しないことです。zarya.0を実行しても何も起こりません。ただし、このフックを C プログラムで呼び出し、現在のプロセスの権限を印刷すると、結果が得られます。使用されるラッパー コードのスニペットを次に示します。

[...]
// Print the current PID, SID, and GID
pid_t pid = getpid();
pid_t sid = getsid(0);  // Passing 0 gets the SID of the calling process
gid_t gid = getgid();

printf("Current PID: %d, SID: %d, GID: %d\n", pid, sid, gid);

// Print all credential-related IDs
uid_t ruid = getuid();    // Real user ID
uid_t euid = geteuid();   // Effective user ID
gid_t rgid = getgid();    // Real group ID
gid_t egid = getegid();   // Effective group ID
uid_t fsuid = setfsuid(-1);  // Filesystem user ID
gid_t fsgid = setfsgid(-1);  // Filesystem group ID

printf("Credentials: UID=%d, EUID=%d, GID=%d, EGID=%d, FSUID=%d, FSGID=%d\n",
    ruid, euid, rgid, egid, fsuid, fsgid);

[...]

関数を実行すると、次の出力が得られます。

ubuntu-rk:~/testing$ whoami;id
ruben
uid=1000(ruben) gid=1000(ruben) groups=1000(ruben),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),117(lxd)

ubuntu-rk:~/testing$ ./rmdir zarya.0
Received data:
zarya.0
Current PID: 41838, SID: 35117, GID: 0
Credentials: UID=0, EUID=0, GID=0, EGID=0, FSUID=0, FSGID=0

このフックを活用するために、 rmdir zarya.0 コマンドを実行し、 /etc/shadow ファイルにアクセスできるかどうかを確認する小さな C ラッパー スクリプトを作成しました。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <errno.h>

int main() {
    const char *directory = "zarya.0";

    // Attempt to remove the directory
    if (syscall(SYS_rmdir, directory) == -1) {
        fprintf(stderr, "rmdir: failed to remove '%s': %s\n", directory, strerror(errno));
    } else {
        printf("rmdir: successfully removed '%s'\n", directory);
    }

    // Execute the `id` command
    printf("\n--- Running 'id' command ---\n");
    if (system("id") == -1) {
        perror("Failed to execute 'id'");
        return 1;
    }

    // Display the contents of /etc/shadow
    printf("\n--- Displaying '/etc/shadow' ---\n");
    if (system("cat /etc/shadow") == -1) {
        perror("Failed to execute 'cat /etc/shadow'");
        return 1;
    }

    return 0;
}

成功して。

ubuntu-rk:~/testing$ ./get_root
rmdir: successfully removed 'zarya.0'

--- Running 'id' command ---
uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),117(lxd),1000(ruben)

--- Displaying '/etc/shadow' ---
root:*:19430:0:99999:7:::
[...]

rmdir()関数で使用できるコマンドは他にもありますが、ここでは次のコマンドに進み、将来のパブリケーションに追加する可能性があります。

LKM ルートキットの概要: getdents() と getdents64() フック

ルートキットの getdents_hook()getdents64_hook() は、ディレクトリ リスト システムコールを操作して、ユーザーとディレクトリを隠す役割を担います。

getdents() と getdents64() システムコールは、ディレクトリエントリを読み取るために使用されます。ルートキットは、これらの関数をフックして、特定の条件に一致するエントリを除外します。具体的には、プレフィックス zov_ が付いたファイルやディレクトリは、ディレクトリの内容を一覧表示しようとするユーザーから非表示になります。

いくつか例をご紹介します。

ubuntu-rk:~/getdents_hook$ mkdir zov_hidden_dir

ubuntu-rk:~/getdents_hook$ ls -lah
total 8.0K
drwxrwxr-x  3 ruben ruben 4.0K Dec  9 11:11 .
drwxr-xr-x 11 ruben ruben 4.0K Dec  9 11:11 ..

ubuntu-rk:~/getdents_hook$ echo "this file is now hidden" > zov_hidden_dir/zov_hidden_file

ubuntu-rk:~/getdents_hook$ ls -lah zov_hidden_dir/
total 8.0K
drwxrwxr-x 2 ruben ruben 4.0K Dec  9 11:11 .
drwxrwxr-x 3 ruben ruben 4.0K Dec  9 11:11 ..

ubuntu-rk:~/getdents_hook$ cat zov_hidden_dir/zov_hidden_file
this file is now hidden

ここでは、ファイル zov_hidden にパス全体を使用して直接アクセスできます。ただし、 ls コマンドを実行すると、ディレクトリ一覧に表示されません。

ステージ4:キツネSOの概要

ルートキットを深く掘り下げると、カーネルオブジェクトファイル内に別のELFファイルが特定されました。このバイナリを抽出した後、これが /lib64/libs.so ファイルであることがわかりました。調べてみると、 Kitsune PID %ldなどの文字列への言及がいくつか見つかりました。これは、SOが開発者によってキツネと呼ばれていることを示唆しています。キツネは、ルートキットで観察される特定の行動に関与している可能性があります。これらの参照は、ルートキットが LD_PRELOADを介してユーザー空間のインタラクションを操作する方法の広範なコンテキストと一致しています。

この SO ファイルは、このルートキットの中心となる永続性とステルス メカニズムを実現する役割を果たし、攻撃チェーンへの統合は、その設計の洗練度を示しています。ここからは、攻撃チェーンの各部分を検出および/または防止する方法を紹介します。

PUMAKIT実行チェーンの検出と防止

このセクションでは、PUMAKIT 実行チェーンのさまざまな部分を防止および検出できるさまざまな EQL/KQL ルールと YARA シグネチャが表示されます。

ステージ1:クロン

ドロッパーを実行すると、一般的でないイベントが syslog に保存されます。このイベントは、プロセスが実行可能スタックで開始されたことを示します。これは珍しく、見ていて面白いです。

[  687.108154] process '/home/ruben_groenewoud/30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f' started with executable stack

これは、次のクエリで検索できます。

host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message: "started with executable stack"

このメッセージは、 /var/log/messages または /var/log/syslogに格納されます。これは、 Filebeat またはElasticエージェント システムインテグレーションを通じてsyslogを読み取ることで検出できます。

ステージ 2: メモリ常駐実行可能ファイル

すぐに異常なファイル記述子の実行を確認できます。これは、次のEQLクエリを使用して検出できます。

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and process.parent.executable like "/dev/fd/*" and not process.parent.command_line == "runc init"

このファイルディスクリプターは、プロセスが終了するまでドロッパーの親のままであり、その結果、この親プロセスを通じていくつかのファイルも実行されます。

file where host.os.type == "linux" and event.type == "creation" and process.executable like "/dev/fd/*" and file.path like (
  "/boot/*", "/dev/shm/*", "/etc/cron.*/*", "/etc/init.d/*", "/var/run/*"
  "/etc/update-motd.d/*", "/tmp/*", "/var/log/*", "/var/tmp/*"
)

/tmp/script.shがドロップされた後(上記のクエリを通じて検出)、ファイル属性の検出とアーカイブ解除アクティビティをクエリすることで、その実行を検出できます。

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and 
(process.parent.args like "/boot/*" or process.args like "/boot/*") and (
  (process.name in ("file", "unlzma", "gunzip", "unxz", "bunzip2", "unzstd", "unzip", "tar")) or
  (process.name == "grep" and process.args == "ELF") or
  (process.name in ("lzop", "lz4") and process.args in ("-d", "--decode"))
) and
not process.parent.name == "mkinitramfs"

スクリプトは、 tail コマンドを使用して Linux カーネル イメージのメモリを引き続き検索します。これは、他のメモリシーキングツールと共に、次のクエリを使用して検出できます。

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and
(process.parent.args like "/boot/*" or process.args like "/boot/*") and (
  (process.name == "tail" and (process.args like "-c*" or process.args == "--bytes")) or
  (process.name == "cmp" and process.args == "-i") or
  (process.name in ("hexdump", "xxd") and process.args == "-s") or
  (process.name == "dd" and process.args : ("skip*", "seek*"))
)

/tmp/script.shの実行が完了すると、/memfd:tgt (deleted)/memfd:wpn (deleted)が作成されます。tgt実行可能ファイル (良性の Cron 実行可能ファイル) は、/run/crond.pid ファイルを作成します。これは悪意のあるものではなく、単純なクエリで検出できるアーティファクトです。

file where host.os.type == "linux" and event.type == "creation" and file.extension in ("lock", "pid") and
file.path like ("/tmp/*", "/var/tmp/*", "/run/*", "/var/run/*", "/var/lock/*", "/dev/shm/*") and process.executable != null

wpn実行可能ファイルは、すべての条件が満たされた場合、LKMrootkitをロードします。

ステージ 3: ルートキット カーネル モジュール

カーネル・モジュールのロードは、Auditd Manager で以下の設定を適用することで検出できます。

-a always,exit -F arch=b64 -S finit_module -S init_module -S delete_module -F auid!=-1 -k modules
-a always,exit -F arch=b32 -S finit_module -S init_module -S delete_module -F auid!=-1 -k modules

そして、次のクエリを使用します。

driver where host.os.type == "linux" and event.action == "loaded-kernel-module" and auditd.data.syscall in ("init_module", "finit_module")

AuditdとElasticセキュリティを活用してLinux検出エンジニアリングエクスペリエンスを向上させる方法の詳細については、Elastic Security Labsサイトに掲載されている 「Auditdを使用したLinux検出エンジニアリング 」の調査をご覧ください。

初期化時に、LKM はカーネルが署名されていないため、カーネルを汚染します。

audit: module verification failed: signature and/or required key missing - tainting kernel

この動作は、次の KQL クエリを使用して検出できます。

host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message:"module verification failed: signature and/or required key missing - tainting kernel"

また、LKM には欠陥のあるコードがあり、セグメンテーション違反が数回発生します。例えば:

Dec  9 13:26:10 ubuntu-rk kernel: [14350.711419] cat[112653]: segfault at 8c ip 00007f70d596b63c sp 00007fff9be81360 error 4
Dec  9 13:26:10 ubuntu-rk kernel: [14350.711422] Code: 83 c4 20 48 89 d0 5b 5d 41 5c c3 48 8d 42 01 48 89 43 08 0f b6 02 41 88 44 2c ff eb c1 8b 7f 78 e9 25 5c 00 00 c3 41 54 55 53 <8b> 87 8c 00 00 00 48 89 fb 85 c0 79 1b e8 d7 00 00 00 48 89 df 89

これは、 kern.log ファイル内の segfault を照会する単純な KQL クエリを使用して検出できます。

host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message:segfault

カーネルモジュールがロードされると、 kthreadd プロセスを通じてコマンド実行のトレースを確認できます。ルートキットは、特定のコマンドを実行するための新しいカーネルスレッドを作成します。たとえば、ルートキットは次のコマンドを短い間隔で実行します。

cat /dev/null
truncate -s 0 /usr/share/zov_f/zov_latest

これらのコマンドや疑わしい可能性のあるコマンドは、次のようなクエリを通じて検出できます。

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and process.parent.name == "kthreadd" and (
  process.executable like ("/tmp/*", "/var/tmp/*", "/dev/shm/*", "/var/www/*", "/bin/*", "/usr/bin/*", "/usr/local/bin/*") or
  process.name in ("bash", "dash", "sh", "tcsh", "csh", "zsh", "ksh", "fish", "whoami", "curl", "wget", "id", "nohup", "setsid") or
  process.command_line like (
    "*/etc/cron*", "*/etc/rc.local*", "*/dev/tcp/*", "*/etc/init.d*", "*/etc/update-motd.d*",
    "*/etc/ld.so*", "*/etc/sudoers*", "*base64 *", "*base32 *", "*base16 *", "*/etc/profile*",
    "*/dev/shm/*", "*/etc/ssh*", "*/home/*/.ssh/*", "*/root/.ssh*" , "*~/.ssh/*", "*autostart*",
    "*xxd *", "*/etc/shadow*"
  )
) and not process.name == "dpkg"

また、ルートキットが特権を昇格させる方法を検出するには、 rmdir コマンドで異常なUID/GIDの変更を分析することもできます。

process where host.os.type == "linux" and event.type == "change" and event.action in ("uid_change", "guid_change") and process.name == "rmdir"

実行チェーンによっては、他のいくつかの動作ルールもトリガーされる場合があります。

それらすべてを支配する1つのYARA署名

Elasticセキュリティは、PUMAKIT(ドロッパー(cron)、ルートキットローダー(/memfd:wpn)、LKMルートキット、Kitsune共有オブジェクトファイルを識別するためのYARAシグネチャを作成しました。署名は以下に表示されます。

rule Linux_Trojan_Pumakit {
    meta:
        author = "Elastic Security"
        creation_date = "2024-12-09"
        last_modified = "2024-12-09"
        os = "Linux"
        arch = "x86, arm64"
        threat_name = "Linux.Trojan.Pumakit"

    strings:
        $str1 = "PUMA %s"
        $str2 = "Kitsune PID %ld"
        $str3 = "/usr/share/zov_f"
        $str4 = "zarya"
        $str5 = ".puma-config"
        $str6 = "ping_interval_s"
        $str7 = "session_timeout_s"
        $str8 = "c2_timeout_s"
        $str9 = "LD_PRELOAD=/lib64/libs.so"
        $str10 = "kit_so_len"
        $str11 = "opsecurity1.art"
        $str12 = "89.23.113.204"
    
    condition:
        4 of them
}

観測

この研究では、次の観測量について議論しました。

すぐれた監視性タイプ名前参考
30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1fSHA256cronPUMAKITスポイト
cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfeSHA256/memfd:wpn (deleted)PUMAKITローダー
934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136SHA256/memfd:tgt (deleted)cronバイナリ
8ef63f9333104ab293eef5f34701669322f1c07c0e44973d688be39c94986e27SHA256libs.soKitsune 共有オブジェクト参照
8ad422f5f3d0409747ab1ac6a0919b1fa8d83c3da43564a685ae4044d0a0ea03SHA256some2.elfプマキットバリアント
bbf0fd636195d51fb5f21596d406b92f9e3d05cd85f7cd663221d7d3da8af804SHA256some1.soKitsune共有オブジェクトバリアント
bc9193c2a8ee47801f5f44beae51ab37a652fda02cd32d01f8e88bb793172491SHA256puma.koLKM rootkit
1aab475fb8ad4a7f94a7aa2b17c769d6ae04b977d984c4e842a61fb12ea99f58SHA256kitsune.soキツネ
sec.opsecurity1[.]artドメイン名PUMAKIT C2サーバー
rhel.opsecurity1[.]artドメイン名PUMAKIT C2サーバー
89.23.113[.]204IPv4-アドレスPUMAKIT C2サーバー

結びの言葉

PUMAKITは、システムコールフック、メモリ常駐型の実行、独自の権限昇格方法などの高度な手法を使用する、複雑でステルス性の高い脅威です。そのマルチアーキテクチャ設計は、Linuxシステムを標的とするマルウェアの高度化を浮き彫りにしています。

Elasticセキュリティラボは、PUMAKITの分析、その動作の監視、アップデートや新しい亜種の追跡を続けます。検出方法を改良し、実用的な洞察を共有することで、防御者が一歩先を行くことを目指しています。

この記事を共有する