asdfslw 发表于 2009-1-24 09:26:26

一个crackme的算法分析

一个crackme的算法分析
o(∩_∩)o...首先声明一下,这是本人第一个破解文章。o(∩_∩)o...

本人属于菜鸟级的,这个crackme也没什么技术含量。这个crackme是一个chm破解教程中的附件,虽然教程已经分析了这个程序,但是可能嫌麻烦(也可能是教程的目的仅仅是练习爆破),没有分析出具体的注册算法,仅仅给出了爆破点。本人耐心地跟踪了这个程序,才有了这个破解的文章,其中给出了注册算法。

废话说完了,下面转入正题。

peid检查,无壳,Microsoft Visual C++ 6.0程序。

运行程序,随便输入注册信息,比如asdf,12345,点击注册,弹出出错对话框Registration fail。有出错提示,真是太好了。(没有提示的话,断点不好下啊)

OD载入,选择 插件->字符串参考->Find ASCII,再找到Registration fail。双击就到达了弹出出错提示的位置。向上找到关键call和关键跳。

00401064   .E8 C7010000   call    00401230                         ;关键call。在这里下断点。
00401069   .85C0          test    eax, eax                         ;检验eax是否等于0。等于0则注册成功。
0040106B   .6A 00         push    0                              ; /Style = MB_OK|MB_APPLMODAL
0040106D   .68 80504000   push    00405080                         ; |ncrackme
00401072   .75 1B         jnz   short 0040108F                   ; |关键跳。要爆破就在这里动手了。
00401074   .A1 B8564000   mov   eax, dword ptr           ; |
00401079   .68 64504000   push    00405064                         ; |registration successful.
0040107E   .50            push    eax                              ; |hOwner => NULL
0040107F   .FF15 C0404000 call    dword ptr [<&USER32.MessageBoxA>>; \MessageBoxA
00401085   .E8 A6020000   call    00401330
0040108A   .33C0          xor   eax, eax
0040108C   .C2 1000       retn    10
0040108F   >8B0D B8564000 mov   ecx, dword ptr           ; |
00401095   .68 50504000   push    00405050                         ; |registration fail.
0040109A   .51            push    ecx                              ; |hOwner => NULL
0040109B   .FF15 C0404000 call    dword ptr [<&USER32.MessageBoxA>>; \MessageBoxA

在00401064处按下F2下断点。
F9运行程序,00401064处程序暂停,按F7跟进去。然后F8单步执行,

00401245|.6A 10         push    10                               ; /Count = 10 (16.)
00401247|.50            push    eax                              ; |Buffer
00401248|.68 E8030000   push    3E8                              ; |ControlID = 3E8 (1000.)
0040124D|.51            push    ecx                              ; |hWnd => NULL
0040124E|.33DB          xor   ebx, ebx                         ; |
00401250|.FFD6          call    esi                              ; \GetDlgItemTextA
00401252|.83F8 03       cmp   eax, 3                           ;eax=用户名长度(字符数)
00401255|.73 0B         jnb   short 00401262                   ;eax>=3,否则eax=1,然后返回,注册失败了。
00401257|.5E            pop   esi
00401258|.B8 01000000   mov   eax, 1
0040125D|.5B            pop   ebx
0040125E|.83C4 30       add   esp, 30
00401261|.C3            retn
00401262|>A1 BC564000   mov   eax, dword ptr

继续跟踪,
00401276|.0FBE4424 08   movsx   eax, byte ptr             ;字节name
0040127B|.0FBE4C24 09   movsx   ecx, byte ptr             ;字节name
00401280|.99            cdq
00401281|.F7F9          idiv    ecx
00401283|.8BCA          mov   ecx, edx                         ; ecx=name%name
00401285|.83C8 FF       or      eax, FFFFFFFF
00401288|.0FBE5424 0A   movsx   edx, byte ptr             ;字节name
0040128D|.0FAFCA      imul    ecx, edx                         ; ecx=(name%name)*name
00401290|.41            inc   ecx
00401291|.33D2          xor   edx, edx
00401293|.F7F1          div   ecx               ; eax=0xffffffff/((name%name)*name+1)
00401295|.50            push    eax
00401296|.E8 A5000000   call    00401340            ;将eax存入地址4050ac,实际上设置了rand函数的种子。

继续,
0040129E|.33F6          xor   esi, esi
004012A0|> /E8 A5000000   /call    0040134A                     ;跟进去,可以知道此处调用的就是rand函数。
004012A5|. |99            |cdq
004012A6|. |B9 1A000000   |mov   ecx, 1A
004012AB|. |F7F9          |idiv    ecx
004012AD|. |80C2 41       |add   dl, 41
004012B0|. |885434 18   |mov   byte ptr , dl       ;存入(eax % 26) + 'A'
004012B4|. |46            |inc   esi
004012B5|. |83FE 0F       |cmp   esi, 0F                         ;循环15次
004012B8|.^\72 E6         \jb      short 004012A0

快到核心的地方了。继续努力。
004012CD|> /8A4434 0C   /mov   al, byte ptr       ;用户名的某个字节
004012D1|. |C0F8 05       |sar   al, 5                           ;eax=name>>5
004012D4|. |0FBEC0      |movsx   eax, al
004012D7|. |8D1480      |lea   edx, dword ptr
004012DA|. |8D04D0      |lea   eax, dword ptr
004012DD|. |8D0440      |lea   eax, dword ptr       ;eax=eax*123(Dec)
004012E0|. |85C0          |test    eax, eax
004012E2|. |7E 0A         |jle   short 004012EE
004012E4|. |8BF8          |mov   edi, eax                        ;edi=(name>>5)*123
004012E6|> |E8 5F000000   |/call    0040134A                     ;调用rand函数
004012EB|. |4F            ||dec   edi
004012EC|.^|75 F8         |\jnz   short 004012E6               ;循环edi次
004012EE|> |E8 57000000   |call    0040134A                        ;调用rand函数
004012F3|. |99            |cdq
004012F4|. |B9 1A000000   |mov   ecx, 1A
004012F9|. |8D7C24 0C   |lea   edi, dword ptr           ;edi指向用户名
004012FD|. |F7F9          |idiv    ecx
004012FF|. |0FBE4C34 2C   |movsx   ecx, byte ptr       ;序列号的某个字节
00401304|. |80C2 41       |add   dl, 41                        ;dl=(eax % 26) + 'A'
00401307|. |0FBEC2      |movsx   eax, dl                         ;eax=(eax % 26) + 'A'
0040130A|. |2BC1          |sub   eax, ecx                        ;只有eax=ecx,才可能注册成功。
0040130C|. |885434 1C   |mov   byte ptr , dl
00401310|. |99            |cdq                                     ;edx=0
00401311|. |33C2          |xor   eax, edx
00401313|. |83C9 FF       |or      ecx, FFFFFFFF
00401316|. |2BC2          |sub   eax, edx
00401318|. |03D8          |add   ebx, eax                        ;ebx 累加
0040131A|. |33C0          |xor   eax, eax
0040131C|. |46            |inc   esi
0040131D|. |F2:AE         |repne   scas byte ptr es:
0040131F|. |F7D1          |not   ecx
00401321|. |49            |dec   ecx
00401322|. |3BF1          |cmp   esi, ecx
00401324|.^\72 A7         \jb      short 004012CD
00401326|> \5F            pop   edi
00401327|.8BC3          mov   eax, ebx
00401329|.5E            pop   esi
0040132A|.5B            pop   ebx
0040132B|.83C4 30       add   esp, 30
0040132E\.C3            retn

这里解释一下“只有eax=ecx,才可能注册成功”:关键call返回后,只有eax=0才会注册成功。而
00401327|.8BC3          mov   eax, ebx
ebx是累加而得。因此每次累加值都应该是0。继续分析就会得到“只有eax=ecx,才可能注册成功”。

最后,总结一下:
name至少3个字符。key的长度不小于name的长度,且前面长度为name长度的部分必须是大写英文字母,后面部分对注册无影响。rand函数的种子设定为0xffffffff/((name%name)*name+1)。
然后调用15次rand函数。
然后调用rand函数 (name>>5)*123 次,
满足条件key = (char)(rand()%26 + 'A') 时才可能注册成功。(注意,这里又调用了一次rand函数)
然后调用rand函数 (name>>5)*123 次,
满足条件key = (char)(rand()%26 + 'A') 时才可能注册成功。
。。。。。。。
直至取完name的所有字节。
这里给一个正确的注册码:asdf,YNMK.

另外,给出产生随机数的rand函数代码:
static long holdrand = 1;
void srand(unsigned seed)
{
    holdrand = seed;
}
int rand()
{
    return (holdrand = holdrand * 214013 + 2531011) >> 16 & 0x7fff;
}
????附件发不了???

kcr 发表于 2009-1-24 15:01:43

学习算法。

香帅 发表于 2009-1-24 16:08:08

/:good 分析得很好。

zaffir 发表于 2009-1-24 18:05:18

好文章,学习下~
页: [1]
查看完整版本: 一个crackme的算法分析