login

段々と世の中の動きが、せわしなくなってきている。もう、おせち料理の内覧会やら予約が始まってるな。競争相手に一歩でも先を行こうという魂胆。

カレンダーなんかは、普通12月に入ってから貰ったり、用意するものだろう。でも、今年は違った。早2種類のカレンダーが我が家には用意された。女房が通販で手配したもの。限定版だから、買っておかないと、12月じゃ手に入らないからと抜かす。踊らされていますなあ。

スケート連盟承認と印刷有り。ここまで書くと、分かる人には分かるだろう。

羽生君のカレンダー、卓上版と壁掛け用のやつ。ユーチューブ見て、過去のTVの録画を残すだけじゃ、ありきたりすぎるって事らしい。幸い、追っかけで、仙台に行きたいとかカナダまで見に行きたいと言い出さないだけ、ましか。

そんなものなんですかね? 某スケーターを追って、長野とか札幌とか函館まで遠征してた過去のある方。

init

shutdownを追いかけていた時、 /fastboot なんてのに出会った。何をしてるの? コードは、こんなの。

void
doitfast(void)
{
        int fastfd;

        if ((fastfd = open(_PATH_FASTBOOT, O_WRONLY|O_CREAT|O_TRUNC,
            0664)) >= 0) {
                dprintf(fastfd, "fastboot file for fsck\n");
                close(fastfd);
        }
}

そしてrebootが呼ばれて、長い旅の果てに、/sbin/initにたどり着く。manには、色々書いてある。rcを見ろとかね。 /etc/rcを見るのが説明見るより早いか。

# Run filesystem check unless a /fastboot file exists.
if [[ -e /fastboot ]]; then
        echo "Fast boot: skipping disk checks."
elif [[ $1 == autoboot ]]; then
        echo "Automatic boot in progress: starting file system checks."
        do_fsck
fi

この部分が、早い段階で呼び出される。何の事は無い。このファイルが存在すると、fsckを実行しませんて事だ。だから起動が速くなるのね。FreeBSDとかだと、ファイルシステムが進化してるので、裏でfsckが行われるから、基本この機構は無いはずだな。(要調査)

そして、initの大事な役目として、ユーザーがログインしてくる準備が有る。ログアウトした時の後処理もやってるのか。

     In multi-user operation, init maintains processes for the terminal ports
     found in the file ttys(5).  init reads this file, and executes the
     command found in the second field.  This command is usually getty(8);
     getty opens and initializes the tty line and executes the login program.
     The login program, when a valid user logs in, executes a shell for that
     user.  When this shell dies, either because the user logged out or an
     abnormal termination occurred (a signal), the init program wakes up,
     deletes the user from the utmp(5) file of current users and records the
     logout in the wtmp file.  The cycle is then restarted by init executing a
     new getty for the line.

getty

そんなこんなで、追っかけていたら、gettyを見るはめになった。 /usr/src/libexec/getty/の中で、軽くloginを検索してみる。

ob6$ grep login *.[ch]
init.c: { "lm", "login: " },            /* login message */
init.c: { "lo", _PATH_LOGIN },          /* login program */
main.c:                 login_tty(i);
main.c:                 execle(LO, "login", "-p", "--", name, NULL, env);
pathnames.h:#define     _PATH_LOGIN     "/usr/bin/login"

確かに、loginを起動してますなあ。でも、forkしてない。と言う事はgettyがloginに変身するんだな。何故かなと考えると、login中は、その回線を占有してるからだな。logoutとかすると、回線が空くんで、再びユーザーをその回線で受け入れ可能になる。それらの仕組みを実現してんのが、上の説明にもあるけど、initの仕事なんだな。

ごちゃごちゃ、回線の設定をしたり、リミット値を設定した上で、login: って言うプロンプトを出し、ユーザー名を入力させる。後は、loginプログラムにお任せって訳か。

loginに成功すれば、それで良し。失敗したら、syslogに記録を残してからgettyを終了って塩梅か。

login_tty(i) なんてのも有るな。manすると

     The login_tty() function prepares for a login on the tty fd (which may be
     a real tty device, or the slave of a pseudo-tty as returned by openpty())
     by creating a new session, making fd the controlling terminal for the
     current process, setting fd to be the standard input, output, and error
     streams of the current process, and closing fd.

FILES
     /dev/pty[p-zP-T][0-9a-zA-Z]   master pseudo terminals
     /dev/tty[p-zP-T][0-9a-zA-Z]   slave pseudo terminals
     /dev/ptm                      pseudo terminal management device

これって、sshdとかで入ってくる時の入り口? ちょいと自信なし。今のターミナルは、/dev/ttyp2 なんで、slave pseudo terminals に相当。slave って、奴隷って意味あるから、使うの止そう、差別語だよと、パイソン方面で叫ばれているらしい。言葉狩りと思うんだけど、どうよ?

ちょっとttyの構造が分かっていないと理解が深まりそうにないので、先に進もう。

login

という事で、gettyから呼び出されるloginです。反対語はlogoutです。ユーザーが手打ちするlogoutは在りません。有ったとしてもそれはshellの仕業です。内部コマンドです。

その代わり、logout(3)は用意されてました。

     The login() function updates the /var/run/utmp and /var/log/wtmp files
     with user information contained in ut.

     The logout() function removes the entry from /var/run/utmp corresponding
     to the device line.

回線に相当するutmpを削除するんだそうです。あくまでも、回線にまつわる仕組みになってるんだな。

login.c/mainを見ていくと

/*
 * login [ name ]
 * login -h hostname    (for telnetd, etc.)
 * login -f name        (for pre-authenticated login: datakit, xterm, etc.)
 * login -p             (preserve existing environment; for getty)
 */

こんな注意書きが有った。昔は、telnetとかでよそのマシンに入ったな。よく行く所は、禁断のrloginとかで、手抜きしてた。

        /*
         * If effective user is not root, just run su(1) to emulate login(1).
         */
        if (geteuid() != 0) {
                 :
                execv(_PATH_SU, av);
                warn("unable to exec %s", _PATH_SU);
                _exit(1);
        }

こんな事をしてた。何故って理由が示されていない。gettyから来た場合かな。

後は、だらだら(失礼)と、長編小説を読まされるがごとく続いていく。ちょいと読む気が失せた。

ああ、途中で、/etc/motd(今日のお知らせ)を出すのは、loginの仕業なのね。それから、/etc/login.confを見て、認証の仕方を変えたり、リミット値を設定したりしてる。最後は、

        execlp(shell, tbuf, (char *)NULL);
        err(1, "%s", shell);

で、loginプログラムから、指定したshellに変身してる。これも、回線の保持を目的にしてるんだろうね。

ssh族

早々loginに見切りを付けて、多用してるssh系を見るか。族って書いたのは、色々なコマンドやユーティリティが提供されてるからだ。

ob6$ find /usr/src/usr.bin/ssh -type d
/usr/src/usr.bin/ssh
/usr/src/usr.bin/ssh/scp
/usr/src/usr.bin/ssh/ssh
/usr/src/usr.bin/ssh/moduli-gen
/usr/src/usr.bin/ssh/sftp
/usr/src/usr.bin/ssh/sftp-server
/usr/src/usr.bin/ssh/ssh-add
/usr/src/usr.bin/ssh/ssh-agent
/usr/src/usr.bin/ssh/ssh-keygen
/usr/src/usr.bin/ssh/ssh-keyscan
/usr/src/usr.bin/ssh/ssh-keysign
/usr/src/usr.bin/ssh/ssh-pkcs11-helper
/usr/src/usr.bin/ssh/sshd

OpenSSH 10月に出るであろうOpenBSD6.4で新しいのになるのかな。

上で、dirだけを調べてみたけど、ソース類は、ssh/ の下に、260個余りが平置きと言うか、べた置きされてる。dirの中には、そのコマンドを作成する為のMakefileだけが置いてあるって寸法。

例えば、旧式のloginに変わる安全コマンドssh(remote login program)を作成するには、

.PATH:          ${.CURDIR}/..

SRCS=   ssh.c readconf.c clientloop.c sshtty.c sshconnect.c sshconnect2.c mux.c
SRCS+=  atomicio.c authfd.c bufaux.c buffer.c compat.c dns.c fatal.c \
        hostfile.c key.c msg.c readpass.c utf8.c
SRCS+=  ${SRCS_BASE} ${SRCS_KEX} ${SRCS_KEXC} ${SRCS_KEY} ${SRCS_KEYP} \
        ${SRCS_KRL} ${SRCS_PROT} ${SRCS_PKT} ${SRCS_UTL} ${SRCS_PKCS11}
  :
.if (${OPENSSL:L} == "yes")
LDADD+= -lcrypto
DPADD+= ${LIBCRYPTO}
.endif

LDADD+= -lutil -lz
DPADD+= ${LIBUTIL} ${LIBZ}

sshコマンドで、ケルベロス(地獄の番犬)認証も追加出来るのね。そんな大がかりじゃなければ、OPENSSLのライブラリィーを追加しとけとMakefileは言っています。

これだけのソースを読んで、どんな事をやってるか調べろ? 尻尾を巻いて逃げ出しましょ。 暗号やらネットワークやらターミナル関係やらが、きっと渦巻いていて降参するに決まってますから。

su doas sudo

sshは大変そうなので、loginの所に出て来た su を追いかけてみるか。suのお仲間にdoasが居る。おっとこれは、OpenBSD限定だった。一般的にはsudoだな。

いつもの通りに、su一式を/tmpに持ってきて、gdbにかけられるようにMakefileに小細工。 そして、出来たものに、少々、手を加える。

ob6$ doas chown root a.out
doas (sakae@ob6.localdomain) password:
ob6$ doas chmod 4555 a.out
doas (sakae@ob6.localdomain) password:

suはオーナーさんがroot、そしてrootさんの権利で実行出来るように権限を付加する。 ちゃんと動くか、確認。

ob6$ ./a.out
a.out: invalid script: /usr/libexec/auth/login_passwd
Sorry
ob6$ su
Password:
ob6# id
uid=0(root) gid=0(wheel) groups=0(wheel), 2(kmem), 3(sys), 4(tty), 5(operator), 20(staff), 31(guest)
ob6# exit
ob6$ id
uid=1000(sakae) gid=1000(sakae) groups=1000(sakae), 0(wheel), 5(operator)

自前で用意したa.out(su)では、見たことも無いエラーに見舞われた。ごめんと言われてもねぇ。一応、既存のsuを確認。ちゃんとrootになった。id関係を確認してもrootそのものである。 exitして元に戻ると、普通になった。あれ? 何気にexitしたけど、これってshellをおさらばする合図だよな。と言う事は、loginがsuを実行してもいいってのも頷けるな。

それより、不思議なエラーを何とかせい。

ob6$ ls /usr/libexec/auth/
ls: /usr/libexec/auth/: Permission denied
ob6$ doas ls /usr/libexec/auth/
doas (sakae@ob6.localdomain) password:
login_activ     login_lchpass   login_reject    login_token
login_chpass    login_passwd    login_skey      login_yubikey
login_crypto    login_radius    login_snk

auth領域って、一般ユーザー立ち入り禁止領域。その中に確かにエラーを出したlogin_passwdってのが存在してた。全くの言いがかりではなさそう。それはそうと、このauth領域に結集してるやつは、ログイン関係の管理スクリプトだな。昔で言うと関所のお役人。今風に言うと、イミグレにいる不愛想な入国審査官か。(あっ、ハワイのその人は、アロハシャツ着て、愛想がよかったか)

login_passwd(8)によると、login、su、ftpdとかに使われる標準の認証手順みたいだ。 どうせgdbにかけられるよう構成してるんで、gdbで追い詰めた方が早いか。

Breakpoint 2, verify_user (from=0x1172120a9310 "sakae", pwd=0x11721ac67b00, style=0x0, lc=0x1172c27a3ae0, as=0x1172bad96000) at su.c:357
378│         auth_verify(as, style, NULL, lc->lc_class, (char *)NULL);
379│         authok = auth_getstate(as);
380│         if ((authok & AUTH_ALLOW) == 0) {
381│                 if ((cp = auth_getvalue(as, "errormsg")) != NULL)
382│                         fprintf(stderr, "%s\n", cp);
383├───────────────> return(1);
384│         }

378行目を実行した時に、上記のエラーメッセージが出て来て、エラーリターンですよ。 なんか初見の事ばかりで、よく分からんなあと思いながら、何気にchmod(1)してたんだ。 そしたら、気になる記述が。

     In addition to the file permission modes, the following mode bits are
     available:

           4000    Set-user-ID on execution.
           2000    Set-group-ID on execution.
           1000    Enable sticky bit; see sticky(8) and chmod(2).

     The execute bit for a directory is often referred to as the "search" bit.
     In order to access a file, a user must have execute permission in each
     directory leading up to it in the filesystem hierarchy.  For example, to
     access the file /bin/ls, execute permission is needed on /, /bin, and, of
     course, the ls binary itself.

このファイルが置かれる場所の記述。ひょっとして/tmp下なんて言う、公共施設にsuなんて言う 危険な代物を置いちゃいけないんじゃなかろうか。こういう連想が野生の勘で働いた。 もし、手心を加えたsuを、だれでもおける/tmpに設置して、実行出来ちゃったら。。。怖い事ですねぇ。

そんな事を思って、a.outを自dirに置いてみた。

ob6$ /home/sakae/a.out
Password:
ob6# id
uid=0(root) gid=0(wheel) groups=0(wheel), 2(kmem), 3(sys), 4(tty), 5(operator), 20(staff), 31(guest)
ob6# exit

やったーーー。

(gdb) r
Starting program: /home/sakae/a.out
a.out: invalid script: /usr/libexec/auth/login_passwd
Sorry
[Inferior 1 (process 14909) exited with code 01]

と思って、gdb上から走らせたら、また小癪なやつが出現。/var/log/messages上に出て来た報告。

Sep 26 15:36:57 ob6 su: cannot stat /usr/libexec/auth/login_passwd: Permission denied
Sep 26 15:36:57 ob6 su: /usr/libexec/auth/login_passwd: path not secure

gdb上からの実行と常用shellからの実行で挙動が違うんだな。gdbはクリチカルな事をやってるので、怒りに触れたのでしょう。

取り合えず走らせて、流れを追うだけなら、rootになって行えばよいので、それで実行したよ。 我ながらトホホな所が有るけど、一般ユーザーがrootに化ける瞬間を捉えられました。

272├───────────────────────> if (setusercontext(lc,
273│                             pwd, pwd->pw_uid, LOGIN_SETPATH|LOGIN_SETUMASK)
274│                                 auth_err(as, 1, "unable to set user context
275│                 }

これが要の部分。この後環境の整備が有って

336│         if (pwd->pw_uid && auth_approval(as, lc, pwd->pw_name, "su") <= 0)
337│                 auth_err(as, 1, "approval failure");
338│         auth_close(as);
339│
340├───────> execv(shell, np);
341│         err(1, "%s", shell);

新しいshellに化ける瞬間。

ついでにdoasを見ておく。/etc/doas.confの解析と適用部分が、suより増えているけど、流れはsuと一緒。

        if (setusercontext(NULL, pw, target, LOGIN_SETGROUP |
            LOGIN_SETPRIORITY | LOGIN_SETRESOURCES | LOGIN_SETUMASK |
            LOGIN_SETUSER) != 0)
                errx(1, "failed to set user context for target");
         :
        execvpe(cmd, argv, envp);
        if (errno == ENOENT)
                errx(1, "%s: command not found", cmd);

forkして実行って流れではなく、execvpeで変身するスタイルでした。

/etc/ttys

initが扱うデバイス

# name  getty                           type    status          comments
#
console "/usr/libexec/getty std.9600"   vt220   off secure
ttyC0   "/usr/libexec/getty std.9600"   vt220   on  secure
 :
ttyCb   "/usr/libexec/getty std.9600"   vt220   off secure
tty00   "/usr/libexec/getty std.9600"   unknown off
tty01   "/usr/libexec/getty std.9600"   unknown off
 :
tty07   "/usr/libexec/getty std.9600"   unknown off
ttyp0   none                            network
ttyp1   none                            network
 :
ttyTZ   none                            network

ttyCnが、コンソールに結び付いた仮想の端末 Ctl+Alt+Fn で切り替えられる。OSが提供するtmuxみたいなやつか。

tty00-07は、リアルな回線。普通はRS232Cで接続かな。

そして、ttyp0-TZまで多数あるやつは、いわゆるpty(4)になるのかな。疑似端末とかいうの。 sshとかで接続すると、これのお世話になる。 実体は、2本のパイプだと思う。パイプは信号が流れる方向が有るんで、それぞれ逆向きにながれるパイプ2本をセットにする。(エラー出力が有るから、実際は3本かな)

片方をマスター、もう一方をスレーブと称するのだな。そして多分マスター側に、流れをコントロールする制御機構が入っているのだろう。

雑な方法だけど、sshのソース群から、/dev/tty (スレーブ)を語句を探してみた。

ob6$ grep -l /dev/tty /usr/src/usr.bin/ssh/*.[ch]
/usr/src/usr.bin/ssh/readpass.c
/usr/src/usr.bin/ssh/sshlogin.c
/usr/src/usr.bin/ssh/sshpty.c

スレーブ側がsshクライアントに接続。マスター側は、対向するsshd側に有るんだな。きっとそういう事だろう。

細かい事は、kern/tty_pty.c を見ろって事かな。冒頭に

/*
 * pts == /dev/tty[p-zP-T][0-9a-zA-Z]
 * ptc == /dev/pty[p-zP-T][0-9a-zA-Z]
 */

こんな事が書いてある1000行のソースでした。

emacs tips

tipsと言うより、オイラーが今まで知らなかった部分。この所、管理者が使うコマンドを見てる。rootじゃなければ動かないとか、時間制限があるとか、gdbにかけられるように再コンパイルして、gdbで追うって方法が取りにくい。

そこで、ソースをじっと眺める事になる。mainから読み下して、大体の流れを追うって方法ね。 途中で出会う関数をちょいと覗いてから、また元に戻る。

こういう事をemacsでやるには? 或る特定の場所でマークを設定すれば,後で容易にその位置へ移動できる。C-x C-x と入力すれば設定したマークへカーソルを移動でき,もう一度 C-x C-x と入力すれば元の位置へ戻る。

あるいは、C-SPCでマークして、何処かへ飛んで行く。とび先を堪能したら、C-u C-SPC で、戻ってくる。どちらでも可能。

それから、検索用単語を取り込んで検索。 カーソルが単語の先頭にあるなら、C-s C-w で、検索対象の単語を取り込んでくれる。 C-s C-w の後に C-w を入力するたびに、取り込む単語が長くなる。

C-sしてから、一文字づつ検索文字を入力してくと、そのたびに画面がパラパラして鬱陶しい。そういう時は、C-sRETsearchRET これで、迷わずsearchの所まで移動する。後は、C-sで、次を探させればよい。

正規表現での検索。C-M-s 関数名を探すなら、^w.*( ぐらいかな。面倒臭い。 こんな荒い指定でも大概は捉えてくれる。但し、int main() みたいに、型と関数名を同一行に書いたのはだめだ。