[反汇编练习] 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 热心。 用户名流程分析错了。用户名并不是按照一正三反的方式叠加的,用户名是按照:用户名+用户名反转+ProductID+RegisteredOwner 的方式组合,然后求 MD5 值的,并不是按照一正三反。ProductID 和 RegisteredOwner 是从注册表中取的值。出现一正三反是因为在 Win10 里注册表没有这两项,所以就把反转的用户名继续叠加到后面。所以注册机生成的序列号在 win10 下可以正常使用,在 winxp 下就会提示错误。 xiaolong23330 发表于 2017-12-21 08:17
最后提示的不是failed吗?
在生成的序列号中间加了横线后,导致读取失败,哈哈,图片和注册机已更新。 最后提示的不是failed吗? 收藏了,学习备用,谢谢分享。 收藏了 谢谢分享 真的很详细,感谢楼主 给力,在学习中……{:1_921:} 学习一下。 学习备用,谢谢分享。 楼主,你这MD5都搞的定呀,厉害厉害,可不可以发一个帖子,用通俗的话给我们讲讲MD5的原理
页:
[1]
2