🔑
タイミング攻撃のシミュレーション
前提条件
パスワードには a
〜 j
の文字しか使えないものとする。
パスワードを決める
PASSWORD = ("a".."j").to_a.sample(8).join # => "faeghcjd"
これを攻撃者は知りたい。
ログイン処理
サーバー管理者が書いたコード。
def login(password)
if password.length != PASSWORD.length
return false
end
password.each_char.with_index do |e, i|
sleep(0.01)
if e != PASSWORD[i]
return false
end
end
true
end
動作確認すると、
login(PASSWORD) # => true
login("x") # => false
となるので問題なさそうだ。
パスワードの長さを特定する
攻撃者は試しに 4〜12 文字の文字列を順番に渡し、処理時間を計測してみた。
(4..12).each do |e|
t = Time.now
login("x" * e)
p "%2d: %f" % [e, Time.now - t]
end
# > " 4: 0.000002"
# > " 5: 0.000000"
# > " 6: 0.000000"
# > " 7: 0.000001"
# > " 8: 0.012518"
# > " 9: 0.000000"
# > "10: 0.000001"
# > "11: 0.000001"
# > "12: 0.000000"
すると一つだけ処理時間が長くなっている。
これによりパスワードの長さが8であると推測できる。
書き直すとこれで、
length = (4..12).max_by do |e|
t = Time.now
login("x" * e)
Time.now - t
end
length # => 8
8 が返ってくる。
パスワードの最初の文字を特定する
********
の先頭の文字を a
〜j
に変えつつログインを試み、最も時間がかかったものを探す。
password = "*" * length # => "********"
("a".."j").each do |e|
password[0] = e
t = Time.now
login(password)
p "%s: %f" % [password, Time.now - t]
end
# > "a*******: 0.010337"
# > "b*******: 0.011287"
# > "c*******: 0.012513"
# > "d*******: 0.012528"
# > "e*******: 0.012558"
# > "f*******: 0.024530"
# > "g*******: 0.012537"
# > "h*******: 0.012559"
# > "i*******: 0.012535"
# > "j*******: 0.012544"
これで最初の文字は f
だとわかる。
全文字を1つずつ特定する
同様にして8文字すべてを特定する。
password = "*" * length
length.times do |i|
char = ("a".."j").max_by do |e|
password[i] = e
t = Time.now
login(password)
Time.now - t
end
password[i] = char # => "f", "a", "e", "g", "h", "c", "j", "b"
end
これでパスワードが特定できた。
password # => "faeghcjb"
しかしログインすると、
login(password) # => false
失敗してしまった。
原因は最後の1文字が時間の差で判断できなかったためである。
最後の1文字は総当たり
最後の文字だけは実際にログインの可否で直接判断する。
("a".."j").each do |e|
password[-1] = e
if login(password)
break
end
end
password # => "faeghcjd"
login(password) # => true
サーバー側の改善例
def login(password)
[*password.chars, nil].each.with_index.inject(true) do |a, (e, i)|
a & (e == PASSWORD[i])
end
end
パスワードが誤っていても最後まで比較を行うことで計算量が一定になる。
ActiveSupport の専用メソッド
require "active_support/security_utils"
ActiveSupport::SecurityUtils.secure_compare("foo", "foo") # => true
ActiveSupport::SecurityUtils.secure_compare("foo", "bar") # => false
必要なときは自作せずにこちらを使うのが望ましい。
タイミング攻撃の実用性は低い
理論上有効であっても現実では難しい。処理時間の差から秘密情報を推測することは理屈の上では可能だが、実際のネットワーク環境では遅延や揺らぎ、CDNやSSLの影響により微細な時間差がノイズに埋もれてしまう。
たとえローカル環境であっても、パスワードの比較程度のごくわずかな処理時間の差を安定して観測するのは困難であり、攻撃が成立するのは極めて限られた条件下においてのみである。
なのでこのシミュレーションでは sleep を入れてインチキしている。
Discussion