ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

mrubyとseccompとptraceでシステムコールをとにかく追いかける

世の中にはseccompというものがあり、知られています。皆さんはBPFですか。人は、、、

seccompについては以前書きました。Linuxシステムコール呼び出しをフィルタリングして許可したり禁止したりするものです。

udzura.hatenablog.jp

さて今回、拙作 mruby-seccomp で SCMP_ACT_TRACE アクションをサポートしました。その辺の話をしてみます。

libseccompのコンテクストに SCMP_ACT_TRACE のアクションを追加してロードすると、当該システムコールの呼び出しを ptrace(2) でトレースできます。 ptrace(2) は普通、あらゆるシステムコールSIGTRAP で止めるみたいな動きをしますが、特定のシステムコールのみを停止でき、またシグナルも SIGTRAP ではなく別のものとすることができます。

ptrace(2) の詳細は先日書きました。

udzura.hatenablog.jp

プルリクは下のものになります。

github.com

サンプル

とても簡単な例として、 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、自作デバッガ、自作、、、