pk8900 发表于 2017-12-21 00:42

[反汇编练习] 160个CrackMe之052(egis.1.exe)算法分析及注册机编写(MD5)

本帖最后由 pk8900 于 2018-1-13 16:25 编辑

  【适合破解新手的160个crackme练手】第52个CrackMe: egis.1.exe,经过尽三天的研究,终于将这个CRACKME拿下,可以说是收获很大,分享一下我的分析过程及注册机编写。
【crackme简介】
       下载地址:http://pan.baidu.com/share/link?shareid=541269&uk=4146939145
       Visual C++ 6.0编写,UPX壳,是一个用户名+序列号式验证方式,有提示文字。
   分析工具:X64dbg,VS2013
【crackme截图】

【预备知识-关于MD5算法原理(度娘搜索)】
1、数据初始化过程
加密运算之前首先要对需加密消息进行数据填充,使消息的长度对512取模后等于448,消息长度 mod 512=448。不足位数在消息(字符串)后面进行填充,填充第一位为1,其余为0。即第一个字节为0x80,其余字节为0x00。
2、添加消息长度
在第一步结果之后再填充上原消息的长度,例如欲加密字符为16个ASCII,即16字节×8位=128位,16进制:0x80,但它是一个QWORD值,占用8个字节。在此步骤进行完毕后,最终消息长度就是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值。
【算法分析过程】
因为分析过程太长,现在将主要部分写一下。关键程序代码部分如下:
00401DC7 | 8D 44 24 2C               | lea eax, dword ptr ss:          |
00401DCB | 49                        | dec ecx                                 |
00401DCC | 50                        | push eax                                  |
00401DCD | 8B F1                     | mov esi, ecx                              | EDX:用户名 一正三反叠加
00401DCF | E8 0C F9 FF FF            | call <egis.1.sub_4016E0>                  | 压栈 4个MD5种子值
00401DD4 | 8D 54 34 40               | lea edx, dword ptr ss:      | edx引用用户名地址
00401DD8 | B9 13 00 00 00            | mov ecx, 0x13                           | 计数器 0x13
00401DDD | 33 C0                     | xor eax, eax                              |
00401DDF | 8B FA                     | mov edi, edx                              |
00401DE1 | F3 AB                     | repe stosd                              | 对上述地址清零
00401DE3 | 83 C4 04                  | add esp, 0x4                              |
00401DE6 | 46                        | inc esi                                 |
00401DE7 | 66 AB                     | stosw                                     |
00401DE9 | 8B CE                     | mov ecx, esi                              |
00401DEB | AA                        | stosb                                     |
00401DEC | 83 E1 3F                  | and ecx, 0x3F                           |
00401DEF | B8 40 00 00 00            | mov eax, 0x40                           | eax 置 40:'@'
00401DF4 | 2B C1                     | sub eax, ecx                              | 0x40-0x19
00401DF6 | C6 02 80                  | mov byte ptr ds:, 0x80               | 填充第一字节 0x80
00401DF9 | 83 F8 07                  | cmp eax, 0x7                              |
00401DFC | 7F 03                     | jg egis.1.401E01                        |
00401DFE | 83 C0 40                  | add eax, 0x40                           |
00401E01 | 03 F0                     | add esi, eax                              |
00401E03 | 8D 7C 24 3C               | lea edi, dword ptr ss:          | EDI为用户名处理后的字串
00401E07 | 83 C9 FF                  | or ecx, 0xFFFFFFFF                        | ecx 置-1
00401E0A | 33 C0                     | xor eax, eax                              |
00401E0C | F2 AE                     | repne scasb                               |
00401E0E | F7 D1                     | not ecx                                 |
00401E10 | 49                        | dec ecx                                 |
00401E11 | 33 FF                     | xor edi, edi                              | 用户名+尾部0x80的 长度
00401E13 | C1 E1 03                  | shl ecx, 0x3                              | *8
00401E16 | 85 F6                     | test esi, esi                           |
00401E18 | 89 4C 34 34               | mov dword ptr ss:, ecx      | 写入一个ECX==C8???
00401E1C | 7E 19                     | jle egis.1.401E37                         |
00401E1E | 8D 54 24 2C               | lea edx, dword ptr ss:          | MD5种子值
00401E22 | 8D 44 3C 3C               | lea eax, dword ptr ss:      | 用户名地址
00401E26 | 52                        | push edx                                  |
00401E27 | 50                        | push eax                                  |
00401E28 | E8 D3 F8 FF FF            | call <egis.1.sub_401700>                  | MD5计算函数
00401E2D | 83 C7 40                  | add edi, 0x40                           |
00401E30 | 83 C4 08                  | add esp, 0x8                              |
00401E33 | 3B FE                     | cmp edi, esi                              |
00401E35 | 7C E7                     | jl egis.1.401E1E                        |
00401E37 | 8B 5C 24 2C               | mov ebx, dword ptr ss:          | md5值第一段
00401E3B | 8D 4C 24 28               | lea ecx, dword ptr ss:          |
00401E3F | 8D 54 24 24               | lea edx, dword ptr ss:          |
00401E43 | 51                        | push ecx                                  |
00401E44 | 8D 44 24 24               | lea eax, dword ptr ss:          |
00401E48 | 52                        | push edx                                  |
00401E49 | 8D 4C 24 24               | lea ecx, dword ptr ss:          |
00401E4D | 50                        | push eax                                  |
00401E4E | 51                        | push ecx                                  |
00401E4F | 8D 94 24 34 04 00 00      | lea edx, dword ptr ss:         |
00401E56 | 81 E3 FF FF 00 00         | and ebx, 0xFFFF                           | md5值第一段 and 0ffff,擦去高位
00401E5C | 68 48 B2 41 00            | push egis.1.41B248                        | 41B248:"%lx%lx%lx%lx"
00401E61 | 52                        | push edx                                  |
00401E62 | 89 5C 24 44               | mov dword ptr ss:, ebx          | 修改了md5值第一段
00401E66 | E8 44 19 00 00            | call <egis.1.sub_4037AF>                  | scanf函数,取4个十六进制字符串
00401E6B | 83 C4 18                  | add esp, 0x18                           |
00401E6E | 83 F8 04                  | cmp eax, 0x4                              | 取的不是四个则失败
00401E71 | 74 1E                     | je egis.1.401E91                        |
00401E73 | 6A 30                     | push 0x30                                 |
00401E75 | 68 40 B2 41 00            | push egis.1.41B240                        | 41B240:"Failed"
00401E7A | 68 08 B2 41 00            | push egis.1.41B208                        | 41B208:"... Hmmm, you don't even pass the first threshold ..."
00401E7F | 8B CD                     | mov ecx, ebp                              |
00401E81 | E8 22 E2 00 00            | call <egis.1.sub_4100A8>                  |
00401E86 | 5F                        | pop edi                                 |
00401E87 | 5E                        | pop esi                                 |
00401E88 | 5D                        | pop ebp                                 |
00401E89 | 5B                        | pop ebx                                 |
00401E8A | 81 C4 08 06 00 00         | add esp, 0x608                            |
00401E90 | C3                        | ret                                       |
00401E91 | 33 F6                     | xor esi, esi                              |
00401E93 | 8D 7C 24 1C               | lea edi, dword ptr ss:          | 取入的十六进制码
00401E97 | B8 DE C0 AD 0B            | mov eax, 0xBADC0DE                        |
00401E9C | 8D 4E 50                  | lea ecx, dword ptr ds:          | 计数器+0x50
00401E9F | 99                        | cdq                                       |
00401EA0 | F7 F9                     | idiv ecx                                  | 扩展 除法
00401EA2 | 50                        | push eax                                  |
00401EA3 | 57                        | push edi                                  |
00401EA4 | E8 E7 FC FF FF            | call <egis.1.sub_401B90>                  | 序列号处理函数
00401EA9 | 83 C4 08                  | add esp, 0x8                              |
00401EAC | 46                        | inc esi                                 |
00401EAD | 83 C7 04                  | add edi, 0x4                              | 下一个十六进制
00401EB0 | 83 FE 03                  | cmp esi, 0x3                              |
00401EB3 | 7C E2                     | jl egis.1.401E97                        | 循环3次
00401EB5 | 33 C0                     | xor eax, eax                              |
00401EB7 | 8B 54 04 1C               | mov edx, dword ptr ss:      |
00401EBB | 8B 4C 04 2C               | mov ecx, dword ptr ss:      |
00401EBF | 3B D1                     | cmp edx, ecx                              |
00401EC1 | 75 26                     | jne egis.1.401EE9                         | 循环比较
00401EC3 | 83 C0 04                  | add eax, 0x4                              |
00401EC6 | 83 F8 10                  | cmp eax, 0x10                           |
00401EC9 | 7C EC                     | jl egis.1.401EB7                        |
00401ECB | 6A 40                     | push 0x40                                 |
00401ECD | 68 00 B2 41 00            | push egis.1.41B200                        | 41B200:"Welcome"
00401ED2 | 68 D0 B1 41 00            | push egis.1.41B1D0                        | 41B1D0:"... Man, you're good enough to join CORE! ..."
00401ED7 | 8B CD                     | mov ecx, ebp                              |
00401ED9 | E8 CA E1 00 00            | call <egis.1.sub_4100A8>                  |
  程序注册流程为:我们输入的用户名一正三反叠加,输入1234,会叠加成:1234432143214321,然后对叠加后的字串求MD5值,然后将对我们输入的序列号进行提取,提取4个十六进制格式数据,提取成功的话,对这四个数值分别处理,然后与之前得来的MD5值四段进行比较,完全一致则为注册成功。
      在这期间有两个细节问题:
       【0x01】MD5计算过程,我发现Crackme计算出来的MD5值和我编的软件或其它工具计算出来的不一样,于是我跟进Crackme的MD5计算函数,结果是晕头转向,只能放弃,于是我百度查了一下MD5的知识,MD5值不可逆,别外生成MD5值需要4个种子,
还有左移加密偏移表,并找到了一个C++写的MD5函数,用VS2013编译生成可执行文件,用另一个X64DBG打开,输入相同的加密字符,进行加密,终于让我找到了加密方法的不同之处,种子,循环左移-位数表都相同,不同的地方是MD5加密数据初始化过程后的添加消息长度不同。请看以下两个截图:
截图一:CRACKME程序:

截图二:我编译的C++程序:

通过这两个图对比,我发现正常MD5函数压入消息长度应该是0x80(16字符X8位=128位),还Crackme程序压入的长度为0x88(17字符X8位=136位),也就是说Crackme程序计算长度时把补位的首字节0x80也计算在内了。这就导致了最终MD5加密函数计算结果不同。

【0x02】MD5值被修改:
在分析程序获取序列号中十六进制数值的代码时,有几句代码修改了MD5值,相关代码如下:
00401E33 | 3B FE                     | cmp edi, esi                              |
00401E35 | 7C E7                     | jl egis.1.401E1E                        |
00401E37 | 8B 5C 24 2C               | mov ebx, dword ptr ss:          | md5值第一段
00401E3B | 8D 4C 24 28               | lea ecx, dword ptr ss:          |
00401E3F | 8D 54 24 24               | lea edx, dword ptr ss:          |
00401E43 | 51                        | push ecx                                  |
00401E44 | 8D 44 24 24               | lea eax, dword ptr ss:          |
00401E48 | 52                        | push edx                                  |
00401E49 | 8D 4C 24 24               | lea ecx, dword ptr ss:          |
00401E4D | 50                        | push eax                                  |
00401E4E | 51                        | push ecx                                  |
00401E4F | 8D 94 24 34 04 00 00      | lea edx, dword ptr ss:         |
00401E56 | 81 E3 FF FF 00 00         | and ebx, 0xFFFF                           | md5值第一段 and 0ffff,擦去高位
00401E5C | 68 48 B2 41 00            | push egis.1.41B248                        | 41B248:"%lx%lx%lx%lx"
00401E61 | 52                        | push edx                                  |
00401E62 | 89 5C 24 44               | mov dword ptr ss:, ebx          | 修改了md5值第一段
00401E66 | E8 44 19 00 00            | call <egis.1.sub_4037AF>                  | scanf函数
CRACKME修改MD5值结果截图如下:

【序列号加密算法逆推】
至此第一部分分析完成,这一步完成,我们就可以写注册机通过任意用户名获得跟程序中生成的一样的MD5值。接下来分析Crackme程序的第二个难点。代码位置为:
| lea edi, dword ptr ss:          | 取入的十六进制码
00401E97 | B8 DE C0 AD 0B            | mov eax, 0xBADC0DE                        |
00401E9C | 8D 4E 50                  | lea ecx, dword ptr ds:          | 计数器+0x50
00401E9F | 99                        | cdq                                       |
00401EA0 | F7 F9                     | idiv ecx                                  | 扩展 除法
00401EA2 | 50                        | push eax                                  |
00401EA3 | 57                        | push edi                                  |
00401EA4 | E8 E7 FC FF FF            | call <egis.1.sub_401B90>                  | 序列号处理函数
00401EA9 | 83 C4 08                  | add esp, 0x8                              |
00401EAC | 46                        | inc esi                                 |
00401EAD | 83 C7 04                  | add edi, 0x4                              | 下一个十六进制
00401EB0 | 83 FE 03                  | cmp esi, 0x3                              |
00401EB3 | 7C E2                     | jl egis.1.401E97                        | 循环3次
00401EB5 | 33 C0                     | xor eax, eax                              |
  这是一个循环,将们们输入的十六进制码的四部分,每次压入一个(另一个在函数内偏移得到),通过固定值:0xBADC0DE 除去 [计数器+0x50] 的商,做为另一个参数压入,关键函数:00401EA4 | E8 E7 FC FF FF            | call <egis.1.sub_401B90> 就成了能否破解此程序的关键。
函数的代码如下:
00401B90 | 8B 44 24 04               | mov eax, dword ptr ss:         |
00401B94 | 53                        | push ebx                                  |
00401B95 | 8B 5C 24 0C               | mov ebx, dword ptr ss:         |
00401B99 | 56                        | push esi                                  |
00401B9A | 8B 30                     | mov esi, dword ptr ds:               | 本段
00401B9C | 57                        | push edi                                  |
00401B9D | 8B 78 04                  | mov edi, dword ptr ds:         | 下一段
00401BA0 | 85 DB                     | test ebx, ebx                           |
00401BA2 | 76 71                     | jbe egis.1.401C15                         | 为空则不处理,直接返回
00401BA4 | 55                        | push ebp                                  |
00401BA5 | 8B EF                     | mov ebp, edi                              | ebp=下一段
00401BA7 | B9 01 00 00 00            | mov ecx, 0x1                              | ECX==1
00401BAC | C1 ED 1F                  | shr ebp, 0x1F                           | 右移下一段1F位存入esp+18,即取最高位
00401BAF | 89 6C 24 18               | mov dword ptr ss:, ebp          |
00401BB3 | 8B C6                     | mov eax, esi                              | A
00401BB5 | 8B D7                     | mov edx, edi                              | N
00401BB7 | 33 ED                     | xor ebp, ebp                              |
00401BB9 | E8 B2 1B 00 00            | call <egis.1.sub_403770>                  | 返回 A1 N1
00401BBE | 8B 4C 24 18               | mov ecx, dword ptr ss:          | 取出最高位
00401BC2 | 0B EA                     | or ebp, edx                               | EBP=N
00401BC4 | 0B C8                     | or ecx, eax                               | ECX=本段
00401BC6 | 33 D2                     | xor edx, edx                              | 清空EDX
00401BC8 | 8B F1                     | mov esi, ecx                              | ESI==最高位
00401BCA | B9 0B 00 00 00            | mov ecx, 0xB                              | ECX计数==10
00401BCF | 8B C6                     | mov eax, esi                              |
00401BD1 | 8B FD                     | mov edi, ebp                              | N2==0
00401BD3 | 83 E0 04                  | and eax, 0x4                              | A2=(N shl 1F) or A1 的第三位
00401BD6 | E8 95 1B 00 00            | call <egis.1.sub_403770>                  | 返回A3 N3
00401BDB | 8B CE                     | mov ecx, esi                              | ecx=N
00401BDD | 33 ED                     | xor ebp, ebp                              |
00401BDF | 81 E1 00 20 00 00         | and ecx, 0x2000                           | 取 N的第14位
00401BE5 | 33 D5                     | xor edx, ebp                              |
00401BE7 | 33 C1                     | xor eax, ecx                              |
00401BE9 | B9 12 00 00 00            | mov ecx, 0x12                           | ECX计数==10
00401BEE | E8 7D 1B 00 00            | call <egis.1.sub_403770>                  |
00401BF3 | 8B CE                     | mov ecx, esi                              |
00401BF5 | 33 D5                     | xor edx, ebp                              |
00401BF7 | 81 E1 00 00 00 80         | and ecx, 0x80000000                     |
00401BFD | 33 C1                     | xor eax, ecx                              |
00401BFF | B9 01 00 00 00            | mov ecx, 0x1                              |
00401C04 | E8 67 1B 00 00            | call <egis.1.sub_403770>                  |
00401C09 | 33 F0                     | xor esi, eax                              |
00401C0B | 33 FA                     | xor edi, edx                              |
00401C0D | 4B                        | dec ebx                                 |
00401C0E | 75 95                     | jne egis.1.401BA5                         |
00401C10 | 8B 44 24 14               | mov eax, dword ptr ss:          |
00401C14 | 5D                        | pop ebp                                 |
00401C15 | 89 30                     | mov dword ptr ds:, esi               |
00401C17 | 89 78 04                  | mov dword ptr ds:, edi         |
00401C1A | 5F                        | pop edi                                 |
      这段代码将我们输入的序列号四部分进行加密处理,加密后和用户名的MD5值比较,程序循环次数为0xBADC0DE 除去 [计数器+0x50] 的商,四次调用<egis.1.sub_403770>进行左移及循环左移操作。为了能分析出逆向还原序列号的方法,我模拟此段代码用VS2013写了一段,下面通过这段代码给大家介绍此部分的加密算法及逆向推算方式。
void Enc(unsigned long * p, unsigned long base)
{
      unsigned long *A = p;
      if (p + 1==nullptr)
                return;
      unsigned long* D = (p + 1);
      unsigned long tmp_D = *D;
      unsigned long tmp_A = *A;
      int C = 0;
      for (unsigned long x = base; x>0; x--)// A:esi->当前序列号段D:edi->下一序列号段 C:bit
      {
                C = *D >> 31;    //逆算法关键 ---取D的最高位
                tmp_A = *A;
                tmp_D = *D;
                SHL(&tmp_A, &tmp_D, 1);   // ---------------01-------位移1次-------      
                tmp_A = tmp_A | C;   //   D循环左移一位,低位挤入A的最高位
                *A = tmp_A;               //    A 左移1位,最低位是D的最高位或操作填充的。
                *D = tmp_D;
                tmp_D = 0;             //逆算法关键: tmp_D 置0
                tmp_A = tmp_A & 0x4;       // (取A二进制第3位)
                SHL(&tmp_A, &tmp_D, 0xB);   //---------------02------位移11次---------

                tmp_A^=(*A & 0x2000); //(取A二进制第14位)
                SHL(&tmp_A, &tmp_D, 0x12);   //--------------03-------位移18次---------

                tmp_A ^= (*A & 0x80000000); //(取A二进制第32位)
                SHL(&tmp_A, &tmp_D, 1);   //-----------------04-------位移1次---------

                *A ^= tmp_A;    //异或 tmp_A应始终为零,
                *D ^= tmp_D;    //异或tmp_D
      }
}
些加密函数分为四部分,序列号也分为四段,第一部分,保存序列2段(D)的最高位,然后对两段序列号分别进循环左移1次和左移一次,之后将保存序列2段(D)的最高位补到位移后的序列号1段(A)低位中。接下来的三次位移没有更换序列号数据,而是分三次在取序列号1段(A)的第3位,第14位,第32位进行异或和左移,共移动了29次,移动的位和取的位置是对应的取A的第3位,移动11位后,被移到14位,再取A的14位,进行异或,后移动了18位,再取A的32位,进行异或,其实就是<A的第3位xor 第14位xor第32位>,不论是1或零,都因为最后的移位一次,被移位到 tmp_D中,与2段(D)做最终异或,tmp_A必然为0。分析出这个结果,让我看到了希望,我们开始反推: tmp_A等于0,则A值就是第一步完成后A值,然后将tmp_D与D异或,得到第一步完成后D值,接下来,就差一步就能推到函数顶部了,因为第一步是他们俩都左移一次,过程为:D循环左移一位,低位挤入A的最高位,A 左移1位,最低位后来又用D的最高位进行或操作填充进来。我们可以取出相应位,然后对其右移并填充修正,得到函数顶部的A和D值,这样再逆向循环,就能得到加密前的序列号了。
附上我的解密函数:
void Dec(unsigned long * p, unsigned long base)
{

      unsigned long *A = p;
      if (p + 1 == nullptr)
                return;
      unsigned long* D = (p + 1);
      unsigned long tmp_D = *D;
      unsigned long tmp_A = *A;
      int AL= 0;
      int DL = 0;
      int C = 0;
      for (unsigned long x = base; x > 0; x--)   // A:esi->当前序列号段D:edi->下一序列号段 C:bit
      {
                tmp_A = *A;
                tmp_D = *D;
                //---------逆到第二步-------
                C = ((*A & 0x4)>>2) ^ ((*A & 0x2000)>>13) ^ ((*A & 0x80000000)>>31);
                tmp_D = *D^C;
                //---------逆到第二步-------
                DL = tmp_D & 1;   //取D的最低位还原到A的最高位
                AL = *A & 1;
                *A = (*A >> 1) | (DL << 31);
                *D = (tmp_D >> 1) | (AL << 31);
      }
}
四次调用的循环左移函数C++模拟代码:
void SHL(unsigned long *A, unsigned long * D, intC)
{
      _asm
      {
                mov esi, A;
                mov eax, ;
                mov edi, D;
                mov edx, ;
                mov ecx, C;
                shld edx, eax, cl;
                shl eax, cl;//A左循环,高位进D低位
                mov,eax;
                mov ,edx ;
      }
}
又是一翻修改,找错,最终做出注册机,提供一组测试号--用户名:52pojie.cn序列号:3DCC2E7F   5372B58F   D4794A48   26E8502B
成功注册的截图:


附注册机:
附VS213注册机源码:
项目文件:
希望这篇帖子能让你从中学到一些经验或知识,欢迎回贴探讨,以及送你的CB and 热心。

closing 发表于 2019-11-7 10:26

用户名流程分析错了。用户名并不是按照一正三反的方式叠加的,用户名是按照:用户名+用户名反转+ProductID+RegisteredOwner 的方式组合,然后求 MD5 值的,并不是按照一正三反。ProductID 和 RegisteredOwner 是从注册表中取的值。出现一正三反是因为在 Win10 里注册表没有这两项,所以就把反转的用户名继续叠加到后面。所以注册机生成的序列号在 win10 下可以正常使用,在 winxp 下就会提示错误。

pk8900 发表于 2017-12-21 08:56

xiaolong23330 发表于 2017-12-21 08:17
最后提示的不是failed吗?

在生成的序列号中间加了横线后,导致读取失败,哈哈,图片和注册机已更新。

xiaolong23330 发表于 2017-12-21 08:17

最后提示的不是failed吗?

168pojie 发表于 2017-12-21 10:57

收藏了,学习备用,谢谢分享。

zyxceng 发表于 2017-12-21 13:35

收藏了 谢谢分享

笙若 发表于 2017-12-21 21:28

真的很详细,感谢楼主

飞翔的大橙子 发表于 2018-1-11 09:18

给力,在学习中……{:1_921:}

jingcard 发表于 2018-1-31 19:13

学习一下。

会成功的人 发表于 2019-1-17 14:01

学习备用,谢谢分享。

wenwen520 发表于 2019-4-20 10:17

楼主,你这MD5都搞的定呀,厉害厉害,可不可以发一个帖子,用通俗的话给我们讲讲MD5的原理
页: [1] 2
查看完整版本: [反汇编练习] 160个CrackMe之052(egis.1.exe)算法分析及注册机编写(MD5)