大大们请轻轻地飘过:D
这个简单的crackme是在crackmes.de上找的
链接:25aK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6h3y4J5j5h3y4C8L8h3g2K6i4K6u0W2k6r3g2Q4x3V1k6#2M7$3g2J5M7#2)9J5c8Y4y4S2L8U0l9I4M7%4g2C8k6g2)9J5c8Y4y4G2L8h3g2U0M7Y4W2H3N6r3)9H3x3g2)9J5c8R3`.`.
首先观察这个crackme,有两个输入框,分别提示输入用户名和序列号,随便输入后没有反应,回车也没用,能得到的信息就这些。(其实,从最后的分析来看,程序应该是在不断读取这两个输入框中的内容的,只有输入正确结果,才会跳出提示框。)用Peid打开,好像也没有特别的地方,接着用OD打开 ,程序停在入口处。
下面要开始找分析的突破点了,CCDebuger大神多年前写的几篇OD入门文章中有很好的介绍。这里主要通过在API上下断点,从两个输入框入手,在OD的命令窗口bp GetDlgItemTextA,那么当读取这两个输入框中的内容时,程序就会断下来。F9跑起来,发现OD断下来了,但这时输入框不能输入了,而且窗口也无法移动,这多少可以说明程序是在不断的读取输入框中的内容。重新载入,先把断点disabled掉,F9跑起来,随便输入zxcvb和123456,接着把窗口移到边上,这时再把断点always起来,这时OD断下来了。栈顶为本次调用的返回地址,接下来是GetDlgItemTextA压入的参数,在buffer参数处右击选择数据窗口跟随,接着单击栈顶,回车,就来到了该调用的下一条指令0040127F处,在此处F2下个断点,再F9断到这里,可以看到数据窗口中得到了zxcvb字符串,说明0040127D处的GetDlgItemTextA调用是获取用户名的。先把断点都disabled掉,F8单步往下跟,不难发现0040128F处的GetDlgItemTextA调用是获取序列号的。再往下看,在0040129D处有一个Call,很可能就是处理得到的用户名和序列号的地方,不过这里通过另一种方法,下硬件断点。在这之前,看到004012B4处的Success了,看来很可能会成功的,分析crackme的寻找字符串突破点就经常找Success和Congratulations这样的字符。
00401267 8B3D 40204000 MOV EDI,DWORD PTR DS:[<&USER32.GetDlgIte>; user32.GetDlgItemTextA
0040126D 6A 40 PUSH 0x40 ; Count = 40 (64.)
0040126F 8D8C24 C40000>LEA ECX,DWORD PTR SS:[ESP+0xC4] ;
00401276 51 PUSH ECX ; Buffer = 0018FC18
00401277 68 E9030000 PUSH 0x3E9 ; ControlID = 3E9 (1001.)
0040127C 56 PUSH ESI ; hWnd = 00490508
0040127D FFD7 CALL EDI ; GetDlgItemTextA
0040127F 6A 40 PUSH 0x40 ; Count = 40 (64.)
00401281 8D9424 840000>LEA EDX,DWORD PTR SS:[ESP+0x84] ;
00401288 52 PUSH EDX ; Buffer = NULL
00401289 68 EA030000 PUSH 0x3EA ; ControlID = 3EA (1002.)
0040128E 56 PUSH ESI ; hWnd = 00490508
0040128F FFD7 CALL EDI ; GetDlgItemTextA
00401291 8D4424 0C LEA EAX,DWORD PTR SS:[ESP+0xC]
00401295 50 PUSH EAX
00401296 8D8C24 840000>LEA ECX,DWORD PTR SS:[ESP+0x84]
0040129D E8 5EFDFFFF CALL SomeCryp.00401000
004012A2 83C4 04 ADD ESP,0x4
004012A5 A2 70324000 MOV BYTE PTR DS:[0x403270],AL
004012AA 84C0 TEST AL,AL
004012AC 74 1C JE SHORT SomeCryp.004012CA
004012AE 8B4C24 0C MOV ECX,DWORD PTR SS:[ESP+0xC]
004012B2 6A 00 PUSH 0x0 ; Style = MB_OK
004012B4 68 58244000 PUSH SomeCryp.00402458 ; Success
004012B9 51 PUSH ECX ; Text = "zxcvb"
004012BA 56 PUSH ESI ; hOwner = 00490508
004012BB FF15 20204000 CALL DWORD PTR DS:[<&USER32.MessageBoxA>>; MessageBoxA
重新载入,按照上面的方法再来一遍,在不同的数据窗口跟随得到用户名zxcvb和序列号123456两个字符串,在数据窗口中选中zxcvb的起始字符z,右击,breakpoint->hardware on access->byte,这时就在用户名的起始字符z处下了一个硬件断点,当程序有引用到的时候就会断下来。同理,在序列号123456的起始字符1上也下一个相同的硬件断点。注意,硬件断点不要的时候要删掉,debug选项->hardware breakpoints->delete掉不需要的,不然在不需要的时候又给断下来。把其它断点暂时disabled掉,只留硬件断点,F9跑起来,这时程序在00401005处断了下来。从接下来的分析中,可以知道这确实是处理输入框中字符串的子函数,不过只有处理序列号,而且这个子函数也确是前面分析的两个GetDlgItemTextA后的Call调用,现在这个简单程序的结构大概理清了,关键就是分析这个Call调用了。
不过,在分析这个Call调用之前,先扯点别的,也算学习中的一些小总结,对后面的分析也有点用。
1)IA-32架构的CPU支持两种类型的断点,软件断点和硬件断点,软件断点就是INT3指令,即0xCC,前面在GetDlgItemTextA下的就是软件断点,调试器要做的就是把下断点地方的指令暂时替换为0xCC,断下来后再恢复。软件断点的好处是可以随便下很多个断点,但前提是这个地方必须是指令且要执行到,对于数据就无能为力了,所以前面对于输入框中的字符得用硬件断点。硬件断点由8个调试寄存器支持,不过除去标志,最多只能下4个硬件断点,具体可以参考《软件调试》这本书。
2)对于分析的这个crackme来说,可以看到00401000的开头是push ebp,mov ebp,esp,sub esp,0x20,而结尾有mov esp,ebp,pop ebp(等价于leave),这在子函数中是很常见的,这主要和压入的参数和局部变量有关。在子函数中执行完开头的那些指令后,当前栈ebp的位置为先前的ebp,如果再假设传递的参数和子函数的局部变量都是4字节且都有两个,那么ebp+0x4的位置为返回地址,ebp+0x8的位置为参数arg2,ebp+0xC的位置为参数arg1,ebp-0x4的位置为局部变量loc1,ebp-0x8的位置为局部变量loc2。而结尾的指令只是让栈顶重新指向返回地址,如果有传递参数,retn指令会带数字,相当于再把栈顶esp加上这个数字,最后使堆栈平衡。当然,也经常通过ecx传递输入参数,eax传递返回参数。
3)要记住一些常见指令,比如xor eax,eax用来将eax清零,test eax,eax用来检测eax是否为零。cmp和test指令后的条件转移指令根据标志寄存器的状态来判断,关键就5个标志位CF(进位),OF(溢出),ZF(零),PF(奇偶),SF(符号),那么jz和jnz就是和ZF标志位相关,其它jc、jnc什么的就类似了,另外记住判断无符号数的英文单词below和above,判断有符号数的less和greater,还有相等equal,那么jb,jnl什么也自然能看懂了。扯完了,下面接着分析。
00401000 55 PUSH EBP
00401001 8BEC MOV EBP,ESP
00401003 8A01 MOV AL,BYTE PTR DS:[ECX]
00401005 83EC 20 SUB ESP,0x20
00401008 56 PUSH ESI
00401009 33F6 XOR ESI,ESI
0040100B 84C0 TEST AL,AL
0040100D 0F84 B3000000 JE SomeCryp.004010C6
00401013 8D55 E0 LEA EDX,DWORD PTR SS:[EBP-0x20]
00401016 2BD1 SUB EDX,ECX
判断序列号的字符是否在a-z(0x61-0x7a)之间,如果是就写入字符数组的局部变量中,不能跳到004010C6处,不然返回就出错了
00401018 3C 61 CMP AL,0x61
0040101A 0F8C A6000000 JL SomeCryp.004010C6
00401020 3C 7A CMP AL,0x7A
00401022 0F8F 9E000000 JG SomeCryp.004010C6
00401028 88040A MOV BYTE PTR DS:[EDX+ECX],AL
0040102B 8A41 01 MOV AL,BYTE PTR DS:[ECX+0x1]
0040102E 41 INC ECX
0040102F 46 INC ESI
00401030 84C0 TEST AL,AL
00401032 75 E4 JNZ SHORT SomeCryp.00401018
判断序列号字符的个数是否为26(0x1a)个
00401034 83FE 1A CMP ESI,0x1A
00401037 0F85 89000000 JNZ SomeCryp.004010C6
将0x00403010处的字符串复制到0x00403140处
0040103D 33C0 XOR EAX,EAX
0040103F 90 NOP
00401040 8A88 10304000 MOV CL,BYTE PTR DS:[EAX+0x403010]
00401046 8888 40314000 MOV BYTE PTR DS:[EAX+0x403140],CL
0040104C 40 INC EAX
0040104D 84C9 TEST CL,CL
0040104F 75 EF JNZ SHORT SomeCryp.00401040
如果0x00403140字符串中的字符在a-z之间则进行替换,否则不做任何操作。
假设输入的序列号为qwertyuiopasdfghjklzxcvbnm,那么0x00403140字符串中的abcdefghijklmnopqrstuvwxyz就进行相应替换,即a->q、b->w、c->e……
00401051 33C9 XOR ECX,ECX
00401053 380D 40314000 CMP BYTE PTR DS:[0x403140],CL
00401059 74 2D JE SHORT SomeCryp.00401088
0040105B EB 03 JMP SHORT SomeCryp.00401060
0040105D 8D49 00 LEA ECX,DWORD PTR DS:[ECX]
00401060 8A81 40314000 MOV AL,BYTE PTR DS:[ECX+0x403140]
00401066 3C 61 CMP AL,0x61
00401068 7C 14 JL SHORT SomeCryp.0040107E
0040106A 3C 7A CMP AL,0x7A
0040106C 7F 10 JG SHORT SomeCryp.0040107E
0040106E 0FBEC0 MOVSX EAX,AL
00401071 8A9405 7FFFFF>MOV DL,BYTE PTR SS:[EBP+EAX-0x81]
00401078 8891 40314000 MOV BYTE PTR DS:[ECX+0x403140],DL
0040107E 41 INC ECX
0040107F 80B9 40314000>CMP BYTE PTR DS:[ECX+0x403140],0x0
00401086 75 D8 JNZ SHORT SomeCryp.00401060
根据0x00403140处的字符串和以0x00402058起始的1024个字节数据,通过一系列的异或和移位操作计算eax的值
00401088 83C8 FF OR EAX,0xFFFFFFFF
0040108B BA 40314000 MOV EDX,SomeCryp.00403140
00401090 85C9 TEST ECX,ECX
00401092 74 19 JE SHORT SomeCryp.004010AD
00401094 0FB632 MOVZX ESI,BYTE PTR DS:[EDX]
00401097 33F0 XOR ESI,EAX
00401099 81E6 FF000000 AND ESI,0xFF
0040109F C1E8 08 SHR EAX,0x8
004010A2 3304B5 582040>XOR EAX,DWORD PTR DS:[ESI*4+0x402058]
004010A9 42 INC EDX
004010AA 49 DEC ECX
004010AB 75 E7 JNZ SHORT SomeCryp.00401094
对eax取反后必须等于0xF891B218,否则跳到004010C6处,返回出错
004010AD F7D0 NOT EAX
004010AF 3D 18B291F8 CMP EAX,0xF891B218
004010B4 75 10 JNZ SHORT SomeCryp.004010C6
004010B6 8B45 08 MOV EAX,DWORD PTR SS:[EBP+0x8]
004010B9 C700 40314000 MOV DWORD PTR DS:[EAX],SomeCryp.00403140
004010BF B0 01 MOV AL,0x1
004010C1 5E POP ESI
004010C2 8BE5 MOV ESP,EBP
004010C4 5D POP EBP
004010C5 C3 RETN
出错返回
004010C6 32C0 XOR AL,AL
004010C8 5E POP ESI
004010C9 8BE5 MOV ESP,EBP
004010CB 5D POP EBP
004010CC C3 RETN
通过IDA可以知道0x00403010处的字符串和0x00402058处的1024字节的数据是初始化过的,并且不存在写入操作。
.text:00401040 8A 88 10 30 40 00 mov cl, byte_403010[eax]
.text:00401046 88 88 40 31 40 00 mov byte_403140[eax], cl
.text:0040104C 40 inc eax
.text:0040104D 84 C9 test cl, cl
.text:0040104F 75 EF jnz short loc_401040
.text:00401099 81 E6 FF 00 00 00 and esi, 0FFh
.text:0040109F C1 E8 08 shr eax, 8
.text:004010A2 33 04 B5 58 20 40 00 xor eax, ds:dword_402058[esi*4]
.text:004010A9 42 inc edx
.text:004010AA 49 dec ecx
.text:004010AB 75 E7 jnz short loc_401094
查看引用了dword_402058的地方,byte_403010同理。
通过上述的分析,从理论上是可以写代码得到正确的序列号来破解这个crackme了,但代码的复杂度明显太大,假设是26个不同的小写字母,那么也有26!种情况,实现起来是不现实的。
当然,可以将004010B4处的指令改为nop
004010B4 75 10 JNZ SHORT SomeCryp.004010C6
这样随便输入26个小写字母的序列号都能通过,但显示的字符串内容是无意义的,下图为输入abcdefghijklmnopqrstuvwxyz后的结果,此时,显示的字符串正是0x00403010处的内容。
因显示的字符串无意义,多少有遗憾。 -----------------------------分割线-------------------------------
后记:
后来到crackmes.de上看了另一位cracker是如何破解的,感觉挺不错的。网址链接:628K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6%4N6#2)9J5k6h3A6G2K9r3q4F1L8X3g2K6j5X3q4V1k6i4u0Q4x3X3g2U0K9q4)9J5c8U0t1H3x3e0c8Q4x3V1j5H3y4#2)9J5c8X3y4J5j5h3y4C8L8h3g2K6i4K6u0V1k6r3g2Q4x3X3c8K6j5h3^5H3x3i4y4#2K9$3g2K6i4K6u0V1M7$3!0E0k6h3y4J5P5i4m8@1L8K6l9I4i4K6u0r3
这里提一下简单替换加密,如下:
明文:a b c d e f g h i j k l m n o p q r s t u v w x y z
密文:q w e r t y u i o p a s d f g h j k l z x c v b n m
将要加密的明文按定好的明文到密文映射进行字符转换,这样就可以得到加密后的密文,要解密只需将各个字符逆着映射回去就行了。
根据前面的分析,可以猜测0x00403010处的字符串是通过简单替换加密得到的。这位cracker假设输入的序列号就是逆着映射回去的字符替换规则,如果这种字符替换的映射是正确的,那么0x00403010处的字符串就应该被解密成一段有意义的字符串,而这时004010AF处的eax应该就等于0xF891B218。于是,问题就变成了如何对简单替换加密后的密文,也就是对0x00403010处的字符串进行解密。当然,如果采用暴力破解,那算法复杂度也还是26!,但这位cracker采用了一种基于英文统计的和遗传算法有些类似的方法。
算法大概思想:
首先,需要一种方法来判断一段字符串对于真实的英文情境是否有意义,即这段字符串的合适度,如果和英文情境的合适度越高,那么这段字符串就会有越高的分值,反之,如果中间有很突兀的字符组合,那么就认为这段字符串的合适度低,所得的分值也相应是低的。当然,这种判断是基于英文单词的统计规律的。
1、随机产生一个默认key,用这个key对字符串密文进行解密,计算相应的分值;
2、接着随机替换key中的两个字符,重新计算用这个key解密后的字符串的分值;
3、如果2中计算的分值比1中计算的分值高,那么将2中的key置为默认;
4、重复2中的操作,直至迭代1000次后分值也没有提升。
如果结果字符串还是无意义的,那就需要一个新的默认key重新迭代。
更多方法可以google一下cracking substitution cipher,最后这位cracker得到的序列号为:mxygabhljizcdsqwkeoptufnvr
SomeCrypto~01.zip
[培训]科锐逆向工程师培训第53期2025年7月8日开班!
上传的附件: