seccomp とは、Linuxでプロセスのシステムコールの発行を制限する機能のこと。今回試すものはseccomp mode 2と呼ばれる、Linux3.5から搭載されたもので、システムコール単位での制限、特定の条件を満たす引数のみの許可/制限を実現できる。
バックエンドではlibpcapなどでも利用しているBPF(Berkeley Packet Filter)を利用していて、JITなどで高速に動作するらしい。
以下のサイトなども参照のこと。
今回、seccompを利用するためのCライブラリlibseccompを用いて、mruby-seccompを作成した。これを利用してシステムコールの発行制限を試す。
基本的な使い方
基本的には、Seccompのコンテクストを作って、ルールを記述し、最終的にそれをカレントプロセスにロードする。libseccompのAPIでは、それぞれ seccomp_init()
、 seccomp_rule_add()
seccomp_load()
に対応する。
Seccompのコンテクストは、デフォルトのアクション(対応しているのは SCMP_ACT_KILL/SCMP_ACT_ALLOW/SCMP_ACT_TRAP
)を指定して、その後ルール追加でここのシステムコール毎の対応を指定することができる。デフォルトが :kill
ならホワイトリスト、 :allow
ならブラックリストのイメージで。
以下は、ホワイトリストで uname(2)
の呼び出しを制限している。 mruby-uname を一緒に用いている。 uname(2)
の引数はポインタなのでここでは特に制限を入れていないが、例えば unshare(2)
のようにフラグなどを取るシステムコールなら、特定のフラグだけ許可/拒絶のようなルールを指定することも可能。
引数制限のための Seccomp::ARG()
の使い方は README などで。
context = Seccomp.new(default: :allow) do |rule| rule.trap(:uname) end context.load Uname.nodename # ここで uname(2) をフック
$ ./mruby/bin/mruby /tmp/test.rb Bad system call
fork()/exec()
との組み合わせ
カーネルドキュメント にあるように、 fork/clone and execve
を経由してもseccompの情報は変更されない。
従って、fork()してからコンテクストをloadし、任意のプログラムにexec()することで、特定のシステムコールを呼び出せないコンテナ的な環境を作成できる(forkは mruby-process 、execは mruby-exec)。
context = Seccomp.new(default: :allow) do |rule| rule.kill(:mkdir, Seccomp::ARG(:>=, 0), Seccomp::ARG(:>=, 0)) end pid = Process.fork do context.load puts "==== It will be jailed. Please try to mkdir" exec "/bin/sh" end p(Process.waitpid2 pid)
$ ./mruby/bin/mruby /tmp/jail.rb ==== It will be jailed. Please try to mkdir sh-4.2$ mkdir /tmp/test1234 Bad system call
考え方は若干Linux Capabilityにも似ているが、より粒度を細かくして制限/許可できる。
sigactionによるシステムコール制限のキャッチ
コンテクストもしくはルールの作成時にアクションとして SCMP_ACT_TRAP
を指定すると、(mruby-seccomp内ではdefault: :trap
または rule.trap(...)
)指定したシステムコールの実行時に SIGSYS
を飛ばすのだが、その時のシグナルハンドラを SA_SIGINFO
を指定して作成すれば、 siginfo_t
構造体を経由してどのシステムコール番号をフックしたかを取得できる。
つまりシステムコールをフックする任意の処理で、どのシステムコールを呼んだかによって処理を変更させるなどできる。
この辺りをラップした機能を、 Seccomp.on_trap
として実装済み(siginfo_t
構造体を利用するというのをCRubyでもmruby-signalでもサポートしていないので、独自の実装となっている)。
context = Seccomp.new(default: :allow) do |rule| rule.trap(:uname) end Seccomp.on_trap do |syscall| puts "Trapped: syscall #{Seccomp.syscall_to_name(syscall)} = ##{syscall}" end context.load begin # Then hit `uname` p "nodename: " + Uname.nodename rescue => e puts "Catch as error: " + e.message puts "Trapping is OK" end
$ ./mruby/bin/mruby /tmp/trap.rb Trapped: syscall uname = #63 Catch as error: uname failed Trapping is OK
uname(2)
のコール自体は失敗した体裁になる。ちゃんとしたmruby gem経由の呼び出しであればシステムコールの失敗はRuntimeErrorになるので、Rubyサイドでいい感じにハンドルすればよさそう。
なお、シグナルハンドラ側でどうにかしないといけない関係で、exec()するとこのフックは消えてしまう。。利用シーンとしては、mrubyのコード内でのシステムコールのトラッキングなどを想定している。
まとめ
seccompによりあるプロセス(ツリー)でのシステムコールの発行の制限ができる。mruby-seccompやその他プロセス関連のmgemを用いることで比較的容易に実験ができる。
seccompはDocker、LXCなどではコンテナのサンドボックス化などの用途で利用することができ、haconiwaにも組み込もうと思ってこのmgemを作ったわけだが、単体でも利用できるように設計している。ということで紹介記事でした。
mruby-seccomp はまだ libseccomp のすべての機能を網羅しているわけではないため、プルリクエストなど歓迎します。
(サンプルコードは mruby-seccomp/examples at master · haconiwa/mruby-seccomp · GitHub にもあります)