#証拠は3か所に残ります
検知のあとで「何が起きたか」を説明するには、記録が残っていて、しかも書き換えられていないことが要ります。Server Edition では、証拠が次の3か所に残ります。
| 場所 | 中身 | 書き換えの確かめ方 |
|---|---|---|
実行の記録 /var/lib/roamswitch/exec_log/ | 起動したプロセスの記録(1行1件) | sudo roamswitch events verify |
証拠のバンドル /var/lib/roamswitch/incident-evidence/ | ある時点のプロセスの状態 | sudo roamswitch-incident-capture verify <バンドル> |
| マシンの外(転送先) | 実行の記録の、チェックポイントのハッシュ | 手元の記録と見比べる |
この講座では、改ざん検知つきの記録 と 封じ込め時の証拠の自動採取 を、実際に書き換えてみて確かめます。最後に、これらの設定を SSH 越しに操作する ターミナルから全機能を操作 を使います。実行の記録の検索とプロセスのたどり方は、不審な実行とマルウェア で扱っています。
#実行の記録の改ざんを見つける
実行の記録は、区切り(セグメント)ごとのファイルです。各セグメントの先頭行は、前のセグメントの SHA-256 を持ちます。書き込み中のセグメントにも、新しい記録があれば 10 秒ごとにチェックポイントの行が入ります。チェックポイントは、それより前の全バイトの SHA-256 です。
設定は exec_recorder_hash_chain(既定 true)です。記録は既定で 200 MB か 14 日を上限に、古いものから消えます。
デーモンを止めて、記録のファイルを書き換える操作です(変更を伴う操作です)。練習用の環境で試してください。書き換える前にコピーを取り、最後に戻します。
cp /usr/bin/true /tmp/lab-marker
/tmp/lab-marker
sleep 15
sudo roamswitch events verify; echo "rc=$?"
S=$(sudo find /var/lib/roamswitch/exec_log -name 'seg-*.jsonl' | sort | tail -1)
echo "$S"
sudo systemctl stop roamswitch-server
sudo cp -a "$S" /root/seg.bak
sudo sed -i 's/lab-marker/lab-mxrker/' "$S"
sudo roamswitch events verify; echo "rc=$?"
sudo cp -a /root/seg.bak "$S"
sudo roamswitch events verify; echo "rc=$?"
sudo systemctl start roamswitch-server✓ ハッシュチェーンは正常です(1 セグメント)。改ざんは検知されていません(検知であり防止ではありません)。
最新のチェックポイントより後の 16 件は、次のチェックポイントまで検証の対象外です。
rc=0
/var/lib/roamswitch/exec_log/seg-0000000000000001.jsonl
✗ ハッシュチェーンが壊れています: セグメント 1 (チェックポイントより前の記録が、チェックポイントと合いません(書き換え・削除・並べ替えの疑い))。ログが改ざん・削除された可能性があります。
rc=2
✓ ハッシュチェーンは正常です(1 セグメント)。改ざんは検知されていません(検知であり防止ではありません)。
rc=0出力は、次のように読みます。
/tmp/lab-markerの記録の綴りを1文字変えただけで、verifyは「壊れています」と答えました。終了コードは 2 です。監視のスクリプトでは、この終了コードで判断できます- コピーを戻すと、正常に戻りました。書き換えの前にコピーを取っておけば、元の記録で確かめ直せます
- 「最新のチェックポイントより後の N 件」は、まだどのハッシュにも入っていない記録です。この部分を書き換えても、
verifyは気づきません - デーモンを止めたあとの
verifyには、この行が出ませんでした。systemctl stopで止めるとき、デーモンが最後のチェックポイントを書くためです
verify が確かめるのは、記録どうしの辻褄です。root の権限があれば、記録を消したうえで、チェックポイントをすべて計算し直せます。次の演習で、それを実際にやってみます。
転送の設定を変え、記録を書き換える操作です(変更を伴う操作です)。デーモンは、最新のチェックポイントのハッシュを、1 分に 1 回まで、EXEC-CHECKPOINT として転送先へ送ります。深刻度は info なので、forward_min_severity の既定の low では送られません。ここでは info にして、ファイルへ転送します。
sudo roamswitch server config set forward_file_path /var/log/roamswitch/events.jsonl
sudo roamswitch server config set forward_min_severity info
sudo roamswitch server config set forward_enabled true
sudo roamswitch server restart
sleep 3
/tmp/lab-marker
sleep 75
sudo grep EXEC-CHECKPOINT /var/log/roamswitch/events.jsonl | tail -1{"host":"ubuntu","event_id":"1a1cd6e705c-5027-2","ts":"2026-11-12T01:45:29.436Z","source":"exec_recorder","severity":"info","rule":"EXEC-CHECKPOINT","action":"checkpoint","pid":null,"proc":null,"summary":"segment 1 sha256 a8124af0a5bd6017f724c955be8a2f7302be8576ab1c7606e64070a4509c87ce"}次に、攻撃者のつもりで、lab-marker の記録を消し、チェックポイントを計算し直すスクリプトを書きます。
cat > /tmp/rewrite.py <<'EOF'
# 練習用: lab-marker を含む記録を消し、チェックポイントを計算し直す
import hashlib, json, sys
p = sys.argv[1]
lines = open(p, encoding="utf-8").read().splitlines(keepends=True)
lines = [l for l in lines if "lab-marker" not in l]
out, h, off = [], hashlib.sha256(), 0
for l in lines:
if l.startswith("{") and '"type":"checkpoint"' in l:
v = json.loads(l)
v["bytes"], v["hash"] = off, h.hexdigest()
l = json.dumps(v, separators=(",", ":"), sort_keys=True) + "\n"
out.append(l); h.update(l.encode()); off += len(l.encode())
open(p, "w", encoding="utf-8").write("".join(out))
EOF転送したハッシュが手元の記録にあるかを、書き換えの前と後で数えます。
S=$(sudo find /var/lib/roamswitch/exec_log -name 'seg-*.jsonl' | sort | tail -1)
H=$(sudo grep EXEC-CHECKPOINT /var/log/roamswitch/events.jsonl | tail -1 | sed 's/.*sha256 \([0-9a-f]*\).*/\1/')
sudo systemctl stop roamswitch-server
sudo cp -a "$S" /root/seg.bak
sudo grep -c "\"hash\":\"$H\"" "$S"
sudo python3 /tmp/rewrite.py "$S"
sudo grep -c lab-marker "$S"
sudo roamswitch events verify; echo "rc=$?"
sudo grep -c "\"hash\":\"$H\"" "$S"
sudo cp -a /root/seg.bak "$S"
sudo grep -c "\"hash\":\"$H\"" "$S"
sudo systemctl start roamswitch-server1
0
✓ ハッシュチェーンは正常です(1 セグメント)。改ざんは検知されていません(検知であり防止ではありません)。
rc=0
0
1順に読みます。
- 書き換える前は、転送したハッシュが、手元の記録のチェックポイントに 1 つありました
- スクリプトで
lab-markerの記録を消しました(残りは 0 件) - それでも
verifyは「正常」と答えました。チェックポイントをそろえ直したためです - 転送したハッシュは、手元の記録から消えました(0 件)。外に残したハッシュと合わないことで、書き換えがわかります
- コピーを戻すと、また見つかりました
- 実行の記録を、あとで証拠として使うなら、
infoにします。チェックポイントのハッシュがマシンの外に残ります。セグメントを閉じたときのEXEC-SEALもinfoです - 転送先の量を増やしたくないなら、
lowのままにします。そのときは、手元のverifyだけが頼りです。root を取られたマシンでは、手元の記録は書き換えられることがあります - 転送先は、このマシンの root が書き換えられない場所にします。演習のように同じマシンのファイルへ送るだけでは、両方を書き換えられます
#証拠のバンドルを取る
封じ込めが動くと、デーモンは roamswitch-incident-capture snapshot を裏で動かし、その時点の証拠を集めます。封じ込めを遅らせないよう、ふつうは取得の終わりを待ちません。プロセスを kill するときだけは、最大 3 秒待って、先にプロセスの状態を残します(どちらもコードで確かめました)。同じコマンドを、手で実行することもできます。プロセスを止める前に、状態を残しておきたいときに使います。
証拠のファイルを作る操作です(変更を伴う操作です)。練習用のプロセスに、ダミーの環境変数を2つ持たせます。LAB_API_TOKEN の値は、本物ではない文字列です。
cp /usr/bin/sleep /tmp/lab-worker
LAB_API_TOKEN=dummy-not-a-real-token LAB_COLOR=blue setsid /tmp/lab-worker 600 >/dev/null 2>&1 < /dev/null &
sleep 1
P=$(pgrep -x lab-worker); echo "PID=$P"
sudo roamswitch-incident-capture snapshot --pid "$P" --no-packets --reason "lab: practice capture"
B=$(sudo find /var/lib/roamswitch/incident-evidence -mindepth 1 -maxdepth 1 -type d | sort | tail -1)
sudo find "$B" -type f | sort
sudo grep LAB_ "$B/process/environ.txt"
sudo cat "$B/process/cmdline.txt"; echoPID=21480
証拠バンドルを保存しました: /var/lib/roamswitch/incident-evidence/20261112T014605Z-c38a513f-21493(取得 6 件、スキップ 0 件)
このフォルダーは root だけが開けます(中に認証情報やメモリーの内容が入りうるためです)。中を見るには: sudo ls -l /var/lib/roamswitch/incident-evidence/20261112T014605Z-c38a513f-21493
[取得] process/status: 取得済み
[取得] process/cmdline: 取得済み
[取得] process/maps: 取得済み
[取得] process/environ: 取得済み(機密情報らしき値は[REDACTED]に置換)
[取得] process/fd_targets: 3 件のファイルディスクリプタのリンク先を記録(内容は取得しない)
[取得] process/ancestry: 祖先プロセス 3 件を記録(PID 1まで、またはプロセス消失/読み取り不能で打ち切り)
/var/lib/roamswitch/incident-evidence/20261112T014605Z-c38a513f-21493/manifest.json
/var/lib/roamswitch/incident-evidence/20261112T014605Z-c38a513f-21493/process/ancestry.json
/var/lib/roamswitch/incident-evidence/20261112T014605Z-c38a513f-21493/process/cmdline.txt
/var/lib/roamswitch/incident-evidence/20261112T014605Z-c38a513f-21493/process/environ.txt
/var/lib/roamswitch/incident-evidence/20261112T014605Z-c38a513f-21493/process/fd_targets.txt
/var/lib/roamswitch/incident-evidence/20261112T014605Z-c38a513f-21493/process/maps.txt
/var/lib/roamswitch/incident-evidence/20261112T014605Z-c38a513f-21493/process/status.txt
LAB_COLOR=blue
LAB_API_TOKEN=[REDACTED]
/tmp/lab-worker 600出力は、次のように読みます。
- バンドルは、取得した時刻の名前のフォルダに入ります。
manifest.jsonに、各ファイルの SHA-256 が記録されます - フォルダは
drwx------(root だけが開ける)です。一般ユーザーのlsはPermission deniedになります。中を見るときはsudoを付けます - 環境変数は、名前に
KEY、TOKEN、SECRET、PASSWORDを含むものだけ、値が[REDACTED]に置き換わりました。LAB_COLORはそのままです - 開いているファイルは、リンク先だけを記録します。中身は取りません
--no-packetsを外すと、約 30 秒のパケットキャプチャも取ります。tsharkが入っていない VM では、packet_captureはスキップになり、入れるパッケージの名前が出ました--memoryを付けると、プロセスの書き込み可能なメモリもmemory.binに保存します。既定はオフです。パスワードや鍵が入り得るので、ファイルは 0600 で書かれました
バンドルの中のファイルを開いて編集したり、解析の道具の出力を同じフォルダに書いたりしないでください。次の章のとおり、1 ファイル増えただけでも、検証は失敗します。調べるときは、フォルダごとコピーして、コピーのほうで作業します。バンドルには、コマンドラインや環境変数が入ります。人に渡す前に、中身を確かめます。
#バンドルが書き換えられていないかを確かめる
verify は、ファイルを manifest.json の SHA-256 と比べます。ただ、それだけでは、ファイルと一緒に manifest.json も書き換えられると、気づけません。そこで、取得したときに manifest.json の SHA-256 を、バンドルの外の2か所に書きます。/var/lib/roamswitch/evidence_anchor.log と、システムのログ(タグ roamswitch-evidence)です。
証拠のファイルを書き換える操作です(変更を伴う操作です)。練習用のバンドルだけで試し、最後にコピーから戻します。
B=$(sudo find /var/lib/roamswitch/incident-evidence -mindepth 1 -maxdepth 1 -type d | sort | tail -1)
sudo roamswitch-incident-capture verify "$B"; echo "rc=$?"
sudo cp -a "$B" /root/bundle.bak
sudo sh -c "echo 'PPid: 1' >> $B/process/status.txt"
sudo roamswitch-incident-capture verify "$B"; echo "rc=$?"
NEW=$(sudo sha256sum "$B/process/status.txt" | cut -d' ' -f1)
OLD=$(sudo python3 -c "import json,sys; m=json.load(open(sys.argv[1])); print(next(a['sha256'] for a in m['artifacts'] if a['name']=='process/status'))" "$B/manifest.json")
sudo sed -i "s/$OLD/$NEW/" "$B/manifest.json"
sudo roamswitch-incident-capture verify "$B"; echo "rc=$?"
sudo rm -rf "$B"; sudo cp -a /root/bundle.bak "$B"
sudo roamswitch-incident-capture verify "$B"; echo "rc=$?"整合性OK: バンドルはmanifest.jsonの記録と一致しています
アンカー: manifest.json は取得時の記録と一致しています
rc=0
✗ 検証に失敗しました: このバンドルは、取得したときのままだと確かめられません
アンカー: manifest.json は取得時の記録と一致しています
整合性の問題が見つかりました:
[変更あり] process/status.txt
rc=1
✗ 検証に失敗しました: このバンドルは、取得したときのままだと確かめられません
アンカー: manifest.json が取得後に書き換えられています
ファイルは manifest.json と一致していますが、その manifest.json が取得後に変わっているため、一致は根拠になりません
rc=1
整合性OK: バンドルはmanifest.jsonの記録と一致しています
アンカー: manifest.json は取得時の記録と一致しています
rc=04回の結果を、順に読みます。
- 取ったままのバンドルは、ファイルもアンカーも一致しました
status.txtに1行足すと、1行目に「検証に失敗しました」、続けて[変更あり]と出ましたmanifest.jsonの SHA-256 も書き換えてそろえると、ファイルどうしの比較は合います。それでも、アンカーが「取得後に書き換えられています」と答え、1行目は「検証に失敗しました」、終了コードは 1 です。最後の行が、ファイルの一致が根拠にならない理由です。監視のスクリプトでは、終了コードで判断します- コピーから戻すと、すべて一致しました
ほかに、記録にないファイルを足したときは [未記録]、消したときは [欠落] と出ます。外の2か所の記録は、次のように確かめられます。
sudo tail -1 /var/lib/roamswitch/evidence_anchor.log
sudo lsattr /var/lib/roamswitch/evidence_anchor.log
sudo journalctl -t roamswitch-evidence --no-pager -o cat | tail -12026-11-12T01:46:05.298491108+00:00 20261112T014605Z-c38a513f-21493 53bea2d7520c9c1d112381110415829571524890d1d57cf93bbd0d37d7959ba7 0000000000000000000000000000000000000000000000000000000000000000 81bf5aa999f072eb3f425f12d59b0f8d8c9df23d6f6cf8a77451a4c8ed94d871
-----a--------e------- /var/lib/roamswitch/evidence_anchor.log
bundle=20261112T014605Z-c38a513f-21493 manifest_sha256=53bea2d7520c9c1d112381110415829571524890d1d57cf93bbd0d37d7959ba7アンカーの1行は、時刻、バンドル名、manifest.json の SHA-256、前の行のハッシュ、この行のハッシュです。前の行のハッシュを引き継ぐので、途中の行を書き換えたり消したりすると、鎖が切れます。この VM では最初の 1 件なので、前の行のハッシュは 0 が並びました。2 件目からは、前の行のハッシュが入ります。lsattr の a は、追記しかできない属性です。システムのログを外へ送っていれば、その転送先にも同じ値が残ります。
#ターミナルの画面で操作する
ターミナルから全機能を操作 は、sudo roamswitch server tui で開きます。SSH 越しの 80x24 の端末で、キーボードだけで操作できます。root で開くと変更でき、一般ユーザーで開くと読み取りだけです。
設定を変える操作です(変更を伴う操作です)。例として、実行の記録の保持日数 exec_recorder_retention_days を 14 から 30 にします。次の章で 14 に戻します。
sudo roamswitch server tui開いたら、次のキーを押します。
6で設定の画面へ移り、/を押してretention_daysと打ち、Enter で絞り込みます。exec_recorder_retention_daysの1行だけが残ります- Enter で編集します
- Ctrl-U で消して
0と打ち、Enter を押します(範囲の外の値です) - もう一度 Enter で編集し、Ctrl-U のあと
30と打って Enter、続けてsを押します - 差分を確かめて
yで保存し、7のサービスの画面でR、yで再起動します。qで閉じます
│サービス: active (running)、起動時: enabled │
│ホストは隔離されていません。 │
│セキュリティスコア 71/100 (B (注意)) 合格 27/38 │
│転送: 送信要求 3 破棄 0 再試行キュー 0件/0 KB │
│ [OK] file: 成功 3 / 失敗 0 直近の成功 2026-11-12T01:46:04.298Z │
│exec レコーダー: netlink 記録 254 アラート 1 破棄 0 ハッシュチェーン正常 │[root] 拒否されました: exec_recorder_retention_days: exec_recorder_retention_day
s: 1〜3650 の範囲で指定してください: '0'
│> *exec_recorder_retention_days 30 │
│実行ログデータの最大保持期間 (日)。 │
│許可: 1 .. 3650 │
│roamswitch-server の再起動後に反映されます(ライブ再読み込みなし)。 │
│ディスク上: 14 │
[root] [未保存 1件] 変更を保留しました(保留中 1 件)。s で確認して保存します。 R┌ 保存前に変更内容を確認 ──────────────────────────────────────────────────┐
┌ │exec_recorder_retention_days: 14 -> 30 │─┐
│ │ │ │
│ │これらの設定は roamswitch-server │ │
│ │の再起動後にのみ反映されます(ライブリロードはありません)。 │ │
│ │現在のファイルはタイムスタンプ付きでバックアップされ、アトミックに置き換え│ │
│ │られます。 │ │
│ │Enter/y: 実行 Esc/n: キャンセル │ │保存したあと、バックアップと今の設定を、別の端末で比べます。
sudo ls /etc/roamswitch | grep bak
B=$(sudo ls -t /etc/roamswitch | grep 'server.conf.bak' | head -1)
sudo diff /etc/roamswitch/$B /etc/roamswitch/server.confserver.conf.bak-20261112-014655
443c443
< exec_recorder_retention_days=14
---
> exec_recorder_retention_days=30読み取れることは、次のとおりです。
- 範囲の外の値(
0)は、保留にもならずに拒否されました。拒否の理由に、許される範囲(1〜3650)が出ます。画面の下にも出ています - 変えた行には
*が付き、「ディスク上: 14」と、保存済みの値が並びます。sを押すまで、ファイルは変わりません - 保存の前に、変える前と後の値が出ます。取り消すときは Esc か
nを押します。nで閉じても、変更は保留のまま残り、もう一度sで確かめられました。保留をやめるときはU(すべて破棄)を押します。行は 14 に戻り、下の行に「保留中の変更を 1 件破棄しました。ファイルは変わっていません。」と出ました。保存すると、server.conf.bak-<日付>-<時刻>にバックアップが残りました - 反映には再起動が要ります。サービスの画面の
Rは、確認を出してから再起動します。x(停止)には「すべての保護が無効になります」と書かれています - 一般ユーザーで開くと、設定の値は
?で隠れ、「読み取り専用」と出ました。server.confは root しか読めないためです - ダッシュボードの点数と合格の数は、該当しない項目と判定できない項目を数えません
1 ダッシュボード、2 保護、3 インシデント、4 通知と転送、5 センサー、6 設定、7 サービス、8 ツール、9 ログ監査、0 情報とヘルプ、F Fleet です。左右の矢印で、画面の中の区切りを移ります。? で、その画面のキーの一覧が出ます。
#元に戻す
演習で変えた設定を戻し、練習用のファイルを消します(変更を伴う操作です)。prune --keep-days 0 は、すべてのバンドルを消します。練習用の環境でだけ使ってください。既定の保持期間は 30 日です。
sudo roamswitch server config set exec_recorder_retention_days 14
sudo roamswitch server config set forward_enabled false
sudo roamswitch server config set forward_min_severity low
sudo roamswitch server config set forward_file_path ""
sudo roamswitch server restart
kill $(pgrep -x lab-worker) 2>/dev/null
rm -f /tmp/lab-marker /tmp/lab-worker /tmp/rewrite.py
sudo rm -f /root/seg.bak; sudo rm -rf /root/bundle.bak
sudo roamswitch-incident-capture prune --keep-days 0
sudo grep -E '^(exec_recorder_retention_days|forward_enabled|forward_min_severity|forward_file_path)=' /etc/roamswitch/server.conf
sudo roamswitch events verify削除しました: /var/lib/roamswitch/incident-evidence/20261112T014615Z-3b47feb6-21585
削除しました: /var/lib/roamswitch/incident-evidence/20261112T014615Z-29479542-21576
削除しました: /var/lib/roamswitch/incident-evidence/20261112T014605Z-c38a513f-21493
プルーン完了: 3/3 件を削除しました(保持期間: 0 日)
forward_enabled=false
forward_file_path=
forward_min_severity=low
exec_recorder_retention_days=14
✓ ハッシュチェーンは正常です(1 セグメント)。改ざんは検知されていません(検知であり防止ではありません)。
最新のチェックポイントより後の 60 件は、次のチェックポイントまで検証の対象外です。消える件数は、それまでに取ったバンドルの数です。ここでは、--no-packets を外した 1 件と --memory の 1 件を、演習とは別に取っていました。バンドルを消しても、evidence_anchor.log の行は残ります。追記しかできないためです。画面で保存したときの server.conf.bak-* も残ります。
#見えない部分
使い捨ての Ubuntu 24.04 の VM で、上の演習をすべて実行しました。そのうえで、次の点に注意します。
- 改ざんを防ぐ仕組みではありません。
verifyも、バンドルのアンカーも、書き換えを見つけるためのものです。root を取られたマシンでは、手元の記録とアンカーは両方とも書き換えられます。外に送ったハッシュやログと見比べて、初めてわかります - 最新のチェックポイントより後の記録は、
verifyでは確かめられません。出力の「N 件」が、その範囲です - 証拠のバンドルは「できれば取る」処理です。
tsharkが無ければパケットはスキップされ、プロセスが先に消えれば、その分は取れません。封じ込めそのものは、この講座では起こしていません。デーモンが取得を起動することは、コードで確かめました - Fleet(組織による集中管理)とセンサーの画面は、開いて「未登録」「ペアリング済みのセンサーはありません」と出ることだけを見ました。登録やペアリングには、組織の登録コードや、別の機器の Sensor が要るため、この講座では扱っていません
sudo roamswitch events verify が「正常です」と答えました。言えることはどれですか
解説を見る
演習2では、記録を消してチェックポイントを計算し直すと、
verifyは正常と答えました。外に送ったEXEC-CHECKPOINTのハッシュと合わないことで、書き換えがわかりました。
バンドルが取得したときのままかを、監視のスクリプトで判断します。何を見ますか
解説を見る
ファイルと
manifest.jsonを一緒にそろえると、ファイルどうしの比較は合います。それでも、バンドルの外に書いたアンカーが書き換えを知らせ、終了コードは 1 になります。演習では、失敗した検証の出力にも「一致しています」という語がありました。
#次に学ぶこと
- 転送の宛先、署名、再送の設定は、通知と転送の設定 で扱います
- 封じ込めのあとの 1 時間に何をするかは、封じ込めが働いたあとの最初の1時間 で練習できます
- 実行の記録の検索と、プロセスのたどり方は、不審な実行とマルウェア で扱います
2026-11-12に確認しました。対象は Linux 1.17.39 です。公開版 1.17.39 を apt で入れた使い捨ての Ubuntu 24.04 の VM で、記録とバンドルの書き換え、転送、TUI(tmux の 80x24 の端末で操作。U の破棄も)での保存と再起動、片付けを、日本語と英語(LANG=en_US.UTF-8。snapshot と TUI は language=en)で実行しました。
コマンドの出力は、製品の版や環境によって変わります。違いが出たら、まず製品の版を確かめてください。