インシデントレスポンスの現場で最も背筋が凍る瞬間を知っているか?それは、「サーバーのプロセス一覧を見ても、ポートのリスン状況を確認しても、一切の痕跡がないのに、明らかに外部へのC2(Command and Control)通信が発生している」と検知アラートが鳴り響いたときだ。
こんにちは。セキュリティチームのチーフエンジニアだ。日々、巧妙化するサイバー攻撃の痕跡を追いかけ、彼らの足跡を暴き出すのが我々の仕事だ。
表面上のログやコマンド結果を信用するな。特にLinux環境において、侵入に成功した高度な攻撃者は、ps や netstat、さらには lsmod といった標準的な管理コマンドが依存するユーザーランド(Userland)のAPIを平然とフックし、自分たちの存在を完璧に隠蔽してくる。
今回は、そんな「見えない脅威」を暴くための最終兵器、Volatility 3を用いたLinuxカーネルオブジェクトの抽出と解析について、実戦の現場で培ったノウハウを余すところなく伝授しよう。
—
なぜ標準コマンドは欺瞞(ぎまん)されるのか?
初心者のエンジニアやジュニアアナリストは、不正アクセスの調査を頼むと、決まって侵害されたサーバーにSSHでログインし、ps aux や top を叩きたがる。だが、プロのインシデントレスポンスでは、ライブ(稼働中)のOS上で標準コマンドを信頼して調査を行うことは「自殺行為」に等しい。
攻撃者がカーネル空間(Kernel Space)へのアクセス権(ルート権限)を掌握した場合、彼らは悪意あるカーネルモジュール(Rootkit)をロードするか、あるいは直接カーネルメモリ上の構造体を書き換える。
例えば、Linuxのプロセス管理の根幹である task_struct という双方向連結リスト(Double-linked list)がある。OSはこのリストを辿ることで実行中のプロセスを認識している。
Rootkitは、この task_struct のリストから自分自身のノードを外す(Unlinkする)。リストから外されたプロセスは、OSのスケジューラにはスケジュールされ続けるが、ps コマンドなどが参照するプロセス一覧からは綺麗に姿を消すことになる。
ユーザーランドのツールがどれだけ目をこすっても、カーネル自体が「そこには何もいない」と嘘をついている以上、見つけることはできない。だからこそ、OSの足元を丸ごと切り取った「メモリダンプ(Core Dump / Memory Image)」を取得し、カーネルの生データを直接解析するメモリフォレンジックが必要なのだ。
—
Volatility 3とシンボルテーブルの要件
メモリフォレンジックのデファクトスタンダードである Volatility 3 は、Pythonで書かれた強力なフレームワークだ。しかし、ここで多くの人間がハマる罠がある。「メモリイメージを読み込ませたが、カーネルの構造体が解釈できない」というエラーだ。
Linuxのカーネル解析において、Volatility 3は、解析対象のカーネルバージョンやコンパイルオプション(DWARF情報)に完全に一致したシンボルテーブル(Symbol Table / Intermediate Representation)を必要とする。これがなければ、単なるバイナリの塊から task_struct のオフセット(構造体内の各メンバの位置)を特定することは不可能に近い。
近年のVolatility 3では、ISF(Intermediate Symbol Format)ファイルを使用する。公式が提供しているもの、あるいはターゲットのカーネルに合わせて自分でビルドしたものを用意し、Volatilityの symbols ディレクトリに配置しておく必要がある。ここを怠ると、調査の初手で完全に詰む。
—
隠蔽されたプロセスとモジュールをあぶり出す実践手順
では、実際にインシデントレスポンスの現場でどのようにコマンドを叩くのか、その手順を解説しよう。
1. プロセスリストの総覧と不整合の検知
まずは、通常の ps コマンドが隠蔽するプロセスを暴く。Volatility 3には、複数の手法でプロセスを列挙するプラグインが用意されている。
# 標準的な task_struct の双方向リストを辿るプラグイン
python3 vol.py -f mem_dump.raw linux.pslist
# プロセスツリー構造で確認する
python3 vol.py -f mem_dump.raw linux.pstree
しかし、これらは隠蔽(Direct Kernel Object Manipulation: DKOM)されている場合、綺麗にスルーされることがある。そこで真価を発揮するのが、メモリ上のカーネルプールをスキャンしてプロセス構造体の断片を強制的に見つけ出すプラグインだ。
# カーネルメモリを総当たりでスキャンし、task_struct の兆候を暴く
python3 vol.py -f mem_dump.raw linux.psscan
もし linux.pslist の出力結果と、linux.psscan の出力結果に「数」や「PIDの不一致」があった場合、それは高確率でRootkitによるプロセス隠蔽(Hiding)が行われている決定的な証拠となる。リストから外されたゴーストプロセスがそこにいる。
2. ロードされた悪意あるカーネルモジュールの特定
プロセスだけでなく、攻撃者はカーネルモジュール(LKM: Loadable Kernel Module)を隠蔽してバックドアを維持することが多い。lsmod コマンドで何も表示されなくても、メモリ上にはひそかに動いているモジュールが存在する。
これを暴くのが linux.lsmod プラグインだ。
# ロードされているカーネルモジュールのリストを抽出
python3 vol.py -f mem_dump.raw linux.lsmod
このプラグインは、カーネル内のモジュールリスト(modules)を走査するだけでなく、非公式な方法でロードされたり、リストから巧妙にアンリンクされたりしたモジュールの痕跡を炙り出すために、メモリ上のセクションをスキャンする。不審な名前のモジュールや、署名の検証できない不明なモジュールを発見したら、即座にそのバイナリをディスク上に抽出する必要がある。
—
悪意あるカーネルモジュール(LKM)の抽出と解析
メモリ上にある不審なモジュールやプロセスの実体を、フォレンジック担当者は手元に引き剥がして解析しなければならない。Volatility 3のダンピング機能を使って、該当するオブジェクトをファイルとして抽出する。
# 特定のプロセスのアドレス空間(メモリ領域)をダンプする
python3 vol.py -f mem_dump.raw -o ./output/ linux.memmap --pid <不審なPID>
# または、カーネルモジュールのバイナリそのものを抽出する
python3 vol.py -f mem_dump.raw -o ./output/ linux.moddump --mod <モジュール名/アドレス>
抽出したバイナリは、静的解析ツール(GhidraやIDA Pro)に放り込み、どのようなシステムコール(sys_call_table の書き換えなど)をフックしているかをリバースエンジニアリングすることになる。これがインシデントハンドリングにおける真相究明の泥臭くも重要なプロセスだ。
—
完全に防御するためのセキュアな設計と設定
さて、フォレンジックの手法を学んだところで、実務において「そもそもこのようなカーネルレベルの侵害をどうやって防ぐのか」という予防策に話を移そう。どれだけ高度なフォレンジックができようとも、インシデントを未然に防ぐに越したことはない。
攻撃者がカーネルモジュールをロードしたり、メモリを改ざんしたりするためには、前提条件として「root権限の奪取」と「モジュールロードの許可」が必要だ。ここを鉄壁の要塞に仕立て上げるための具体的な設定を紹介する。
1. Linuxカーネルモジュールのロード制限(sysctl設定)
本番環境のサーバーにおいて、通常運用時に後から勝手にカーネルモジュールを追加する正当な理由はほとんどない。モジュールの動的ロードを無効化、あるいは厳格に制限することが、Rootkit対策の第一歩だ。
/etc/sysctl.d/99-security.conf などの設定ファイルに以下のパラメータを記述し、適用せよ。
# /etc/sysctl.d/99-security.conf
# カーネルモジュールの動的なロードを完全に禁止する(※運用上必要な場合は慎重に検討)
kernel.modules_disabled = 1
# kexec を無効化し、メモリ上から別のカーネルに不正に置き換えられるのを防ぐ
kernel.kexec_load_disabled = 1
# システムのセキュリティを強化するためのその他のカーネル保護設定
# 厳格なmmapのパターンの制限
vm.unprivileged_userfaultfd = 0
> チーフからの注意点: kernel.modules_disabled = 1 を有効化すると、再起動するまで新しいモジュール(NICのドライバやコンテナ用のネットワークドライバなど)の一切の追加ロードができなくなる。本番デプロイ時は、インフラの構築が完全に完了し、必要なモジュールがすべてロードされていることを確認した上で適用すること。
2. セキュアブート(Secure Boot)とKernel Lockdownの強制
ソフトウェアレベルの対策だけでは、root権限を取られた瞬間にカーネルは突破される。ハードウェアの根幹から信頼性を担保する必要がある。
- UEFI セキュアブートの有効化:
BIOS/UEFIレベルでセキュアブートを有効化し、未署名のカーネルや悪意あるブートローダー、不正なカーネルモジュールの読み込みをハードウェアレベルで阻止する。
- Kernel Lockdown機能の有効化:
現代のLinuxカーネル(v5.4以降など)には「Lockdown」モードがある。これが有効になると、たとえrootであっても、実行中のカーネルメモリの直接的な読み書き(/dev/mem や /dev/kmem へのアクセス)や、未署名のモジュールロードが厳格に禁止される。
ブートローダー(GRUBなど)の設定ファイルを確認し、ロックダウンモードが有効になっているかチェックせよ。
# 現在のカーネルのロックダウン状態を確認するコマンド
cat /sys/kernel/security/lockdown
これが [integrity] または [confidentiality] に設定されていれば、root権限を持った攻撃者であっても、カーネルメモリの直接改ざんや一般的なRootkitの埋め込みが極めて困難になる。
—
最後に:インシデントは「起きる前提」で備えろ
どれほど強固なセキュリティを組もうとも、ゼロデイ脆弱性やサプライチェーン攻撃、あるいはアプリケーション層の深刻な脆弱性(RCEなど)を突かれて侵入を許すリスクは常にゼロにはならない。
だからこそ、インシデントが発生した際に、慌てて ps コマンドを叩いて「何も異常ありません」と誤認するようなジュニアエンジニアを育ててはならない。
「システムは嘘をつくが、メモリは嘘をつかない」。
この鉄則を胸に刻み、有事の際には躊躇なくメモリイメージを採取し、Volatility 3を駆使して隠蔽された脅威の尻尾を掴み取れる体制を、平時のうちに構築しておくことだ。
それが、プロのセキュリティエンジニアの仕事である。
コメント