什么是MD5

md5是一种密码散列函数,也叫密码散列算法。

密码散列函数是一种单向散列函数,它可以将给定的数据提取出信息摘要,也就是给定数据的指纹信息。结果的摘要信息格式是一致的,通常用一个短的随机字母和数字组成的字符串来代表。

密码散列函数的特点

  • 对于任何一个给定的消息,它都很容易就能运算出散列数值。
  • 难以用散列数值推算出原始数据。
  • 数据变动(哪怕很微小),散列数值也会发生很大的变动。
  • 单向散列函数生成的信息摘要是不可预见的。

算数模型为: h = H(M)
h为散列数值结果
H为散列函数
M为原始数据

模型特点

  • h需要有固定的长度,即生成的散列数值格式需要一致,跟原始数据M的长度和格式无关
  • 给定h和H,很难甚至根本无法计算出原始数据M
  • 给定H,找到M1和M2,使得 H(M1) = H(M2) 在计算上是不可行的 (但是这不代表不存在散列数值相等的M1和M2,只是想通过计算得出是不可行的)

MD5的应用

一致性验证
在UNIX下有很多软件在下载的时候都提供了一个后缀为.md5的文件,这个文件通常的内容只有一行,格式大概为: MD5 (xxx.tar.gz) = 38b8c2c1093dd0fec383a9d9ac940515

这是软件或者下载包的md5散列数值,我们可以计算我们下载的包的散列数值,并与该值进行对比,只有数值相同的才是正确、安全的下载。

这是防止软件被篡改,或者在传输过程造成的文件损坏,只要数据内部结构产生微小的变化,散列数值的结果就会发生很大的变动。

安全访问认证
当我们在程序中保存用户密码的时候,如果我们采用明文储存,当服务器权限或者管理员账号泄露,用户的密码就会被查询出来,根据我们的习惯,我们往往会在多个不同系统中使用相同的密码,这会造成更大的影响。

我们可以将用户的密码进行md5加密储存,在用户登录的时候,将输入内容进行md5加密,与储存的数值对比,这样子就可以在不需要知道用户的明文密码请求下完成认证验证。

当然这也不是绝对安全的,常见的方式有:字典反查、暴力穷举

暴力穷举先设定一个范围,并在这个范围内逐一地对数据进行验证,需要的运算量和时间比较大。

黑客往往拥有强大的彩虹表,这就是密码字典。这种表是为了破解密码的散列值而准备的,它将提前计算好的散列数值储存起来,通常都是100G以上。

当黑客拿到了hash散列数值,它可以通过在彩虹表中反查出对应该散列数值的原文,这样子就可以直接登录系统进行操作。

php中md5函数的漏洞

在PHP中,我们也常将md5哈希字符串进行对比,然而却没有在意处理的细节,导致漏洞的出现。

我们在运行以下的php脚本

<?php
$str = md5('QNKCDZO');
var_dump($str == '0');

打印出来的结果是:bool(true)

是不是与我们预想中的情况不一样,这明显是两个不一样的字符串,为什么会得到相等的结果。

我们将$str的值打印出来得到:0e830400451993494058024219903391

为什么"0e830400451993494058024219903391" == "0"会得到true的结果,这是因为PHP的语言特性,导致了问题的发生。

php是弱类型语言

因为php是弱类型语言,在使用==进行对比的时候,只判断两个参数的值,而不判断参数的类型。

我们运行该脚本,也一样能得到true的结果

<?php
var_dump("0e830400451993494058024219903391" == 0);

0e代表什么

除了以上demo的QNKCDZO,以下的字符进行MD5运行后的哈希值也会出现一样的问题

QNKCDZO   => 0e830400451993494058024219903391
240610708 => 0e462097431906509019562988736854
s878926199a => 0e545993274517709034328855841020
s155964671a => 0e342768416822451524974117254469
s214587387a => 0e848240448830537924465865611904
s214587387a => 0e848240448830537924465865611904

这些值的md5哈希结果全都是以0e开头的,我们来看看0e代表的是什么

首先我们了解一下科学计数法。

这是一种计数的写法,把一个数表示成a与10的n次幂相乘的形式(1≤a<10,n为整数)

比如将650000记成 6.5E+5,在支持科学计数法的计算器中都可以测试,我们手机自带的计算器一般都有该功能。

但是在输入的时候要把+号省略,并且显示的E是小写的e

在PHP中 以下几种写法的结果相同

<?php
echo 6.5E+5;
echo "\n";
echo 6.5E5;
echo "\n";
echo 6.5e5;
echo "\n";

那么就可以来解释我们上面出现的问题了,以0e开头的数,如果是按科学计数法来计算,不管后面的幂是多大,它的值永远是等于0的。

所以0e830400451993494058024219903391 == 0

php对比数据时的类型选择

由于php是弱类型语言,在处理变量的时候,php内部会根据需要转换数据的格式

<?php
$str = "100";
var_dump($str); // string(3) "100"

echo ($str - 99); // 1

以上例子中,当一个字符串变量需要进行数值运算的时候,php先把它变成了一个数值类型,再计算。

那么我们一开始遇到问题的时候的==比较运算符号中,php也会根据场景将值转换为对应格式来比较

  • 如果比较的数据中,有布尔值,则转为布尔值比较,布尔值比较有一个规则:true> false
  • 如果比较的数据中,有数字值,就转为数字值比较
  • 如果比较的数据中,两边的值都为 纯数字字符串 ,就转为数字值比较
  • 如果以上都不符合,则按常规字符串比较

那么当我们 “0e830400451993494058024219903391” == “0” 的时候,符合第三点要求,两边都是数字字符串,会转为数字值比较,所以得到的结果是true。

0e830400451993494058024219903391 === 0 是错误的哦! 因为科学计数法在php中会转为float类型 可以通过var_dump(0e830400451993494058024219903391)查看类型;

问题以及解决

假设有一个会员账号设置的密码是 240610708 ,那么登录的时候如果输入s155964671a或者其他的值(上面有列举了一些),他也是能登录成功的。

那么需要我们如何处理呢

我们将用户的密码md5储存在数据库中,取出来之后应该是string类型的,我们应该使用恒等运算符,来让php脚本限定两个参数的类型。

<?php
var_dump("0e830400451993494058024219903391" === "0e342768416822451524974117254469") 

脚本将会得到不相等的结果。

在php中,使用比较运算符的时候需要考虑数据类型的问题,防止特殊数据影响了判断的结果。

提示

关于MD5在PHP中的使用注意事项 将会有一篇新的文章罗列讲解,有兴趣可以在博客内搜索看一下。