第7章 セキュアUnix/Linux プログラミング
[7-3.]
setuid は慎重に
setuidは,プログラム実行時アカウントを一時的に変更することで一般ユーザが投入したコマンドの中で特殊な権限を必要とする処理の実行を可能にする機能である。ところが,利用できる機能の限定の仕方が不十分だと,一般ユーザに大きな権限の使用を許してしまう問題が生じる。



 PDF
setuid/setgid
リスト1のサンプルプログラムsetuid_test.c をご覧いただきたい。「setuid_test.out」という名前のファイルを作成(13行目)するだけの簡単なプログラムだ。
リスト1 サンプルプログラムsetuid_test.c
  1 #include <sys/types.h>
  2 #include <sys/stat.h>
  3 #include <fcntl.h>
  4 #include <stdlib.h>
  5
  6 int main(void);
  7
  8 int main(void)
  9
 10     int fd;
 11
 12     /* create a file */
 13     fd = creat("setuid_test.out", 0644);
 14     if(fd!=-1) close(fd);
 15 }
リスト1をコンパイルしたsetuid_testプログラムを使って,setuid/setgid について説明しよう。実行例1をご覧いただきたい。この実行例は一般ユーザfoo による操作を記録したものだ。
実行例1 setuid ビット/setgid ビットがセットされたプログラムsetuid_test の実行
  1 $ gcc -o setuid_test setuid_test.c      ← コンパイル
  2 $ ls -l setuid_test
  3 -rwxrwxr-x 1 foo foo 11856 Feb 15 15:34 setuid_test
                  ↑  ↑
                  所有ユーザ/グループは現在foo/foo

  4 $ su                                    ← root になる
  5 Password: < パスワードを入力>
  6 [root]# chown root.nobody setuid_test   ← 所有者/グループをroot/nobody に変更
  7 [root]# chmod ug+s setuid_test          ← setuid/setgid ビットをセット
  8 [root]# exit                            ← root から一般ユーザfoo へ戻る
  9 exit
 10 $ ls -l setuid_test
 11 -rwsrwsr-x 1 root nobody 11856 Feb 15 15:34 setuid_test
      ↑  ↑      ↑   ↑
  setuid  setgid  所有ユーザ/グループがroot/nobody に変更された

 12 $ ./setuid_test                         ← サンプルプログラム実行
 13 $ ls -l setuid_test.out                 ← 作成されたファイル属性を表示してみる
 14 -rw-r--r-- 1 root nobody 0 Feb 15 15:35 setuid_test.out
                  ↑   ↑
                  所有ユーザ/グループがroot/nobody でファイルが生成された
1 〜 3 行目に注目していただきたい。コンパイルされた実行ファイルsetuid_testは,所有ユーザ/所有グループがfoo/fooとなっている。これはコンパイラ(gcc コマンド)が,一般ユーザfoo の権限,つまりユーザfoo/グループfoo 権限で実行されたことを表している。ユーザがプログラムを実行させると,プログラムはそのユーザの権限で動作する。これがUnix(およびLinux)の保護機構の基盤だ。
 
ここで実験用に実行ファイルsetuid_testの属性を操作する。4〜5行目でrootユーザになり,6行目で所有ユーザ/所有グループをroot/nobody に変更,7 行目で実行ファイルの属性に含まれるsetuid ビット/setgid ビットをセットする。このビットをセットするとsetuid機能/setgid機能というものが有効となるが,この機能については後述する。8 行目のexit コマンドでroot ユーザから元の一般ユーザfoo に戻る。
 
実行ファイルに加えた変更を10 〜 11 行目で確認している。属性部分rwsrwsr-xに普段「x」と表示されていたところに「s」が2 つあることに注意していただきたい。それぞれsetuid ビット,setgid ビットがセットされていることを示している。また所有ユーザ/所有グループがroot/nobody になっていることにも注意していただきたい。
setuid 機能/setgid 機能
さてここまででsetuidビット/setgidビットを有効にした実行ファイルsetuid_testが準備できた。一体setuid機能/setgid 機能とはどんな機能なのだろうか?
 
12〜 14行目の実行例を見ていただきたい。12 行目でsetuid_testプログラムを実行している。このプログラムが作成したファイルsetuid_test.outを見てみる(14行目)と,なんと所有ユーザ/所有グループがroot/nobodyとなっているのだ。このプログラムを起動したユーザが一般ユーザfoo であるにも関わらずである。
 
作成されたファイルの所有者がroot/nobody になったのはプログラムがroot/nobody の権限で動作していたからだ。これがsetuid機能/setgid機能の効果だ。プログラムを起動するユーザの権限とは別の権限でプログラムを動作させることが可能である。setuid 機能/setgid 機能は次のように説明することができる。
  • setuid ビットがセットされたプログラムは,その実行ファイルの所有ユーザ権限で動作する
  • setgid ビットがセットされたプログラムは,その実行ファイルの所有グループ権限で動作する
つまり,実行例1のsetuid_test プログラムの場合,所有ユーザ/所有グループがroot/nobody であり,且つsetuidビット/setgidビットの両方がセットされているため,setuid_testプログラムはroot/nobody権限で動作することとなる。
 
実行例1ではsetuid 機能/setgid 機能の両方を有効にしたが,もちろんsetuid 機能とsetgid 機能は独立しており,それぞれ別々に有効にできる。
 
このようにsetuid 機能/setgid 機能を活用することにより,プログラムを実行させるユーザの権限によらず,プログラマが意図した権限でプログラムを実行させることが可能だ。一般に,root権限を一時的に必要とするコマンドなどで用いられる。例えば/usr/bin/passwdコマンドも/etc/passwdファイルへの書き込み権限を必要とするため,setuid 機能を使用している。
  1 $ ls -l /usr/bin/passwd
  2 -r-s--x--x 1 root root 12244 Feb 8 2000 /usr/bin/passwd
      
      setuid ビット
パスワードファイル破壊
実はリスト1のサンプルプログラムsetuid_testには重大なセキュリティホールがある。一般ユーザ権限だけでパスワードファイルを破壊するシンボリックリンク攻撃が可能だ。次の1行を実行すると/etc/passwdファイルへのシンボリックリンクとして,「setuid_test.out」ファイルが作成される。
  1 lrwxrwxrwx 1 foo foo 11 Feb 16 01:30 setuid_test.out -> /etc/passwd
このシンボリックリンクと同じディレクトリで,先ほどのsetuid機能を有効にしたsetuid_testプログラムを実行してしまうと,/etc/passwd ファイルの内容が空っぽになってしまう。setuid_test プログラムがsetuid 機能によりroot 権限で動作するため,ファイル作成のcreat( )システムコールがシンボリックリンク先の/etc/passwdファイルのファイルサイズを0(ゼロ)にしてしまうからだ。シンボリックリンク攻撃については関連記事『7-1. シンボリックリンクの悪用』を参考にしていただきたい。
 
このセキュリティホールの直接的原因はcreat( )システムコールを使ったことにある。安全にファイルを作成するにはopen( )システムコールを適切に使うべきだ。安全なファイル作成手法については関連記事『7-5. Unixのレースコンディション』を参考にしていただきたい。
 
以降ではsetuid/setgid について,もう少し踏み込んで説明する。
クリデンシャル(Credential)
Unix(およびLinux)プロセスは権限を表すクリデンシャル(Credential)という属性を持つ。前節までで何度も出てきたユーザroot/グループnobody 権限はクリデンシャルの具体例だ。
 
クリデンシャルはID で表現される。ユーザroot という権限はユーザID が0(root)というクリデンシャルで表現される。またグループnobody という権限はグループID が99(nobody)というクリデンシャルで表現される。
 
前節までで見てきたクリデンシャルは実際には実効ユーザIDと実効グループIDである。実はプロセスが持つクリデンシャルには,ユーザ/グループのそれぞれに3 種類ずつある。
  • 実ユーザID/実グループID
  • 実効ユーザID/実効グループID
  • 保存ユーザID/保存グループID
実ユーザID/実グループID
setuid機能/setgid 機能が有効であると,プログラムは実行ファイルの所有ユーザ/所有グループを権限として動作する。プログラムを起動したユーザの権限とは無関係になる。
 
しかしプログラムを実行するプロセスは,プログラムを起動した実際のユーザをしっかりと覚えている。それが実ユーザID/実グループID である。次のシステムコールによりこれらのクリデンシャルを取得可能だ。
  1 uid_t getuid(void);   ← 実ユーザID を取得
  2 gid_t getgid(void);   ← 実グループID を取得
実効ユーザID/実効グループID
実効ユーザID/実効グループIDはプログラムが動作するときの実際の権限で,通常我々がプログラムの動作権限として認識しているものだ。オペレーティングシステムがアクセス権限判断の際に参照するクリデンシャルである。
 
実効ユーザID/実効グループID は次のシステムコールにより取得可能だ。
  1 uid_t geteuid(void);   ← 実効ユーザID を取得
  2 gid_t getegid(void);   ← 実効グループID を取得
通常のプログラムの場合,実ユーザID/実グループIDと実効ユーザID/実効グループIDは一致するが,setuid機能/setgid 機能が有効な場合,これが一致しなくなる。
保存ユーザID/保存グループID
保存ユーザID/保存グループIDはシステムコールなどから取得することのできない内部的なクリデンシャルだ。これらのID はプログラム起動時の実効ユーザID/実効グループID を保存したものだ。
 
実効ユーザID/実効グループID はsetuid( )/setgid( )システムコールにより(制約はあるものの)プログラム実行中に動的に変更可能だ。つまりプログラムの動作権限はそのプログラム自身がある程度変更できるのだ。プログラム起動時の実効ユーザID/実効グループIDをオペレーティングシステムが忘れないために保存ユーザID/保存グループID は用意されている。
実ID,実効ID,保存ID
以降では説明の簡素化を目的として,実ユーザID と実グループID をまとめて「実ID」,実効ユーザID と実効グループID をまとめて「実効ID」,保存ユーザID と保存グループID をまとめて「保存ID」と記述する。
 
ただし「実効ユーザID」などのように個別に記述する個所は,実効グループIDとまとめて扱うことができない内容であるので注意していただきたい。
クリデンシャルの動的な変更
プロセスのクリデンシャルは後述するset*id( )システムコールを呼び出すことにより,ある制約の範囲内でプログラム実行中に変更できる。set*id( )システムコールを呼び出すときのプロセスの実効ユーザID が,rootユーザであるか一般ユーザであるかによって制約は異なる。
一般ユーザの制約
実効ユーザIDが一般ユーザ(非rootユーザ)である場合は,実行時に変更できるクリデンシャルは実効IDのみである。実ID および保存ID を変更することはできない。
 
さらに実IDを変更するときに設定できる値は実IDおよび保存IDのみである。実効IDが変化するといっても実IDと保存IDの間を行き来できる程度だ。つまり実効IDは実IDか保存IDのどちらかの値と常に一致する。一般ユーザが妄りにプロセスの動作権限を設定できないよう保護されている。
 
setuid/setgid 機能が有効なプログラムの場合,実ID と保存ID が異なる。実効ID を変更することによって,setuid/setgid権限と(プログラムを起動した)もとのユーザ権限とを切り替えることができる。しかしsetuid/setgid機能を利用していないプログラムの場合,実IDと保存IDは同じ値であるため,実効IDは常にもとのユーザ権限にしかならない。
root ユーザの制約
rootユーザの場合は一般ユーザに比べクリデンシャルの変更に制約は少ない。実ID,実効IDには任意の値を設定できる。しかし保存ID には任意の値を設定することができず,実ID と実効ID の両方がroot でなくなったとき自動的に実効ID と同じ値が設定される。
 
一旦,実IDと実効IDの両方がrootでなくなった場合,一般ユーザ権限で動作しているのと同じ状態なるため,それ以降は一般ユーザの制約の範囲でのみクリデンシャルの変更ができる。
set*id( )システムコール
プログラム実行時に動的にクリデンシャルを変更するにはset*id( )システムコールを使用する。set*id( )のアスタリスク「*」表記は,クリデンシャルを操作する複数のシステムコールを総称する意味でこの記事中で便宜上使っている表記だ。
 
利用可能なset*id( )システムコールは各種Unixオペレーティングシステムごとに異なる。以下の6つは(Linuxを含めた)ほぼすべてのシステムで利用できるものだ(図1)。
  1 int setuid(uid_t uid);
  2 int setgid(gid_t gid);
  3 int setreuid(uid_t ruid, uid_t euid);
  4 int setregid(gid_t rgid, gid_t egid);
  5 int seteuid(uid_t uid);
  6 int setegid(gid_t gid);
図1 set*id( )システムコールで変更されるID
 図1 set*id( )システムコールで変更されるID
setuid(uid)/setgid(gid)
実効ユーザIDがroot ユーザである場合,setuid(uid)は実効ユーザIDだけでなく実ユーザIDおよび保存ユーザID にもuid を設定する。setgid(gid)も同様に,実効グループID だけでなく実グループID および保存グループID にもgid を設定する。
 
実効ユーザID が非root ユーザである場合,setuid(uid)は実効ユーザID のみにuid を設定し,実ユーザID と保存ユーザID は変化しない。setgid(gid)も同様に,実効グループID のみにgid を設定し,実グループID と保存グループID は変化しない。
setreuid(ruid, euid)/setregid(rgid, egid)
実効ユーザID がroot ユーザである場合,setreuid(ruid, euid)は実ユーザID にruid,実効ユーザID にeuid を設定する。もしruidおよびeuidの両方が非rootユーザである場合,さらに保存ユーザIDにもeuidが設定される。setregid(rgid, egid)もsetreuid( )と同様の動作だ。
 
実効ユーザID が非root ユーザである場合,setreuid(ruid, euid)は実ユーザID にruid,実効ユーザID にeuid を設定するが,ruidとeuidには実ユーザIDもしくは実効ユーザIDの値のみが許可されるという制約がある。そうでなければsetreuid( )はエラーを返す。setregid(rgid, egid)もsetreuid( )と同様の動作だ。
 
またruid,euid,rgid,egid に-1 を指定すると,現在の値をそのまま変更しないという指定になる。
seteuid(uid)/setegid(gid)
seteuid( )/setegid( )は実効IDのみを変更するシステムコールであるが,実際には以下の式で示すようにsetreuid( )/setregid( )で実装されている。
  1 seteuid(uid) = setreuid(-1, uid);
  2 setegid(gid) = setregid(-1, gid);
setuid/setgid の安全対策
setuid/setgidはプログラムに大きな権限(root権限など)を与えることができる,パワフルであると同時にとても危険な機能だ。
 
setuid/setgid を利用するプログラムを作成する場合,セキュリティホールを作らないように細心の注意を払うことは当然であるが,システムやアプリケーションへ大きな影響を及ぼす危険性があるため,セキュリティホールが生じても影響を最小限に食い止める安全対策が必要だ。
 
setuid 機能を活用してプログラムをroot 権限で動作させるということは,プログラムに特別な権限(特権)を与えることになる。関連記事『9-4. 特権処理の局所化』では特権を扱う場合の安全対策を説明しているが,そのままsetuid/setgid の安全対策にも適用できる対策手法である。是非とも『9-4. 特権処理の局所化』をご一読いただいてから,以降の説明を読み進めていただきたい。
専用グループ
危険性があるにもかかわらず,不用意にsetuid機能を利用してrootユーザ権限で動作させるように設計されたプログラムは意外と多い。しかしながら,実際にはわざわざroot ユーザ権限(特権)で動作させなくても目的を達成できることも多い。
 
アプリケーションが独自に管理するパスワードファイルのように,一般ユーザからの読み書きを禁止し,専用プログラムを経由してのみ読み書きできるような例を考える。ここで不用意に専用プログラムをsetuidしてrootユーザ所有にしてはならない。プログラムがrootユーザ権限で動作してしまうので,セキュリティホールが存在すると大きな問題に繋がるからだ。
 
こういった場合によく使われる定番の手法がある。専用グループだ。例えば専用グループbbsgroupを作成し,そのグループにはどのユーザも所属させない。パスワードファイルには一般ユーザからアクセスさせたくないので,その所有者をroot/bbsgroup とし,読み書き権限をグループのみに与える。
 ----rw---- 1 root bbsgroup 130 Feb 16 20:08 bbspasswd
さらに専用プログラムbbschangepwdの所有グループをbbsgroupとし,setgidビットをセットする。setuidビットは使用しない。setgid 機能により専用プログラムbbschangepwd は実効グループID がbbsgroup で動作する。これによりパスワードファイルbbspasswd へのアクセスが可能となる。
  -rwxrwsr-x 1 root bbsgroup 65536 Feb 16 19:47 bbschangepwd
        
       setgid ビット
専用グループを利用する手法により,プログラムをroot ユーザ権限で動作させなくて済んだ。プログラムにセキュリティホールがあったとしても,実効グループbbsgroup の権限までしか奪取されない。
特権放棄と特権復帰
rootユーザ権限のようにシステムに大きな影響を与えることが可能な特別な権限を「特権」と呼び,特権を必要とする処理を「特権処理」と呼ぶ。setuid機能は一時的な特権処理を必要とするプログラムでよく利用される。
 
ここでは「特権の放棄」と「特権の復帰」の手法を説明する。
 
rootユーザ所有の実行ファイルにsetuidビットがセットされているとする。特権処理が完了した後の一般処理には特権(root ユーザ権限)は不要だ。次の1 行により特権を放棄できる。
if(seteuid(getuid())!=0) abort();    (注1)
getuid( )システムコールで実ユーザID を取得し,これをseteuid( )システムコールで実効ユーザID に設定している。つまりプログラムの動作権限をプログラムを起動したユーザの権限(つまり一般ユーザの権限)に降格している。
プログラムが再び特権処理を行う直前には特権を復帰しなければならない。次の1行により特権を復帰できる。seteuid( )システムコールにroot ユーザID である0 を指定することで,特権を復帰している。
if(seteuid(0)!=0) abort();
(注1・seteuid(getuid( ))は失敗しないはずであるが,特権放棄に失敗すると特権状態で動作を継続してしまうため,特権放棄の失敗をチェックしプログラムを強制終了させるのがよい。)
特権の完全放棄
特権の完全放棄とは,特権放棄後,二度と特権復帰できない状態にする保護手段だ。関連記事『9-4. 特権処理の局所化』には,この手法を使ったサンプルが掲載されている。
 
もしプログラムにバッファオーバーラン脆弱性が存在すると,たとえプログラムが特権を放棄していたとしても,送り込まれたプログラムで特権を復帰され,特権状態で任意の処理を実行される危険性が生じる。二度と特権を復帰できないように,特権を完全放棄しなければならない。次の1行により特権を完全放棄できる。
if(setuid(getuid())!=0) abort();
getuid( )システムコールにより実ユーザID を取得し,これをsetuid( )システムコールで実効ユーザID および保存ユーザID に設定している。保存ユーザID が実ユーザID,つまりプログラムを起動した一般ユーザ権限に設定されることにより,二度とroot ユーザ権限を取得できなくなる。
まとめ
setuid/setgidはプログラムを一時的に高い権限で動作させるUnix(およびLinux)の特殊なオプションである。パワフルな機能であると同時に大きな危険を伴うため十分な安全対策が必要だ。専用グループ,特権の放棄と復帰,特権の完全放棄の設計パターンをうまく活用することで,万一のときの被害を最小限に抑えることができる。
関連記事
参考文献
『詳細UNIX プログラミング[新装版]』,W・リチャード・スティーヴンス,大木敦雄訳,2000 年,株式会社ピアソン・エデュケーション
修正履歴
2002年3月13日
「setuid/setgid の安全対策」節の「特権放棄と特権復帰」および「特権の完全 放棄」のコード例3点ならびに注1を訂正。