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マルウェアサンプルのメインロジックハンドラとして機能します。その主な目標は次のとおりです。
- 特定のキーワード (
"Huinder"
) のコマンドライン引数を確認します。 - 見つからない場合は、隠しペイロードをファイルシステムにドロップせずに、メモリから完全に埋め込んで実行します。
- 見つかった場合は、特定の「抽出」引数を処理して、埋め込まれたコンポーネントをディスクにダンプし、正常に終了します。
要するに、マルウェアはステルス性を保とうとします。通常 (特定の引数なし) を実行すると、ディスクにトレースを残さずに非表示の 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()
、サポートされている圧縮形式の署名に基づいて、gunzip
、unxz
、bunzip2
などのさまざまなユーティリティを使用して、特定のファイルを解凍しようとします。grep
とtail
を使用して、特定の圧縮セグメントを特定して抽出します。 - このスクリプトは、ファイル (
$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_function
と ftrace_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.0 | Retrieve 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.t
で rmdir
を使用すると、カーネル モジュールが正常に読み込まれたことを示す出力は返されません。
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
}
観測
この研究では、次の観測量について議論しました。
すぐれた監視性 | タイプ | 名前 | 参考 |
---|---|---|---|
30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f | SHA256 | cron | PUMAKITスポイト |
cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfe | SHA256 | /memfd:wpn (deleted ) | PUMAKITローダー |
934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136 | SHA256 | /memfd:tgt (deleted) | cronバイナリ |
8ef63f9333104ab293eef5f34701669322f1c07c0e44973d688be39c94986e27 | SHA256 | libs.so | Kitsune 共有オブジェクト参照 |
8ad422f5f3d0409747ab1ac6a0919b1fa8d83c3da43564a685ae4044d0a0ea03 | SHA256 | some2.elf | プマキットバリアント |
bbf0fd636195d51fb5f21596d406b92f9e3d05cd85f7cd663221d7d3da8af804 | SHA256 | some1.so | Kitsune共有オブジェクトバリアント |
bc9193c2a8ee47801f5f44beae51ab37a652fda02cd32d01f8e88bb793172491 | SHA256 | puma.ko | LKM rootkit |
1aab475fb8ad4a7f94a7aa2b17c769d6ae04b977d984c4e842a61fb12ea99f58 | SHA256 | kitsune.so | キツネ |
sec.opsecurity1[.]art | ドメイン名 | PUMAKIT C2サーバー | |
rhel.opsecurity1[.]art | ドメイン名 | PUMAKIT C2サーバー | |
89.23.113[.]204 | IPv4-アドレス | PUMAKIT C2サーバー |
結びの言葉
PUMAKITは、システムコールフック、メモリ常駐型の実行、独自の権限昇格方法などの高度な手法を使用する、複雑でステルス性の高い脅威です。そのマルチアーキテクチャ設計は、Linuxシステムを標的とするマルウェアの高度化を浮き彫りにしています。
Elasticセキュリティラボは、PUMAKITの分析、その動作の監視、アップデートや新しい亜種の追跡を続けます。検出方法を改良し、実用的な洞察を共有することで、防御者が一歩先を行くことを目指しています。