目次
セキュリティ強化は多くの現場で課題になりますが、今回はその解決策の候補としてeBPFを利用した監視・検知ができるツールを探していました。実際にどのような挙動を検知できるのか、Traceeを触って検証してみましたのでご紹介します。
Traceeとは
Aqua Security1社が開発したeBPF技術を活用したオープンソースのLinuxシステム・コンテナセキュリティ向けの監視ツールです。システムコールやネットワークアクティビティなどカーネル内で発生するイベントをリアルタイムでキャプチャし、挙動を可視化・検知することができます。
eBPFとは
Linuxカーネルに起源を持った技術です。
OSにおけるカーネルのような特権を必要とする環境内で、サンドボックス化されたプログラムを実行できます。カーネルのソースコードの改変や、カーネルモジュールのロードを必要とせず、安全かつ効率的にカーネルの機能拡張ができます。
Traceeで何ができるのか
低レイヤーでの挙動の可視化
eBPFを利用することでカーネルレベルでのイベント検知が可能です。
下記Traceeで検知できる主なシステムイベント
- システムコール
- ファイルの読み書きやネットワーク接続、プロセス生成など。
- プロセスライフサイクル
- プロセスのフォーク、実行、終了などのスケジューリング。
- ファイルシステム
- 仮想ファイルシステム2での読み書き。
- ネットワーク
- LSM3フックによるソケット接続やバインドなど。
- コンテナ
- コンテナの作成や削除など。
- カーネル
- 権限変更やカーネルモジュールのロードなど。
ルールエンジンによる検知
イベントをただ検知するだけではなく、Binary自体にBuilt-inされているシグネチャーや自作のシグネチャーを使用し、条件をより細かく設定することができます。
下記はTraceeに組み込まれているシグネチャーを使って検知できる攻撃パターンの一部です。
- ファイルレス実行
- 従来のマルウェアのように実行ファイルをディスクに保存せず、システム上の正規ツールやメモリのみを利用する。
- 従来のマルウェアのように実行ファイルをディスクに保存せず、システム上の正規ツールやメモリのみを利用する。
- 隠しファイルの生成
- マルウェアの実行ファイルを隠しファイルにし発見されにくくする。
- マルウェアの実行ファイルを隠しファイルにし発見されにくくする。
- プロセスメモリコードインジェクション
- 実行中の正規プロセスのメモリ空間に悪意あるコードを注入し実行させる。
- 実行中の正規プロセスのメモリ空間に悪意あるコードを注入し実行させる。
- 不正なシェル
- webサーバのプロセスからshellが起動されたり、リバースシェルのように攻撃者が外部から操作するためのshell実行するなど。…etc
フォレンジックツールとしての使用
単なる監視ツールとしての使い方だけでなく、「どのような攻撃が行われたか」など攻撃方法の詳細分析にも使えます。
柔軟な出力形式の指定
Traceeでは出力形式も柔軟に設定できます。
下記は設定できる形式です。
- json
- table
- ファイル
- 標準出力
- Webhook
- セキュリティ情報管理システム(SIEM)への送信
実際に動かしてみる
手始めにVM内でtraceeをコンパイルして動かしてみます。
動作環境
- OS
- RockyLinux 9
- Lang
- Golang v1.26
環境構築
ビルドに必要なパッケージをinstallします。
dnf update && \
dnf install --enablerepo=crb -y git make clang bpftrace pkgconfig libbpf-devel
miseでgolangをinstallします。
# miseをセットアップ
curl https://mise.run | sh
# PATHを通す
echo "eval \"\$(/root/.local/bin/mise activate bash)\"" >> ~/.bashrc
. ~/.bashrc
# miseバージョン確認
mise version
_ __
____ ___ (_)_______ ___ ____ ____ / /___ _________
/ __ `__ \/ / ___/ _ \______/ _ \/ __ \______/ __ \/ / __ `/ ___/ _ \
/ / / / / / (__ ) __/_____/ __/ / / /_____/ /_/ / / /_/ / /__/ __/
/_/ /_/ /_/_/____/\___/ \___/_/ /_/ / .___/_/\__,_/\___/\___/
/_/ by @jdx
2026.3.5 linux-x64 (2026-03-07)
# golangをinstall
mise use -g go@1.26.1
# バージョン確認
go version
go version go1.26.1 linux/amd64
traceeのソースコードを配置します。
# 最新のバージョンでclone
git clone -b release-v0.24 https://github.com/aquasecurity/tracee.git
ビルドします。
cd tracee
make tracee
PATHを通しておきます。
echo "export PATH=\$PATH:$(pwd)/dist" >> ~/.bashrc && source ~/.bashrc
traceeを動かす
traceeで検知できるイベントを確認できます。
tracee list
ログレベルをerrorに設定し、イベントを検知してみます。
tracee --log error
出力されたログを確認してみます。
下記はログに出力される項目です。
- TIME
- イベントを検知した時刻。(マイクロ秒)
- イベントを検知した時刻。(マイクロ秒)
- UID
- プロセスを実行しているユーザ識別番号。
- プロセスを実行しているユーザ識別番号。
- COMM
- イベントを発生させた実行ファイルの名前。
- イベントを発生させた実行ファイルの名前。
- PID
- OSがプロセスを一意に識別するために割り振った番号。
- OSがプロセスを一意に識別するために割り振った番号。
- TID
- プロセス内の特定のスレッドを識別する番号。
- プロセス内の特定のスレッドを識別する番号。
- RET
- システムコールや関数の実行結果。Exit Code。
- システムコールや関数の実行結果。Exit Code。
- EVENT
- Traceeが検知した具体的な動作。
- Traceeが検知した具体的な動作。
- ARGS
- イベントに付随する詳細情報。
下記は実行した実際のログです。
TIME UID COMM PID TID RET EVENT ARGS
12:47:56:948807 0 getconf 2893 2893 0 sched_process_exec cmdpath: /usr/bin/getconf, pathname: /usr/bin/getconf, dev: 8388610, inode: 18324111, ctime: 1772955715964564757, inode_mode: 33261, interpreter_pathname: /usr/lib64/ld-linux-x86-64.so.2, interpreter_dev: 8388610, interpreter_inode: 33688254, interpreter_ctime: 1772955716772537203, argv: [getconf CLK_TCK], interp: /usr/bin/getconf, stdin_type: S_IFCHR, stdin_path: /dev/null, invoked_from_kernel: false, prev_comm: google_guest_ag, env: [], pwd: /
ログを見てみると下記が出力されていました。
- UID
- rootユーザで実行された。
- rootユーザで実行された。
- COMM
- getconfが実行された。
- システム設定値を取得するコマンド
- システム設定値を取得するコマンド
- getconfが実行された。
- PID
- プロセスIDが2893で実行された。
- プロセスIDが2893で実行された。
- TID
- スレッドIDが2893で実行された。
- スレッドIDが2893で実行された。
- RET
- 正常終了された。
- 正常終了された。
- EVENT
- sched_process_execで検知された。
- プログラムが新しく起動されたことを示すカーネルイベント
- プログラムが新しく起動されたことを示すカーネルイベント
- sched_process_execで検知された。
- ARGS
- cmdpath/pathname (/usr/bin/getconf)
- 実行されたファイルのフルパス。
- 実行されたファイルのフルパス。
- dev (8388610)
- 実行ファイルが保存されているデバイス番号。
- 実行ファイルが保存されているデバイス番号。
- inode (18324111)
- 実行ファイルのファイルシステム上の固有番号。この番号で物理的なファイルを特定可能。
- 実行ファイルのファイルシステム上の固有番号。この番号で物理的なファイルを特定可能。
- ctime (1772955715964564757)
- ファイルのinodeが更新された日時。権限や所有者、ファイル名、ファイルの中身の変更などがされた場合に日時更新。
- ファイルのinodeが更新された日時。権限や所有者、ファイル名、ファイルの中身の変更などがされた場合に日時更新。
- inode_mode (33261)
- ファイルのパーミッション。8進数に直すと100755で通常実行可能ファイルであることがわかる。
- ファイルのパーミッション。8進数に直すと100755で通常実行可能ファイルであることがわかる。
- interpreter_pathname (/lib64/ld-linux-x86-64.so.2)
- コマンドを実行するにあたり必要になった動的ライブラリをメモリにロードしてプログラムを実行させるための「動的リンカー」のパス。
- コマンドを実行するにあたり必要になった動的ライブラリをメモリにロードしてプログラムを実行させるための「動的リンカー」のパス。
- interpreter_dev (8388610)
- 動的リンカーが保存されているディスクのデバイス番号。
- 動的リンカーが保存されているディスクのデバイス番号。
- interpreter_inode (33688254)
- 動的リンカーのファイルシステム上の固有番号。
- 動的リンカーのファイルシステム上の固有番号。
- interpreter_ctime (1772955716772537203)
- 動的リンカーのinodeが更新された日時。
- 動的リンカーのinodeが更新された日時。
- argv ([getconf CLK_TCK])
- コマンドラインに打ち込まれた引数の配列。
- コマンドラインに打ち込まれた引数の配列。
- interp (/usr/bin/getconf)
- 実際に実行を制御しているバイナリのパス。
- 実際に実行を制御しているバイナリのパス。
- stdin_type (S_IFCHR)
- 標準入力の種類。S_IFCHRはキャラクタデバイスの意味。
- 標準入力の種類。S_IFCHRはキャラクタデバイスの意味。
- stdin_path (/dev/null)
- 標準入力がどこにつながっているかのパス。/dev/nullのため、空デバイス。
- 標準入力がどこにつながっているかのパス。/dev/nullのため、空デバイス。
- invoked_from_kernel (false)
- カーネル内部から呼び出されたかどうかを示す。falseはカーネルではなく、ユーザ空間からの実行。
- カーネル内部から呼び出されたかどうかを示す。falseはカーネルではなく、ユーザ空間からの実行。
- prev_comm (google_guest_ag)
- このプロセスを起動した親プロセスの名前。getconfはgoogle_guest_agが起動。
- このプロセスを起動した親プロセスの名前。getconfはgoogle_guest_agが起動。
- env ([])
- 実行時に引き継がれた環境変数の配列。
- 実行時に引き継がれた環境変数の配列。
- pwd (/)
- プロセスが実行された時のカレントワーキングディレクトリ。/ のため、ルートディレクトリでの実行。
- cmdpath/pathname (/usr/bin/getconf)
自作したシグネチャーを組み込んでみる
今回はnginxを追加し、設定ファイルの改竄等の更新を検知するシグネチャーを自作します。
- {traceeのsrcディレクトリ}/signatures/golang/nginx_config_modified.go
package main
import (
"fmt"
"strings"
"github.com/aquasecurity/tracee/common/parsers"
"github.com/aquasecurity/tracee/types/detect"
"github.com/aquasecurity/tracee/types/protocol"
"github.com/aquasecurity/tracee/types/trace"
)
// NginxConfigModified は、Nginxの設定ファイルが変更されたことを検知するシグネチャ
type NginxConfigModified struct {
cb detect.SignatureHandler
nginxConfigFiles []string
nginxConfigDirs []string
}
func (sig *NginxConfigModified) Init(ctx detect.SignatureContext) error {
sig.cb = ctx.Callback
// 対象Nginx設定ファイルおよびディレクトリのパスを指定
sig.nginxConfigFiles = []string{"/etc/nginx/nginx.conf"}
sig.nginxConfigDirs = []string{"/etc/nginx/conf.d/", "/etc/nginx/sites-enabled/"}
return nil
}
func (sig *NginxConfigModified) GetMetadata() (detect.SignatureMetadata, error) {
return detect.SignatureMetadata{
ID: "TRC-C-1002",
Version: "1.0",
Name: "Nginx設定ファイルの変更検知",
EventName: "nginx_config_modified",
Description: "Nginxの設定ファイルに対して書き込み権限を伴うオープン操作が検知されました。",
Properties: map[string]interface{}{
"Severity": 2, // High
"Category": "persistence",
"Technique": "Modify System Process",
},
}, nil
}
func (sig *NginxConfigModified) GetSelectedEvents() ([]detect.SignatureEventSelector, error) {
return []detect.SignatureEventSelector{
// ファイルシステムレベルでのオープン操作を監視
{Source: "tracee", Name: "security_file_open", Origin: "*"},
}, nil
}
func (sig *NginxConfigModified) OnEvent(event protocol.Event) error {
eventObj, ok := event.Payload.(trace.Event)
if !ok {
return fmt.Errorf("invalid event payload")
}
if eventObj.EventName != "security_file_open" {
return nil
}
// 書き込みフラグ (O_WRONLY, O_RDWR 等) が含まれているか確認
flags, err := eventObj.GetIntArgumentByName("flags")
if err != nil {
return nil
}
if !parsers.IsFileWrite(flags) {
return nil
}
// パス情報の取得
pathname, err := eventObj.GetStringArgumentByName("pathname")
if err != nil {
return nil
}
// ファイル単体パスのチェック
for _, configFile := range sig.nginxConfigFiles {
if pathname == configFile {
return sig.emitFinding(event)
}
}
// ディレクトリ配下のチェック
for _, configDir := range sig.nginxConfigDirs {
if strings.HasPrefix(pathname, configDir) {
return sig.emitFinding(event)
}
}
return nil
}
func (sig *NginxConfigModified) emitFinding(event protocol.Event) error {
metadata, err := sig.GetMetadata()
if err != nil {
return err
}
sig.cb(&detect.Finding{
SigMetadata: metadata,
Event: event,
Data: nil,
})
return nil
}
func (sig *NginxConfigModified) OnSignal(s detect.Signal) error {
return nil
}
func (sig *NginxConfigModified) Close() {}
下記ファイルへシグネチャーの構造体のポインタを渡し、Tracee本体から参照できるようになります。
- {traceeのsrcディレクトリ}/signatures/golang/export.go
package main
import "github.com/aquasecurity/tracee/types/detect"
// ExportedSignatures fulfills the goplugins contract required by the rule-engine
// this is a list of signatures that this plugin exports
var ExportedSignatures = []detect.Signature{
&StdioOverSocket{},
&K8sApiConnection{},
&AslrInspection{},
&ProcMemCodeInjection{},
&DockerAbuse{},
&ScheduledTaskModification{},
&LdPreload{},
&CgroupNotifyOnReleaseModification{},
&DefaultLoaderModification{},
&SudoersModification{},
&SchedDebugRecon{},
&SystemRequestKeyConfigModification{},
&CgroupReleaseAgentModification{},
&RcdModification{},
&CorePatternModification{},
&ProcKcoreRead{},
&ProcMemAccess{},
&HiddenFileCreated{},
&AntiDebuggingPtraceme{},
&PtraceCodeInjection{},
&ProcessVmWriteCodeInjection{},
&DiskMount{},
&DynamicCodeLoading{},
&FilelessExecution{},
&IllegitimateShell{},
&KernelModuleLoading{},
&KubernetesCertificateTheftAttempt{},
&ProcFopsHooking{},
&SyscallTableHooking{},
&DroppedExecutable{},
&NginxChildProcess{}, # 追加
&NginxConfigModified{}, # 追加
}
// ExportedDataSources fulfills the goplugins contract required by the rule-engine
// this is a list of data-sources that this plugin exports
var ExportedDataSources = []detect.DataSource{
// add data-sources here
}
先ほどビルドしたファイルを一度削除します。
cd {traceeのsrcディレクトリ}
make clean
ビルドし直します。
make tracee
シグネチャーが含まれているか確認します。
tracee list | grep nginx | column -t
| nginx_child_process | signatures, default | |
| nginx_config_modified | signatures, default | |
含まれていることが確認できました。
作成したシグネチャーを使えるようにポリシーを作成します。
- policy-nginx-config-modified.yaml
apiVersion: tracee.aquasec.com/v1beta1
kind: Policy
metadata:
name: integrity-combined-policy
spec:
scope:
- global
rules:
- event: nginx_config_modified
ポリシーとは
traceeでは検知するイベントや対象のプロセスなどの設定をPolicyと呼ばれる塊で管理します。
ポリシーはyamlファイルで記述され、scopeやfilterという機能を使い検知範囲を絞り込むことができます。
自作シグネチャーが機能するか確認してみる
ターゲットになるnginxを追加して起動します。
dnf install -y nginx && systemctl start nginx && systemctl status nginx
下記コマンドで複数ポリシーを読み込んでTraceeを起動させます。
tracee -p policy-nginx-config-modified.yaml
別ウィンドウでnginx.confへ書き込みを行います。
echo "hoge" >> /etc/nginx/nginx.conf
ログが出力され、nginx.confの更新を検知したことが確認できます。
TIME UID COMM PID TID RET EVENT ARGS
11:32:48:312168 0 bash 40043 40043 0 nginx_config_modified detectedFrom: security_file_open
所感
traceeはeBPFを利用しさまざまなログを取得できるため、いかにノイズになるログを減らすかが重要だと感じました。nginxのように外部ネットワークに直接触れているプロセスの場合は、その範囲に絞って検知できるように適用するなどシステム特性に合わせてログを選別するのが良さそうです。
また、Tracee自体にログを検知してwebhookでアラートを飛ばす仕組みもあるため、Tracee単体でも使いやすいと思いました。プロジェクトに組み込む場合はGrafana LokiやElasticSearchなどログを確認しやすい環境を作った上で組み込む方が分析等もしやすいと感じました。
ただ検証を進めていく中でポリシーのfilter機能が期待通り動作しないなど不具合もあったため、その辺りが解消されてからでも良いかなと。また実際に現在Traceeの開発バージョンにはより多くの機能が追加されているため、次のバージョンを待ってからでも遅くないかなと思いました。
終わりに
今回はTraceeがプロジェクトに導入できるか検証しました。
Tracee以外にもFalcoやTetragonなどeBPFを利用したセキュリティツールは様々あります。しかし一方で、SingularityのようなeBPFをバイパスできるRootkitも存在するため、単一のツールだけに頼るのではなく、複数のセキュリティツールを組み合わせて使用することが大切だと思いました。
また Traceeの場合、シグネチャーがどのような仕組みでイベントを検知して、それが問題ある/ないと判断しているかを理解していないといざ検知しても何を対応すれば良いか判断できず、適切な対応をできません。
なのでこの辺りのコードリーディングも今度していきたいと思いました。
- https://www.aquasec.com/ ↩︎
- クライアントアプリケーションが様々なファイルシステムに同じ方法でアクセスできるようにするための仕組み。ファイルシステムの上位に位置する抽象化層。
出典: 仮想ファイルシステム-Wikipedia ↩︎ - 新しいカーネル拡張によって様々なセキュリティチェックをフックするためのメカニズムを提供する。
出典: Linux Security Module Usage ↩︎




