お題:su(1)コマンドが異なるユーザーIDでプログラムを起動する仕組みを調査せよ
※この章は「デーモン君のソース探検」に載っていませんが、msakamoto-sf自身が個人的に興味を持って調べ、"Appendix"として読書メモシリーズに入れてありますのでご注意下さい。
su(1)の実行ファイル、manページの確認:
$ which su su: aliased to su -m $ ls -l /usr/bin/su -r-sr-xr-x 1 root wheel 16276 Sep 9 2002 /usr/bin/su* $ /usr/bin/su --help su: unknown option -- h Usage: su [-Kflmc:] [login [shell arguments]] $ man 1 su
su(1)のソースの流れは一本道だが、ユーザー情報周りのシステムコールを複数組み合わせている。都度解説すると話の流れが悪くなるので、先に予習として、中心となるシステムコールの使い方を確認しておく。
getuid(2)が"real"ユーザーIDを、geteuid(2)が"effective"ユーザーIDを返す。
"real"はプログラムを実行したユーザーのID、"effective"はプログラムの実行ファイルにSUID(set-user-id)フラグがセットされていたとき、実行ファイルの所有者のユーザーIDが返される。
GETUID(2) NetBSD Programmer's Manual GETUID(2) NAME getuid, geteuid - get user identification LIBRARY Standard C Library (libc, -lc) SYNOPSIS #include <unistd.h> uid_t getuid(2) uid_t geteuid(2)
getlogin(2)は、現在のセッションのログイン名を返す。setlogin(2)はスーパーユーザー専用で現在のセッションのログイン名を設定する。setlogin(2)が使われる例としては、リモートログインによりシェルが起動され、ログインユーザーのセッションが生成される場面がある。
あくまでもプロセスが所属するセッションのログイン名であり、そのプロセスのreal-user-idやeffective-user-idとは無関係である。
GETLOGIN(2) NetBSD Programmer's Manual GETLOGIN(2) NAME getlogin, setlogin - get/set login name LIBRARY Standard C Library (libc, -lc) SYNOPSIS #include <unistd.h> char *getlogin(void); int setlogin(const char *name);
パスワードデータベースのエントリを struct passwd 構造体で取得する。
getpwnam(2)はユーザー名の文字列から取得し、getpwuid(2)はユーザーIDから取得する。
GETPWENT(3) NetBSD Programmer's Manual GETPWENT(3) NAME getpwent, getpwnam, getpwuid, setpassent, setpwent, endpwent - password database operations LIBRARY Standard C Library (libc, -lc) SYNOPSIS #include <pwd.h> struct passwd *getpwnam(const char *login); struct passwd *getpwuid(uid_t uid); (他のgetpwXXYY()は省略)
struct passwd 構造体は pwd.h で以下のように定義されている。
struct passwd { char *pw_name; /* user name */ char *pw_passwd; /* encrypted password */ uid_t pw_uid; /* user uid */ gid_t pw_gid; /* user gid */ time_t pw_change; /* password change time */ char *pw_class; /* user access class */ char *pw_gecos; /* Honeywell login info */ char *pw_dir; /* home directory */ char *pw_shell; /* default shell */ time_t pw_expire; /* account expiration */ };
端末上でパスワード入力プロンプトを表示し、入力されたパスワードを返す。
GETPASS(3) NetBSD Programmer's Manual GETPASS(3) NAME getpass - get a password LIBRARY Standard C Library (libc, -lc) SYNOPSIS #include <pwd.h> #include <unistd.h> char *getpass(const char *prompt);
参考:
パスワードの暗号化処理。
CRYPT(3) NetBSD Programmer's Manual CRYPT(3) NAME crypt, setkey, encrypt, des_setkey, des_cipher - password encryption LIBRARY Crypt Library (libcrypt, -lcrypt) SYNOPSIS #include <unistd.h> char *crypt(const char *key, const char *setting);
プロセスのreal/effective/saved-set uid|gid を設定する。現在のreal uid/gidと同じか、そうでなければ現在のeffective-uidがスーパーユーザーのuidでなければならない。
SETUID(2) NetBSD Programmer's Manual SETUID(2) NAME setuid, seteuid, setgid, setegid - set user and group ID LIBRARY Standard C Library (libc, -lc) SYNOPSIS #include <unistd.h> int setuid(uid_t uid); int setgid(gid_t gid);
ここまで紹介したシステムコールを組み合わせれば、機能は貧弱ですが "su(1)" コマンドを自作することが出来る。
というわけで自作版 mysu.c :
#include <stdio.h> #include <errno.h> #include <unistd.h> #include <pwd.h> int main(int argc, char *argv[]) { char *new_user_name; struct passwd *new_user_pwentry; char *given_password; char *crypt_password; char *shell; if (2 != argc) { fprintf(stderr, "usage: %s new-user-name\n", argv[0]); exit(1); } /* 新しいユーザー名から struct passwd エントリを取得 */ new_user_name = argv[1]; new_user_pwentry = getpwnam(new_user_name); if (NULL == new_user_pwentry) { perror("getpwnam()"); exit(1); } printf("User entry for %s found:\n", new_user_name); printf("pw_name = [%s]\n", new_user_pwentry->pw_name); printf("pw_uid = [%d]\n", new_user_pwentry->pw_uid); printf("pw_gid = [%d]\n", new_user_pwentry->pw_gid); printf("pw_dir = [%s]\n", new_user_pwentry->pw_dir); printf("pw_shell = [%s]\n", new_user_pwentry->pw_shell); shell = new_user_pwentry->pw_shell; /* パスワードを入力してもらう */ given_password = getpass("Password:"); /* crypt(3)で暗号化し、struct passwd の pw_passwd と一致するかチェック */ crypt_password = crypt(given_password, new_user_pwentry->pw_passwd); if (strcmp(new_user_pwentry->pw_passwd, crypt_password)) { fprintf(stderr, "Sorry.\n"); exit(1); } /* プロセスのuid, gidを変更 */ if (-1 == setuid(new_user_pwentry->pw_uid)) { perror("setuid()"); exit(1); } if (-1 == setgid(new_user_pwentry->pw_gid)) { perror("setgid()"); exit(1); } printf("setuid(), setgid() okay, invoking new shell...\n"); /* ログインシェルを実行 */ if (-1 == execl(shell, shell, NULL)) { perror("execl()"); exit(1); } /* don't reach here */ return -1; }
コンパイル:
$ gcc -o mysu mysu.c -lcrypt
setuid(2)/setgid(2)および、struct passwd 構造体のpw_passwdエントリのために実行ファイルの所有者をrootにし、set-user-idビットをセットする。
$ su # chown root mysu # chown +s mysu
試してみる:
[msakamoto@netbsd01 ex01]$ ./mysu root User entry for root found: pw_name = [root] pw_uid = [0] pw_gid = [0] pw_dir = [/root] pw_shell = [/usr/pkg/bin/tcsh] Password: setuid(), setgid() okay, invoking new shell... netbsd01: {1} id uid=0(root) gid=0(wheel) groups=0(wheel) netbsd01: {2} ps ux USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND ... root 432 0.0 0.5 764 1260 p1 S 8:58PM 0:00.03 -usr/pkg/bin/tcsh ... netbsd01: {3} echo $USER msakamoto netbsd01: {4} echo $SHELL /usr/pkg/bin/bash netbsd01: {5} echo $HOME /home/msakamoto netbsd01: {6} pwd /home/msakamoto/lang.c/ex01 netbsd01: {7} echo "foobar" > /root/test.txt netbsd01: {8} cat /root/test.txt foobar netbsd01: {9} exit exit [msakamoto@netbsd01 ex01]$
環境変数やシェルのプロンプトなどが未調整だが、idコマンドの結果およびrootでしか書き込めない "/root/" 以下へのファイル書き込みが正常に動作していることから、無事rootユーザーにsu出来たことは確認できた。
あと$HOMEディレクトリへのchdir()も必要だったが、環境変数の調整諸共、面倒くさかったので手抜きした。
ソースコード:
$ locate su ... /usr/src/usr.bin/su /usr/src/usr.bin/su/CVS /usr/src/usr.bin/su/CVS/Entries /usr/src/usr.bin/su/CVS/Repository /usr/src/usr.bin/su/CVS/Root /usr/src/usr.bin/su/CVS/Tag /usr/src/usr.bin/su/Makefile /usr/src/usr.bin/su/su.1 /usr/src/usr.bin/su/su.c ...
su.cはほぼ一本道のソースコードになっている。kerberosや利用可能なshellのチェックなどでいくつか関数にまとめられているが、とりわけ難しいアルゴリズムや再帰処理が使われているわけではない。
ソースコードを読むと、いくつかの機能のサポート状況に応じた"#ifdef" - "#endif"マクロが見つかる。
おおよそ次の三つの機能サポートに関連している。
今回はsuの一番基本的な仕組みをみていくため、上記三機能についてはすべて未サポートの環境を想定して読み進めてみる。
また、一本道で難易度で言えば易しめとはいえ、分量まで少ないとは言えない。ポイントを絞って見ていくので、細かい変数やフラグ一つ一つの調査・検討・解説は省いている。
さらに、"su -m" と "su -l" オプションに対応するフラグによる分岐も無視し、分岐内で重要そうな処理についてのみ紹介する。
では main() 関数から読み進めてみる。
int main(argc, argv) int argc; char **argv; { extern char **environ; struct passwd *pwd;
環境変数のenvironを参照し、struct passwd のポインタを宣言。
これに各種フラグや一時変数、文字列へのポインタなどの宣言、さらにコマンドラインオプションの解析が続くが、思い切って省略。
続けて、プロセスの優先度を上げている。
/* Lower the priority so su runs faster */ errno = 0; prio = getpriority(PRIO_PROCESS, 0); if (errno) prio = 0; if (prio > -2) (void)setpriority(PRIO_PROCESS, 0, -2); openlog("su", 0, LOG_AUTH);
次のポイントは、su対象となるユーザー名から getpwnam(2) で struct passwd エントリを取得している箇所:
/* get target login information, default to root */ user = *argv ? *argv : "root"; np = *argv ? argv : argv-1; if ((pwd = getpwnam(user)) == NULL) errx(1, "unknown login %s", user);
これに続いて、もし現在プロセスの"real"ユーザーIDが非ゼロ(= 非rootユーザー)であればパスワード入力・検証処理に進む:
/* main()の前半で、 ruid = getuid() が呼ばれている */ if (ruid #ifdef KERBEROS5 && (!use_kerberos || kerberos5(username, user, pwd->pw_uid)) #endif #ifdef KERBEROS && (!use_kerberos || kerberos(username, user, pwd->pw_uid)) #endif ) { char *pass = pwd->pw_passwd; /* if target requires a password, verify it */ if (*pass) { p = getpass("Password:"); if (strcmp(pass, crypt(p, pass))) { fprintf(stderr, "Sorry\n"); syslog(LOG_WARNING, "BAD SU %s to %s%s", username, pwd->pw_name, ontty()); exit(1); } } }
暫く読み進めると、setgid(2)/setuid(2)している箇所が見つかる。mysu.cでは手を抜いて使わなかったが、セカンダリグループ以降のグループIDの初期化処理としてinitgroups(3)も呼ばれている。
if (setgid(pwd->pw_gid) < 0) err(1, "setgid"); if (initgroups(user, pwd->pw_gid)) errx(1, "initgroups failed"); if (setuid(pwd->pw_uid) < 0) err(1, "setuid");
この後ろに、"-m"や"-l"オプションと連動して環境変数を調整する箇所がある。詳細なコード解説は省略するが、setenv()している環境変数は以下のとおり。
TERM PATH USER HOME SHELL
ちなみに
SU_FROM
という環境変数もsetenv()している。
さらに、"-l"オプション指定に対応し、HOMEにchdir(2)するコードもsetenv(2)に混じっている。
最後の〆として、priorityを元に戻し、shellとその引数をexecv(2)で実行、これによりsuされたユーザーのシェルが立ち上がることになる。
/* Raise our priority back to what we had before */ (void)setpriority(PRIO_PROCESS, 0, prio); execv(shell, np); err(1, "%s", shell); /* NOTREACHED */ } /* main() 終了 */
以上でsu(1)がユーザーを切り替えてプロセスを実行する仕組みが判明した。
今回のお題については、ここまで。