近期,我的一个朋友(大家可以叫他Jon)与我分享了一个有关信息安全的故事。这是一个非常有趣的故事,但是这个故事背后所隐藏的问题却是非常可怕的:
1. 相关人员没有及时发现这些安全问题
2. 信息被泄漏
为了提升他们应用程序的安全性,Jon的开发团队决定为应用程序引入密码熵机制,并鼓励用户使用更加健壮的密码。这是一个非常有用的功能,例如Dropbox,Twitter,以及eBay等大型公司都已经采用了这一机制。Jon的开发团队打算使用Zxcvbn代码库来作为熵的生成器。目前为止一切都还正常。
如果大家不了解什么是熵,大家可以暂时将其理解为密码复杂程度的数字量度。
但是,当Jon的团队决定将密码的熵值存储进数据库中时,问题就发生了。原因是,随着信息安全技术不断地发展和进步,密码破解也变得更加的容易了,用户可以经常修改他们的密码。那么现在的问题就是,这样的一种操作不仅会极大程度地降低密码哈希算法(Jon的团队使用的是BCrypt)的安全有效性,而且还会减少攻击者暴力破解哈希密码的时间。
但是值得庆幸的是,这个故事的结局是皆大欢喜的。Jon的团队在一次安全审计的过程中发现了这个问题,并立刻将其修复了。
首先,有的读者可能不太了解BCrypt。BCrypt是一个跨平台的文件加密工具。由它加密的文件可在所有支持的操作系统和处理器上进行迁移。它的密码口令必须是8至56个字符,密码口令将会在内部被转化为448位的密钥。BCrypt除了可以对你的数据进行加密,默认情况下,BCrypt在删除数据之前将使用随机数据三次覆盖原始输入文件,以阻止可能会获得你的计算机数据的人恢复数据的尝试。如果大家不想使用此功能,可通过设置来禁用此功能。
接下来,我将会在这篇文章中向大家解释,为何将密码熵存储进数据库之后,将会完全破坏哈希密码的安全性。
使用Zxcvbn来计算熵
从上面这张图片中,我们可以看到Dropbox的密码框界面。它利用了四个条形栏和颜色来向用户实时显示密码的强度,而底层的运算全部由Zxcvbn库来完成。实现这个功能的代码是非常简单的,如下所示:
require 'zxcvbn'
result = Zxcvbn.test('@lfred2004')
puts result.entropy # => 14.814
那么我们现在来讨论一下Jon所开发的应用程序。当用户输入他们的密码之后,应用程序会利用BCrypt来计算密码的哈希值,然后将其存储进数据库中。下图所示的是一个用户表的简单版本:
如果你没有对这些数据进行定期的安全检测,那么一切看起来会很正常。那么接下来的问题就是,攻击者如何能够利用用户信息表中的信息来破解出密码熵和明文密码呢?
为什么我们说将熵值存储在数据库中是一种非常糟糕的做法?
我们知道,熵是会泄漏信息的。如果涉及到密码(通常情况下会涉及到安全系统)的话,我们都希望它所泄漏的信息应该尽可能的少。否则,攻击者一旦获取到了这些信息,他们就可以利用这些信息来进行下一步操作了。为了充分利用这些信息,你还需要了解一些有关哈希速度的内容。
大家可以从上图中看到,BCrypt在校对一万个密码哈希与对应的MD5/SHA1值时,会耗费大量的时间。BCrypt的设计初衷就是为了让暴力破解密码哈希的时间更加的久,但是MD5和SHA1在设计之初并没有考虑到这一点。这样一来,我们就会产生一个疑问:那么用它计算Zxcvbn的值将会需要多长时间呢?
你可以从上面这张图片中看到,Zxcvbn在运行一万次迭代运算时所花费的时间要远远少于BCrypt所花费的时间(大约要快124倍)。这也就意味着,你可以将密码输入至Zxcvbn中,然后生成一个待选密码的子集,接下来BCrypt就可以对这些密码数据进行哈希处理。具体的算法如下图所示:
1. 获取常用的密码列表(越大越好)
2. 利用Zxcvbn来对这些常见密码进行计算,并获取到对应的熵值
3. 将计算得到的熵值作为哈希密钥,并将密码作为值来存储进一个数组中
4. 将用户表中的数据按熵值大小,从低到高进行排序
5. 遍历用户数据表,并使用表中的熵来进行索引处理
-如果哈希密钥存在:(1)数组中的值就是候选密码;(2)BCrypt会利用候选密码来与数据库中的密码哈希进行对比;
编写一个密码破解程序
现在,我们可以将文中所描述的方法编写成具体的实现算法。首先,我们要进行的是前三步操作:
require 'zxcvbn'
tester = Zxcvbn::Tester.new
entropies = {}
dictionary_pwds = open("common_passwords.txt").readlines
dictionary_pwds.map(&:chomp).each do |pwd|
value = tester.test(pwd).entropy
entropies[value] ||= []
entropies[value] << pwd
end
上述代码将会产生一个数组的哈希值,在这个数组中,不仅存储有密码的熵值,而且还存储有每一个熵值所对应的候选密码,具体情况如下图所示:
{
0.0 => [ 'password', 'james', 'smith', 'mary' ],
11.784 => [ 'Turkey50', 'zigzag', 'bearcat' ],
11.236 => [ 'samsung1', 'istheman' ],
...
17.434 => [ '01012011', '01011980', ... '01011910' ],
...
}
接下来的这部分算法会加载数据库,并且对表中的熵值进行由低到高的排序,然后该算法会尝试对密码进行破解:
require 'sqlite3'
require 'bcrypt'
# Open a SQLite 3 database file
db = SQLite3::Database.new 'entropy.sqlite3'
db.execute("SELECT * FROM users ORDER BY entropy") do |user|
# Load user record
email = user[1]
pwd_hash = user[6]
pwd_entropy = user[7]
puts "User: #{email}, entropy: #{pwd_entropy}, password_hash: #{pwd_hash} "
candidate_passwords = entropies[pwd_entropy]
if candidate_passwords != nil
passwords = candidate_passwords.select do |candidate|
BCrypt::Password.new(pwd_hash) == candidate
end.flatten
# Should be 0 or 1 -- if > 1, something wrong
if passwords.length == 0
puts "No Matching Candidates"
elsif passwords.length == 1
puts "Password is: #{passwords.first}"
end
else
puts "No Candidates Found"
end
end
其实整个过程非常的简单,我们也并没有涉及到非常复杂的算法。它基本上就是三次循环迭代和几个哈希函数,但是它所实现的功能却是非常有用的。如果这是现实生活中的一个真正的数据库,你就可以得到电子邮箱和密码的组合了。这也就意味着,用户的数据已经泄漏了。
正如我在文中所提到的,对于Jon和他的团队而言,这也是一次非常好的经历。他们在数据库被攻击之前,他们及时地发现了这个问题,并将其修复了,这也就避免了他们的名字出现在第二天的媒体头条上。但是,Ashley Madison所经历的事情就截然不同了。没错,Ashley Madison的技术人员也犯下了同样的错误,他们直接将用户密码的MD5哈希直接存储进了数据库中。这也就直接导致研究人员成功破解了其3000万密码哈希中的三分之一密码!
当然了,这种置身事外的感觉肯定是很舒服的。但是我们应该注意,如果我们无法对数据库中的数据进行适当的安全处理,那么发生在Ashley Madison身上的悲剧也有可能发生在我们自己身上。我已经将示例代码和数据库文件上传至我的Github上了,如果你想编写你自己的代码破解器,你也许可以从中获取到一些有价值的信息。
还没有评论,来说两句吧...