首页
论坛
专栏
课程

[原创]密码小游戏:暴力破解CRC32密码

2004-12-31 15:46 9854

[原创]密码小游戏:暴力破解CRC32密码

2004-12-31 15:46
9854
今天是04年最后一天了,回首往昔,感概万千。
发一篇以前整理的文章,附件包含pdf格式的文章、
对象程序,程序为应一个朋友而破的一个东西。

注:文章仅为交流,有任何错误请指正。由于CRC32
存在非常多的碰撞,我这里得出的密码并不能使程序
正常往下执行,可能是还需要其它文章,或可能是
我得的密码不多,由于时间不多,没有细看。

本文仅为学习CRC32作的一个教程。
http://bbs.pediy.com/upload/file/2004/12/CRC32.part1.rar_617.rar
http://bbs.pediy.com/upload/file/2004/12/CRC32.part2.rar_632.rar

by lordor
04.12.31

密码游戏:实例解说暴力破解CRC32 密码的思路及实现

By lordor

前置知识:调试器ollydbg 的简单使用,汇编基本知识,VC 编程的基本使用。对象:电脑爱好者、软件破解爱好者、逆向工程爱好者工具:ollyDbg1.1 英文版,VC.NET 、PEID0.92 、Hex WorkShop4.23 目的:找到密码,激活灰色按键。

说明:请打开附件带的程序PKProc_98.exe ,运行这个程序时要输入密码才能激活确定按键,现在我们就要找到密码,本文将介绍怎么找到程序的处理加密的过程,怎么用VC 编写穷举程序进行密码的破解。

一、分析程序密码验证过程先用PEID 检测一下程序编写语言,显示:Install Stub 32-bit -> InstallShield [Overlay] ,这样可以用OllyDbg 直接反汇编,也可以动态跟踪。

这个程序的反汇编语句比较好读,流程清晰,有兴趣的朋友可以动态跟踪它的加载过程。这里我们用API 断点法定位关键代码。由于程序是要密码正确才能激活确定按键,我们可以用EnableWindow 下断。用OllyDbg 载入程序,在命令行下输入bp EnableWindow( 注意字母大小写),然后F9 直接让程序运行起来,在密码框中附件输入一个字符,马上会中断在ollydbg 中,按一下Alt+F9 返回到程序的代码中,往上看就是程序的验证算法了。我们看一下这些代码

100071EE CALL DWORD PTR DS:[<&USER32.GetDlgItemTe>; \GetDlgItemTextA
100071F4 LEA ECX,DWORD PTR SS:[EBP-200]              //取得输入字符
100071FA PUSH ECX ; /String
100071FB CALL DWORD PTR DS:[<&KERNEL32.lstrlenA>]    //求字符长度
10007201 LEA EDX,DWORD PTR DS:[EAX+1]                //字符长度加1
10007204 LEA ECX,DWORD PTR SS:[EBP-200]              //输入字符串地址入ECX
1000720A CALL ginstall.10002F73                      //关键验证函数,得检验值入EAX
1000720F MOV ECX,DWORD PTR DS:[1000BCA8]             //1000BCA8 处的值0EE2AF34 赋给ECX
10007215 SUB ECX,EAX                                 //执行减操作,这里改变CF 标志
10007217 CMP ECX,1                                   //比较是否为1
1000721A MOV ECX,3EF
1000721F SBB EAX,EAX                                 //相当于:EAX=EAX-(EAX+1)
10007221 NEG EAX                                     //相当于:EAX=-EAX
10007223 PUSH EAX                                    //压入EAX
10007224 CALL ginstall.10006497                      //取得窗口HWND 值
10007229 PUSH EAX ; |hWnd                            //压入HWND
1000722A CALL DWORD PTR DS:[<&USER32.EnableWindow>;  //这里执行否要激活
10007230 POP ESI

大家可以看到这个过程比较清晰,也就是先取得输入的字符串,通过CALL ginstall.10002F73 求这串字符串的CRC32( 后面我们会说),然后与[1000BCA8] 处的值比较,如果相等,则会执行EAX 值是1 还是0,这个值就是EnableWindow 的是否激活参数。我们再进CALL ginstall.10002F73 看看这个CRC32 算法,过程也不长,先记住执行这个CALL 传入的两个参数,即EDX 为字符串的长度,ECX 为字符的地址:

10002F73  PUSH ESI        //保存
10002F74  MOV EAX,-1      //EAX=FFFFFFFF  
10002F79  PUSH EDI        //保存
10002F7A  MOV ESI,EDX     //字符串长度入ESI  
10002F7C  ADD EDX,EAX     //EDX=EDX+EAX,长度减1  
10002F7E  TEST ESI,ESI    //看ESI 是否为0,即是否有字符
10002F80  JE SHORT ginstall.10002FA6  //为0 则直接跳走
10002F82  /MOVZX ESI,BYTE PTR DS:[ECX]  //取一位字符入ESI  
10002F85  |MOVZX EDI,AX   //EDI=AX  
10002F88  |SHR EAX,8      //EAX 右移8 位
10002F8B  |XOR ESI,EDI    //EDI 与一位字符作异或运算
10002F8D  |AND ESI,0FF    //ESI 取低8 位
10002F93  |INC ECX        //字符地址加一
10002F94  |MOV ESI,DWORD PTR DS:[ESI*4+1000EBE0] //以1000EBE0 为基址,读入4 位字节
10002F9B  |XOR ESI,EAX    //ESI=ESI XOR EAX  
10002F9D  |MOV EAX,ESI    //保存ESI 到EAX 中
10002F9F  |MOV ESI,EDX    //ESI=EDX  
10002FA1  |DEC EDX        //长度减一
10002FA2  |TEST ESI,ESI   //长度是否为0  
10002FA4  \JNZ SHORT ginstall.10002F82  //字符长度不为0,则继续循环
10002FA6  NOT EAX         //执行NOT 操作
10002FA8  POP EDI   
10002FA9  POP ESI   

10002FAA RETN 这个Call 是一个变形的CRC32,1000EBE0 处存放的是CRC32 用到的256 个4 个字节长的表,其表值与标准的CRC32 表值不一样,在后面我也看到,作者修改的这个表值使得CRC32 算法安全性大大降低。更详细的CRC32 算法说明请大家参考相关的资料。

二、分离出主要验证关键函数从上面的分析,我们已经清楚程序的验证过程了。由于CRC32 属于散列值,我们没方法据CRC32 的值求得原先的值,所以必须穷举所有可能的密码串的CRC32 值,比较是否与预定的值相等。为了更可靠的穷举,我们要从以上分析的代码中生成我们的关键函数:求CRC32 算法。

首先可以把这段汇编代码拷贝出来,只要在VC 中嵌入汇编代码就可以使用了。其次因为1000EBE0 处存放着表,所以必须要把1000EBE0 处的内容拷贝出来,我是用IsDebuggerPresent 插件中的DUMP 工具保存下来的。为什是400 的长度?因为256*4=1024 个字节,转换为十六进制则为400。然后用Hex WorkShop 打开dump 出来的文件,先择所有的字节,再点击“EDIT/Copy As/C Source”,再在VC 的文件中粘贴,就会生成适合VC 使用的数据了。最后构成关键代码如下:

//功能:求输入字符串的crc32值
//参数:name为字符串地址,len为字符串的长度
//返回:CRC32值

DWORD GetCrc(char* name,size_t len) {

//edx=字符串的长度
//ecx=字符串地址

DWORD dwcrc;

__asm{
     mov edx,len;
     mov ecx,name[0];
     //PUSH ESI;
     MOV EAX,-1;
     //PUSH EDI;
     MOV ESI,EDX;
     ADD EDX,EAX;
     TEST ESI,ESI;
     JE SHORT _10002FA6;
_10002F82:
     MOVZX ESI,BYTE PTR DS:[ECX];
     MOVZX EDI,AX;
     SHR EAX,8;
     XOR ESI,EDI;
     AND ESI,0xFF;
     INC ECX;
     MOV ESI,DWORD PTR DS:[ESI*4+basetable];
     XOR ESI,EAX;
     MOV EAX,ESI;
     MOV ESI,EDX;
     DEC EDX;
     TEST ESI,ESI;
     JNZ SHORT _10002F82;
_10002FA6:
     NOT EAX ;==>必须等于 eax=0EE2AF34
     mov dwcrc,eax;
     //POP EDI;
     //POP ESI;
}

if(dwcrc==0x0EE2AF34) {
     char buff[256];
     wsprintf(buff,"Find the Key:%s",name);
     MessageBoxA(NULL,buff,"Succeed!",MB_OK|MB_TOPMOST);
     }
return dwcrc;
}

说明:其中basetable 定义为数组:unsigned char basetable[1024] ,内容就是上面拷贝出来的数据;0x0EE2AF34

为程序中正确的密码的CRC32值。

三、设计我们的穷举程序

有了前面的分析,后面的工作就简单了,现在我们来穷举长度为10 位以内的密码,编写代码如下。下

面的代码只是为了方面说明,没有采用其它算法,也没有作算法及代码的优化。

void CbrutforceDlg::OnBnClickedBegin() {

// TODO: Add your control notification handler code here

char BaseCode[]="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; //可见的字符36
size_t  BaseCodeLen=strlen(BaseCode);  //长度
char PassWord[256];  //定义测试的密码串,并初始化为0  
memset(PassWord,0x0,256);   
size_t a1,a2,a3,a4,a5,a6,a7,a8,a9,a10;  //定义密码下标位

for(a1=0;a1<BaseCodeLen;a1++)  //这里穷举10位的密码
{   
//求长度为1位的密码  

   PassWord[0]=BaseCode[a1];
   PassWord[1]=0x0;
   GetCrc(PassWord,2);
   for(a2=0;a2<BaseCodeLen;a2++) {
   //求长度为2位的密码
    PassWord[1]=BaseCode[a2];
    PassWord[2]=0x0;
    GetCrc(PassWord,3);
         for(a3=0;a3<BaseCodeLen;a3++) {
        //求长度为3位的密码
        PassWord[2]=BaseCode[a3];
        PassWord[3]=0x0;
        GetCrc(PassWord,4);
                for(a4=0;a4<BaseCodeLen;a4++) {
                //求长度为4位的密码
                        PassWord[3]=BaseCode[a4];
                        PassWord[4]=0x0;
                        GetCrc(PassWord,5);
                        for(a5=0;a5<BaseCodeLen;a5++) {
                             //求长度为5位的密码
                             PassWord[4]=BaseCode[a5];
                             PassWord[5]=0x0;
                             GetCrc(PassWord,6);
                                  for(a6=0;a6<BaseCodeLen;a6++) {
                                 //求长度为6位的密码
                                 PassWord[5]=BaseCode[a6];
                                 PassWord[6]=0x0;
                                 GetCrc(PassWord,7);
                                 for(a7=0;a7<BaseCodeLen;a7++) {
                                     //求长度为7位的密码
                                     PassWord[6]=BaseCode[a7];
                                     PassWord[7]=0x0;
                                     GetCrc(PassWord,8);
                                       for(a8=0;a8<BaseCodeLen;a8++) {
                                       //求长度为8位的密码
                                       PassWord[7]=BaseCode[a8];
                                       PassWord[8]=0x0;
                                       GetCrc(PassWord,9);
                                          for(a9=0;a9<BaseCodeLen;a9++) {
                                          //求长度为9位的密码
                                          PassWord[8]=BaseCode[a9];
                                          PassWord[9]=0x0;
                                          GetCrc(PassWord,10);
                                          for(a10=0;a10<BaseCodeLen;a10++) {
                                             //求长度为10位的密码
                                            PassWord[9]=BaseCode[a10];
                                            PassWord[10]=0x0;
                                            GetCrc(PassWord,11);
                                            }
                                         }
                                      }
                                   }
                             }
                         }
                    }
              }
       }
   }
AfxMessageBox("Finish!");
}

算法说明:定义所有的可见字符、测试的密码串及密码串的下标,过程就是通过循环来穷举。详细的请看源代码。

现在开始编译一下程序,运行程序就开始进行密码穷举了。在我的机器AMD Athlon 1.8G+512M DDR333 的机器下,十几分钟就会找到密码。按确定后还可以继续查找下一个密码,由于这个算法存在的漏洞,半个小时内会找到好几个。注意:由于穷举很耗系统资源,运行程序后,CPU 会占用100%。看看成功的画面.

总结:对于这个分析的过程来说,首先要找到处理密码的地方,并找出加密判断的地方,如本例找到CRC32 加密后值,再后模拟它的过程,对密码进行暴力破解。这种方法可能推广使用到其它地方,如WIPZIP/WINRAR 加密码,Word 文档加密码,还有如Md5 的暴力破解等场合。对于要破解的密码来说,关键是要找到要处理密码的函数,以及判断密码是否正确的地方,而穷举过程来说,关键是对穷举算法的设计,达到缩短穷举算法的时间。

不过对于穷举来说,目前还没有一个很好的思路来设计一个穷举算法。如果大家有好的建议请发邮件给我

关于我:网名:lordor
Mail: lordor@163.com
QQ:88378557

介绍:自由撰稿人。喜欢系统底层、密码学、程序逆向分析及软件加密。目前兴趣方向:嵌入式操作系统、移动操作平台等

于04.12.31


[公告][征集寄语] 看雪20周年年会 | 感恩有你,一路同行

最新回复 (3)
lzgxw 2004-12-31 16:42
2
0
cnbragon 18 2004-12-31 16:42
3
0
Ya, :)
DonQuixote 8 2004-12-31 18:56
4
0
用这篇文章中提到的方程解法可以不必穷举直接计算CRC32的碰撞:D
http://bbs.pediy.com/showthread.php?s=&threadid=8699

实际上如果只效验4个字节,那么 原始效验值(标准CRC32中=0xFFFFFFFF) 效验数据 效验结果 三者中知道两个可以求另一个
游客
登录 | 注册 方可回帖
返回