Linuxのpasswdで使っている辞書とcracklib
以前に、辞書攻撃に使う元ネタの辞書ファイルという記事を書いた。その中で、Linuxのpasswdコマンドが利用しているのは /usr/share/dict/linux.words だということを書いたけど、この辺については色々とややこしいバックグラウンドがあるので詳しく書いてみたい。ちょっとググってみたけど、ここについて突っ込んで書いてある日本語の記事はネットには存在しないと思う(たぶん)。
なお以下記事は、CentOS 6.4で調査したものです。他のディストリビューションでは細部が色々と違っているかもしれません。
passwdコマンドと辞書ファイル
passwdコマンドでパスワードを変更する際、辞書に載っている言葉をパスワード設定しようとするとこういうエラーが表示される。
そこで、この"BAD PASSWORD: it is based on a dictionary word"がどうやって表示されているのかを調べて、辞書ファイルからこの単語を抜いてエラーが出ないようにする、というのをやってみよう。
PAM認証と/etc/pam.d/passwd
Linuxのpasswdコマンドでパスワードを変更する際には、認証部分にPAMの仕組みが使われている。PAMとはPluggable Authentication Modulesの略で、コマンド名やライブラリ名というより、もう少し大きい「認証システムの仕組み」を指す。
ちょっと図を描いてみた。
Linuxではログイン処理など認証機能を利用するアプリケーションが多い。それらアプリケーションがそれぞれ独自に認証処理を実装していてはよろしくないので、共通モジュールとして利用できるようにしたものがPAMの仕組みだ。
PAMを利用する際は、サーバ側の設定でプログラムごとに使うPAMモジュールを選ぶことができる。passwdコマンドで使われているモジュールを確認するには、/etc/pam.d/passwdを見れば良い。
#%PAM-1.0 auth include system-auth account include system-auth password substack system-auth -password optional pam_gnome_keyring.so
ほほう。system-authを参照しているので、/etc/pam.d/system-authを見てみる。
#%PAM-1.0 # This file is auto-generated. # User changes will be destroyed the next time authconfig is run. auth required pam_env.so auth sufficient pam_fprintd.so auth sufficient pam_unix.so nullok try_first_pass auth requisite pam_succeed_if.so uid >= 500 quiet auth required pam_deny.so account required pam_unix.so account sufficient pam_localuser.so account sufficient pam_succeed_if.so uid < 500 quiet account required pam_permit.so password requisite pam_cracklib.so try_first_pass retry=3 type= password sufficient pam_unix.so sha512 shadow nullok try_first_pass use_authtok password required pam_deny.so session optional pam_keyinit.so revoke session required pam_limits.so session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid session required pam_unix.so
ここで詳しく説明すると、PAMの設定のお話だけで一冊の本が出来てしまうので、思いっきりはしょることにすると……。注目すべきは、以下1行である。
password requisite pam_cracklib.so try_first_pass retry=3 type=
[passsword]についてpam_cracklib.soが[requisite]になっているので、パスワード変更時には必ずcracklibのチェックでOKとならなくてはいけない。ということだ。
cracklibと辞書ファイル
さてcracklibでのパスワードチェックだが、これはテストコマンドが用意されているので手で打って簡単に試すことができる。例えば homeworker が辞書に載っているかどうかは以下のようにチェックできる。標準入力から食わせないといけないのがちょっと面倒くさいのだが……。
$ echo "homeworker" | cracklib-check homeworker: it is based on a dictionary word
passwdでも、パスワード変更の際にcracklibでパスワードチェックをおこない、辞書ファイルに載っているかどうかを調べているわけだ。
cracklibが使う辞書ファイル
cracklibが参照している辞書ファイルの元ネタは、/usr/share/dict/wordsというテキストファイルである。これはCentOSではシンボリックリンクとなっており、実体は/usr/share/dict/linux.wordsである。
ただ、実際のモジュールはこのwordsファイルを直接読むわけではなく、/usr/share/cracklib配下のバイナリファイルを読むようになっている。
/usr/share/cracklib/pw_dict.hwm /usr/share/cracklib/pw_dict.pwd /usr/share/cracklib/pw_dict.pwi
これらファイルは、create-cracklib-dictコマンドを叩いてwordsテキストファイルから変換して作成する。だから辞書をいじりたい場合はlinux.wordsを書き換えるだけではダメで、wordsファイル更新後に以下のように辞書ファイルを再構築する必要がある。
# create-cracklib-dict /usr/share/dict/linux.words
CentOSでのwordsファイルとcracklib辞書との不整合
さて、ここで気をつけないといけないことがある。本来ならばOS初期状態では、/usr/share/dict/linux.wordsから作られたpw_dict.pwd群がインストールされている……と思うだろうがこれは違う。実はcracklib群のインストールパッケージと、wordsファイルのインストールパッケージは別管理となっており、これが同期していないのだ。
$ rpm -qf /usr/share/dict/linux.words words-3.0-17.el6.noarch $ rpm -qf /usr/share/cracklib/pw_dict.pwd cracklib-dicts-2.8.16-4.el6.x86_64 $ rpm -qf /usr/sbin/cracklib-check cracklib-2.8.16-4.el6.x86_64
ええと、つまり何が言いたいかと言うと。「/usr/share/dict/linux.wordsに単語は載っているけど、/usr/share/cracklib/pw_dict.pwdには載っていないので通ってしまうパスワードがある」ということだ。
例えば "homichlophobia" という単語は、/usr/share/dict/linux.wordsに載っている。しかし初期状態のpw_dict.pwdには載っていないので、passwdコマンドでエラーにならずに変更することができる。しかし、OS初期状態でcreate-cracklib-dictを打った後は、"it is based on a dictionary word"とエラーとなる。
この辺の動き、実用上は問題になることはまず無い。しかしこのようにPAMとcracklibの勉強を目的としているときに色々いじっていると、猛烈にハマることになる。気をつけよう。私もハマった。
ちなみにhomichlophobiaというのは、「霧恐怖症」らしい。知らんがな (^^;
辞書から特定単語を抜くには
さてと。ここまで来れば、あるパスワードを入れて"it is based on a dictionary word"と言われて怒られないようにするのは簡単だろう。
- /usr/share/dict/linux.wordsから該当の単語を削除する。
- create-cracklib-dictして、cracklib辞書ファイルを作り直す。
しかしここにまた落とし穴があった。確かに単語を消したはずなのに、まだ"it is based on a dictionary word"と言われる。なぜだ? どうも原因がよく分からずcracklibのソースコードを読んでみたが……うーむ、fascist.c内のr_destructorsを定義しているところで、文字列をいったん分解して様々な組み合わせを作っている(ようだ)。
static char *r_destructors[] = { ":", /* noop - must do this to test raw word. */ #ifdef DEBUG2 (char *) 0, #endif "[", /* trimming leading/trailing junk */ "]", "[[", "]]", "[[[", "]]]", "/?p@?p", /* purging out punctuation/symbols/junk */ "/?s@?s", "/?X@?X", /* attempt reverse engineering of password strings */ "/$s$s", "/$s$s/0s0o", "/$s$s/0s0o/2s2a", "/$s$s/0s0o/2s2a/3s3e", "/$s$s/0s0o/2s2a/3s3e/5s5s", "/$s$s/0s0o/2s2a/3s3e/5s5s/1s1i", "/$s$s/0s0o/2s2a/3s3e/5s5s/1s1l", (略)...
その機構で引っかかってしまっているっぽい。cracklibの辞書ファイルマッチはけっこう凝っていて、例えばカッコがある場合はそれを除いたり、文字列内の文字の「入れ替え」をしてみる、など色々しているようだ。
いや、結構ハマってしまった。まだまだ奥が深そうだけど、とりあえずこれでおしまい。