密码学系列-hash双璧之一CRC32详解
kflnig
上面这是以前的一则真实的故事,我给那个我最爱的女孩看过。可是现在,当我最爱的女孩说要我做她大哥!我想在这个时候,我的心中,有点痛。我一共有了两个妹妹。
l1:
作者声明:文章太长,错误难免,唯恐误人子弟,大家踊跃提出批评,在下努力改正。
一个好的hacker得需要很多东西,汇编是一个,密码学也必须了解,但作为一个cracker,那么对于密码学一定要精通的!
而《手册》上就缺少这一类文章。所以我就打算写了一些关于算法的文章。顺便详细谈及一下:软件保护的课题。
本文不会涉及到很多概念,但是却会涉及到不少代码。
CRC32出现在很多的地方,网络上的报文,几乎都用CRC32作校验。所以CRC32的使用范围是非常广的。winrar中也常常出现说:crc校验错误!在我们软件保护中,也常常用CRC32作为校验,来防止爆破!
所以我希望你好好学好这个算法。我希望你看了我的文章之后可以牢牢掌握它!内容有些枯燥,但是希望你可以看懂!但希望你在看这个文章之前,有一点c编程的知识。这篇文章太长了,所以最好你可以分多次看完。
首先需要一些前置知识。
little-endian和big-endian原则,我只讲一遍。
Big-endian和Little-endian是用来表述一组有序的字节数存放在计算机内存中时的顺序的术语。Big-endian(即“大端结束”或者“大尾”)是将高位字节(序列中最重要的值)先存放在低地址处的顺序,而Little-endian(即“小端结束”或者“小尾”)是将低位字节(序列中最不重要的值)先存放在低地址处的顺序。举例来说,在使用Big-endian顺序的计算机中,要存储一个十六进制数4F52所需要的字节将会以4F 52的形式存储(比如4F存放在内存的1000位置,而52将会被存储在1001位置)。而在使用Little-endian顺序的系统中,存储的形式将会是52 4F(52在地址1000处,4F在地址1001处)。IBM的370种大型机、大多数基于RISC的计算机以及Motorola的微处理器使用的是Big-endian顺序,TCP/IP协议也是。而Intel的处理器和DEC公司的一些程序则使用的Little-endian方式。
为了防止小鸟不懂我还是再来个示例吧。比如说
esp=12FF400
这个地址的指向的内存中的值是00 01 02 03。
我来一条mov语句,mov eax,[esp];
那么按照little-endian原则,eax中的值是03 02 01 00
然后推荐给众位朋友一个好东西,borland c++ builder 6.0,因为它的编译效率比vc的高,我写了一个求1——1百万的素数算法比vc6.0高了10%。而且,在控制台编程方面,基本和vc没有区别,还有vc6.0用内联汇编会有bug,而她没有,这么好的东西,你还不去用。
还有一定要懂内联汇编,bbs.pediy.com上有我的内联汇编教程,但我不知道光盘中编辑有没有放。(我前几天写了篇文章,打包了我自己的内联汇编教程,如果发表,编辑会把它放进去的)
本文分三部分,
1、初识CRC32
2、CRC32的冲突寻找
3、CRC32的高级状态
初识CRC32
首先看下面的代码,这是我从某个使用CRC32作为算法的软件中copy出来的。可以给你一个对CRC32有一个感性的认识
/*403163*/ MOVZX EDX,BYTE PTR DS:[EAX];EAX中是要进行CRC32校验的值的地址,取一个byte作为待会儿将要进行校验的
/*403166*/ MOV ECX,ESI;esi中是CRC校验值
/*403168*/ AND ECX,0FF;保留末尾的两个byte
/*40316E*/ XOR ECX,EDX;和要进行CRC校验的值的一个byte进行异或运算
/*403170*/ MOV EBP,DWORD PTR DS:[EDI+ECX*4];查表
/*403173*/ SHR ESI,8;esi右移1个byte
/*403176*/ XOR ESI,EBP;和查表得到的值进行异或
/*403178*/ INC EAX;指向下一个将要进行CRC校验的值
/*403179*/ DEC EBX;ebx是用来控制循环次数的
/*40317A*/ JNZ SHORT 00403163
如上面的代码,这样最后的esi就是我们的CRC32校验值!(esi的初始值是0xFFFFFFFF)
但是也有些时候,软件会在最后执行not esi作为CRC32的校验值,我也不知道真正的CRC32算法是怎么设计的。但是这么一条语句关系不大。
至于那张上面提到的表,我截了张图,
图1
这是表中的一部分。下面是生成表的算法。
voidGenCrc32Tbl()
{
int i,j;
for(i=0;i<256;++i){
CRC=i;
for(j=0;j<8;++j)
{//这个循环实际上就是用"计算法"来求取CRC的校验码
if(CRC & 1)
CRC=(CRC>>1)^0xEDB88320;
else //0xEDB88320就是CRC-32多项表达式的值
CRC>>=1;
}
CRC_32_Tbl[i]=CRC.
}
}
稍微注释一下,&是and运算;^是xor运算;>>是shr(右移)运算
上面就是生成CRC32表的算法,当然我们也可以直接把静态生成后的码表直接copy到代码中。是不是很简单,的确CRC32是hash算法中最最简单的一个,而且它需要的计算量很低,所以应用才会如此广泛!
上面就差不多把CRC32讲完了。总结一下,你要大概知道它的算法流程,也得知道大概有一个表要查。表的生成,记不记就无所谓了,毕竟这年头要记的东西太多了,如果一一记住纵然是电脑也不怎么可能。
真正的代码实现,网上太多了,我来写个大家都看的懂的。
int calcCRC32(char *DataBuff,unsigned int BufLen)
{//DataBuff是指向数据串的指针,BufLen是数据串的长度
unsigned int CRC32=0xffffffff; //设置初始值,还有一定要加上unsigned,否则在下面进行移位>>时,会使用sar(算术右移),而不是我们期待的shr(逻辑右移)
int i;
unsigned char ch; //这里的unsigned,加与不加一个样
for(i=0; i<BufLen; ++i)
{
ch=*DataBuff;
CRC32 = table[(CRC32^ch)& 0xff] ^ (CRC32>>8);
++DataBuff;
}
}
只要这么一些就够了。耐心点,努力看懂它。真正意义上了解CRC32的算法。
CRC32的冲突寻找
假设,你已经颇了解CRC32了。我们首先了解一下,一个好的hash算法的定义(我自己的记忆中好像老鸟们是这么说的)。
HASH值很平均,不会在某一个位上出现某个值特别多
crc32,在上面这个著名的定义中是完全满足的。
可是我们看,crc32校验值,只是一个32bit的值,也就是一个DWORD而已。
先说个概念“碰撞”,所谓碰撞就是说不同的数据经过hash算法之后,却得到同样的结果。
所以假使你crc32的hash值非常平均,但搜索到碰撞的概率还是有是0xFFFFFFFF分之1。不要觉得它很小,我们自己的微机,完全可以在短时间内搜索到一组碰撞。HASH实际上应该很容易得出碰撞。
比如,一个hash值是0x15A7393A,这是我随便举的,我并不知道,是什么数据的crc32校验值。但我就是可以另外找到一组值,也可以得到这个校验值。看我如何寻找冲突!
#include <iostream.h>
const int sj[]={……这是码表,太长了所以就略去了};
void main()
{
unsigned int n;
int b;
for (n=0x00000000;n<=0x10000000;++n)
{
_asm
{
mov ebx,4
lea EAX,n
MOV ESI,0xFFFFFFFF
line1:
MOVZX EDX,byte ptr[EAX+ebx-1];EAX是伪码的地址
MOV ECX,ESI
and ECX,0xFF
XOR ECX,EDX
shl ecx,2;相当于ecx=ecx*4
MOV edi,sj[ECX];
SHR ESI,8
XOR ESI,edi
dec EBX;EBX是注册码长度
JNZ line1
CMP ESI,0x15A7393A
JZ line2
push 0
pop b
jmp line3
line2:
push 1
pop b
line3:
}
if (b) cout<<n;
}
}
//
不到30s出来一个2b 2a 30 35。化成ASCII码就是 +*05
强烈的说一遍,注意看和第一部分的代码区别,还要注意little-endian原则。一般情况下,是可以找到冲突的。如果找不到冲突,我们可以再稍微改变一下。只不过这个稍微有点麻烦,因为我不能向上面那样投机取巧的用一条for语句就OK了,我只得自己去操纵内存,不过也很简单,^_^,你不妨自己去看看!所以crc32在人为的去寻找冲突的情况下是不牢靠的,但是crc32在自然状态下是很好的。
CRC32的高级状态
我将会用很长很长很长的篇幅来讲这个,主要涉及到的是利用crc32作软件保护!如果你是个想保护自己软件的程序员,那么下面的就看好了。
为了下面的可读性,我将引入一些符号
crc32(data)表示用crc32对data进行处理生成校验值。
看到过一个软件它的注册算法是
if (crc32(key)==a)注册成功;
else 注册失败;
a表示某个int常量。很明显,这个是在人为的去寻找冲突的情况下是不牢靠的。但是这已经比明码比较的软件有了质的提升了,可以阻挡30%的cracker了。可是它偏偏遇到了我,^_^!
也许你会说假如这样
if (md5(key)==a)注册成功;
else 注册失败;
会安全的多,的确,md5在现在的状态下很牢靠!可是无可否认的,md5算法很难写,比起crc32不知要难多少倍。所以我们的目标是用crc32作为算法写一个安全的注册过程!
我推荐的是假如注册码key是一个8byte的长度。
那么我们可以这样分DOWRD high,low。
用high表示高4byte,low表示低4byte。
我们分别这样进行crc32
if (crc32(key)==a1)注册成功;
else 注册失败;
if (crc32(high)==a2)注册成功;
else 注册失败;
if (crc32(low)==a3)注册成功;
else 注册失败;
这样是绝对不是一台微机凭着暴力搜索,可以搜索出来的。^_^,你的软件在这个时候就可以安全了。
现在讲讲这个该死的PEID。先要纠正一下kanxue兄在其书《加密与解密》中的一个小错误,这也算不上是错误,是由于时代的局限性^_^
他说:用动态生成的CRC32的码表是不会被PEID的插件kanal查到算法的。只有你静态嵌入码表数组才会被发现。
可是这个时代已经一去不复返了。所以,为了安全,我们一定要找到一种方法躲过这个该死的PEID。
我们直接去反汇编PEID的插件看代码是不怎么现实的,因为PEID扫描之后说:ACProtect V1.3X-1.4X DLL -> risco *看到这个我想就意味着壳盲止步。但我想,它不会很智能,通过黑箱方法也可以躲过PEID的法眼。
首先我们静态放入码表到程序中,我们用kanal插件查找,kanal如图2所说:
图2
上面一行是说在哪里找到了这个crc32的码表,前面一个数字说明的是磁盘中的地点,后面一个说的是内存中的地点。下面一行是说明在哪里检测到操作这个码表。
好了,它给了我们一个很好的提示,假如……我们用winhex打开这个应用程序,来到0x14A4。数据如下:
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F
000014A0 00 00 00 00 00 00 00 00 96 30 07 77 2C 61 0E EE.
根据此我们可以看到0x14a4处是crc32码表的开始处。我就暂时,把这个码表的第一个值,由0x00000000改成0x00000001。
同样我们用winhex,来到0x16a4。
Offset 0 1 2 3 4 5 6 7
000016A0 AD 6C BA C0 20 83 B8 ED
我们很容易知道,0x16a4是码表的第129个值。在静态码表中,我们也先暂时把它改掉。上面我这么写,只是为了告诉小鸟你在碰到问题时应该具有的思路!当然如果你自己去试,还是会发现不少问题,但因为篇幅总是有限的,我现在只能告诉你怎么做。sj是码表的数组。我们要把它改回来,成为真正的crc32码表。
最好这么写
sj[0]=0;
sj[128]=0xedb88320-1;
++sj[128];
在我这个版本的kanal中如果这么写
sj[0]=0;
sj[128]=0xedb88320;
依旧可以被发现在使用CRC32算法。
看来kanal判断是否为crc32的码表的方法是取了其中的第1个和第129个值作为判断的。
CRC32还有一个不得不提的作用就是作为校验,虽然现在看来那么不牢靠。我将在另外一篇关于软件保护的文章中谈到这个东西,相信大家也看累了。休息一会。
最后谈及一下kyo的《一个经典的crackme算法分析》,作为对他的补充。这是篇不错的文章,适合刚刚起步的小鸟看!
可是kyo可能也是一个刚出道不久的cracker吧!不过他已经很不错了,因为想当年,我搞CRC32(当时我不知道,软件应用了这个算法),^_^,丑事不说了。因为他犯了不少错误。
1、 这个crackme用了经典的算法,叫CRC32
2、 PEID的插件查不到,并不是说,就没有算法,所以总是查不到,也不能大意
我一看他截的图就知道,这个算法是CRC32,首先是代码很像,其次他说是一张100h的表,在经典算法中,不是CRC32那还是什么呢?
40101B LEA EAX,DWORD PTR DS:[ECX-1]
40101E PUSH ECX
40101F MOV ECX,8
401024 SHR EAX,1
401026 JNB SHORT CrackMe_.0040102A
401028 XOR EAX,EDX
40102A DEC ECX
40102B JNZ SHORT CrackMe_.00401024
40102D POP ECX
40102E MOV DWORD PTR DS:[ECX*4+403017],EAX
401035 DEC ECX
401036 JNZ SHORT CrackMe_.0040101B
这里就是在动态生成码表,只是作者生成这个码表的方法怪怪的,也正是因为这个方法,所以可以不被kanal找到算法。因为我在第一部分给出的动态生成码表的方法是从第一个算到最后一个,这个正好相反,从最后一个开始生成。记住又是一个可以fool kanal的好方法。
还有一个地方他说:这个让程序自己来算,最后用户名是kyo327的话值为37969166。
嘿,这个小家伙,这么懒惰!作为一个cracker怎么可以这样,所以我这里就点一下。就是用户名的CRC32校验值(要not一下的)。很容易的。不用动态调试,直接IDA分析一下就知道了。用汇编写的好的代码可读性很好。
不过kyo的masm功底肯定比我好,因为我不会用masm写注册机。他说:如果各位读者知道跟那两个call的简单思路不妨告诉我,我感激不尽。现在他肯定也对我感激不尽了。
他说:一直搞到晚上3点半。^_^,看来实力不如我了,我只搞了几分钟,就都知道了。可是也是前程可谓,多多努力啊,kyo。
写到这里大家一定要去调试一下那个crackme,以提高自己的CRC32水平!
期待以后的密码学系列文章吧!我们共同提高。
翻到5月的这本手册的最后一页,看到了一点好玩的,有人说:快码加编和cracker都是讲一些代码的利用,并在一起就可以了!我想,要不以后只要一个版块就可以了,computer,反正都是讲电脑知识吗!
lnig1@163.com
浙江省绍兴县柯桥中学高二(3)班李宁