攻撃的オペレーションにおける可観測性
そのメールはある火曜日の朝 09:14 に届いた。クライアントの CISO は簡潔だった。「昨晩 03:42 にお宅のエージェントは正確に何をしたんだ?SOC が runbook にない送信接続を見つけてブロックした。これが君たちの仕業かどうか知る必要がある。」
私たちは十一分で返信した。記憶力が良かったからではない。SOC が送ってきたコマンドの trace_id を検索し、連鎖全体を再構築したからだ。エージェントは /api/internal/health というエンドポイントを見つけ、バージョン banner で応答することを確認し、それが exploit 可能か検証することを決定し、何かを送信する前に Sentinel が認可された CIDR の外側にあるという理由でオペレーションを切断した。「送信接続」は handshake すら完了していない SYN だった。私たちはクライアントに完全な span を送付した。ATT&CK ID、policy engine の判定、そしてワイヤを通過した 47 バイトを添えて。
その日、私たちは可観測性を nice-to-have ではなく、プロダクトにすると決めた。
攻撃的可観測性は防御的可観測性とは似ていない
私は何年も SOC 向けにテレメトリパイプラインを構築してきた。Loki、Tempo、Jaeger、必要なものは何でも。最初は攻撃的エージェントを計装するのは同じ問題で、別の schema を使うだけだと思っていた。間違っていた。
防御的 SIEM はシステムのノイズがシグナルだと仮定する。あらゆる auth.fail が重要、あらゆる奇妙な DNS が重要。カーディナリティは爆発するが、メンタルモデルは明確だ。全て保存し、後で判断する。攻撃ではモデルが反転する。エージェントは定義上 ノイズそのもの だ。スキャンし、試し、失敗し、リトライする。EDR レベルの粒度で全てを保存すると、4 時間のオペレーションで誰も読まない 18 GB のテレメトリが生成される。
しかし 外科的精度 で再構築できなければならないものがある。クライアントに触れたあらゆるアクションの因果連鎖だ。どれであっても。エージェントが本番に payload を送信したなら、なぜその payload を選んだのか、それを正当化した先行情報は何か、policy engine が何を検証したか、ターゲットが何を返したかを知らなければならない。これはパラノイアではない。プロフェッショナルな red team を予算を持った script kiddie から分かつものだ。
MITRE ATT&CK マッピングガイドは率直に述べている。証拠は実際のテレメトリに錨を下ろす必要があり、オペレータの仮定に基づくべきではない(CISA, 2023)[1]。あなたのレポートが「ホスト X に対して T1190 が実行された」と言うなら、私は http.request.body を含む span、エージェントの判断、そしてレスポンスコードを見たい。それがなければ、それは意見にすぎない。
イベント設計:span、trace、属性
背骨として OpenTelemetry を使う。流行っているからではない。semantic conventions が私たちが抱えていた実問題を解決するからだ。各オペレータが好き勝手にログを書いていた。ある日は target_host、別の日は dst、また別の日は victim_ip。相関不可能だった。OpenTelemetry は共有された schema を強制し、さらに重要なのは、プロセスと worker をまたいで trace context を伝播することだ(OpenTelemetry, 2024)[2]。
Gandalf CLI は各コマンドを span として発行する。各 span は自身の trace_id、span_id、その発生原因となった推論を指す parent_span_id、そして標準セマンティクスの上に私たちの berialabs.* 規約に従う属性を持つ。内部ルールは次の通りだ。属性が公式 spec に存在するなら、そのまま使う。攻撃固有(ATT&CK technique、エージェントの判断、payload のハッシュ)であれば、namespace を汚染しないよう接頭辞を付ける。
具体例。これはエージェントが検索パラメータに対して SQL injection をテストしている実際の span(匿名化済み)だ。
{
"name": "gandalf.exploit.sqli_attempt",
"trace_id": "4a1f9b2c8e3d7f6a5b4c3d2e1f0a9b8c",
"span_id": "7c8b9a0d1e2f3a4b",
"parent_span_id": "6b7a8c9d0e1f2a3b",
"start_time_unix_nano": 1709823742891000000,
"end_time_unix_nano": 1709823743104000000,
"kind": "SPAN_KIND_CLIENT",
"status": { "code": "STATUS_CODE_OK" },
"attributes": {
"http.request.method": "GET",
"http.response.status_code": 500,
"url.full": "https://target.example.com/api/search?q=*REDACTED*",
"server.address": "10.42.7.18",
"server.port": 443,
"berialabs.attack.tactic": "TA0001",
"berialabs.attack.technique": "T1190",
"berialabs.agent.decision_id": "dec_8f3a",
"berialabs.agent.reasoning_ref": "trace://4a1f9b2c.../6b7a8c9d",
"berialabs.payload.sha256": "9e3f...c7a1",
"berialabs.payload.family": "boolean_blind_sqli",
"berialabs.sentinel.scope_check": "passed",
"berialabs.sentinel.cidr_match": "10.42.0.0/16",
"berialabs.evidence.response_signature": "mysql_error_xpath"
},
"events": [
{
"name": "sentinel.validation",
"attributes": {
"policy.id": "scope_v3",
"policy.result": "allow"
}
},
{
"name": "response.received",
"attributes": {
"response.size_bytes": 1247,
"response.contains_error_signature": true
}
}
]
}このフォーマットで私が重視するのは三つだ。第一に、parent_span_id はエージェントの推論を指しており、前のコマンドではない。これにより、何を したか だけでなく、なぜ したかを再構築できる。第二に、payload の sha256 は参照されるだけで、インライン化されない。完全なボディは別のストアに置かれ、span はハッシュだけを保持する。第三に、span のイベントは重要な遷移(Sentinel の検証、レスポンス)を捕捉する。これらは平坦な属性ではうまく表現できない。
属性としての ATT&CK、ばらまかれた tag ではなく
私たちは各アクションを ATT&CK technique にマッピングするのを 事後 ではなく、span を発行する瞬間に行う。エージェントは payload ファミリーを technique ID に関連付ける内部テーブルを持ち、属性は span とともに Tempo まで旅をする。クライアントからレポートを求められたとき、それは考古学的な作業ではない。berialabs.attack.technique でフィルタリングする Grafana 上の query だ。
エージェントが教えてくれないものを捕捉する eBPF
受け入れるのに時間がかかったトリックがある。エージェントをどれほど良く計装しても、エージェント 自身が自分のしていることを知らない ものがある。あなたが予期しなかったソケットを開くサードパーティライブラリ。HTTP クライアントを経由せず getaddrinfo を通る DNS コール。一時ファイルを書く子プロセス。userspace の計装だけを信頼していると、トレーサビリティに穴が空く。
だから私たちはエージェントが動くホストの kernel に eBPF フックを設置した。eBPF は kernel 内で sandbox 化されたプログラムを実行し、それを改変することなくイベントを捕捉できる(Gregg, 2019; ebpf.io)[3]。私たちは四つをフックする。tcp_connect、execve、openat、そして udp_sendmsg 経由の DNS 解決。各イベントはプロセスの cgroup_id で enrichment され、共有メモリ内の小さなテーブルを通じてエージェントのアクティブな trace_id と相関付けられる。
結果はこうだ。エージェントが span で開いたとされている IP に接続を開けば、完璧、全てが一致する。どの span にも 現れない IP に接続を開けば、アラートが発火する。これを使って、ある scraping ライブラリが警告なしに favicon を prefetch していることを 2 回ほど発見した。悪意ではなかったが、そうであった可能性もあり、クライアントには知る権利があった。
不愉快な注釈。eBPF は強力だが、不可侵ではない。攻撃者がすでに kernel を制御している場合、専用に設計された rootkit が eBPF ベースのツールを盲目にできることを示す公開された研究がある(Matheuz, 2024)[4]。私たちのケースでは脅威モデルが異なる(自分自身のエージェントを監査したいのであって、root を持つ敵対者から守るのではない)が、念頭に置く価値はある。
パイプライン
エージェントから Grafana まで、ホップはこうだ。Gandalf CLI は OTLP を gRPC で sidecar として動く OpenTelemetry Collector に発行する。Collector は三つのことを行う。PII パターン(メール、カード番号らしき数字、auth ヘッダ)にマッチするものを /dev/null に書き出す processor でセンシティブな属性をフィルタし、batch し、再エクスポートする。Trace は Tempo へ行く。構造化ログは otlphttp exporter 経由で Loki へ行き、Loki は 3.0 からこれをネイティブに受け入れる(Grafana Labs, 2024)[5]。メトリクス(techinique ごとのレイテンシ、Sentinel にブロックされた payload の比率、リクエストスループット)は Prometheus へ行く。
その上に Grafana。そして killer feature は美しい dashboard ではない。trace-to-logs だ。任意の span をクリックすると、trace_id で相関したログにジャンプする。任意のログをクリックすると、span にジャンプする。この相関こそが、私たちが CISO に十一分で返答できた理由だ。
痛みを伴うトレードオフ
全てが美しいわけではない。私たちが今も交渉している三つの実在する緊張。
ノイズ対シグナル。 エージェントのあらゆる内部判断を計装すると、オペレーションごとに数百万の span を生成する。外部コマンドだけを計装すると、因果連鎖を失う。私たちは妥当なバランスを見つけた。エージェントの判断ノード(あらゆるトークンではなく)と外部の side-effects を例外なく計装する。それでも、長時間のオペレーションでは 200k spans/時間のピークを見たことがある。
Retention。 S3 上の Tempo は安いが、高カーディナリティの Loki のログはコストがかかる。現在のポリシー:完全な trace は 90 日、ログは 30 日、集計データ(メトリクスと要約)は 3 年。SOC 2 と red team 契約からの法的圧力は、少なく保つよう促すのではなく、より多く保つよう促す。
ログ内の PII。 これは私が最も心配するものだ。エージェントが影響を実証するためにデータベースのダンプを抽出するなら、そのダンプはログに 残ってはならない。Collector のフィルタは助けにはなるが、十分ではない。私たちは第二の層を維持している。センシティブな発見はパイプラインに触れる 前 にクライアントの公開鍵で暗号化され、参照のみが保存される。
私たちの仕事の仕方をどう変えたか
このパイプラインを持つ前は、red team レポートを手作業で作成していた。スクリーンショット、出力のコピペ、記憶から再構築した物語。何日もかかった。今ではレポートのほとんどは Tempo と Loki にクエリすることで生成され、人間のオペレータは書き写すのではなく、解釈に専念する。
さらに重要なこと。私たちはクライアントと共通の土俵から議論する。「エージェントが X をしたと思う」ではなく「03:42:18 の span がここにあり、判断、payload、レスポンスが含まれている」と。先月、あるクライアントが私たちに告げた。red team が彼らの SOC が検出ルールを訓練するためにそのまま取り込めるテレメトリを渡してきたのは初めてだと。私にとって、これこそが重要なメトリクスだ。攻撃的可観測性が防御者にとっても 同様に 有用であるということ。
そしてある火曜の朝 09:14 にメールが届いたとき、私たちは十一分で返答する。
参考文献
- CISA (2023). Best Practices for MITRE ATT&CK Mapping.
- OpenTelemetry Authors (2024). Semantic Conventions Specification 1.41.
- eBPF Foundation (2024). What is eBPF? An Introduction and Deep Dive into the eBPF Technology.
- Matheuz (2024). Breaking eBPF Security: How Kernel Rootkits Blind Observability Tools.
- Grafana Labs (2024). Ingesting logs to Loki using OpenTelemetry Collector.