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"と言われて怒られないようにするのは簡単だろう。

  1. /usr/share/dict/linux.wordsから該当の単語を削除する。
  2. 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の辞書ファイルマッチはけっこう凝っていて、例えばカッコがある場合はそれを除いたり、文字列内の文字の「入れ替え」をしてみる、など色々しているようだ。

いや、結構ハマってしまった。まだまだ奥が深そうだけど、とりあえずこれでおしまい。