MD5 加密原理&&实际应用

原理

  • MD5特点
    • 唯一性,是有损加密,不可逆向,现有破解都是用撞库实现
    • 不管多长的字符串,加密后长度都是一样长
    • 唯一性:一个文件,不管多大,小到几k,大到几G,你只要改变里面某个字符,那么都会导致MD5值改变.
      作用:很多软件和应用在网站提供下载资源,其中包含了对文件的MD5码,用户下载后只需要用工具测一下下载好的文件,通过对比就知道该文件是否有过更改变动.
    • 不可逆性
  • MD5消息摘要算法,属Hash算法一类。MD5算法对输入任意长度的消息进行运行,产生一个128位的消息摘要。
  • MD5的用处不是用来加密信息解密信息的,个人观点:用来做一个全局唯一标记,比如impdx或者图片文件产生的md5永远只会是一个值,我们不用去对比文件或者文本是否相同,只需要判断md5是否相同就可以判断了。

算法原理

1、数据填充

对消息进行数据填充,使消息的长度对512取模得448,设消息长度为X,即满足X mod 512=448。根据此公式得出需要填充的数据长度。

填充方法:在消息后面进行填充,填充第一位为1,其余为0。

2、添加消息长度

在第一步结果之后再填充上原消息的长度,可用来进行的存储长度为64位。如果消息长度大于264,则只使用其低64位的值,即(消息长度 对 264取模)。

在此步骤进行完毕后,最终消息长度就是512的整数倍。

3、数据处理

准备需要用到的数据:

  • 4个常数: A = 0x67452301, B = 0x0EFCDAB89, C = 0x98BADCFE, D = 0x10325476;
  • 4个函数:F(X,Y,Z)=(X & Y) | ((~X) & Z); G(X,Y,Z)=(X & Z) | (Y & (~Z)); H(X,Y,Z)=X ^ Y ^ Z; I(X,Y,Z)=Y ^ (X | (~Z));

把消息分以512位为一分组进行处理,每一个分组进行4轮变换,以上面所说4个常数为起始变量进行计算,

重新输出4个变量,以这4个变量再进行下一分组的运算,如果已经是最后一个分组,则这4个变量为最后的结果,即MD5值。

^ | ~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
运算(6 & 2)

6的二进制:0110

2的二进制:0010
6 000···000 0110(一共32位)

&(相同位置都为1,才能去1,否则为0)
2 000···000 0010(一共32位)
结果 000···000 0010

结果首位(32位的第一位)为0,是正数二进制,不需要转换,这结果为2。
与:1 & 1 = 1, 1 & 0 = 0, 0 & 1 = 0, 0 & 0 = 0; // 不同时为0,都是1时得1,都是2时得2
或:1 | 1 = 1, 1 | 0 = 1, 0 | 1 = 1, 0 | 0 = 0; // 有1得1,都是0得0;
非:~0 = 1, ~1 = 0; //取反
异或运算(^):0^0=0; 0^1=1;1^0=1;1^1=0; //不同时为1

具体计算的实现较为复杂,建议查阅相关书籍。
1

2

  • 我们可以这么理解
    X:字符串长度
    要求
    X mod 512=448
    如果不行则加长度,进行填充,填充第一位为1,其余为0。
    接着 长度如果超过2的64次方 位,只取低64位,即对 2的64取模

接着分组进行循环运算,最后换位就变成了加密MD5

MD5的用处

  • 用来检验文件是否被修改,通常和sha1或者sha256配合检查,比如txt文件中修改了一个字母,那么他的md5会完全不相同。

  • 对于某些明文密码传输,需要保护,普通加密方式具有可逆性,但是MD5不可逆。但常常不会单独使用MD5进行,因为通常的密码都可以通过撞库来获取(撞库:通过用空间换时间的方式,由于MD5的唯一性,我可以用计算机跑出任何字符串的MD5,比如12345的MD5,可以跑出来,也可以通过别人分享来获取)

实战利用PHP弱类型来比较MD5

  • 例子只演示MD5在PHP中的漏洞

  • 什么是弱类型,众所周知PHP是一门弱语言,不必向 PHP 声明该变量的数据类型,PHP 会根据变量的值,自动把变量的值转换为正确的数据类型,但在这个转换过程中就有可能引发一些安全问题。

  • 当一个字符串被当作一个数值来取值,其结果和类型如下:如果该字符串没有包含’.',‘e’,'E’并且其数值值在整形的范围之内,该字符串被当作int来取值。其他所有情况下都被作为float来取值,该字符串的开始部分决定了它的值,如果该字符串以合法的数值开始,则使用该数值,否则其值为0。

题目来自于bugku

https://ctf.bugku.com/challenges/detail/id/94.html

一进去我们可以看到题目是用MD5,同时用?a=a显示false。

  • 知识点
    • PHP在处理哈希字符串时,会利用"!=“或”=="来对哈希值进行比较,它把每一个以"0E"开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以"0E"开头的,那么PHP将会认为他们相同,都是0。
    • 比如s1885207154a这串字符串通过MD5加密后为0e509367213418206700842008763514

攻击者可以利用这一漏洞,通过输入一个经过哈希后以"0E"开头的字符串,即会被PHP解释为0,如果数据库中存在这种哈希值以"0E"开头的密码的话,他就可以以这个用户的身份登录进去,尽管并没有真正的密码。

输入 a=s1885207154a

成功绕过

最后附上c++版本的MD5实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#include "MD5.h"

/*4组计算函数*/
inline unsigned int F(unsigned int X, unsigned int Y, unsigned int Z)
{
return (X & Y) | ((~X) & Z);
}
inline unsigned int G(unsigned int X, unsigned int Y, unsigned int Z)
{
return (X & Z) | (Y & (~Z));
}
inline unsigned int H(unsigned int X, unsigned int Y, unsigned int Z)
{
return X ^ Y ^ Z;
}
inline unsigned int I(unsigned int X, unsigned int Y, unsigned int Z)
{
return Y ^ (X | (~Z));
}
/*4组计算函数结束*/

/*32位数循环左移实现函数*/
void ROL(unsigned int &s, unsigned short cx)
{
if (cx > 32)cx %= 32;
s = (s << cx) | (s >> (32 - cx));
return;
}

/*B\L互转,接收UINT类型*/
void ltob(unsigned int &i)
{
unsigned int tmp = i;//保存副本
byte *psour = (byte*)&tmp, *pdes = (byte*)&i;
pdes += 3;//调整指针,准备左右调转
for (short i = 3; i >= 0; --i)
{
CopyMemory(pdes - i, psour + i, 1);
}
return;
}

/*
MD5循环计算函数,label=第几轮循环(1<=label<=4),lGroup数组=4个种子副本,M=数据(16组32位数指针)
种子数组排列方式: --A--D--C--B--,即 lGroup[0]=A; lGroup[1]=D; lGroup[2]=C; lGroup[3]=B;
*/
void AccLoop(unsigned short label, unsigned int *lGroup, void *M)
{
unsigned int *i1, *i2, *i3, *i4, TAcc, tmpi = 0; //定义:4个指针; T表累加器; 局部变量
typedef unsigned int(*clac)(unsigned int X, unsigned int Y, unsigned int Z); //定义函数类型
const unsigned int rolarray[4][4] = {
{ 7, 12, 17, 22 },
{ 5, 9, 14, 20 },
{ 4, 11, 16, 23 },
{ 6, 10, 15, 21 }
};//循环左移-位数表
const unsigned short mN[4][16] = {
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 },
{ 1, 6, 11, 0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12 },
{ 5, 8, 11, 14, 1, 4, 7, 10, 13, 0, 3, 6, 9, 12, 15, 2 },
{ 0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11, 2, 9 }
};//数据坐标表
const unsigned int *pM = static_cast<unsigned int*>(M);//转换类型为32位的Uint
TAcc = ((label - 1) * 16) + 1; //根据第几轮循环初始化T表累加器
clac clacArr[4] = { F, G, H, I }; //定义并初始化计算函数指针数组

/*一轮循环开始(16组->16次)*/
for (short i = 0; i < 16; ++i)
{
/*进行指针自变换*/
i1 = lGroup + ((0 + i) % 4);
i2 = lGroup + ((3 + i) % 4);
i3 = lGroup + ((2 + i) % 4);
i4 = lGroup + ((1 + i) % 4);

/*第一步计算开始: A+F(B,C,D)+M[i]+T[i+1] 注:第一步中直接计算T表*/
tmpi = (*i1 + clacArr[label - 1](*i2, *i3, *i4) + pM[(mN[label - 1][i])] + (unsigned int)(0x100000000UL * abs(sin((double)(TAcc + i)))));
ROL(tmpi, rolarray[label - 1][i % 4]);//第二步:循环左移
*i1 = *i2 + tmpi;//第三步:相加并赋值到种子
}
return;
}

/*接口函数,并执行数据填充*/
unsigned int* MD5(const char* mStr)
{
unsigned int mLen = strlen(mStr); //计算字符串长度
if (mLen < 0) return 0;
unsigned int FillSize = 448 - ((mLen * 8) % 512); //计算需填充的bit数
unsigned int FSbyte = FillSize / 8; //以字节表示的填充数
unsigned int BuffLen = mLen + 8 + FSbyte; //缓冲区长度或者说填充后的长度
unsigned char *md5Buff = new unsigned char[BuffLen]; //分配缓冲区
CopyMemory(md5Buff, mStr, mLen); //复制字符串到缓冲区

/*数据填充开始*/
md5Buff[mLen] = 0x80; //第一个bit填充1
ZeroMemory(&md5Buff[mLen + 1], FSbyte - 1); //其它bit填充0,另一可用函数为FillMemory
unsigned long long lenBit = mLen * 8ULL; //计算字符串长度,准备填充
CopyMemory(&md5Buff[mLen + FSbyte], &lenBit, 8); //填充长度
/*数据填充结束*/

/*运算开始*/
unsigned int LoopNumber = BuffLen / 64; //以16个字为一分组,计算分组数量
unsigned int A = 0x67452301, B = 0x0EFCDAB89, C = 0x98BADCFE, D = 0x10325476;//初始4个种子,小端类型
unsigned int *lGroup = new unsigned int[4]{ A, D, C, B}; //种子副本数组,并作为返回值返回
for (unsigned int Bcount = 0; Bcount < LoopNumber; ++Bcount) //分组大循环开始
{
/*进入4次计算的小循环*/
for (unsigned short Lcount = 0; Lcount < 4;)
{
AccLoop(++Lcount, lGroup, &md5Buff[Bcount * 64]);
}
/*数据相加作为下一轮的种子或者最终输出*/
A = (lGroup[0] += A);
B = (lGroup[3] += B);
C = (lGroup[2] += C);
D = (lGroup[1] += D);
}
/*转换内存中的布局后才能正常显示*/
ltob(lGroup[0]);
ltob(lGroup[1]);
ltob(lGroup[2]);
ltob(lGroup[3]);
delete[] md5Buff; //清除内存并返回
return lGroup;
}