ハッシュとパスワードのオフライン攻撃(レインボーテーブル・GPU)
ハッシュさえ保存すれば安全、は誤解です。流出ハッシュを攻撃者がオフラインで殴る経済を、レインボーテーブルの時間/空間取引と、GPU/ASICの実効スループットから腹落ちさせます。
- 1.ハッシュ流出後の総当たりはレート制限が効かないオフライン攻撃になり、防御は1回の計算をどれだけ重くできるかだけに収束する。
- 2.レインボーテーブルは事前計算結果を還元関数で鎖状に圧縮し、空間を時間に交換して原像を引く手法。ユーザーごとに違うソルトが事前計算の使い回しを完全に無効化する。
- 3.GPUは数千コア、ASICは専用回路で高速ハッシュを毎秒数百億回回す。salt は無力化、pepper は秘密の上乗せ。決定打は計算とメモリを重くする専用ハッシュ。
オフライン攻撃という前提:レート制限が消える世界
パスワードの安全な保存 では「ハッシュ化して保存せよ」を扱いました。本稿はその裏側、ハッシュが流出した後に攻撃者が何をするかを経済の視点で詰めます。
鍵になるのはオンライン攻撃とオフライン攻撃の断絶です。ログインフォーム越しの試行(オンライン)は、レート制限 とアカウントロックで毎秒数回に縛れます。ところが users テーブルのハッシュ列が漏れた瞬間、攻撃者は自分の手元のハードで照合を回せる――サーバーは一切関与しないため、レートリミットもロックも認証ログも完全に無効になります。
オンライン攻撃: 候補 -> ネットワーク -> サーバー認証(レート制限あり) -> 可否
オフライン攻撃: 候補 -> 攻撃者ローカルでハッシュ計算 -> 流出ハッシュと比較 ← 制限なし
この瞬間、守りに残る変数は一つだけです。1回のハッシュ計算をどれだけ重くできるか。攻撃者が単位時間に試せる候補数は「実効スループット ÷ 1回のコスト」で決まり、防御側が触れるのは分母だけになります。以降、レインボーテーブル(空間で殴る)と GPU/ASIC(速度で殴る)という二つの増幅手段を見ていきます。
事前計算とレインボーテーブル:空間を時間に交換する
総当たりの素朴な高速化は事前計算です。よくある候補を片端からハッシュし、ハッシュ -> 平文 の巨大な辞書を作っておけば、流出ハッシュは表引き一発で平文に落ちます。問題は容量で、全候補を保存するのは非現実的です。ここで時間と空間のトレードオフを突くのがレインボーテーブルです。
中核は還元関数(reduction function)Rです。R はハッシュ値を「別のもっともらしい平文」へ写す関数で、ハッシュ H と交互に適用して**鎖(chain)**を作ります。
鎖の生成(長さ t の例):
p0 --H--> h0 --R1--> p1 --H--> h1 --R2--> p2 ... --H--> h(t-1)
保存するのは各鎖の「先頭 p0」と「末尾 h(t-1)」だけ(途中は捨てる)
照合: 流出ハッシュ y から R/H を適用しながら末尾集合と一致するまで前進
-> 一致した鎖を先頭から再生成し、y の直前の平文を特定
途中を捨てて両端だけ持つので、保存量は鎖の本数に比例し、長さ t 分は計算で取り戻します。つまり空間を t 分の1に圧縮する代わりに、照合時に最大 t 回の再計算を払う。各段で異なる還元関数 R1, R2, ... を使うのが「レインボー」の名の由来で、これにより異なる鎖が同じ値で合流して途中で潰れるチェーン衝突を大幅に減らし、被覆率(カバレッジ)を高めます。
| 手法 | 空間コスト | 照合あたり計算 | 弱点 |
|---|---|---|---|
| 全数の平文辞書 | 膨大(全候補を保存) | 表引き1回で最速 | 現実的な空間に収まらない |
| 素朴なハッシュチェーン | 中(両端のみ) | 鎖長 t に比例 | チェーン衝突で被覆率が落ちる |
| レインボーテーブル | 中(両端のみ) | 鎖長 t に比例 | 段ごとに R を変え衝突を抑制。ソルトで完全無効化 |
要点は、レインボーテーブルが効くのは**「同じ入力は常に同じハッシュになる」から事前計算が使い回せる**という一点にかかっていることです。ここを壊すのが次のソルトです。
salt が無効化する仕組み:事前計算の使い回しを断つ
ソルト(salt)は、ユーザーごとに違うランダム値をパスワードへ連結してからハッシュする手法です。保存するのは salt と H(salt || password) の組で、ソルト自体は秘密にしません(CSPRNG で生成し、ハッシュ文字列に同梱するのが通例)。
ソルトが攻撃を壊すのは事前計算の前提を破壊するからです。レインボーテーブルは特定のハッシュ関数に対して一度作れば全員に使い回せました。しかしソルトが入ると、攻撃者はソルト値ごとに別のテーブルを作り直さねばならず、事前計算のコストが攻撃対象の人数分に膨れ上がります。16バイトのランダムソルトなら取りうる値は天文学的で、事前計算は経済的に完全に破綻します。
ソルトなし: H("password123") は誰でも同じ -> 1つの表で全員ヒット
ソルトあり: H(salt_A || "password123") と H(salt_B || "password123") は別物
-> 表を使い回せず、ユーザーごとに最初から計算し直し
副次効果として、同じパスワードを使う二人のハッシュが一致しないため、流出DBを眺めて「同一ハッシュ=同一パスワード」を一網打尽にする攻撃も防げます。ただし注意すべきは、ソルトはオフラインの総当たりそのものを遅くはしない点です。攻撃者が一人のユーザーに狙いを絞れば、そのソルトを使って候補を順に試すコストは1回のハッシュ計算と変わりません。ソルトが潰すのはあくまで「事前計算と複数ユーザーへの償却」であり、ここから先の単独総当たりの速度勝負を決めるのが次の GPU/ASIC です。
ソルトは事前計算(レインボーテーブル)と償却攻撃を無効化しますが、特定ハッシュ1個への総当たり速度は1ミリも下げません。「ソルトを付けたから安心」は誤りで、1回の計算を重くする専用ハッシュと必ずセットで初めて防御になります。
pepper の位置づけ:DB外に置く共通の秘密
ペッパー(pepper)は全ユーザー共通の秘密値で、ソルトと対になる概念です。違いは秘匿性にあります。ソルトは公開前提でDBに同梱しますが、ペッパーはDBとは別の場所(環境変数や シークレット管理 のKMS/HSM)に保管し、漏らしません。
狙いはDBだけが漏れたケースの最後の防壁です。攻撃者がハッシュとソルトを手にしても、未知のペッパーが連結されている限り、正しいパスワード候補を入れてもハッシュが一致しません。ペッパーの値そのものを総当たりする必要が生じ、十分長ければ事実上不可能になります。実装は連結よりも HMAC(pepper, password) の形が安全で、後段の専用ハッシュへ渡します。
| 観点 | ソルト(salt) | ペッパー(pepper) |
|---|---|---|
| 値の単位 | ユーザーごとに別 | 全ユーザー共通(または鍵単位) |
| 秘匿性 | 公開可(DBに同梱) | 秘密(DB外で厳重保管) |
| 主目的 | 事前計算と償却攻撃の無効化 | DB単独漏洩時の上乗せ防御 |
| DBが漏れた時 | 効力は維持されるが速度は下げない | 値が漏れなければ総当たり自体を阻止 |
GPU/ASIC の経済:なぜ高速ハッシュは“ほぼ平文”なのか
事前計算をソルトで潰しても、攻撃者には生の計算力で殴る道が残ります。ここで効くのが、汎用ハッシュ(MD5, SHA-1, SHA-256 など)の設計目標が「高速・小状態」だという事実です。1回の計算が分岐もメモリ参照もほぼ無い純算術の連なりなので、超並列ハードに完璧に適合します。
- GPU:数千の演算コアが異なる候補を同時にハッシュする。単純な算術の塊である高速ハッシュは、GPUのスループットをほぼ100%引き出せる。汎用CPUの毎秒数百万回に対し、ハイエンドGPUは MD5/SHA-1 を毎秒数百億回回せる。
- ASIC/FPGA:ハッシュ専用回路を焼き、1チップに大量のパイプラインを並べる。汎用性を捨てた分、面積・電力あたりの試行数でGPUをさらに上回る。仮想通貨マイニングが実証したとおり、量産で単価は劇的に下がる。
攻撃者の試行レート ≈ 実効ハッシュスループット ÷ 1候補あたりの計算コスト
高速ハッシュ: 数百億 hash/s(GPU) -> 一般的な弱パスワードは秒〜分で陥落
専用ハッシュ: 1回が0.2〜0.5秒相当 -> 同じGPUでも試行レートが桁違いに落ちる
候補の選び方も総当たりの全数列挙だけではありません。実際の攻撃は辞書攻撃(流出済みパスワード集や単語リスト)とルールベース変換(password -> P@ssw0rd! のような人間の癖の自動適用)、漏洩パスワードを別サービスへ流用するクレデンシャルスタッフィングが主力です。人間が選ぶパスワードの分布は極端に偏っているため、高速ハッシュなら多数のアカウントが現実的な時間で割れます。
決定打は、攻撃者の実効スループットを下げること――すなわち分母の「1回のコスト」を意図的に膨らませる専用ハッシュです。bcrypt は計算時間を、scrypt/Argon2 は時間に加えてメモリを人質に取り、GPUのコアあたり少メモリやASICの面積優位を相殺します。その内部は パスワードハッシュの内部 で詳説しています。
オフライン攻撃ではレート制限が無効になり、防御は「1回の計算コスト」に収束する、をまず押さえます。レインボーテーブルは還元関数で鎖を作り両端だけ保存する時間/空間トレードオフで、ソルトが事前計算の使い回しを壊して無効化します。ただしソルトは単独総当たりの速度は下げない点が頻出のひっかけ。salt は公開・ユーザー単位、pepper は秘密・共通という対比と、最終的な速度対策は**GPU/ASICを意識した専用ハッシュ(メモリ困難性)**だと結べれば十分です。
まとめ:攻撃の経済を一段ずつ潰す
オフライン攻撃の本質は、レート制限という外側の守りが消え、勝負が1回のハッシュコスト対攻撃者の実効スループットに純化することです。レインボーテーブルは空間を時間に換えて事前計算を圧縮し、ソルトはその前提(使い回し)を壊して無効化する。だがソルトは速度を下げないため、GPU/ASICの桁違いのスループットには別の手当てが要る。
したがって正しい積み上げは、**ユーザー単位のソルト(事前計算と償却の遮断)+ DB外のペッパー(単独漏洩の保険)+ メモリ困難な専用ハッシュ(実効スループットの直撃)**の三段重ねです。どれか一つでは穴が残り、三つ揃って初めて「ハッシュが漏れても割られにくい」状態に到達します。
セキュリティ Article
ハッシュとパスワードのオフライン攻撃(レインボーテーブル・GPU)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
パスワード
比較で見る軸
難易度: advanced / カテゴリ: セキュリティ / タグ数: 5
導入後に効く点
レインボーテーブルは事前計算結果を還元関数で鎖状に圧縮し、空間を時間に交換して原像を引く手法。ユーザーごとに違うソルトが事前計算の使い回しを完全に無効化する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- セキュリティ
- タグ数
- 5
判断チェックリスト
- 自社の用途が「パスワード / ハッシュ」に近いか確認する。
- 強みである「ハッシュ流出後の総当たりはレート制限が効かないオフライン攻撃になり、防御は1回の計算をどれだけ重くできるかだけに収束する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。