読者です 読者をやめる 読者になる 読者になる

ローファイ日記

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

seccompをmrubyで試す

seccomp とは、Linuxでプロセスのシステムコールの発行を制限する機能のこと。今回試すものはseccomp mode 2と呼ばれる、Linux3.5から搭載されたもので、システムコール単位での制限、特定の条件を満たす引数のみの許可/制限を実現できる。

バックエンドではlibpcapなどでも利用しているBPF(Berkeley Packet Filter)を利用していて、JITなどで高速に動作するらしい。

以下のサイトなども参照のこと。

mmi.hatenablog.com

yuzuhara.hatenablog.jp

今回、seccompを利用するためのCライブラリlibseccompを用いて、mruby-seccompを作成した。これを利用してシステムコールの発行制限を試す。

github.com

基本的な使い方

基本的には、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 にもあります)