コンテナの深淵を覗く:ファイルレス攻撃と「メモリの痕跡」を追い詰める技術
こんにちは。インシデントレスポンスの現場にいると、よく「ディスクに何も落ちていないのに、なぜか不審な通信が発生している」という相談を受けます。
現代の攻撃者は賢い。彼らはコンテナの軽量な環境を逆手に取り、ディスクへの書き込みを一切行わない「ファイルレス攻撃」を好みます。コンテナのメモリ上で動くマルウェアは、再起動すれば消えます。しかし、逆に言えば、再起動するまでの間、彼らはメモリという舞台でやりたい放題できるということです。
今日は、コンテナ環境におけるメモリフォレンジックの勘所と、それを防ぐための泥臭い実装について、現場の視点から解説します。
—
1. ファイルレス攻撃の「盲点」を突く
攻撃者がコンテナ内でよくやる手口は、memfd_create を利用したメモリ上でのバイナリ実行です。これは、ディスク上にファイルを作成せずにメモリ内で匿名ファイルを作成し、それを実行する手法です。
従来のウイルス対策ソフトやEDRが「ファイルパス」を監視していても、この攻撃は「メモリ上の匿名領域」で完結するため、シグネチャベースの検知をすり抜けます。
なぜコンテナは危ないのか?
コンテナは「プロセス=アプリケーション」という思想が強いため、メモリ空間の隔離が甘い設定だと、攻撃者が一度侵入したコンテナから、ホスト側のメモリを覗こうとする試み(あるいはその逆)が発生しやすくなります。
—
2. 実務で使える検知と防御の「黄金セット」
メモリ上の異常を完璧に検知するのは困難ですが、「不審な動作を許さない」環境を作ることは可能です。
A. Linuxカーネルの制限(セキュアなコンテナ設定)
まず、コンテナ内で不用意なメモリ操作やシステムコールを許さないことが第一歩です。Docker実行時に、seccomp プロファイルを適用して、memfd_create などの危険なシステムコールをブロックします。
seccomp-profile.json の設定例:
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"name": "memfd_create",
"action": "SCMP_ACT_ERRNO" // memfd_createを試みるとエラーを返す
}
]
}
これを --security-opt seccomp=seccomp-profile.json でコンテナ実行時に適用するだけで、ファイルレス攻撃の足場を大きく削ぐことができます。
B. Pythonによる「メモリ割り当て」の監視
もしあなたがアプリケーション層でメモリ消費を監視したいなら、Pythonの psutil を使って、特定のプロセスが異常なメモリ割り当て(急激なヒープ増大)をしていないか監視するサイドカーコンテナを走らせるのが有効です。
memory_monitor.py (監視スクリプト例):
import psutil
import time
# 異常なメモリ使用量の閾値(MB)
THRESHOLD = 500
def monitor_process(pid):
try:
process = psutil.Process(pid)
mem_info = process.memory_info().rss / (1024 * 1024)
if mem_info > THRESHOLD:
# ここでSlack通知やコンテナの強制終了を行う
print(f"警告: プロセス {pid} が異常なメモリ消費を検出: {mem_info:.2f} MB")
except psutil.NoSuchProcess:
pass
# メインループ
while True:
# 監視対象のPIDを指定(例: アプリケーションのメインプロセス)
monitor_process(1)
time.sleep(10)
—
3. Webアプリエンジニアができる「防御の砦」
メモリにマルウェアをロードさせるための「入口」を塞ぐのも重要です。PHPやNode.js環境では、eval() や exec() といった関数を禁じ手にしてください。
PHPでのセキュアな設定例 (php.ini):
; 攻撃者がメモリ上でスクリプトを動かすための関数を無効化
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_multi_exec,parse_ini_file,show_source
また、JavaScript(Node.js)で child_process を使う際は、必ず入力をサニタイズし、パスの直打ちを避けること。
Node.jsでの防御的実装例:
const { execFile } = require('child_process');
// ユーザー入力から直接コマンドを組み立てない
const allowedCommands = ['/usr/bin/git', '/usr/bin/ls'];
const command = '/usr/bin/git';
const args = ['status']; // ユーザー入力ではなく定数から生成する
if (allowedCommands.includes(command)) {
execFile(command, args, (error, stdout, stderr) => {
if (error) {
console.error(`実行エラー: ${error.message}`);
return;
}
console.log(stdout);
});
}
—
最後に:フォレンジックは「ログ」から始まる
どんなに堅牢な防御を敷いても、ゼロデイ攻撃は防ぎきれないことがあります。その時のために、コンテナのメモリダンプを定期的に取得し、保存しておく運用を推奨します。
gcore コマンドで実行中のプロセスのメモリイメージをファイルに落とすことができます。
# 特定のプロセスIDのメモリダンプを取得(調査用に保存しておく)
gcore -o /tmp/dump_$(date +%Y%m%d) <PID>
現場で最も頼りになるのは、攻撃された瞬間の「生のデータ」です。教科書的な知識だけでなく、こうした泥臭い収集作業こそが、インシデント発生時にあなたをヒーローにする唯一の手段です。
準備はいいですか?次はあなたの番です。システムの深淵を覗き、彼らの足跡を見つけ出してください。
コメント