世の中にはseccompというものがあり、知られています。皆さんはBPFですか。人は、、、
seccompについては以前書きました。Linuxのシステムコール呼び出しをフィルタリングして許可したり禁止したりするものです。
さて今回、拙作 mruby-seccomp で SCMP_ACT_TRACE
アクションをサポートしました。その辺の話をしてみます。
libseccompのコンテクストに SCMP_ACT_TRACE
のアクションを追加してロードすると、当該システムコールの呼び出しを ptrace(2) でトレースできます。 ptrace(2) は普通、あらゆるシステムコールを SIGTRAP
で止めるみたいな動きをしますが、特定のシステムコールのみを停止でき、またシグナルも SIGTRAP
ではなく別のものとすることができます。
ptrace(2) の詳細は先日書きました。
プルリクは下のものになります。
サンプル
とても簡単な例として、 connect
の呼び出しを標準出力にロギングするシェルを立ち上げてみます。 https://github.com/haconiwa/mruby-seccomp
を適当な新し目のLinuxにチェックアウトして、rake
でmrubyバイナリを作ると、デフォルトでmruby-seccompのほか、fork、execを利用できるものができます。
コンテクストはこんな感じで定義します。第2引数は、他のルールと違い必須で、ユーザーデータ的に任意の数字をトレーサに渡す機能があるためですが、今回は特に使いません。
context = Seccomp.new(default: :allow) do |rule| # 第2引数は無視 rule.trace(:connect, 0) end
これをforkして読み込ませ、シェルに exec()
するところまではこういう感じです。
pid = Process.fork do context = Seccomp.new(default: :allow) do |rule| rule.trace(:connect, 0) end context.load exec '/bin/sh', '-l' end
pid
が取れるので、それに対し、 Seccomp.start_trace
というメソッドでトレースを開始します。
ret = Seccomp.start_trace(pid) do |syscall, _pid, ud| name = Seccomp.syscall_to_name(syscall) # システムコール番号を、その環境のシステムコール名に変換 puts "[#{_pid}]: syscall #{name}(##{syscall}) called. (ud: #{ud})" end
全体
pid = Process.fork do context = Seccomp.new(default: :allow) do |rule| rule.trace(:connect, 0) end context.load exec '/bin/sh', '-l' end ret = Seccomp.start_trace(pid) do |syscall, _pid, ud| name = Seccomp.syscall_to_name(syscall) puts "[#{_pid}]: syscall #{name}(##{syscall}) called. (ud: #{ud})" end p ret
これをmrubyバイナリで実行すると、 curl
などを呼び出すとめっちゃconnect(2)が呼ばれることが確認できます。もちろん、 kill
などの他のアクションも定義できますし、例えば何回connect(2)が呼ばれたか数えておいて任意の回数でプロセスごと殺すみたいなこともできるかもしれません。
vagrant@foo:$ ./mruby/bin/mruby /tmp/sample-sh.rb $ curl -s udzura.jp >/dev/null [14011]: syscall connect(#42) called. (ud: 0) [14011]: syscall connect(#42) called. (ud: 0) [14011]: syscall connect(#42) called. (ud: 0) [14011]: syscall connect(#42) called. (ud: 0) [14011]: syscall connect(#42) called. (ud: 0) [14011]: syscall connect(#42) called. (ud: 0) [14010]: syscall connect(#42) called. (ud: 0) ...
工夫したところ
ptraceするときに、 PTRACE_O_TRACEFORK | PTRACE_O_TRACEVFORK | ...
みたいなことをしないとexecした先でforkしたプロセスまで追ってくれない。
あと、そうした場合、traceしている根元がexitしたのか、その子供や孫がexitしたのかを区別しないといけない。詳細は man 2 ptrace とにらめっこ…。
ひとまず、全体にまずは動いてるね、という趣きなので引き続きやっていきましょう。
この辺の深掘りを頑張れば、straceのようなものを自作できると思います。あとは、プロセスのメモリにある情報を取得してきて、表示するか、mrubyのオブジェクトへマップするというタスクがあります。straceの中身の記事で言及した通り、概ねシステムコールごとに表示用の実装を用意する必要があり…。
今雑に確認したところLinux 4.4のx86_64環境においてはシステムコールは323種類あるようです。ポケモン第一世代+第二世代よりは多く、第三世代を含めるならそれよりは少なくなります。筆者は第一世代は大体全部言えますが。さすがにそういう年代なんで。第二世代はマジで知らない。
自作コンテナはもう古い、時代は自作strace、自作デバッガ、自作、、、