学破解第115天,《C++之MD5消息摘要算法》学习
[ 本帖最后由 小菜鸟一枚 于 2020-7-11 12:44 编辑 ]\n\n## 学破解第115天,《C++之MD5消息摘要算法》学习前言:
从小学到大专(计算机网络技术专业),玩过去的,所以学习成绩惨不忍睹,什么证书也没考,直到找不到工作才后悔,不知道怎么办才好。
2017年12月16日,通过19元注册码注册论坛账号,开始做伸手党,潜水一年多,上来就是找软件。(拿论坛高大上的软件出去装X)
2018年8月某一天,报名了华中科技大学网络教育本科(计算机科学与技术专业)2018级秋季。(开始提升学历)
2019年6月17日,不愿再做小菜鸟一枚,开始零基础学习破解。(感谢小糊涂虫大哥在我刚开始学习脱壳时,录制视频解答我的问题)
2020年7月7日,感谢H大对我的鼓励,拥有了第一篇获得优秀的文章。(接下来希望学习逆向,逆天改命)
坛友们,年轻就是资本,和我一起逆天改命吧,我的学习过程全部记录及学习资源:(https://www.52pojie.cn/thread-1208234-1-1.html)
立帖为证!--------记录学习的点点滴滴
**本文对应视频教程地址:https://www.52pojie.cn/thread-1217182-1-1.html**
### 0x1MD5的基本介绍
1.MD5是一个安全的散列算法,输入两个不同的明文不会得到相同的输出值,根据输出值,不能得到原始的明文,即其过程不可逆;所以要解密MD5没有现成的算法,只能用穷举法,把可能出现的明文,用MD5算法散列之后,把得到的散列值和原始的数据形成一个一对一的映射表,通过比在表中比破解密码的MD5算法散列值,通过匹配从映射表中找出破解密码所对应的原始明文。
2.在线查询密码。一些在线的MD5值查询网站提供MD5密码值的查询,输入MD5密码值后,如果在数据库中存在,那么可以很快获取其密码值。
3.使用MD5破解工具。网络上有许多针对MD5破解的专用软件,通过设置字典来进行破解。
### 0x2MD5算法的原理
1.填充
在MD5算法中,首先需要对输入信息进行填充,使其位长对512求余的结果等于448,并且填充必须进行,即使其位长对512求余的结果等于448。因此,信息的位长(Bits Length)将被扩展至N*512+448,N为一个非负整数,N可以是零。
填充的方法如下:
1) 在信息的后面填充一个1和无数个0,直到满足上面的条件时才停止用0对信息的填充。
2) 在这个结果后面附加一个以64位二进制表示的填充前信息长度(单位为Bit),如果二进制表示的填充前信息长度超过64位,则取低64位。
经过这两步的处理,信息的位长=N\*512+448+64=(N+1)\*512,即长度恰好是512的整数倍。这样做的原因是为满足后面处理中对信息长度的要求。
2.初始化变量(变量值一般不变)
初始的128位值为初试链接变量,这些参数用于第一轮的运算,以大端字节序来表示,他们分别为:
A=0x01234567,
B=0x89ABCDEF,
C=0xFEDCBA98,
D=0x76543210。
(每一个变量给出的数值是高字节存于内存低地址,低字节存于内存高地址,即大端字节序。在程序中变量A、B、C、D的值分别为0x67452301,0xEFCDAB89,0x98BADCFE,0x10325476)。
3.处理分组数据
每一分组的算法流程如下:
(1)第一分组需要将上面四个链接变量复制到另外四个变量中:A到a,B到b,C到c,D到d。
(2)从第二分组开始的变量为上一分组的运算结果,即A = a, B = b, C = c, D = d。
主循环有四轮(MD4只有三轮),每轮循环都很相似。第一轮进行16次操作。每次操作对a、b、c和d中的其中三个作一次非线性函数运算,然后将所得结果加上第四个变量,文本的一个子分组和一个常数。再将所得结果向左环移一个不定的数,并加上a、b、c或d中之一。最后用该结果取代a、b、c或d中之一。
一个MD5运算由类似的64次循环构成,分成4组16次。
以下是每次操作中用到的四个非线性函数(每轮一个)。
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) )
(&是与(And),|是或(Or),~是非(Not),^是异或(Xor))
这四个函数的说明:如果X、Y和Z的对应位是独立和均匀的,那么结果的每一位也应是独立和均匀的。
F是一个逐位运算的函数。即,如果X,那么Y,否则Z。函数H是逐位奇偶操作符。
现定义:
FF(a ,b ,c ,d ,Mj ,s ,ti ) 操作为 a = b + ( (a + F(b,c,d) + Mj + ti) << s)
GG(a ,b ,c ,d ,Mj ,s ,ti ) 操作为 a = b + ( (a + G(b,c,d) + Mj + ti) << s)
HH(a ,b ,c ,d ,Mj ,s ,ti) 操作为 a = b + ( (a + H(b,c,d) + Mj + ti) << s)
II(a ,b ,c ,d ,Mj ,s ,ti) 操作为 a = b + ( (a + I(b,c,d) + Mj + ti) << s)
注意:“<<”表示循环左移位,不是左移位。
ti:T = 4294967296 * abs(sin(i))所得结果的整数,共64个,通过正弦函数消除算法变换中的线性操作。
这四轮(共64步)是:
第一轮
FF(a ,b ,c ,d ,M0 ,7 ,0xd76aa478 )
FF(d ,a ,b ,c ,M1 ,12 ,0xe8c7b756 )
FF(c ,d ,a ,b ,M2 ,17 ,0x242070db )
FF(b ,c ,d ,a ,M3 ,22 ,0xc1bdceee )
FF(a ,b ,c ,d ,M4 ,7 ,0xf57c0faf )
FF(d ,a ,b ,c ,M5 ,12 ,0x4787c62a )
FF(c ,d ,a ,b ,M6 ,17 ,0xa8304613 )
FF(b ,c ,d ,a ,M7 ,22 ,0xfd469501)
FF(a ,b ,c ,d ,M8 ,7 ,0x698098d8 )
FF(d ,a ,b ,c ,M9 ,12 ,0x8b44f7af )
FF(c ,d ,a ,b ,M10 ,17 ,0xffff5bb1 )
FF(b ,c ,d ,a ,M11 ,22 ,0x895cd7be )
FF(a ,b ,c ,d ,M12 ,7 ,0x6b901122 )
FF(d ,a ,b ,c ,M13 ,12 ,0xfd987193 )
FF(c ,d ,a ,b ,M14 ,17 ,0xa679438e )
FF(b ,c ,d ,a ,M15 ,22 ,0x49b40821 )
第二轮
GG(a ,b ,c ,d ,M1 ,5 ,0xf61e2562 )
GG(d ,a ,b ,c ,M6 ,9 ,0xc040b340 )
GG(c ,d ,a ,b ,M11 ,14 ,0x265e5a51 )
GG(b ,c ,d ,a ,M0 ,20 ,0xe9b6c7aa )
GG(a ,b ,c ,d ,M5 ,5 ,0xd62f105d )
GG(d ,a ,b ,c ,M10 ,9 ,0x02441453 )
GG(c ,d ,a ,b ,M15 ,14 ,0xd8a1e681 )
GG(b ,c ,d ,a ,M4 ,20 ,0xe7d3fbc8 )
GG(a ,b ,c ,d ,M9 ,5 ,0x21e1cde6 )
GG(d ,a ,b ,c ,M14 ,9 ,0xc33707d6 )
GG(c ,d ,a ,b ,M3 ,14 ,0xf4d50d87 )
GG(b ,c ,d ,a ,M8 ,20 ,0x455a14ed )
GG(a ,b ,c ,d ,M13 ,5 ,0xa9e3e905 )
GG(d ,a ,b ,c ,M2 ,9 ,0xfcefa3f8 )
GG(c ,d ,a ,b ,M7 ,14 ,0x676f02d9 )
GG(b ,c ,d ,a ,M12 ,20 ,0x8d2a4c8a )
第三轮
HH(a ,b ,c ,d ,M5 ,4 ,0xfffa3942 )
HH(d ,a ,b ,c ,M8 ,11 ,0x8771f681 )
HH(c ,d ,a ,b ,M11 ,16 ,0x6d9d6122 )
HH(b ,c ,d ,a ,M14 ,23 ,0xfde5380c )
HH(a ,b ,c ,d ,M1 ,4 ,0xa4beea44 )
HH(d ,a ,b ,c ,M4 ,11 ,0x4bdecfa9 )
HH(c ,d ,a ,b ,M7 ,16 ,0xf6bb4b60 )
HH(b ,c ,d ,a ,M10 ,23 ,0xbebfbc70 )
HH(a ,b ,c ,d ,M13 ,4 ,0x289b7ec6 )
HH(d ,a ,b ,c ,M0 ,11 ,0xeaa127fa )
HH(c ,d ,a ,b ,M3 ,16 ,0xd4ef3085 )
HH(b ,c ,d ,a ,M6 ,23 ,0x04881d05 )
HH(a ,b ,c ,d ,M9 ,4 ,0xd9d4d039 )
HH(d ,a ,b ,c ,M12 ,11 ,0xe6db99e5 )
HH(c ,d ,a ,b ,M15 ,16 ,0x1fa27cf8 )
HH(b ,c ,d ,a ,M2 ,23 ,0xc4ac5665 )
第四轮
II(a ,b ,c ,d ,M0 ,6 ,0xf4292244 )
II(d ,a ,b ,c ,M7 ,10 ,0x432aff97 )
II(c ,d ,a ,b ,M14 ,15 ,0xab9423a7 )
II(b ,c ,d ,a ,M5 ,21 ,0xfc93a039 )
II(a ,b ,c ,d ,M12 ,6 ,0x655b59c3 )
II(d ,a ,b ,c ,M3 ,10 ,0x8f0ccc92 )
II(c ,d ,a ,b ,M10 ,15 ,0xffeff47d )
II(b ,c ,d ,a ,M1 ,21 ,0x85845dd1 )
II(a ,b ,c ,d ,M8 ,6 ,0x6fa87e4f )
II(d ,a ,b ,c ,M15 ,10 ,0xfe2ce6e0 )
II(c ,d ,a ,b ,M6 ,15 ,0xa3014314 )
II(b ,c ,d ,a ,M13 ,21 ,0x4e0811a1 )
II(a ,b ,c ,d ,M4 ,6 ,0xf7537e82 )
II(d ,a ,b ,c ,M11 ,10 ,0xbd3af235 )
II(c ,d ,a ,b ,M2 ,15 ,0x2ad7d2bb )
II(b ,c ,d ,a ,M9 ,21 ,0xeb86d391 )
所有这些完成之后,将a、b、c、d分别在原来基础上再加上A、B、C、D。
即a = a + A,b = b + B,c = c + C,d = d + D
然后用下一分组数据继续运行以上算法。
4.最后的输出是A、B、C、D的级联。
### 0x3MD5算法的实现
1.按照上述过程,我首先想到的就是要定义四个初始化常量,和四个线性函数,参考百度百科的做法,使用宏定义,这样就不必在代码中去定义四个函数。
```
#define F(x, y, z) (((x) & (y)) | ((~x) & (z)))
#define G(x, y, z) (((x) & (z)) | ((y) & (~z)))
#define H(x, y, z) ((x) ^ (y) ^ (z))
#define I(x, y, z) ((y) ^ ((x) | (~z)))
#define A 0x67452301
#define B 0xefcdab89
#define C 0x98badcfe
#define D 0x10325476
```
既然如此为什么,我不把FF,GG,HH,II四个函数也一起宏定义呢?
看信息分组加密过程,每一轮处理处理128位,处理的是M0-M15不固定,而且后面的常数ti也不固定,所以不能用宏定义处理。
2.根据1中引出的问题,我们再来看一下那些不固定的常数导致函数不能用宏定义,这些常量是不是可以事先声明好呢?
```
//常量ti unsigned int(abs(sin(i+1))*(2pow32)),格式化一下,每行16个,共4行,对应轮数
const unsigned int k[]={
0xd76aa478,0xe8c7b756,0x242070db,0xc1bdceee,0xf57c0faf,0x4787c62a,0xa8304613,0xfd469501,0x698098d8,0x8b44f7af,0xffff5bb1,0x895cd7be,0x6b901122,0xfd987193,0xa679438e,0x49b40821,
0xf61e2562,0xc040b340,0x265e5a51,0xe9b6c7aa,0xd62f105d,0x02441453,0xd8a1e681,0xe7d3fbc8,0x21e1cde6,0xc33707d6,0xf4d50d87,0x455a14ed,0xa9e3e905,0xfcefa3f8,0x676f02d9,0x8d2a4c8a,
0xfffa3942,0x8771f681,0x6d9d6122,0xfde5380c,0xa4beea44,0x4bdecfa9,0xf6bb4b60,0xbebfbc70,0x289b7ec6,0xeaa127fa,0xd4ef3085,0x04881d05,0xd9d4d039,0xe6db99e5,0x1fa27cf8,0xc4ac5665,
0xf4292244,0x432aff97,0xab9423a7,0xfc93a039,0x655b59c3,0x8f0ccc92,0xffeff47d,0x85845dd1,0x6fa87e4f,0xfe2ce6e0,0xa3014314,0x4e0811a1,0xf7537e82,0xbd3af235,0x2ad7d2bb,0xeb86d391};
//常量s,是向左位移数,格式化一下,每行16个,共4行,对应轮数
const unsigned int s[]={7,12,17,22,7,12,17,22,7,12,17,22,7,12,17,22,
5,9,14,20,5,9,14,20,5,9,14,20,5,9,14,20,
4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23,
6,10,15,21,6,10,15,21,6,10,15,21,6,10,15,21};
```
除此之外,还定义了一些全局变量,用来保存一些信息,方便最后输出结果。
```
//strBaye的长度
unsigned int strlength;
//A,B,C,D的临时变量
unsigned int atemp;//加密后的A
unsigned int btemp;//加密后的B
unsigned int ctemp;//加密后的C
unsigned int dtemp;//加密后的D
const char str16[] = "0123456789abcdef";//这个决定最后是以大写字母输出还是小写字母输出
```
3.接下来就是第1步,填充信息长度,说明一下,string就是封装了对字符串(字符数组char[]或char \*)的操作,提供了很多强大的函数。
```
*填充函数
*处理后应满足bits≡448(mod512),字节就是bytes≡56(mode64),即使余数刚好为0也要进行填充,也就是最少填充1位,最多填充512位
*填充方式为先加一个1,其它位补零
*最后加上64位的原来长度
*/
unsigned int* add(string str)
{
unsigned int num=((str.length()+8)/64)+1;//以512位,64个字节为一组
unsigned int *strByte=new unsigned int; //64/4=16,所以有16个整数
strlength=num*16;
for (unsigned int i = 0; i < num*16; i++)
strByte=0;
for (unsigned int i=0; i <str.length(); i++)
{
strByte|=(str)<<((i%4)*8);//一个整数存储四个字节,i>>2表示i/4 一个unsigned int对应4个字节,保存4个字符信息
}
strByte|=0x80<<(((str.length()%4))*8);//尾部添加1 一个unsigned int保存4个字符信息,所以用128左移
/*
*添加原长度,长度指位的长度,所以要乘8,然后是小端序,所以放在倒数第二个,这里长度只用了32位
*/
strByte=str.length()*8;
return strByte;
}
```
看源代码,发现声明的变量都是unsigned无符号型,这是因为后面的右移高位是补0,而不是补符号位(算术右移),也就是逻辑右移了。
注意:unsigned int,在32位程序中占4个字节。
可能大家又有一个疑问:4个4字节A,B,C,D级联那不就是16位吗???
我刚开始也这么想,来来,4\*(32\*4) = 4\*(16\*8),四个32位变成16进制不就是8个数嘛,然后级联4\*8不就是32位了。
4.信息长度补齐了,接下来就是四轮分组加密了,每一次处理的都是512位,也就是64字节。
```
void mainLoop(unsigned int M[])
{
unsigned int f,g;
unsigned int a=atemp;
unsigned int b=btemp;
unsigned int c=ctemp;
unsigned int d=dtemp;
for (unsigned int i = 0; i < 64; i++)
{
//执行四轮加密
if(i<16){
f=F(b,c,d);
g=i;
}else if (i<32)
{
f=G(b,c,d);
g=(5*i+1)%16;
}else if(i<48){
f=H(b,c,d);
g=(3*i+5)%16;
}else{
f=I(b,c,d);
g=(7*i)%16;
}
每加密一轮将消息加密起来
unsigned int tmp=d;
d=c;
c=b;
b=b+shift((a+f+k+M),s);
a=tmp;
}
//第一组512位数据加密后的结果,作为下一轮加密的输入,所以在前面声明成了全局变量
atemp=a+atemp;
btemp=b+btemp;
ctemp=c+ctemp;
dtemp=d+dtemp;
}
```
这里可不一定是一轮就处理完了,所以写成函数,循环调用。
5.最后经过n次mainLoop后,就是级联输出了,下面就是输出函数。
```
string changeHex(int a)
{
int b;
string str1;
string str="";
for(int i=0;i<4;i++)
{
str1="";
b=((a>>i*8)%(1<<8))&0xff; //逆序处理每个字节
for (int j = 0; j < 2; j++)
{
str1.insert(0,1,str16);在下标为0的位置插入1个字符
b=b/16;//数学里进制转换就是用十进制除以进制数(2、4、8、16),在这里也就是相当于是算术右移4位。
}
str+=str1;//将它的每一位变成16进制拼起来
}
return str;
}
string getMD5(string source)
{
atemp=A; //每一次都要初始化,方便下一次调用
btemp=B;
ctemp=C;
dtemp=D;
unsigned int *strByte=add(source);//填充一下字符串
for(unsigned int i=0;i<strlength/16;i++)//每16个字符,也就是512位为一组,进行下面的四轮加密
{
unsigned int num;
for(unsigned int j=0;j<16;j++)//
num=strByte;//循环把一组数据送入num
mainLoop(num);
}
return changeHex(atemp).append(changeHex(btemp)).append(changeHex(ctemp)).append(changeHex(dtemp));
}
```
看到这里是不是有疑问,为神马要这样输出,不能直接变成16进制字符输出吗?
看原始常量A:0x 01 23 45 67,在计算机中存储的是先从低位存起,00000001 00100011 01000101 01100111,首先存67,然后45,接着是23,01,两个16进制是一个字节。
所以程序中四个常数按照这种规则定义的,输出的时候我们得将它同样逆序过来输出。
### 0x4MD5加密的“解密”
1.实际上md5是一种不可逆算法,自然不存在解密,当然百度上也有说以差分法碰撞,只需要数秒钟,然而那并不是解密。
我输入123456
加密后的密文为:e10adc3949ba59abbe56e057f20f883e
一种是特殊的差分攻击法:能找到另一个和密文相同的md5输入,如果找到一写不可显示字符,你觉得明文输入框能允许你输入吗?
它并不能还原成你的输入123456,当然从算法的角度上,既然存在碰撞,算法当然就是不安全的了。
2.还有一些彩虹表法,利用的是社会工程学原理,把常用的字母数字组合用md5加密,然后比较密文md5,如果在数据库内,就能得到明文。
3.再来看一下强行碰撞法,假设用户密码就是纯数字组成,那么我们至少需要10^1+10^10^2+10^3+10^n,按照等比数列公式:
![](https://dss1.baidu.com/6ONXsjip0QIZ8tyhnq/it/u=3063566503,3480622283&fm=58)
需要存储这么多:(10*(1-10^n))/(1-10)
等比数列是成指数增长的,位数低的时候容易被使用数据字典爆破,但是明文位数每增加1为10的n次方增加。
4.大家不重要的密码一般都是123456,典型的弱口令,但是即使你取得了密文,也不能解密,为什么呢?
因为加盐了或是用了多重算法,例如明文是123456,我在md5运算之前,拼接一个32位的字符串作为盐,然后再去参与MD5运算,你觉得需要多大的字典才能爆破呢?10的n+32次方个数据?
### 0x5使用OD分析MD5程序
1.打开MD5KeyGenMe.exe(加密解密第四版中光盘随书文件),发现要求输入name和serial number验证,首先查壳,发现EXEInfo PE查壳,无壳,那就使用虚拟机里面工具包中密码学综合工具:Kanal V2.3扫描算法。
2.看到了401535可能是MD5加密函数,先不管,windows程序中获得文本框输入的值有两个API,分别是GetDlgItemText和GetWindowText。所以这里我直接给它下这两个断点,跑起来用户名输入52pojie,序列号输入123456,点击按钮断了下来。(当然了这个程序有弹窗,所以大家也可以通过messageBOX回溯试试)
```
0040116F|.FFD5 call ebp ; \GetDlgItemTextA
00401171|.8BF8 mov edi,eax ;eax保存着52pojie
00401173|.3BFB cmp edi,ebx
00401175|.0F84 0E010000 je MD5KeyGe.00401289 ;除非输入的name为空,否则肯定不会跳,下面还要去取serial number
0040117B|.8D5424 60 lea edx,dword ptr ss:
0040117F|.68 C9000000 push 0xC9 ; /Count = C9 (201.)
00401184|.52 push edx ; |Buffer = 44434241
00401185|.68 E9030000 push 0x3E9 ; |ControlID = 3E9 (1001.)
0040118A|.56 push esi ; |hWnd = 000706A8 ('MD5 *KeyGenMe*',class='#32770')
0040118B|.FFD5 call ebp ; \GetDlgItemTextA
0040118D|.83F8 13 cmp eax,0x13 ;这里比较serial number长度是否等于0x13
00401190|.0F85 F3000000 jnz MD5KeyGe.00401289
00401196|.8A4C24 64 mov cl,byte ptr ss: ;这里是将序列号第5个字符,也就5给cl
0040119A|.B0 2D mov al,0x2D ;再将0x2D,也就是 - 号给al
0040119C|.3AC8 cmp cl,al ;判断第5个字符是否等于-
0040119E|.0F85 E5000000 jnz MD5KeyGe.00401289
004011A4|.384424 69 cmp byte ptr ss:,al ;序列号第10个字符
004011A8|.0F85 DB000000 jnz MD5KeyGe.00401289
004011AE|.384424 6E cmp byte ptr ss:,al ;序列号第15个字符
004011B2|.0F85 D1000000 jnz MD5KeyGe.00401289
004011B8|.8B4C24 65 mov ecx,dword ptr ss: ;取第6,7,8,9个字符
004011BC|.8B4424 60 mov eax,dword ptr ss: ;取第1,2,3,4个字符
004011C0|.8B5424 6A mov edx,dword ptr ss: ;取第11,12,13,14个字符
004011C4|.894C24 14 mov dword ptr ss:,ecx
004011C8|.894424 10 mov dword ptr ss:,eax
004011CC|.8B4424 6F mov eax,dword ptr ss: ;取第16,17,18,19个字符
004011D0|.8D8C24 280100>lea ecx,dword ptr ss:
004011D7|.895424 18 mov dword ptr ss:,edx
004011DB|.51 push ecx ;MD5的上下文地址
004011DC|.894424 20 mov dword ptr ss:,eax ;当前esp存储的是去除三个-号后的序列号
004011E0|.E8 CB000000 call MD5KeyGe.004012B0
```
3.接下来,显然就要看一下004011E0这个call对序列号做了什么,看到四个常量可以猜到是MD5在初始化。
```
004012B0/$8B4424 04 mov eax,dword ptr ss:
004012B4|.33C9 xor ecx,ecx
004012B6|.8948 14 mov dword ptr ds:,ecx
004012B9|.8948 10 mov dword ptr ds:,ecx
004012BC|.C700 01234567 mov dword ptr ds:,0x67452301 ;看到了接下来的四个常数,是不是能猜到md5初始化
004012C2|.C740 04 89ABC>mov dword ptr ds:,0xEFCDAB89
004012C9|.C740 08 FEDCB>mov dword ptr ds:,0x98BADCFE
004012D0|.C740 0C 76543>mov dword ptr ds:,0x10325476
004012D7\.C3 retn
```
4.我们出来后继续继续单步向下分析,看到这样的一片代码:
```
004011E5|.8D9424 4C0200>lea edx,dword ptr ss: ;edx是我输入的name,52pojie
004011EC|.57 push edi
004011ED|.8D8424 300100>lea eax,dword ptr ss:
004011F4|.52 push edx
004011F5|.50 push eax
004011F6|.E8 E5000000 call MD5KeyGe.004012E0
004011FB|.83C4 10 add esp,0x10
004011FE|.8D4C24 24 lea ecx,dword ptr ss:
00401202|.51 push ecx ; /www.pediy.com
00401203|.FF15 04604000 call dword ptr ds:[<&KERNEL32.lstrlenA>] ; \lstrlenA
00401209|.50 push eax ;计算www.pediy.com的长度
0040120A|.8D5424 28 lea edx,dword ptr ss: ;将www.pediy.com给edx
0040120E|.8D8424 2C0100>lea eax,dword ptr ss:
00401215|.52 push edx
00401216|.50 push eax
00401217|.E8 C4000000 call MD5KeyGe.004012E0
0040121C|.8D8C24 340100>lea ecx,dword ptr ss:
00401223|.8D9424 8C0100>lea edx,dword ptr ss:
0040122A|.51 push ecx
0040122B|.52 push edx
0040122C|.E8 5F010000 call MD5KeyGe.00401390
00401231|.83C4 14 add esp,0x14
00401234|.33C0 xor eax,eax
00401236|>8A8C04 800100>/mov cl,byte ptr ss:
0040123D|.83E1 1F |and ecx,0x1F
00401240|.40 |inc eax
00401241|.83F8 10 |cmp eax,0x10
00401244|.8A540C 3C |mov dl,byte ptr ss:
00401248|.889404 0F0300>|mov byte ptr ss:,dl
0040124F|.^ 7C E5 \jl short MD5KeyGe.00401236
00401251|.8D8424 100300>lea eax,dword ptr ss:
00401258|.8D4C24 10 lea ecx,dword ptr ss:
0040125C|.50 push eax ; /String2 = "P?s"
0040125D|.51 push ecx ; |String1 = NULL
0040125E|.FF15 00604000 call dword ptr ds:[<&KERNEL32.lstrcmpA>] ; \lstrcmpA
```
很明显上面这几个call很关键,因为下面就是lstrcmpA比较两个字符串是否相等了,那么关键的比较函数上面必然有关键call,进去瞧瞧,一路F8+F7注意堆栈,可以看到
```
004013A4|.E8 A7090000 call MD5KeyGe.00401D50 ;经过这个call,ecx的值为52pojiewww.pediy.com
004013CC|.E8 0FFFFFFF call MD5KeyGe.004012E0 ;这已经出现好几次了都是比较长度是否为64,里面的call都跳过去了,这一次再进去看看
004013D9|.E8 02FFFFFF call MD5KeyGe.004012E0 ;又是它,再进去看看
```
这一次它进去了没有跳走:
```
00401321|. /72 46 jb short MD5KeyGe.00401369 ;这一次没跳走
00401323|. |8B5424 18 mov edx,dword ptr ss:
00401327|. |53 push ebx
00401328|. |8D4430 18 lea eax,dword ptr ds:
0040132C|. |52 push edx
0040132D|. |50 push eax
0040132E|. |E8 BD0A0000 call MD5KeyGe.00401DF0
00401333|. |8D4E 18 lea ecx,dword ptr ds:
00401336|. |51 push ecx
00401337|. |56 push esi
00401338|. |E8 C3000000 call MD5KeyGe.00401400
0040133D|. |8BEB mov ebp,ebx
0040133F|. |83C3 3F add ebx,0x3F
00401342|. |83C4 14 add esp,0x14
00401345|. |3BDF cmp ebx,edi
00401347|. |73 1C jnb short MD5KeyGe.00401365
00401349|> |8B5424 18 /mov edx,dword ptr ss:
0040134D|. |8D441A C1 |lea eax,dword ptr ds:
00401351|. |50 |push eax
00401352|. |56 |push esi
00401353|. |E8 A8000000 |call MD5KeyGe.00401400
00401358|. |83C3 40 |add ebx,0x40
0040135B|. |83C4 08 |add esp,0x8
0040135E|. |83C5 40 |add ebp,0x40
00401361|. |3BDF |cmp ebx,edi
00401363|.^|72 E4 \jb short MD5KeyGe.00401349
00401365|> |33C0 xor eax,eax
00401367|. |EB 02 jmp short MD5KeyGe.0040136B
00401369|> \33ED xor ebp,ebp ;user32.GetDlgItemTextA
```
5.00401338这个call点进去就看到了md5相关的一些常量信息
```
00401415|.8B06 mov eax,dword ptr ds: ;0x67452301
00401417|.8B7E 04 mov edi,dword ptr ds: ;0xefcdab89
0040141A|.8B5E 08 mov ebx,dword ptr ds: ;0x98badcfe
0040141D|.8B6E 0C mov ebp,dword ptr ds: ;0x10325476
00401441|.8D8C02 78A46A>lea ecx,dword ptr ds: ;下面等等对应64个Ti表中的常量
0040148D|.8D9C13 DB7020>lea ebx,dword ptr ds:
004014B0|.8D9C1F EECEBD>lea ebx,dword ptr ds:
004014E1|.8D8438 AF0F7C>lea eax,dword ptr ds:
```
看一下最开始的Ti表前四个常数是:0xd76aa478,0xe8c7b756,0x242070db,,0xc1bdceee
上面四句对应的常量是:0xd76aa478,0x242070db,0xc1bdceee,0xf57c0faf
可能有的人问:楼主你怎么知道00401441对应的是0xd76aa478,直接从书上抄的?
其实你找个编译器定义int a = -0x28955B88,然后以16进制输出,想必就明白了。(我在这里也是卡了半天)
6.接下里就不在跟这个函数,已经完全确定就是MD5加密了,常量都没变。再看从这个md5加密的call出来之后,分析一下
```
0040122C|.E8 5F010000 call MD5KeyGe.00401390 ;标准的md5加密
00401231|.83C4 14 add esp,0x14 ;销毁变量,平衡堆栈
00401236|> /8A8C04 800100>/mov cl,byte ptr ss:
0040123D|. |83E1 1F |and ecx,0x1F
00401240|. |40 |inc eax
00401241|. |83F8 10 |cmp eax,0x10
00401244|. |8A540C 3C |mov dl,byte ptr ss:
00401248|. |889404 0F0300>|mov byte ptr ss:,dl
0040124F|.^\7C E5 \jl short MD5KeyGe.00401236
00401251|.8D8424 100300>lea eax,dword ptr ss: ;name加密后的密文
00401258|.8D4C24 10 lea ecx,dword ptr ss: ;输入的序列号
0040125C|.50 push eax ; /String2 = NULL
0040125D|.51 push ecx ; |String1 = NULL
0040125E|.FF15 00604000 call dword ptr ds:[<&KERNEL32.lstrcmpA>] ; \lstrcmpA
```
很明显上面那个循环需要重点看一下干了什么,首先看寄存器窗口:eax是0,ecx也是0,esp是0012F760,接着模拟一下00401236到0040124F的循环操作。
1)看00401236这一行,esp+eax+0x180这像啥?数组,指针?
2)然后后面看到了inc eax,循环加一,像是遍历数组或者是那种连续存储空间
3)再看esp+0x180是个啥?0F C4 4E 31 6E 06 D6 0E CC 9F 08 B5 CF B3 A2 DA,然后把值给了ecx
4)and ecx,0x1F就是用0x0F与与运算0x1F,结果还是F,也就是十进制15
5)cmp eax,0x10那就是说要循环15次了,正好能把上面这些数据取完。
6)还看不出什么,继续向下走,mov dl,byte ptr ss:,看看这esp+0x3c是啥(ecx寄存器是0)23456789ABCDEFGHJKLMNPQRSTUVWXYZ,32个连续字符,再加15,对应字母H,取出来给edx。
7)mov byte ptr ss:,dl,将取到的字符H放到0012FA70+eax处
8)然后就是循环这个过程,最后0012FA70处存放的字符串为:H6GKG8QGEZAPHM4U
7.最后dword ptr ss: 也就是0012FA70处的字符串了,然后与我输入的serial number比较,整个程序分析就到此结束了。
8.根据以上分析思路,我们就可以编写注册机了,这里写一下伪代码:
serial number格式为xxxx-xxxx-xxxx-xxxx,判断对应位数是否为-,然后长度是否为19,去掉-组成new number
MD5(name + "www.pediy.com"),得到hash结果,然后与运算0x1F(32),也就是%32取余数,查字符串表得到最终加密后的字符串,与new number进行比较
9.这个程序的注册就其实就相当于把这个算法重写一遍,其实刚刚到最后,我就相当于得到一组注册码:52pojie | H6GK-G8QG-EZAP-HM4U,好了这个程序到此结束。
### 0x6总结
  1.分析前可以先用算法扫描工具简单的看一下可能的算法。
  2.md5中的常量是不可变的,所以可以把常量作为特征码判断算法。
  3.md5是以16进制输出的,所以密文只可能在0123456789abcdef的范围内,但是很多时候密文还会经过其他算法再次变换,例如本例中最后查表base32。
  4.为了明文的安全性考虑,可以拼接一个随机32位字符串作为盐,存入数据库,这样逆向人员每次得到的盐都不一样。(如果用于加密,那么读取输入字符串,然后查数据库中的盐,拼接上去,生成md5进行比较)
### 0x7参考资料:
(https://blog.csdn.net/hla199106/article/details/45129963)
(https://baike.baidu.com/item/MD5?fromtitle=MD5%E5%8A%A0%E5%AF%86&fromid=5706230)
《加密解密第四版》P223-P227
**PS:善于总结,善于发现,找到分析问题的思路和解决问题的办法。虽然我现在还是零基础的小菜鸟一枚,也许逆天改命我会失败,但也有着成功的可能,只要还有希望,就决不放弃!**
**声明:禁止在我的帖子和论坛短消息里留联系方式或者出现违反版规的行为,否则投诉区举报了:(https://www.52pojie.cn/thread-1215989-1-1.html)** 看了你以前很多帖子,感触很深,真的很佩服你有如此毅力坚持到了现在,我注册论坛也有好几年了,一直想学,但是一看到那些代码头都大了,感觉完全看不懂,不知道以后会怎么样,好迷茫! 本帖最后由 小菜鸟一枚 于 2020-7-12 12:15 编辑
wntdjhnr 发表于 2020-7-12 12:00
学习资源 链接挂了
哪里挂了???{:1_904:}
==========补充===========
你说的是c++那一套资料吗?你去下面看沙发,我前几天更新的链接,目前好像还没失效
https://www.52pojie.cn/forum.php?mod=redirect&goto=findpost&ptid=1208234&pid=32601911 看起来很厉害的样子{:301_1000:} 看起来很厉害的样子 只能点攒了!{:1_893:} 看了你以前很多帖子,很佩服你有毅力坚持到现在加油{:301_978:} 学习资源 链接挂了 我要向楼主学习 向楼主学习!!!