ソルトの効用

以前、Rainbow Table の説明で、ソルトに関して

id:JULY:20100515

Windows のパスワードの場合、「ソルト」と呼ばれる、パスワードに付加する乱数が無いので、同じパスワードから必ず同じハッシュ値が得られる、という側面もあります。UNIX 系 OS では「ソルト」が付加されるので、Rainbow Table が作りづらくなっています。

とサラッと流したのが、自分でも気になっていたのですが、エフセキュアブログの「ソルト付き SHA-1 は大丈夫か?」という話に言及*1したので、ソルトの効用に関して書いてみます。

ソルトとは

塩です。

というボケは置いといて、パスワードを保存する時に、何らかの「非可逆処理」を行った結果を保存しておく事は多いです。Windows での LM ハッシュや NTLM ハッシュ、UNIX 系であれば、古くは伝統的な「crypt」関数を使ったものだったり、今時だと MD5SHA1 といったハッシュ関数を使ったりしています。

こうしておくと、

  • 保存されているデータからは、元のパスワードを計算する事ができない。
  • しかし、入力された文字列に対して、同じ非可逆処理を行った結果と、保存されているデータを比較することで、パスワードの検証が可能である。

という状態になります。

この非可逆処理を行う際に、ランダムなデータをパスワードに付け加えてから処理を行うと、同じパスワードであっても、処理結果は違ったものになります。この付加されるデータを「ソルト」と言います。

パスワード情報を保存する時は、このソルトの値と処理結果の値を保存しておきます。検証する時には、入力されたパスワードと保存されているソルトの値を組み合わせて非可逆処理を行い、その結果と保存されている値を比較します。

OpenLDAP での SSHA の例

OpenLDAP でパスワード情報を保存する userPassword アトリビュートに、SSHA(ソルト付き SHA)のパスワード情報が保存されているケースを例に見てみます。

下記の例は、「testtest」というパスワードが保存されている様子です。

userPassword:: e1NTSEF9Qmc3UEwwN2RQSjB0bzBrYm5rUWNJeFQ4MU1ibTl0VUo=

「userPassword」の後ろにコロンが2つ続いているのは、続く文字列が Base64エンコードされた結果である事を表すので、デコードしてみます。以下は、CentOS 5.5 上で実行した様子ですが、他のディストリビューションでも同様だと思います。使っているコマンドは GNU 版の echo、base64、od、sha1sum です。

$ echo -n 'e1NTSEF9Qmc3UEwwN2RQSjB0bzBrYm5rUWNJeFQ4MU1ibTl0VUo=' | base64 -d; echo
{SSHA}Bg7PL07dPJ0to0kbnkQcIxT81Mbm9tUJ

先頭にある「{SSHA}」は、この userPassword が SSHA の形式である事を示していて、後ろがその値になります。つまり、保存されているパスワード情報は「Bg7PL07dPJ0to0kbnkQcIxT81Mbm9tUJ」です。

この文字列自体、Base64エンコードされた結果なのでデコードしてみます。但し、デコードした結果は文字データではないので、デコードした結果を 16 進ダンプしてみます。

$ echo -n 'Bg7PL07dPJ0to0kbnkQcIxT81Mbm9tUJ' | base64 -d | od -txC
0000000 06 0e cf 2f 4e dd 3c 9d 2d a3 49 1b 9e 44 1c 23
0000020 14 fc d4 c6 e6 f6 d5 09
0000030

SHA1ハッシュ値は 160bit = 20 バイト固定です。実際のデータは 24 バイトあるので、後ろ 4 バイトがソルトの値になります*2。つまり「e6 f6 d5 09」がソルトの値です。

では、逆にパスワードから計算して見ましょう。先の例では 16 進でダンプしてますが、echo コマンドで 8 進表記をする都合があるので、8 進でダンプしてみます。

$ echo -n 'Bg7PL07dPJ0to0kbnkQcIxT81Mbm9tUJ' | base64 -d | od -toC
0000000 006 016 317 057 116 335 074 235 055 243 111 033 236 104 034 043
0000020 024 374 324 306 346 366 325 011
0000030

8 進表記だと、ソルトの値は「346 366 325 011」になります。パスワード文字列が「testtest」ですから、これを組み合わせると、下記のようになります。

$ echo -en 'testtest\0346\0366\0325\0011' | sha1sum
060ecf2f4edd3c9d2da3491b9e441c2314fcd4c6  -

この結果にソルトの値をつなげると、{SSHA} の後ろの文字列をデコードした結果に一致している事が分かると思います。

ソルトと Rainbow Table

ソルトを加えることで、同じパスワードでも得られる処理結果が違ってきます。OpenLDAP の SSHA の場合、ソルトは 4 バイト = 32bit のデータなので、232 = 4,294,967,296 通りのソルトが考えられます。

という事は、1つのパスワード文字列に対して、4,294,967,296 通りのハッシュ値が考えられる、ということになります。

Rainbow Table は、「ハッシュ値がこうだったら、パスワードはこれ」というテーブルです。もしソルトが無ければ、

ハッシュ値 パスワード
51abb9636078defbf888d8457a7c76f85c8f114c testtest

という表が作られます。ところがソルトがあると、同じパスワードでもソルトが違えばハッシュ値が違ってきます。もし、OpenLDAP の SSHA 用 Rainbow Table を作るとなると、

ハッシュ値 + ソルト パスワード
55bc8442d5d0614101515ab345ee0a12b5989d4000000000 testtest
3955ef9cf1ba8a6341e40511d5347ba472d42a4600000001 testtest
c8c951ffb7c35a83eb36fad72105a8d66dcb8d3d00000002 testtest
.... ....
455f99a067b95a9477cd47f478224f188f91a411ffffffff testtest

といった具合になり、1つのパスワードで 4,294,967,296 行のデータが作られる事になります

Ophcrack で使える 7 文字までの LM ハッシュに対する Rainbow Table が 7.5 GB ですが、LM ハッシュでは大文字小文字を区別しないので、かなりコンパクトなサイズになっています。しかし、仮に LM ハッシュに 4 バイトのソルトを加えると、単純計算で 32,212,254 TB になります*3。実際の SSHA では大文字小文字を区別しますから、この数字のおよそ 100 倍になるはずです*4。既に非現実的な容量な上に、7 文字までです。

ソルトとブルートフォース

ソルトを付けることで、Rainbow Table を使うアプローチは実質的に使えなくなる事が分かりました。しかし、愚直にブルートフォースを行う場合、それに対抗するための効果はソルトにはありません。

前述のように、保存されているパスワード情報からソルトの値自体は分かります。試そうとするパスワード候補に対して、読み取ったソルトを加えてハッシュ値を求めれば、そのパスワード候補が当たっているかどうかの確認はできます。もちろん、1つのパスワードに対する検証処理で、若干、処理が多くなる(ソルト値を読み出す処理と、パスワード候補にその値を付け加える処理)のですが、全体の処理から見れば、ゴミみたいなものです。

まとめ

ソルトの効用は、あくまでも Rainbow Table のような、「予めハッシュ値を計算しておく」という手段に対抗するもので、ブルートフォースの様に「ひたすらハッシュ値を計算して、合致するものを探す」という手段には、ほとんど影響はありません。

ただ、Ophcrack で分かるように、Rainbow Table が使えると、ブルートフォースに比べて劇的に短い時間でパスワードが判明します。それを防げるのは大きなメリットでしょう。

エフセキュアのブログで「ソルト付き SHA-1 は大丈夫か」というのは、GPU を使うとハッシュ値が効率よく計算できるので、これまでだったら「ブルートフォースで見つかる時には、死んじゃってるよ」と言えたパスワードが、意外に早く見つかってしまう、という事です*5

でも、パスワードを1文字増やすだけで、見つかる時間は約 100 倍 になります。2 文字増やせば 10,000 倍です。今までより、2文字ぐらい、パスワードを長くした方が良さそうです。

*1:id:JULY:20110216

*2:OpenLDAP が userPassword に保存する際のソルトは 4 バイトですが、ソースを見ると、検証時にはソルトのサイズは決まってなく、得られたデータのうち、21 バイト目以降がソルト、という扱いになっているようです。なので、自前で 4 バイトより大きなソルトを使って計算した結果を保存しても、正しく検証されるはずです。

*3:もちろん、データベースの構造などを工夫して、圧縮できる可能性はあるかもしれませんが、10 分 1 にできても焼け石に水です。

*4:id:JULY:20100515 で、7 文字までのパスワードが大文字小文字を区別しないと 100 分の 1 になる、というところを参照

*5:その意味では、ソルト付きか否かは無関係だと思うのですが....