ふと Linux ディストリビューションのユーザ認証周りについて気になって、その中でも特に UNIX パスワード認証について調べてみた。
UNIX パスワード認証というのは、Linux に限らず Unix 系のディストリビューションで広く採用されているパスワードを使った認証の仕組み。
特に、ログイン用のパスワードを暗号化 (ハッシュ化) した上でパスワードファイル (/etc/passwd
) やシャドウパスワードファイル (/etc/shadow
) に保存するところが特徴となっている。
UNIX パスワード認証は Unix 系の色々なディストリビューションでそれぞれ実装されている。
その中でも、今回は Linux ディストリビューションの Ubuntu 18.04 LTS について見ていく。
先に断っておくと、かなり長い。
注: この記事では、やっていることがハッシュ化でも manpage の "encrypted" や "encryption" という表現を尊重して「暗号化」と記載している箇所がある
動作確認に使った環境は次の通り。
$ cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=18.04 DISTRIB_CODENAME=bionic DISTRIB_DESCRIPTION="Ubuntu 18.04.1 LTS" $ uname -r 4.15.0-29-generic
パスワードファイルとシャドウパスワードファイルについて
太古の昔、ユーザのログインに使うパスワードはパスワードファイル (/etc/passwd
) に記述されていたらしい。
しかし、今ではここにパスワードに関する情報が書かれることはほとんどない。
過去に暗号化されたパスワードが記載されていたフィールドには、代わりに x
といった文字が載っている。
$ cat /etc/passwd | grep $(whoami) vagrant:x:1000:1000:vagrant,,,:/home/vagrant:/bin/bash
なぜなら、このファイルの内容は誰でも読めるため。
仮にパスワードが暗号化 (ハッシュ化) されていたとしても、誰からでも読めるのはセキュリティ的には致命的といえる。
そこで、ログインに必要な情報はシャドウパスワードファイル (例えば /etc/shadow
) に分離された。
$ ls -l /etc/passwd -rw-r--r-- 1 root root 1662 Aug 24 09:07 /etc/passwd $ ls -l /etc/shadow -rw-r----- 1 root shadow 979 Aug 24 09:07 /etc/shadow
上記を見て分かる通り /etc/shadow
は root
ユーザか shadow
グループのメンバー以外からは閲覧できない。
シャドウパスワードファイルにおいて、カンマで区切られた二つ目のカラムがユーザがログインに用いるパスワードを暗号化した文字列になる。 これは、ソルト付きのハッシュ化されたパスワードになっている。
$ sudo cat /etc/shadow | grep $(whoami) vagrant:$6$xUMKs3vr$r/rGFiHf6.B0xsKDpustA2G9m1zGcWJnjZLMlyQeh.VDEpDEMEQ3AKSvrRv.NQEc2OBbban3BAHj4Nhwpa0t4.:17767:0:99999:7:::
ユーザがシステムにログインするときは、ユーザが入力した内容と上記の内容を突合して一致するかを確認することになる。
シャドウパスワードファイルの詳細については man
コマンドで調べることができる。
man のセクション 5 には設定ファイルに関する情報が記載されている。
$ man 5 shadow
上記の内容を確認していくと、次のような記述がある。
encrypted password Refer to crypt(3) for details on how this string is interpreted.
どうやらファイルに記載されている暗号化されたパスワードは crypt(3)
に関連しているようだ。
man のセクション 3 は開発者向けのライブラリ関数に関する情報なので、それ用の manpage をインストールした上で確認する。
$ sudo apt-get -y install manpages-dev
$ man 3 crypt
上記を見ると暗号化されたパスワードは次のようなフォーマットになっていることが分かる。
id
がハッシュアルゴリズムで salt
がハッシュするときに使うソルト、そして最後の encrypted
がハッシュ化されたパスワードになる。
$id$salt$encrypted
id
については次のような記述がある。
使えるハッシュアルゴリズムは環境や glibc のバージョンによって異なるようだ。
glibc というのは C 言語の標準ライブラリである libc の、GNU Project による実装を指している。
id identifies the encryption method used instead of DES and this then determines how the rest of the password string is interpreted. The following values of id are supported: ID | Method ───────────────────────────────────────────────────────── 1 | MD5 2a | Blowfish (not in mainline glibc; added in some | Linux distributions) 5 | SHA-256 (since glibc 2.7) 6 | SHA-512 (since glibc 2.7)
C 言語の標準ライブラリというのは、例えば多くの入門書で「おまじない」として紹介されている #include <stdio.h>
なんかがそれ。
先ほど確認した自分自身のパスワードハッシュでは id
が 6
になっていた。
つまり、ハッシュアルゴリズムとして SHA-512
が使われていることが分かる。
$ sudo cat /etc/shadow | grep $(whoami) vagrant:$6$xUMKs3vr$r/rGFiHf6.B0xsKDpustA2G9m1zGcWJnjZLMlyQeh.VDEpDEMEQ3AKSvrRv.NQEc2OBbban3BAHj4Nhwpa0t4.:17767:0:99999:7:::
crypt(3) を使ってみよう
それでは、実際に crypt(3) を使ってみることにしよう。 ただ、C 言語で書くのではなく Python のラッパーである crypt モジュールを使うことにした。
まずは Python のインタプリタを起動する。
$ python3
先ほど確認した自分自身の暗号化されたパスワードを再掲する。
ハッシュアルゴリズムは SHA-512
でソルトが xUMKs3vr
だった。
そして、使われているパスワードは Vagrant の環境なので vagrant
になっている。
$ sudo cat /etc/shadow | grep $(whoami) vagrant:$6$xUMKs3vr$r/rGFiHf6.B0xsKDpustA2G9m1zGcWJnjZLMlyQeh.VDEpDEMEQ3AKSvrRv.NQEc2OBbban3BAHj4Nhwpa0t4.:17767:0:99999:7:::
上記の内容にもとづいて crypt モジュールの関数を呼び出してみよう。
ソルトについては暗号化されたパスワードの $
で区切られた部分をそのまま使って構わない。
>>> import crypt >>> crypt.crypt('vagrant', '$6$xUMKs3vr$') '$6$xUMKs3vr$r/rGFiHf6.B0xsKDpustA2G9m1zGcWJnjZLMlyQeh.VDEpDEMEQ3AKSvrRv.NQEc2OBbban3BAHj4Nhwpa0t4.'
見事に自分自身の暗号化されたパスワードと同じ文字列が生成できた。
ちなみに、システムで使えるハッシュアルゴリズムは crypt.methods
で確認できる。
>>> crypt.methods [<crypt.METHOD_SHA512>, <crypt.METHOD_SHA256>, <crypt.METHOD_MD5>, <crypt.METHOD_CRYPT>]
上記を見て分かる通り id
が 2a
に対応するハッシュアルゴリズムの Blowfish
はこのシステムでは使えないらしい。
crypt(3) の実装を見てみよう
次は、念のため crypt(3)
を実装している場所についても調べておくことにしよう。
特に興味がなければ、次のセクションまで飛ばしてもらって構わない。
先ほど見た通り、実装しているのは glibc と思われる。
そこで、まずは今使っているシステムの glibc のバージョンを調べよう。
検索パスに入っている共有ライブラリを ldconfig
コマンドで調べたら、そこから libc を探す。
$ ldconfig -p | grep libc.so libc.so.6 (libc6,x86-64, OS ABI: Linux 3.2.0) => /lib/x86_64-linux-gnu/libc.so.6
あとは共有ライブラリをそのまま実行するとバージョンが確認できる。
どうやら、この環境ではバージョン 2.27 が使われているようだ。
先ほど crypt(3)
のハッシュアルゴリズムとして SHA-512
が使えるのはバージョン 2.7 以降とあったので、その内容とも整合している。
$ /lib/x86_64-linux-gnu/libc.so.6 GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27. Copyright (C) 2018 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Compiled by GNU CC version 7.3.0. libc ABIs: UNIQUE IFUNC For bug reporting instructions, please see: <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
続いて公式サイトの記述に沿ってソースコードを取得する。
$ sudo apt-get -y install git $ git clone git://sourceware.org/git/glibc.git $ cd glibc/ $ git checkout release/2.27/master Branch 'release/2.27/master' set up to track remote branch 'release/2.27/master' from 'origin'. Switched to a new branch 'release/2.27/master'
とりあえず crypt(3)
で検索してみると、それっぽいものが見つかる。
$ grep -r "crypt(3)" . ./crypt/ufc.c: * UFC-crypt: ultra fast crypt(3) implementation ./crypt/crypt-entry.c: * UFC-crypt: ultra fast crypt(3) implementation ./crypt/cert.c: * This crypt(3) validation program shipped with UFC-crypt ./crypt/crypt.c: * UFC-crypt: ultra fast crypt(3) implementation ./crypt/crypt_util.c: * UFC-crypt: ultra fast crypt(3) implementation ./crypt/README.ufc-crypt:- Crypt implementation plugin compatible with crypt(3)/fcrypt. ./crypt/README.ufc-crypt:- On most machines, UFC-crypt runs 30-60 times faster than crypt(3) when ./crypt/README.ufc-crypt: that of crypt(3). ./crypt/README.ufc-crypt:such as the one said to be used in crypt(3). ./crypt/crypt-private.h: * UFC-crypt: ultra fast crypt(3) implementation ./crypt/crypt.h: * UFC-crypt: ultra fast crypt(3) implementation
ヘッダファイルに crypt()
関数らしき宣言があったり、各ハッシュアルゴリズムの実装も見受けられる。
関数のエントリポイントは crypt-entry.c
にあって、そこから各実装に分岐しているみたい。
$ grep extern crypt/crypt.h | head -n 1 extern char *crypt (const char *__key, const char *__salt) $ ls crypt | egrep "(md5|sha256|sha512)\.c" md5.c sha256.c sha512.c
たしかに crypt(3)
は glibc に実装があることが分かった。
crypt(3) は何処から使われているのか?
続いて気になるのは、暗号化されたパスワードを生成する crypt(3)
が何処から使われているのかという点。
おそらくユーザがログインするタイミングで入力されたパスワードに対して crypt(3)
を使っているはず。
そして、使った結果とシャドウパスワードファイルの内容を比較して、一致すればログイン成功という処理が何処かにあると考えられる。
一般的なシステムであれば、ユーザがログインする経路として代表的なものが二つ考えられる。 まず一つ目が、コンピュータに直接接続された物理的なディスプレイやキーボードを通してユーザ名とパスワードを入力するパターン。 この場合、端末デバイスには制御端末 (tty: teletypewriter) が割り当てられる。 そして二つ目が、SSH クライアントを通してネットワーク越しにユーザ名やパスワード、あるいは公開鍵の情報を渡すパターン。 この場合、端末デバイスには擬似端末 (pty: pseudo terminal) が割り当てられる。
制御端末 (tty: teletypewriter) からログインするパターン
制御端末 (tty) を通してログインするパターンから見ていく。 このパターンでは、まず Linux ディストリビューションの初期化プロセスから考える必要がある。 Ubuntu 18.04 LTS では init プロセス (pid に 1 を持つプロセス) として systemd が用いられる。
そして systemd は agetty(8)
というプログラムを起動する。
このプログラムはログインプロンプトの表示部分を担当している。
システムのプロセスを確認すると、制御端末が割り当てられた状態で agetty(8)
が起動していることが分かる。
$ ps auxww | grep [a]getty root 678 0.0 0.1 16180 1656 tty1 Ss+ 16:00 0:00 /sbin/agetty -o -p -- \u --noclear tty1 linux $ pstree | head -n 3 systemd-+-VBoxService---7*[{VBoxService}] |-accounts-daemon---2*[{accounts-daemon}] |-agetty
このプログラムに関する詳細は man ページのセクション 8 の agetty に記載されている。
$ man 8 agetty
上記の内容を見ると、このプログラムは /bin/login
を呼び出すとある。
どうやら、このプログラムが実際のログイン部分を担当しているようだ。
DESCRIPTION agetty opens a tty port, prompts for a login name and invokes the /bin/login command. It is normally invoked by init(8).
ということで、次はこの login(1)
のソースコードが読みたい。
なので、まずはこのコマンドがどのパッケージに含まれているのかを dpkg
コマンドで確認する。
$ dpkg -S /bin/login login: /bin/login
どうやら、そのものズバリ login
というパッケージに含まれているらしい。
パッケージ名が分かったので apt-get source
コマンドを使ってソースコードを取り寄せる。
すると、どうやら login(1)
の実体は shadow
というプログラム群に含まれることが分かった。
$ sudo apt-get -y install dpkg-dev $ sudo sed -i.back -e "s/^# deb-src/deb-src/g" /etc/apt/sources.list $ sudo apt-get update $ apt-get source login Reading package lists... Done Picking 'shadow' as source package instead of 'login' NOTICE: 'shadow' packaging is maintained in the 'Git' version control system at: https://anonscm.debian.org/git/pkg-shadow/shadow.git Please use: git clone https://anonscm.debian.org/git/pkg-shadow/shadow.git to retrieve the latest (possibly unreleased) updates to the package. ...
ドキュメントを見ると、このプログラム群が Linux のシャドウパスワードを扱うためのものだということが分かる。 どうやら核心に近づいてきたようだ。
$ cd shadow-4.5/ $ head doc/HOWTO -n 6 | tail -n 2 Linux Shadow Password HOWTO Michael H. Jackson, mhjack@tscnet.com
中身を確認すると src
というディレクトリにコマンドラインから叩くプログラムがまとまっていることが分かった。
例えば login(1)
もここにあるし、他には usermod(8)
や groupmod(8)
なんかも見つかる。
$ ls src/ | grep login login.c login_nopam.c nologin.c sulogin.c $ ls src/ | grep usermod usermod.c $ ls src/ | grep groupmod groupmod.c
(2018/11/5 追記: 以降の説明はシステムで PAM を用いない場合の説明になっているため、一般的な環境の説明と異なっている。例えば、後述する関数が login
コマンドにおいて呼び出されるフローは、ビルドする際に USE_PAM
フラグを無効にした場合にしか通らない。)
改めて src/login.c
を読み進めていくと、ログインパスワードを pw_auth()
という関数に突っ込んでいる。
この関数は lib/pwauth.c
で定義されていて、さらに lib/encrypt.c
の pw_encrypt()
という関数を呼び出している。
そして、この pw_encrypt()
関数の中に、探し求めていた crypt(3)
の呼び出しが見つかる!
ついに見つけることができた。
$ grep "crypt (" lib/encrypt.c
/*@exposed@*//*@null@*/char *pw_encrypt (const char *clear, const char *salt)
cp = crypt (clear, salt);
さらに src/login.c
周辺を読み進めると crypt(3)
で暗号化したパスワードは getpwnam(3)
や getspnam(3)
と比較していることが分かった。
この関数は /etc/passwd
や /etc/shadow
を読み取って、そこに記載されている暗号化されたパスワードを読み取るためのものだ。
関数を提供しているのは、まさにこの shadow で、ソースコードの実体は lib
以下に存在する。
それぞれの関数の情報は man コマンドから参照できる。
$ man 3 getspnam
その後、認証に成功するとパスワードファイルから読み取ったログインシェルを起動することもソースコードから確認できる。
$ grep -r shell login.c if (pwd->pw_shell[0] == '*') { /* subsystem root */ pwd->pw_shell++; /* skip the '*' */ err = shell (tmp, pwd->pw_shell, newenvp); /* fake shell */ /* exec the shell finally */ err = shell (pwd->pw_shell, (char *) 0, newenvp);
これで Ubuntu 18.04 LTS におけるログインの流れをソースコードから確認できた。
- ユーザからの入力パスワードを暗号化する
- 上記をシャドウパスワードファイルに記載されている暗号化されたパスワードと比較する
- 一致するときはログインシェルを起動する
実際に制御端末 (tty) からログインした状態で pstree を確認すると agetty(8)
がいなくなって、代わりに login(1)
経由で bash(1)
が起動している。
$ pstree
systemd─┬─VBoxService───7*[{VBoxService}]
...
├─login───bash
ちなみにシステムの /sbin/agetty
や /bin/login
をリネームすると、なかなか面白い挙動になるので試してみるのもおすすめ。
例えば agetty が見つからない状態でシステムが起動するとログインプロンプトが表示されないことを確認できる。
擬似端末 (pty: pseudo terminal) からログインするパターン
続いては SSH クライアントを通してシステムにログインするパターン。 これは単純で、ようするに SSH デーモンに認証部分があるだろうことは容易に想像がつく。
なので、まずは sshd(8)
の含まれるパッケージを確認する。
$ dpkg -S $(which sshd) openssh-server: /usr/sbin/sshd
先ほどと同じ要領で OpenSSH のソースコードを取り寄せる。
$ sudo apt-get source openssh-server Reading package lists... Done Picking 'openssh' as source package instead of 'openssh-server' NOTICE: 'openssh' packaging is maintained in the 'Git' version control system at: https://salsa.debian.org/ssh-team/openssh Please use: git clone https://salsa.debian.org/ssh-team/openssh to retrieve the latest (possibly unreleased) updates to the package. ... $ cd openssh-7.6p1/
この中では、例えば crypt(3)
を xcrypt()
関数というプラットフォーム非依存な形でラップしていたりする。
$ grep " crypt(" */*.c
openbsd-compat/xcrypt.c: crypted = crypt(password, salt);
openbsd-compat/xcrypt.c: crypted = crypt(password, salt);
openbsd-compat/xcrypt.c: crypted = crypt(password, salt);
あるいは各所で getspnam(3)
や getpwnam(3)
が使われている。
$ grep "getspnam" *.c auth.c: spw = getspnam(pw->pw_name); auth-shadow.c: if ((spw = getspnam((char *)user)) == NULL) { $ grep "getspnam" */*.c openbsd-compat/xcrypt.c: struct spwd *spw = getspnam(pw->pw_name);
ということで、こちらの経路に関しても crypt(3)
や getspnam(3)
を駆使してユーザを認証しているらしいことが分かった。
プロセスを確認しても、ユーザがログインした状態では sshd(8)
経由で bash(1)
が起動していることが分かる。
$ pstree | grep sshd |-sshd---sshd---sshd---bash-+-grep
まとめ
分かったことは次の通り。
- シャドウパスワードファイル (例えば /etc/shadow) には暗号化されたユーザのログインパスワードが書き込まれている
- 暗号化されたパスワードは
crypt(3)
で生成する - シャドウパスワードファイルに書き込まれている情報は
getspnam(3)
で読み取る - ユーザの入力内容から生成したパスワードハッシュと、シャドウパスワードファイルに書き込まれた内容を比較することで認証する
いじょう。
ふつうのLinuxプログラミング 第2版 Linuxの仕組みから学べるgccプログラミングの王道
- 作者: 青木峰郎
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2017/09/22
- メディア: 単行本
- この商品を含むブログを見る
- 作者: W. Richard Stevens,Stephen A. Rago
- 出版社/メーカー: 翔泳社
- 発売日: 2014/06/05
- メディア: Kindle版
- この商品を含むブログ (7件) を見る