安全随笔1:谨慎一次MD5值的可被穷举性

MD5不再安全不是从算法本身而言。如果从可逆性角度出发, MD5值不存在被破解的可能性。MD5的算法公式如下:

R=H(S)

该公式指出:对于给定的一个源内容S,H可以将其映射为R。这里要注意几个特点。首先,S到R的映射是一种多对一的映射;其次,R作为目标内容,是一个无规律的定长的字符串;最后,映射H是一种压缩映射,即R的空间远远小于S。

MD5的算法特性使其无法存在一个逆过程,即:将R还原成为S,下面的公式不成立:

R=H-1(S)

正是基于以上的特点,MD5被广泛用于密码验证和消息体完整性验证。相信大家对于密码验证使用MD5算法都不陌生。假设新注册了一个用户,当注册用户的密码第一次被存储到数据库的时候,我们往往将其转换为MD5值存储:

static void Main(string[] args)
{
string source = "luminji's key";
string hash = GetMd5Hash(source);
Console.WriteLine(
"保存密码原文:{0}的MD5值:{1}到数据库。", source, hash);
}

static string GetMd5Hash(string input)
{
using (MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider())
{
return BitConverter.ToString(md5.ComputeHash(UTF8Encoding.Default.GetBytes(input))).Replace("-", "");
}
}

以上代码输出:

保存密码原文:luminji's key的MD5值:D3A8E4D76A0AEF23B65D9F6D6BCB358F到数据库。

有了MD5值存储在数据库中,在用户进行登录的时候,只要校验MD5就可以确保用户输入的是否是正确的密码了。如下:

static void Main(string[] args)
{
Console.WriteLine(
"请输入密码,按回车键结束……");
string source = Console.ReadLine();
if (VerifyMd5Hash(source, "D3A8E4D76A0AEF23B65D9F6D6BCB358F"))
{
Console.WriteLine(
"密码正确,准许登录系统。");
}
else
{
Console.WriteLine(
"密码有误,拒绝登录。");
}
}

static bool VerifyMd5Hash(string input, string hash)
{
string hashOfInput = GetMd5Hash(input);
StringComparer comparer
= StringComparer.OrdinalIgnoreCase;
return comparer.Compare(hashOfInput, hash) == 0 ? true : false;
}

本段代码的输出为:

请输入密码,按回车键结束……
luminji
's key
密码正确,准许登录系统。

或许大家会问:为什么不直接存储密码,而使用MD5值呢?如果非要回答这个问题,我想更大程度上还是要从保护我们自己的隐私着想。即便是一个银行系统,我们也不想让银行的后台管理人员看到我们的密码。而通过MD5值,就可以确保这一点。通过验证MD5值,即保证了无人可以查看或破解我们的密码,也到达了密码验证的目的。虽然有人可能会质疑,MD5的算法不是一个多对一的映射吗?也就是说,很有可能存在一个另外的密码,求出来的MD5值和我的这个密码是一样的。但是,在实际应用场合中,这个概率会很小,小到可以忽略不计。

既然到目前为止,说到的都是MD5的好处,那么,为什么说MD5是不安全的呢?因为,这个世界上还有一个方法,叫做穷举法。鉴于使用我们的软件产品的用户大多数不是计算机专家,安全意识普遍比较薄弱,所以这类用户设置的密码很有可能是简单的数字组合。这个时候,穷举法就会派上很大的用处。以密码“8888”为例,测试下我们的穷举算法可以多长时间破解掉密码:

static void Main(string[] args)
{
Console.WriteLine(
"开始穷举法破解用户密码……");
string key = string.Empty;
Stopwatch watch
= new Stopwatch();
watch.Start();
for (int i = 0; i < 9999; i++)
{
if (VerifyMd5Hash(i.ToString(), "CF79AE6ADDBA60AD018347359BD144D2"))
{
key
= i.ToString();
break;
}
}
watch.Stop();
Console.WriteLine(
"密码已破解,为:{0},耗时{1}毫秒。", key, watch.ElapsedMilliseconds);
}

在上面的代码中,我们假设用户代码是“0”到“9999”的字符串,并在此范围内进行匹配。结果输出为:

开始穷举法破解用户密码……
密码已破解,为:
8888,耗时271毫秒。

可见,如果我们的密码输入的过份简单,计算机甚至都不需要1秒时间就能完成暴力破解。当然,这种算法不是针对MD5的可逆破解,而是非常愚笨的穷举。但是,即便是这样,穷举带来的危害仍然是巨大的。现在,已经有很多的免费或商业的MD5字典库,存储了相当数量的字符串的MD5值,我们只要提交一个MD5值进去,立刻就可以得到它的原文,只要这个原文不是非常复杂。所以,从这个方面来说,MD5不再安全。

明白了这一点,我们就需要找一个方法来改进MD5求值。目前,最通用的做法是多次MD5值法。我们修改GetMd5Hash方法,如下:

static string GetMd5Hash(string input)
{
string hashKey = "Aa1@#$,.Klj+{>.45oP";
using (MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider())
{
string hashCode = BitConverter.ToString(md5.ComputeHash(UTF8Encoding.Default.GetBytes(input))).Replace("-", "") + BitConverter.ToString(md5.ComputeHash(UTF8Encoding.Default.GetBytes(hashKey))).Replace("-", "");
return BitConverter.ToString(md5.ComputeHash(UTF8Encoding.Default.GetBytes(hashCode))).Replace("-", "");
}
}

在改进过后的方法中,我们首先设计了一个足够复杂的密码hashKey,然后将它的MD5值和用户输入密码的MD5值相加,再求一次MD5值作为返回值。经过这个过程以后,密码的长度够了,复杂度也够了,想要通过穷举法来得到真正的密码值的成本就大大增加了。

原文地址:https://www.cnblogs.com/luminji/p/2055021.html