刚刚看到E大人(EricHo)发的一个帖子感触颇深,众所周知MD5在彩虹表下基本上是很轻松的,正常MD5解密还是通过对比,但是E大人给出了一个非常好的结局MD5穷举爆破的解决方案,完整转发如下
文章来自于技术宅~作者为E大人

[分享] 谨慎一次MD5值的可被穷举性
 EricHo 发表
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 = "EricHo\'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("-", "");
      }
    }

以上代码输出:

保存密码原文:EricHo\'s key的MD5值:68D78A6433245308D935602726B20750到数据库。

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

static void Main(string[] args)
   {
    Console.WriteLine("请输入密码,按回车键结束……");
    string source = Console.ReadLine();
    if (VerifyMd5Hash(source, "68D78A6433245308D935602726B20750"))
     {
       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;
   }

本段代码的输出为:

请输入密码,按回车键结束……
EricHo\'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 实则就是一个盐,这里方便演示,使用了固定盐。实际应用中,最好是用随机盐,每个密码加的盐都不同,这样子可以增加碰撞难度。

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

E大人的方案很好的解决了关于穷举爆破~再一次仰慕E大人~

1 对 “MD5值穷举爆破的解决方案”的想法;

评论被关闭。