ハッシュ値をアンハッシュ化する
こんにちは
株式会社 Gizumo でエンジニアをしている Ocat です。
前提・対象とする読者
- 暗号に興味がある人
- ハッシュ値をアンハッシュ化してみたい人
- セキュリティに興味がある人
- この記事はセキュリティ意識向上を目的とした記事ですので悪用は絶対にしないで下さい。悪用によって生じたトラブルに関して、弊社では一切の責任を追いかねます。
ハッシュと暗号
暗号学をかじり始める前、自分の中で「ハッシュ化」と呼ばれるものと「暗号化」と呼ばれるものは同意義だと思っていましたが、暗号学を学ぶ中でそれらは実は別物であることを知りました。
ハッシュ化
=ハッシュ関数
を使ってデータ置換し元の状態に戻らないようにする作業暗号化
=鍵
を使ってデータ置換し、必要に応じて元の状態に戻せるようにする作業
ハッシュ関数
あるデータ(メッセージ、ファイルなど)を受け取り、そのデータを固定長の値(ハッシュ値、ダイジェスト、フィンガープリントなどと呼ばれる)に変換する関数です。
ハッシュ値の例
- メッセージ
"Hello, world!"
のSHA-256
ハッシュ値:3adbbad1791fbae7be82d99c4ab5644baca4b49f7d9f648bcc648d2b9d27f88d
- ファイル
example.pdf
のMD5
ハッシュ値:9d377b18f8c84e40b6885a5f5a5e2941
ハッシュ関数は、同じデータに対しては必ず同じハッシュ値を返すようになっていて、わずかな変更がある場合だと異なるハッシュ値を返すようになっています。
先述の特徴を踏まえ、ハッシュ関数の主な仕事は次のようなものが挙げられます。
- データの改竄や盗用を検知する
- データの一意の識別子として扱われる
ハッシュ関数には様々は種類があります。
その中でも今回は MD 系に焦点を当てていきます。
ハッシュ化
ハッシュ化って意外と簡単にできます。
以下は Rust の標準ライブラリの crypto を使って MD5 ハッシュ値を計算した例です。
// crypto.rs
use crypto::md5::Md5;
use crypto::digest::Digest;
fn main() {
let mut md5 = Md5::new();
md5.input_str("password");
let result = md5.result_str();
println!("MD5ハッシュ値: {}", result);
}
結果は以下です
MD5ハッシュ値: 5f4dcc3b5aa765d61d8327deb882cf99
普段利用するサービスのパスワードが内部でこれだけの処理しかされていないとなると怖い話です。
勘が良い人ならすぐ気づくと思いますが、単純に MD5 や SHA-2 などのハッシュ関数で同じ値をハッシュ化すると、常に同じハッシュ値が得られます。
ソルト化
しかし、パスワードにランダムな文字列を結合することで、同じパスワードでも異なるハッシュ値を取得することができます。
このランダムな文字列をソルトと呼びます。
// salt.rs
use std::io::Write;
use crypto::md5::Md5;
use crypto::digest::Digest;
use rand::Rng;
fn main() {
// 入力データ(パスワード)
let password = "password";
// ランダムなソルトを生成
let mut rng = rand::thread_rng(); // 暗号的に安全なランダムなバイト列を生成しています
let mut salt = [0u8; 16]; //長さ16のバイト配列
rng.fill(&mut salt);
// ソルトとパスワードを結合
let mut input_data = salt.to_vec();
input_data.extend_from_slice(password.as_bytes());
// ソルトとパスワードのハッシュ値を計算
let mut md5 = Md5::new();
md5.input(&input_data);
let md5_hash = md5.result_str();
// 結果を表示
println!("パスワード: {}", password);
println!("ソルト: {:?}", salt);
println!("ソルト付き入力データ: {:?}", input_data);
println!("MD5ハッシュ値: {:?}", md5_hash);
}
結果は以下です
パスワード: password
ソルト: [61, 90, 156, 98, 49, 8, 27, 214, 50, 46, 6, 225, 217, 30, 218, 138]
ソルト付き入力データ: [61, 90, 156, 98, 49, 8, 27, 214, 50, 46, 6, 225, 217, 30, 218, 138, 112, 97, 115, 115, 119, 111, 114, 100]
MD5ハッシュ値: "5f4dcc3b5aa765d61d8327deb882cf99"
先ほどの単純に MD5 でハッシュ化しただけのパスワードよりもセキュリティーが向上しました。ランダムなソルトを使うことで総当たり攻撃に対する耐性が強くなりました。
上記ではランダムなソルトを使用することで、同じパスワードでも異なるハッシュ値を取得することを可能にしています。つまり、攻撃者は総当たり攻撃を行う際に、各パスワードに対して別々に総当たり攻撃をしなければならなくなります。このため、攻撃者が必要な時間や計算量が増え、パスワードを推測することが困難になります。
またランダムであることによってパスワードを推測するための辞書攻撃も難しくなります。
しかしもっと安全でいたいと思うのが human。
ストレッチング
ストレッチングとは、パスワードをハッシュ化する際に、複数回ハッシュ関数を適用することです。
MD5 とか SHA-2 等は単純に一回だけハッシュ化すると脆弱ですが、複数回ハッシュ化すると耐性を強くすることができます。
先ほどのコードのソルトとパスワードのハッシュ値を計算
のところを書き直してあげましょう。
// salt.rs
...
// ストレッチングを行い、ハッシュ値を計算
let mut md5 = Md5::new();
md5.input(&input_data);
for _ in 0..1000 {
md5.input(md5.result_str().as_bytes());
}
let md5_hash = md5.result_str();
// 結果を表示
// println!("パスワード: {}", password);
// println!("ソルト: {:?}", salt);
// println!("ソルト付き入力データ: {:?}", input_data);
println!("ストレッチング回数: 1000");
println!("MD5ハッシュ値: {:?}", md5_hash);
結果は以下です
ストレッチング回数: 1000
MD5ハッシュ値: a55c9f20789c2c1f59f7dcfe120f431e
ストレッチングの回数が多いほど、パスワードのハッシュ化に必要な時間が長くなり、総当たり攻撃に対する耐性は高くなりますが、ストレッチングの回数が多すぎると、パフォーマンスに影響が出てしまい、ユーザーにとって不便になってしまいます。
ストレッチングの回数についてはセキュリティとパフォーマンスのトレードオフを考慮する必要があります。(この課題については検索すると色々ヒットします。
アンハッシュ化
では、タイトルの通りハッシュ値をアンハッシュ化していきたいと思います。
前述で生成した単純な MD5 ハッシュ値、ソルト化した MD5 ハッシュ値、ソルト化してさらにストレッチングした MD5 ハッシュ値をアンハッシュ化します。
今回は hashcat と呼ばれるツールを使います。
hashcat は、CPU や GPU を使用してパスワードのハッシュ値をアンハッシュ化できるクラッキングツールです。
使い方によっては諸法律に引っかかるので注意してください。
1. hashcat インストール
brew でもインストールできますが、今回は Nix で入れてます。
- Homebrew
brew install hashcat
- Nix Package
nix-env -iA nixpkgs.hashcat
2. 辞書を取ってくる
今回行うのは辞書攻撃です。
辞書攻撃は世の中に流出したデータベース、ユーザーのパスワードをリスト化したものを使います。
世界一有名なrockyou.txt
というリストを今回は使います。
https://github.com/danielmiessler/SecLists/tree/master/Passwords/Leaked-Databases
↑ ※開いたページにある各辞書ファイルは重い可能性があるので、ファイルを表示する際は注意してください。
3. アンハッシュ化する
それぞれのコマンドの第 1 引数にはアンハッシュ化したいハッシュ値を書いたテキストファイルを。
第 2 引数には rockyou.txt を。rockyou.txt
に含まれるパスワードの中から、MD5 ハッシュ値が一致するものを探し出して、解読します。
単純な MD5 ハッシュ値を解読する場合
hashcat -m 0 md5_hash.txt rockyou.txt
-m 0
は、ハッシュタイプを MD5 に指定するオプションです。
今回は簡単なパスワードだった場合なので辞書に登録されていた”password”という文字列はすぐに引っかかりました。
ちなみに辞書攻撃で行わない場合の一つにブルートフォース攻撃というものがあります。
hashcat -m 0 -a 3 5f4dcc3b5aa765d61d8327deb882cf99 '?a?a?a?a?a?a?a?a'
- attack-mode をブルートフォース攻撃に
-a 3
?
は、hashcat がブルートフォース攻撃を行う際に、自動的にパスワード候補を生成するために使用する単一の文字を表します
ブルートフォース攻撃は、可能なすべての組み合わせを試行することで、パスワードを見つける方法です。
つまり、すべての文字列の組み合わせを順番に試行し、正しいパスワードを見つけるまで繰り返します。
そのためパスワードの長さが長い場合には、この攻撃方法は非常に時間がかかることになります。
短いパスワードの場合
一秒かかってません
長いパスワードの場合
33 日と 17 時間かかるようです
ソルト化した MD5 ハッシュ値を解読する場合
今回の場合ソルトはランダムで作成されました。
ランダムなソルトの値を知ることは、攻撃者にとっては非常に困難なタスクです。攻撃者がランダムなソルトを知ることができるのは、以下のような場合が考えられます。
ソルトが不十分にランダムに生成されている場合
:ランダムなソルトを生成するには、暗号学的に安全な乱数生成器を使用する必要があります。もし、その乱数生成器が不十分である場合、攻撃者がソルトの値を推測することができる可能性があります。攻撃者がシステムにアクセスできる場合
:もし、攻撃者がシステムにアクセスできる場合、ランダムなソルトを保存している場所にアクセスすることができます。この場合、攻撃者はランダムなソルトの値を知ることができるため、ハッシュ値を解読することが可能になります。ソルトが公開されている場合
:もし、ソルトが公開されている場合、攻撃者はそれを入手することができます。例えば、ウェブサイトがセキュリティ侵害に遭った場合、攻撃者はソルトの値を含むデータを入手することができます。この場合、攻撃者はランダムなソルトの値を知ることができるため、ハッシュ値を解読することが可能になります。
素人知識で hashcat を使いソルト化された md5 に対してオプションを当てて割れるか試したのですが、今回は敗北しました。
ソルト化してストレッチングした MD5 ハッシュ値を解読する場合
上記同様、うまく解読することはできませんでした。
まとめ
パスワードのセキュリティを向上させるためには、ソルト化とストレッチングが必要です。これらの技術を組み合わせることで、より強力なパスワードを作成することができます。
以上は特定サイトなどインターネットに繋がったサービスを狙った検証、ペネストレーションテストではなくローカル上で用意した脆弱な例をもとに行った検証です。
実際に脆弱なサイトなどを狙って攻撃をするとお縄になるのは目に見えてるので行わないでください。