首页
社区
课程
招聘
[原创]一个简单的crackme分析 -- SomeCrypto~01
发表于: 2015-5-7 01:27 10421

[原创]一个简单的crackme分析 -- SomeCrypto~01

2015-5-7 01:27
10421

    大大们请轻轻地飘过: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日开班!

上传的附件:
收藏
免费 3
支持
分享
最新回复 (8)
雪    币: 1169
活跃值: (505)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
不错不错。
2015-5-7 03:16
0
雪    币: 28
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
不错!大晚上码这么多字,要注意休息啊!
2015-5-7 10:25
0
雪    币: 135
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
最关键的地方居然没看懂。我真是佩服自己了。
不过还是从楼主的帖子中学到了很多知识,谢谢楼主。
2015-5-13 21:36
0
雪    币: 33
活跃值: (254)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
5
学习一下,不错的贴!
2015-5-14 00:14
0
雪    币: 4
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
感谢楼主分享 学习了
2015-5-15 22:25
0
雪    币: 7591
活跃值: (4766)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
如果是爆破的话, MessageBox就没有内容, 感谢楼主分享.
2015-9-19 11:27
0
雪    币: 0
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
膜拜大神
2016-11-25 13:34
0
游客
登录 | 注册 方可回帖
返回