home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

C言語系/「デーモン君のソース探検」読書メモ/A06, su(1)

C言語系/「デーモン君のソース探検」読書メモ/A06, su(1)

C言語系 / 「デーモン君のソース探検」読書メモ / A06, su(1)
id: 846 所有者: msakamoto-sf    作成日: 2010-11-22 21:40:16
カテゴリ: BSD C言語 

お題:su(1)コマンドが異なるユーザーIDでプログラムを起動する仕組みを調査せよ

※この章は「デーモン君のソース探検」に載っていませんが、msakamoto-sf自身が個人的に興味を持って調べ、"Appendix"として読書メモシリーズに入れてありますのでご注意下さい。


su(1)の確認

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), geteuid(2)

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)

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);

getpwnam(2), getpwuid(2)

パスワードデータベースのエントリを 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)

端末上でパスワード入力プロンプトを表示し、入力されたパスワードを返す。

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)

パスワードの暗号化処理。

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);

setuid(2), setgid(2)

プロセスの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)"コマンドを自作してみる

ここまで紹介したシステムコールを組み合わせれば、機能は貧弱ですが "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()も必要だったが、環境変数の調整諸共、面倒くさかったので手抜きした。

su(1)のソースコード

ソースコード:

$ 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"マクロが見つかる。
おおよそ次の三つの機能サポートに関連している。

  • Kerberos認証
  • s/key
  • login_cap(3)

今回は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)がユーザーを切り替えてプロセスを実行する仕組みが判明した。

  1. setuid()/setgid()でプロセスのuid/gidを変更し、コマンドラインまたはユーザーデータベースに設定されているシェルを起動する。
  2. 非rootで実行された場合は、パスワード入力を求め、Kerberos認証なりs/keyなりcrypt(3)なりでパスワードを確認する。
  3. setuid()/setgid()およびパスワードのチェック(struct passwd の pw_passwd メンバ)のために、su(1)の実行ファイルは所有者rootでset-user-idフラグがセットされている必要がある。

今回のお題については、ここまで。



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2010-11-22 21:49:00
md5:a5175cd1b15c369f1a9ca95a88b668ad
sha1:f2c34081a8695a96f80d17882388ac6b61c23d20
コメント
コメントを投稿するにはログインして下さい。