特殊なアクセス権 SUID(Set User ID) 設定時の動作を確認した

「例解UNIXプログラミング教室」の「6.1.5 ユーザ識別子設定とグループ識別子設定」の説明について、プログラム例が載っていなかったので、実際に作成して動作確認してみた。

下図は書籍に載っていた図で、自分の環境ではシェルの euid のみ dam に変更している。

図の実行ファイル boggle は、 hiscores ファイルに書き込む機能を持っている。 boggle と hiscores の所有者は同じである (uid = wizard) 。 ログインしているユーザは dam で、シェルの実効ユーザ識別子 (euid) は dam となっている。

dam が boggle を実行すると、boggle プロセスの実効ユーザ識別子は dam となる。dam は hiscores の所有者と異なるので、書き込みできない。
ユーザ識別子設定 (SUID設定) を行うと、boggle プロセスの実効ユーザ識別子は自身の実行ファイルの所有者 (wizard) を引き継ぐ。よって hiscores の所有者と一致して書き込み可能となる。

これを以下で具体的に確認していく。

実行するユーザ識別子の確認

最初にログイン中のシェルプロセスの実効ユーザ識別子 (euid)、実行グループ識別子 (egid) を確認する。

1$ ps -p $$ -o euser,egroup,euid,egid
2EUSER    EGROUP    EUID  EGID
3dam      user      1000  1000

これは、ログインユーザの所有者 (uid)、グループ (gid) と一致している。

1$ id
2uid=1000(dam) gid=1000(user) groups=1000(user) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

プログラム boggle の内容

このプログラムは ./boggle 12345 のように実行される。すなわち、引数に数値(最高得点)を指定して実行されるものとする。
hiscores ファイルに、当プログラムを起動した uid と最高得点を出力する。

 1#include<stdio.h>     /* fopen, fclose, fprintf */
 2#include<stdlib.h>    /* atoi */
 3#include<unistd.h>    /* getresuid */
 4#include<sys/types.h> /* uid_t */
 5
 6int getresuid(uid_t *ruid, uid_t *euid, uid_t *suid);
 7
 8int main(int argc, char *argv[]){
 9    FILE *fp;
10    uid_t ruid, euid, suid;
11
12    int hiscore = atoi(argv[1]);
13
14	// 各種 uid の取得
15    getresuid(&ruid, &euid, &suid);
16
17	// hiscores ファイルを追加モードで開く
18    if ((fp = fopen("hiscores", "a")) == NULL){
19        perror("fopen");
20        exit(1);
21    };
22
23	// hiscores に ruid と最高得点を出力
24    fprintf(fp, "%d, %d\n", ruid, hiscore);
25
26	// hiscores ファイルを閉じる
27    if (fclose(fp) == EOF){
28        perror("fclose");
29        exit(1);
30    }
31
32    return 0;
33}

このプログラムでは、見通しを良くするため、エラー処理を適宜省略している(本来は引数チェックをすべき)。

ユーザ識別子設定前は hisocres に書けないことを確認する

上のプログラムをコンパイルして生成された実行ファイル boggle とファイル hiscores のそれぞれについて、 uid, gid を確認する。

1$ ls -l boggle hiscores
2-rwxr-xr-x. 1 wizard games 79112  3月 31 00:44 boggle
3-rw-r--r--. 1 wizard games     0  3月 31 00:49 hiscores

以下のように boggle を実行すると、ファイルを開くための権限がないためエラーとなる。

1$ ./boggle 12345
2fopen: Permission denied

ユーザ識別子設定する

以下のようにユーザ識別子設定して、権限を確認する。

1$ ls -l boggle hiscores
2-rwxr-xr-x. 1 wizard games 79208  3月 31 01:27 boggle
3-rw-r--r--. 1 wizard games    12  3月 31 01:18 hiscores
4$
5$ sudo chmod u+s boggle
6$
7$ ls -l boggle hiscores
8-rwsr-xr-x. 1 wizard games 79208  3月 31 01:27 boggle
9-rw-r--r--. 1 wizard games    12  3月 31 01:18 hiscores

boggle の所有者の実行権限の部分が x から s に変わっている。

ユーザ識別子設定後の boggle 再実行

以下のように、boggle が正常終了して、 呼び出したユーザのユーザ識別子と最高得点が hiscores に出力されることを確認した。

1$ ./boggle 12345
2[dam@localhost mypg]$ echo $?
30
4[dam@localhost mypg]$ cat hiscores
51000, 12345

boggle 実行中のユーザ識別子を確認してみる

boggle プログラムを少し弄って、実行中のユーザ識別子と実効ユーザ識別子を出力してみる。 また、グループ識別子も出力してみる。

 1#include<stdio.h>     /* fopen, fclose, fprintf */
 2#include<stdlib.h>    /* atoi */
 3#include<unistd.h>    /* getresuid */
 4#include<sys/types.h> /* uid_t */
 5
 6int getresuid(uid_t *ruid, uid_t *euid, uid_t *suid);
 7int getresgid(gid_t *rgid, gid_t *egid, gid_t *sgid);
 8
 9int main(int argc, char *argv[]){
10    FILE *fp;
11    uid_t ruid, euid, suid;
12    gid_t rgid, egid, sgid;
13
14    int hiscore = atoi(argv[1]);
15
16	// 各種 uid, gid の取得
17    getresuid(&ruid, &euid, &suid);
18    getresgid(&rgid, &egid, &sgid);
19
20    // 取得した各種 uid, gid を表示
21    printf("ruid=[%d], euid=[%d], suid=[%d]\n", ruid, euid, suid);
22    printf("rgid=[%d], egid=[%d], sgid=[%d]\n", rgid, egid, sgid);
23
24	// hiscores ファイルを追加モードで開く
25    if ((fp = fopen("hiscores", "a")) == NULL){
26        perror("fopen");
27        exit(1);
28    };
29
30	// hiscores に ruid と最高得点を出力
31    fprintf(fp, "%d, %d\n", ruid, hiscore);
32
33	// hiscores ファイルを閉じる
34    if (fclose(fp) == EOF){
35        perror("fclose");
36        exit(1);
37    }
38
39    return 0;
40}
 1$ ./boggle 12345
 2ruid=[1000], euid=[1001], suid=[1001]
 3rgid=[1000], egid=[1000], sgid=[1000]
 4$
 5$ ls -l boggle hiscores
 6-rwsr-xr-x. 1 wizard games 79112  3月 31 00:44 boggle
 7-rw-r--r--. 1 wizard games    12  3月 31 00:49 hiscores
 8$
 9$ cat /etc/passwd | grep wizard
10wizard:x:1001:20::/home/wizard:/bin/bash
11$
12$ ps -p $$ -o euser,egroup,euid,egid
13EUSER    EGROUP    EUID  EGID
14dam      user      1000  1000
15$

euid は 1001 となっている。これは boggle ファイルの所有者 wizard(1001) である。
hiscores ファイルの所有者と一致しているので、権限エラーが起きることなく書き込みできたわけだ。

おまけ①: 保存されたユーザ識別子について

再びプログラムを弄ってみる。 seteuid() により、実行ユーザ識別子を実ユーザ識別子に変えてみる。

 1#include<stdio.h>
 2#include<unistd.h>
 3#include<sys/types.h> /* uid_t */
 4
 5int getresuid(uid_t *ruid, uid_t *euid, uid_t *suid);
 6
 7int main(int argc, char *argv[]){
 8    uid_t ruid, euid, suid;
 9    getresuid(&ruid, &euid, &suid);
10    printf("seteuid before: ruid=[%d], euid=[%d], suid=[%d]\n", ruid, euid, suid);
11    if(seteuid(ruid) != 0){
12        perror("setuid");
13    };
14    getresuid(&ruid, &euid, &suid);
15    printf("seteuid after : ruid=[%d], euid=[%d], suid=[%d]\n", ruid, euid, suid);
16    return 0;
17}
1$ ./boggle
2seteuid before: ruid=[1000], euid=[1001], suid=[1001]
3seteuid after : ruid=[1000], euid=[1000], suid=[1001]
4[dam@localhost mypg]$

seteuid() 呼び出し前後で各種 uid を出力してみた。 euid の値が 1001 から 1000 に変わっている。 suid(保存されたユーザ識別子)は seteuid() 呼び出し前の euid の値を保持している。
euid を何らかの理由により変更した後で、元に戻したい時に suid を使うようである。

以下リンク先では、「保存されたユーザ識別子は、実ユーザ識別子と実効ユーザ識別子を切り替えるために存在するため、実効ユーザ識別子のコピーとして開始されます。」と記述されている。
https://unix.stackexchange.com/questions/548985/questions-about-the-saved-user-id

以下リンク先では、「プロセスが昇格された特権 (通常は root) で実行されているときに、特権の低い作業を行う必要がある場合に使用されます。これは、特権のないアカウントに一時的に切り替えることで実現できます。 権限の低い作業を実行している間、実効ユーザ識別子 euid はより低い権限値に変更され、 euid は保存されたユーザ識別子 suid に保存されるため、タスクの完了時に特権アカウントに切り替えるために使用できます。」と記述されている。
https://www.geeksforgeeks.org/real-effective-and-saved-userid-in-linux/

以下リンク先にも保存されたユーザ識別子の目的が書かれているが、よくわからなかった。
https://stackoverflow-com.translate.goog/questions/32455684/difference-between-real-user-id-effective-user-id-and-saved-user-id?_x_tr_sl=auto&_x_tr_tl=ja&_x_tr_hl=ja

おまけ②: シェルスクリプトにユーザ識別子設定しても無視される!

最初、ユーザ識別子設定の動作確認する時にシェルスクリプトで試していたのだが、ユーザ識別子設定しても権限エラーで hiscores に書き込むことができず、しばらくの間泥沼で踠いていた。 調査の結果、シェルスクリプトにユーザ識別子設定しても効かないことがわかったので、ここで参考までに調査内容を載せておく。 これはユーザ識別子設定の罠と言えるかもしれない。

以下のように、boggle を一行のみのシェルスクリプトにして、ユーザ識別子設定後に実行したところ、エラーになった。

 1$ cat boggle
 2echo $1 >> hiscores
 3$
 4$ ls -l boggle hiscores
 5-rwsr-xr-x. 1 wizard games 20  3月 31 03:02 boggle
 6-rw-r--r--. 1 wizard games 72  3月 31 01:59 hiscores
 7$
 8$ ./boggle 12345
 9./boggle: 行 1: hiscores: 許可がありません
10$

調査のため、シェルスクリプトを以下のように実行ユーザ識別子を出力するよう弄って、再実行した。

 1$ cat boggle
 2#echo $1 >> hiscores
 3ps -p $$ -o euser,egroup,euid,egid
 4$
 5$ ls -l boggle hiscores
 6-rwsr-xr-x. 1 wizard games 56  3月 31 03:08 boggle
 7-rw-r--r--. 1 wizard games 72  3月 31 01:59 hiscores
 8$
 9$ ./boggle
10EUSER    EGROUP    EUID  EGID
11dam      user      1000  1000
12$

なんと! ユーザ識別子設定しているにも関わらず、実効ユーザ識別子が dam のままではないか! だから、 hiscores に書けなかったのだ。

execve(2) のマニュアルに次のような記載があった。

Linux (like most other modern UNIX systems) ignores the set-user-ID and set-group-ID bits on scripts.

[翻訳]
Linux (他のほとんどの最新の UNIX システムと同様) は、スクリプトの set-user-ID ビットと set-group-ID ビットを無視します。

この仕様はセキュリティ対策のようだ。

スクリプトにユーザ識別子設定することによるセキュリティリスクについては、かなり古い記事だが以下リンク先に載っている。
http://www.nurs.or.jp/~asada/FAQ/UNIX/section4.7.html
上記の原文サイト
http://www.faqs.org/faqs/unix-faq/faq/part4/section-7.html

また、ユーザ識別子設定されたスクリプトの実行方法などについては、以下が参考になりそう。
https://dminor11th.blogspot.com/2011/01/setuid.html
https://stackoverflow.com/questions/18698976/suid-not-working-with-shell-script
https://stackoverflow.com/questions/25933399/why-does-setsuid-not-work-for-shell-script

関連マニュアル

chmod: Change access permissions
Structure of File Mode Bits

関連ページ