2011年1月16日日曜日

setuid なスクリプト

シェルやPerlで書かれたスクリプトも、setuidプログラムのように動作して欲しいときがある。その場合にどうすればいいか、という話(セキュリティの問題があるため仕事ではほとんど役に立たない、教養レベルの話)。

いろいろと方法があるだろうけれど、ここではそのうちの2つ記録しておく。

その1. setuid(0)system()を用いたラッパープログラム

setuid on shell scripts に書いてあるとおり、シェルやPerl等のスクリプトファイルの場合は、setuidビットをセットして実行しても無視される。そこで、ラッパーとしてELFの実行ファイルをC言語で作成してsetuidビットをセットするのだが、その内部でただ単にsystem(…)関数でスクリプトを実行するだけでは足りない。system(…)の前に、setuid(0)を実行しないといけない。

…
int main()
{
   setuid( 0 );
   system( "/path/to/script.sh" );

   return 0;
}
  

ここで、setuid(0)が必要となる理由は、"man 3 system", "man sh" を調べると分かる。

man 3 system:
DESCRIPTION system() executes a command specified in command by calling /bin/sh -c command, and returns after the command has been completed. During execution of the command, SIGCHLD will be blocked, and SIGINT and SIGQUIT will be ignored.
man sh:
If the shell is started with the effective user (group) id not equal to the real user (group) id, and the -p option is not supplied, no startup files are read, shell functions are not inherited from the environment, the SHELLOPTS variable, if it appears in the environment, is ignored, and the effective user id is set to the real user id. If the -p option is supplied at invocation, the startup behavior is the same, but the effective user id is not reset.

整理すると、system("/path/to/script.sh")/bin/sh -c "/path/to/script.sh" に変換される。この/bin/shは、effective use id=root で real user id=一般ユーザーの状態で実行されるから、effective user id が一般ユーザーのidで上書きされ、結局はsetuidプログラムのクレデンシャル(credential)が失われてしまう、という流れ。

これを防ぐには、effective use id と real user id が一致した状態でsystem(…)を呼べばよいから、事前にsetuid(0)を呼ぶことになる(setuid(0)により、effective use id と real use id の両方に 0 つまりrootが設定される)。

※UNIXプロセスのクレデンシャル関係の用語は、邦訳が揺らいでいて曖昧なので英語のままがいい。ちなみに、手元にある「Amazon.co.jp: 詳解UNIXプログラミング: W.リチャード スティーヴンス, W.Richard Stevens, 大木 敦雄: 本」では"real user id"が「実ユーザID」、"effective user id"が「実効ユーザID」と訳されている。

その2. execve() を用いたラッパープログラム

ここから別解。system()setuid(0)の使用をやめて、exec()系のexecve()を用いる方法。

なぜexecve()を使うかと言えば、man 3 systemおよび「IPA ISEC セキュア・プログラミング講座:C/C++言語編 第10章 著名な脆弱性対策:コマンド注入攻撃対策」で推奨されているから。要するに、スクリプトに引き継がれる環境変数を制御できるから。

man 3 system
Do not use system() from a program with set-user-ID or set-group-ID privileges, because strange values for some environment variables might be used to subvert system integrity. Use the exec(3) family of functions instead, but not execlp(3) or execvp(3).
IPA ISEC セキュア・プログラミング講座より引用:
これらのうち使用を推奨するのは、 execle execve execvP の3つである。なぜならば、環境変数 PATH が改ざんされていても影響を受けず、起動するプログラムに与える環境変数を制御できるからである。

同時に、実行される側、スクリプトのほうにも一工夫必要となる。具体的には、man shに記載のとおり、shebang行に-pオプション( Turn on privileged mode)を加えて、子プロセスの effective user id の変更を防ぐ。

man sh:
-p Turn on privileged mode. In this mode, the $ENV and $BASH_ENV files are not processed, shell functions are not inherited from the environment, and the SHELLOPTS variable, if it appears in the environment, is ignored. If the shell is started with the effective user (group) id not equal to the real user (group) id, and the -p option is not supplied, these actions are taken and the effective user id is set to the real user id. If the -p option is supplied at startup, the effective user id is not reset. Turning this option off causes the effective user and group ids to be set to the real user and group ids.

以上をまとめると、次のようなコードになる。

execve()を使った場合のコード(C)

fork()してexecve()する流れ。コンパイル後、chmod で所有者root の 4755 に変更しておく。

/* setuid_test.c */
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
int main(int argc, char *argv[]){
  int pid;
  if((pid=fork()) < 0){
    return 1;
  }else if(pid > 0){
    wait(NULL);
  }else{
    if(execve("/path/to/script.sh", NULL, NULL) == -1){
      return 1;
    }
    return 0;
  }
  return 0;
}
    

スクリプトのコード(shebang行の -p が必要)

スクリプトのほうは chmod で所有者が一般ユーザーの 744 にしておく。

#!/bin/bash -p
tail /var/log/maillog;  # rootだけが読み込めるログを表示してみる
    

実行例

以下のような感じ(CentOS5で確認)。

$ ./setuid_test
Jan 16 10:11:16 example postfix/smtp[9884]: D446F10C4F9B: host mx.example.com[999.999.999.999] refused to talk to me: 421 Message from (999.999.999.999) temporarily deferred - …
Jan 16 10:11:17 example postfix/smtp[9884]: D446F10C4F9B: host mx.example.com[999.999.999.999] refused to talk to me: 421 Message from (999.999.999.999) temporarily deferred - …
…
    

ちなみに、shebang行の-pを消して実行した場合、execve("/path/to/script.sh", NULL, NULL)の後にsetuid(x)setgid(x)が勝手に実行されて、setuidプログラムのクレデンシャルが失われることが分かる(ここで、x は一般ユーザーのID)。以下は、その現象を strace で記録したもの。

$ id
uid=505(testuser) gid=505(testuser) groups=505(testuser)
$ strace -f -o strace.txt ./setuid_test
$ cat strace.txt
…略…
25971 execve("/path/to/script.sh", [0], [/* 0 vars */]) = 0
…略…
25971 getuid()                          = 505
25971 getgid()                          = 505
25971 geteuid()                         = 0
25971 getegid()                         = 505
25971 rt_sigprocmask(SIG_BLOCK, NULL, [], 8) = 0
25971 setuid(505)                       = 0
25971 setgid(505)                       = 0
25971 open("/proc/meminfo", O_RDONLY)   = 3
…略…
    

0 件のコメント:

コメントを投稿