コンテンツにスキップ

eBPFによる実行時証拠の観測調査

  • 状態: ローカル環境で計測済み(権限・負荷・実運用環境での確認は実行時証拠の観測環境の検証で扱う)
  • 開始日: 2026年9月13日
  • 最終更新: 2026年9月23日

目的

実行時証拠による優先順位付けの成立性調査で検証したサンプリング方式を拡張し、実行とファイル読み込みのイベントによって、残された利用確認の不足を補えるかを調査する。

  • サンプリング方式で確認できなかったこと

    • サンプリングで常駐する実行ファイルと共有ライブラリをOSパッケージへ紐づけられたが、短命なプログラムは安定して捉えられなかった
    • サンプリング間隔を短くしても捕捉はタイミングに左右され、実行を見逃した割合は未判定のままである
    • 言語パッケージとOSパッケージマネージャーを使わずにインストールされたランタイムは、紐づけを確認できた範囲に含まれなかった
  • 本番環境での動機

    • 本番ホスト1台の4コンテナを1回計測した結果、言語パッケージ84件を含むHIGH/CRITICAL(以下「高重大度」)103件の検出結果はすべて未確認であった
    • 「監視」8件もすべて未確認で、評価対象となる「今すぐ対応」の検出結果はなかった
    • 標本には静的Goバイナリ、JDK同梱のJVM、2つのNode.jsサービスが含まれていた
    • 一部の検出結果は常駐しないプログラムに集中し、スキャン時にのみ動作する同梱スキャナには高重大度23件が紐づいていた
    • これらの観測結果は追加調査の動機となるが、実運用全体での有効性を示すものではない
  • 調査の状況

    • 2026年9月13日〜2026年9月16日に開発機で計測を実施。結果は計測結果に記載
    • 本番環境での利用確認の不足がどの程度重要か、必要な権限を利用者が許容するかは未評価である

方針

  • カーネルのイベントトレーシングを使わない方法から試す

    • スキャンしたイメージと観測したファイルに結びつく証拠を使い、実行中の静的バイナリをそのバイナリに対するTrivyの検出結果と照合する
    • プロセスが開いたままにしているファイルから、特にJavaアーカイブを中心に言語パッケージを特定できるかを確認する
    • インストール済みパッケージの記録で裏付けられる場合は、すでに観測できているPython拡張のパッケージへの紐づけを拡張する
    • これらの追加手法を先に計測し、どの不足にイベント観測が引き続き必要かを明らかにする
  • 専用の観測プログラムを作る前にイベント観測の成立性を検証する

    • 既存のトレーシングツールを使い、成功した実行とファイルオープンのイベント、および検出結果への紐づけを調査する
    • イベント証拠によって有用な紐づけと信頼できるコンテナへの帰属を確認できた場合にのみ、最小限の専用プログラムの作成へ進む
    • そのプログラムを使い、製品実装に近い形で必要最小限の権限と負荷を計測する
    • トレーシングツールの負荷はその構成での計測値として扱い、将来の製品実装の負荷の範囲を示すとは仮定しない
  • 利用の証拠は優先順位を上げるためだけに使う

    • ここでの証拠は、プロセスがそのパッケージのファイルを実行・読み込み・保持した観測記録を指す
    • 証拠は優先順位の引き上げにのみ使い、証拠がないことを理由に優先順位を下げたり、安全を示唆したりしない
    • 観測の一部が失敗しても、独立して有効な証拠は有効なままとする
    • 失敗した手法と、そのために取得できなかった計測値を分けて記録する
    • ファイルが開かれたこととプログラムが実行されたことを区別し、同梱された依存関係は実際に観測できた粒度で説明する
  • 検出結果の割合と、実行・ロードの捕捉率を別に測る

    • Trivyの検出結果のうち、観測期間内に利用の証拠を紐づけられたものの割合を計測し、どれだけの脆弱性に利用の根拠を付けられたかを示す
    • コンテナ内のプログラムが独立したログに記録した個々の実行・ロードのうち、イベント観測で捕捉できたものの割合を別に計測し、取りこぼしの度合いを示す
    • 1回の実行・ロードを捕捉できれば利用の証拠がある検出結果の割合は上がるが、個々の実行・ロードの捕捉率は低いままのことがあるため、片方だけで結論を出さない
  • 観測は任意とする

    • イベント観測はサンプリングとは別のオプトインとし、イベントトレーシングに対応していなくてもサンプリングを利用できるようにする
    • 実行時観測機能が無効でも既存機能を維持する
    • 初期の収集対象はファイルパス、実行の事実、それらの帰属に必要な識別情報と時刻に限定し、コマンドライン引数と環境変数は標準では収集しない

検証項目

  1. 短時間の活動の捕捉
    • 個々の発生を基準に計測したとき、実行と一時的なロードを安定して捉えられるか
  2. 正確な集計
    • 1回のロード操作で複数のファイルを開く場合や、ファイルを開かずにキャッシュ済みの内容を使う場合も、ロード操作の捕捉を正しく評価できるか
  3. パッケージへの紐づけと証拠の限界
    • どの言語エコシステムでスキャン済みパッケージへ曖昧さなく紐づけられ、同梱された依存関係について各依存関係のコードが実行されたと示唆せずに説明できるか
  4. コンテナへの帰属
    • 別のコンテナやホスト上のプログラムを対象と取り違えずに、イベントを正しいコンテナへ帰属させられるか
  5. 優先順位付けへの価値
    • 特に「今すぐ対応」と「監視」で、これまで未確認だった検出結果の何件に証拠が得られ、順位がどう変わるか
  6. 運用上の成立性
    • どのカーネル機能と権限が必要で、ワークロードにどの程度の負荷がかかり、利用者がそれらを許容するか判断するために何が残るか

今回の対象外

  • 脆弱なコードの実行確認と攻撃検知

    • 脆弱な関数が実行されたことの証明は試みない
    • 攻撃検知、攻撃アラート、攻撃タイムラインの再構成は本調査の対象外とする
  • ネットワーク履歴と到達可能性

    • 確立された接続の履歴は収集せず、イベント証拠からネットワーク到達可能性を推定しない
  • Kubernetesノードの観測

    • Docker Engineでの成立性を確認するまで、Kubernetes側の収集は見送る
  • イベント観測の必須化

    • イベントトレーシングをサンプリングや既存のイメージスキャン機能の前提条件にしない
  • 古いカーネルでの動作保証

    • 結論は検証したカーネルと構成に限定し、新しいカーネルでの成功を根拠に古いカーネルへの対応を主張しない

検証方法(計画)

サンプリング方式の検証に使ったハーネスのREADMEに記載したツールを拡張し、製品とは独立して調査する。

  • 再利用するテストケース

    • 短命なcurlとgitのケースを拡張し、実行ごとの独立した記録を追加する
    • SQLiteの一時的ロードのケースを拡張し、ロード操作ごとの独立した記録を追加する
    • Pythonのケースを拡張し、異なるキャッシュ条件でのネイティブ拡張とPythonモジュールを対象にする
  • 追加する言語別のケース

    • 通常のパッケージ配置とリンクを使った配置で、既知の脆弱な依存関係をロードするNode.jsアプリケーションを追加する
    • 通常のアーカイブと同梱アーカイブから、既知の脆弱な依存関係をロードするJVMアプリケーションを追加する
    • スキャナが検出する脆弱な依存関係を含み、実行を独立したログに記録する静的Goプログラムを追加する
    • 意図した依存関係がそれぞれスキャン結果に含まれ、テストアプリケーションが意図した実行またはロードを記録することを確認する
  • 帰属を検証する対照ケースと本番標本

    • 誤った帰属を検出するため、実行ファイルの名前とパスが重複する2つのコンテナとホスト上のプログラムを動かす
    • 帰属の確認には、コンテナの再起動とプロセス識別子の再利用を含める
    • 静的Goバイナリ、JDK同梱のJVM、2つのNode.jsサービスからなる本番標本を、すでに稼働している状態から観測する
    • 本番標本にはすべての実行またはロードの発生を裏付ける独立したログがないため、別に報告する
  • 観測タイミング

    • 観測を開始して準備完了を確認した後に、アプリケーションの実行またはロードを発生させる
    • 実行またはロードを発生させる前に、既知のテストイベントの到着、対象コンテナの認識、裏付けとなるパッケージ情報の収集を確認する
    • コンテナの起動時刻とアプリケーションの起動時刻を分けて記録する
    • 起動を制御する場合と、すでに稼働中のアプリケーションに途中から観測を開始する場合を比較し、観測期間内に再度発生させた活動を記録する
    • 計測前に反復回数と予定する発生回数を固定する
  • 未観測のケース

    • 独立したログで観測期間外の発生を確認できた場合にのみ、観測期間外として記録する
    • 独立したログで観測中の発生を確認できたにもかかわらず捕捉できなかった場合は、観測期間内の見逃しとして記録する
    • 本番標本の未観測の活動を含め、発生の有無や時刻を確認できない場合は理由を不明として記録する
    • 未観測のケースを、パッケージが使われなかった証拠として解釈しない
  • 3通りの比較

    • 同じ保存データを、従来のルール、従来のルールに読み取り専用の手法を追加したもの、さらにイベント証拠を追加したものの順に評価する
    • 収集失敗の影響を受けた検出結果も含め、すべての検出結果を主な集計の分母に含める
    • 読み取り専用の手法の追加とイベント証拠による利用確認の増加を分け、優先度、エコシステム、証拠の粒度ごとに報告する
    • 順位の変化を比較し、検出結果数と併せて観測したバイナリ数とファイル数を報告する
  • 個々の発生に関する独立した記録

    • テストアプリケーションに、発生ごとの一意な識別子、プロセス識別情報、時刻、対象ファイル、成功の有無を観測プログラムとは独立して記録させる
    • 実行は個別に照合し、各ロード操作は対応するファイルイベントの集合に紐づける
    • 失敗した操作、観測期間外の活動、キャッシュだけで完了したロードは、該当する捕捉率の分母から除外する
    • 個々のファイルオープンの捕捉率は、それらのオープンに関する独立した記録がある場合にのみ計算し、対象となる発生がない場合は算出不可として報告する
  • 精度と収集の欠落

    • 誤帰属率は、誤って帰属させたイベント数を帰属先を割り当てた全イベント数で割った値として報告する
    • 正しい帰属の割合は、正しいコンテナへ帰属させた実行数を独立したログに記録された実行数で割った値として報告する
    • 独立して確認した利用実態に照らして誤った利用判定を調べ、その確認が検証対象全体のどの程度を網羅するかを報告する
    • イベントの欠落、バッファあふれ、未解決のパス、曖昧な照合、観測の中断を分けて記録する
  • 負荷と権限の条件

    • 実行順を交互にするか変化させた反復計測により、観測の有効時と無効時を比較する
    • 観測プログラムのCPUとメモリ、DockerデーモンのCPU、イベント量、対象アプリケーションのCPU、スループット、応答時間を計測する
    • 成立性を確認した後、rootの条件と、rootなしでトレーシング専用の権限を付与する条件、その権限に保護されたファイルの読み取り権限を加える条件、より広い管理権限を付与する条件を比較する
    • トレーシング権限、ファイルアクセス、ホストのセキュリティ制約を分けて、操作ごとの成否を記録する
    • 全体として必要な条件を示すため、イベント観測の権限と併せてDockerへのアクセス権とサンプリングの権限を記載する
    • root権限が成功を保証するとはみなさず、検証したカーネル機能、有効なセキュリティ制約、リソース制限を記録する

結果(2026年9月16日)

イベント証拠によって、従来の方法では利用を確認できなかった短命な curl・git と Node.js の依存パッケージに証拠を紐づけられたが、確認できる範囲は観測の開始時刻と記録の取りこぼしに左右される。

検証環境はローカル環境の WSL2、Linux 6.6、bpftrace 0.25、root 権限で、観測窓は5分、各ケースの計測は1〜3回である。

検出結果の利用確認

  • 全検出結果: そのケースのイメージを Trivy でスキャンして得た HIGH/CRITICAL の検出結果の全件数
  • 使ったパッケージ: 独立したログ(コンテナ内のプログラム自身が、観測ツールとは別に、何をいつ実行・読み込みしたかを書き残した記録)で実際に使ったと確定したパッケージの数と名前
  • その検出結果の件数: 使ったパッケージに属する HIGH/CRITICAL の検出結果の件数で、このケースで紐づけ可能な上限
  • 各方法の確認数: 全検出結果のうち、観測期間内に「そのパッケージのファイルが実行・読み込みされた」証拠を各方法で紐づけられた件数

「紐づけができているか」は「確認数 / 使ったパッケージの検出結果の件数」で、「全体のうちどれだけに証拠が付くか」は「確認数 / 全検出結果」で判断する。

使ったパッケージは、コンテナ内のプログラム自身が独立したログに記録した実行ファイル、読み込んだライブラリ、import・require したモジュール、開いた jar を、OS のパッケージ DB、Python の RECORD、Node.js の package.json、jar の pom.properties、Go バイナリの埋め込み情報による所有者情報と照合して独立に確定した。

git も使ったが、HIGH/CRITICAL の検出結果がないため件数には現れない。

比較する方法は次の3つで、表の右へ進むごとに左の方法へ証拠を追加する。

  • 従来のルール: 実行時証拠による優先順位付けの成立性調査で使った、procfs で定期的にプロセスを覗き、実行ファイルと読み込み済みライブラリを OS パッケージに紐づける方法
  • 読み取り専用の追加手法: eBPF を使わず、procfs から読める情報を増やし、Go バイナリに埋め込まれたモジュール情報、JVM が開いたままの jar、Python 拡張の対応付けによって紐づけを広げた方法
  • イベント証拠: eBPF で「実行された」「ファイルを開いた」という出来事を記録して紐づける方法

この表は、ワークロードより先に観測を始めた条件で、全検出結果、使ったパッケージとその検出結果の件数、各方法の確認数を比較したものである。

コンテナの動作 全検出結果 使ったパッケージ その検出結果の件数 従来のルール 追加手法 イベント証拠
数ミリ秒で終わる curl と git を5秒ごとに実行 101 5(curl、libcurl4 と依存ライブラリ3つ) 14 0 0 14
共有ライブラリを繰り返しロード・アンロード 61 1(libsqlite3) 3 3 3 3
Python が cryptography を import(キャッシュあり・なしの各構成) 61 1(cryptography) 5 0 5 5
Node.js が lodash を require(npm) 17 1(lodash) 4 0 0 4
Node.js が lodash を require(pnpm) 47 1(lodash) 4 0 0 4
Java が log4j-core の jar を読み込む(通常の jar、fat jar、遅延ロードの各構成) 11 1(log4j-core) 3 0 3 3
静的 Go バイナリを実行 62 1(golang.org/x/text) 4 0 4 4
  • イベント証拠を含めると、すべてのケースで使ったパッケージの検出結果を全件紐づけられた
  • 短命な curl・git のケースの14件と Node.js の各構成の4件は、イベント証拠でのみ紐づけられた
  • ワークロード開始後に観測を始めると、Node.js の一度きりの require は観測前に済んでおり、イベント証拠を含めても npm・pnpm ともに使ったパッケージの検出結果4件に対する確認数は0件となるが、Python を含む他のケースの確認数は変わらない

個々の実行・ロードの捕捉

表の「発生数」は独立したログに記録された評価対象の実行・ロードの件数で、「捕捉数」はそのうち eBPF の記録と一対一で対応した件数である。

この表は、ワークロードより先に観測を始めた条件で、個々の実行・ロードをどれだけ捕捉できたかを示す。

コンテナ内の活動 捕捉数 / 発生数
数ミリ秒で終わる curl と git を5秒ごとに実行 116/116
共有ライブラリを繰り返しロード・アンロード 22/22
Python が cryptography を import(キャッシュあり・なしの各構成) 各1/1
Node.js が lodash を require(npm・pnpm の各構成) 各1/1
依存モジュールを埋め込んだ静的 Go バイナリを実行 1/1
  • 短命な curl・git の実行は、ワークロード開始後に観測を始めた条件でも116/116を捕捉できた
  • Java の jar 読み込みは、独立したログにスレッドの識別情報がないため、この捕捉率の評価対象外
  • 評価対象の発生をすべて捕捉できても収集全体に取りこぼしがないとは限らず、検出結果への利用証拠の紐づけとも別の指標

コンテナへの帰属

帰属は、記録された実行がどのコンテナのものかを判定することを指し、表の「正帰属数」は正しいコンテナに帰属できた実行数、「実行数」は独立したログに記録された実行数となる。

この表は、同じイメージの2コンテナを単独または同時に動かした検証のうち、取りこぼしのない条件での帰属結果を示す。

実行条件 正帰属数 / 実行数
各コンテナを単独で実行 各116/116
2コンテナを同時に実行 232/232、236/236
  • 表の条件では独立したログに記録された実行をすべて正しいコンテナに帰属でき、同時実行の数値は両コンテナの実行を重複なく合算したもの
  • ホストで同じプログラムを動かした検証では、41回・48回のホスト上の実行はいずれもどのコンテナにも帰属しなかった
  • コンテナ間の取り違えの割合は今回の照合方法では評価できず、表の結果だけではその割合を判断できない

記録バッファと取りこぼし

カーネル側の観測プログラムが出来事を書き込み、収集ツールが読み出すまで保持するメモリ領域を記録バッファと呼ぶ。出来事の発生が読み出しより速いと領域が埋まり、あふれた分は失われる。

この表は、記録バッファの容量ごとに、バッファあふれで失われたイベントの件数を示す。

観測対象 バッファ(ページ) 取りこぼし(件)
短命な curl と git の反復実行 64 2763〜2790
短命な curl と git の反復実行 256 0〜12
短命な curl と git の反復実行 512 0
同じイメージの2コンテナを同時に実行 256 1465
同じイメージの2コンテナを同時に実行 512 0
Python が cryptography を import(キャッシュなし) 256 66
Python が cryptography を import(キャッシュなし) 512 0
上記以外のケース 256 0
  • 短命な curl・git、2コンテナの同時実行、キャッシュなしの Python では、512ページに増やした計測で取りこぼしが0件になった
  • 取りこぼしが0件の計測だけを「観測窓が完全」として扱う
  • 各ケースの計測は1〜3回に限られ、今回取りこぼしが0件だった容量でも、別の環境や負荷で同じ結果になるとは限らない

検証環境から得た知見

  • カーネル側でパスを絞り込むフィルタは検証器の上限に掛かって読み込めず、対象の絞り込みは記録後に行った
  • PID 名前空間が入れ子の環境では観測側とワークロード側でプロセス番号が異なるため、照合には名前空間内の番号と名前空間の識別子を使った
  • musl を使う Alpine は旧来のファイルオープンの仕組みを使うため、新しい仕組みだけを観測すると活動が見えない
  • 取りこぼしはバッファ容量で変わるため、捕捉率は取りこぼしの記録と併せて読む必要がある

次の作業

  • 準備と計測
    • 済み: 読み取り専用の追加手法の計測
    • 済み: 既存のトレーシングツールによる、検出結果の利用確認と個々の実行・ロードの捕捉の計測
    • 未了: 本番相当の環境での確認
  • 次の実装段階を評価
    • 未了: 最小限の専用プログラム(CO-RE)の作成と、それを使った最小権限と負荷の計測
    • 未了: 次の実装に進むかを判断する前に、結果・制約・利用者の許容範囲に関する問いを公開

更新履歴

2026年9月13日

  • 調査を計画
    • 本調査の計測を始める前に、段階的な方針、検証項目、対象範囲、検証計画を記録

2026年9月16日

  • 開発環境での計測結果を記録
    • 利用確認数、個々の発生の捕捉、帰属、収集の取りこぼし、開発環境で得た知見を追加
  • 作業の実施状況を記録
    • 準備と計測の完了分、および最小権限、負荷、本番相当の環境、本番標本の観測と報告、条件付きのCO-RE実装・計測、判断前の問いの公開の未了分を記録

2026年9月23日

  • 表記と状態の更新
    • 状態行を現状に合わせ、権限・負荷・実運用環境での確認を扱う実行時証拠の観測環境の検証への参照を追加した
    • 計測環境の表記を「ローカル環境」に統一した

この記事は、KestreLynxの開発記録です。KestreLynxについて