为了方便大家浏览,把分析文档的内容贴出来
这个CrackMe是看雪论坛网友windrand发布在论坛的多个密码学CrackMe之一,windrand把它命名为TeaCrackMe,但在后面的分析中我们可以看到,这个CrackMe使用的算法其实是XTEA(又名TEAN)。
首先我们把这个CrackMe直接运行起来,然后顺便输入一个用户名和注册码看看有什么反应,结果如图一
(图一)
接下来用OD载入它并运行,对GetDlgItemTextA下断,然后再次随便输入一个用户名和密码,点注册认证,断下来后Alt+F9返回看看
0040A1ED . 68 00010000 push 100 ; /Count = 100 (256.)
0040A1F2 . 51 push ecx ; |Buffer
0040A1F3 . 68 E8030000 push 3E8 ; |ControlID = 3E8 (1000.)
0040A1F8 . 56 push esi ; |hWnd
0040A1F9 . FFD3 call ebx ; \GetDlgItemTextA
0040A1FB . 8DBD F0FEFFFF lea edi, dword ptr [ebp-110]
[COLOR="Red"]0040A201 . 83C9 FF or ecx, FFFFFFFF
0040A204 . 33C0 xor eax, eax
0040A206 . F2:AE repne scas byte ptr es:[edi]
0040A208 . F7D1 not ecx
0040A20A . 49 dec ecx[/COLOR]
0040A20B . 83F9 01 cmp ecx, 1
0040A20E . 73 26 jnb short 0040A236
0040A210 . 6A 40 push 40 ; /Style = MB_OK|MB_ICONASTERISK|MB_APPLMODAL
0040A212 . 68 A4B44200 push 0042B4A4 ; |Title = ""D7,"",A2,"",B2,"崽崾?
0040A217 . 68 8CB44200 push 0042B48C ; |Text = "用",BB,"?,B2,"",BB,"能为空请输入!"
0040A21C . 56 push esi ; |hOwner
0040A21D . FF15 04514200 call dword ptr [<&USER32.MessageBoxA>>; \MessageBoxA
0040A223 . 33C0 xor eax, eax
0040A225 . 8B4D F4 mov ecx, dword ptr [ebp-C]
0040A228 . 64:890D 00000>mov dword ptr fs:[0], ecx
0040A22F . 5F pop edi
0040A230 . 5E pop esi
0040A231 . 5B pop ebx
0040A232 . 8BE5 mov esp, ebp
0040A234 . 5D pop ebp
0040A235 . C3 retn
0040A236 > 8D95 F0FDFFFF lea edx, dword ptr [ebp-210]
0040A23C . 68 00010000 push 100
0040A241 . 52 push edx
0040A242 . 68 07040000 push 407
0040A247 . 56 push esi
0040A248 . FFD3 call ebx
0040A24A . 8DBD F0FDFFFF lea edi, dword ptr [ebp-210]
[COLOR="Red"]0040A250 . 83C9 FF or ecx, FFFFFFFF
0040A253 . 33C0 xor eax, eax
0040A255 . F2:AE repne scas byte ptr es:[edi]
0040A257 . F7D1 not ecx
0040A259 . 49 dec ecx[/COLOR]
0040A25A . 83F9 01 cmp ecx, 1
0040A25D . 73 26 jnb short 0040A285
0040A25F . 6A 40 push 40 ; /Style = MB_OK|MB_ICONASTERISK|MB_APPLMODAL
0040A261 . 68 A4B44200 push 0042B4A4 ; |Title = ""D7,"",A2,"",B2,"崽崾?
0040A266 . 68 74B44200 push 0042B474 ; |Text = ""D7,"",A2,"",B2,"崧?,B2,"",BB,"能为空请输入!"
0040A26B . 56 push esi ; |hOwner
0040A26C . FF15 04514200 call dword ptr [<&USER32.MessageBoxA>>; \MessageBoxA
0040A272 . 33C0 xor eax, eax
0040A274 . 8B4D F4 mov ecx, dword ptr [ebp-C]
0040A277 . 64:890D 00000>mov dword ptr fs:[0], ecx
0040A27E . 5F pop edi
0040A27F . 5E pop esi
0040A280 . 5B pop ebx
0040A281 . 8BE5 mov esp, ebp
0040A283 . 5D pop ebp
0040A284 . C3 retn
上面代码中红色的部分是很经典的汇编版strlen函数,执行完毕之后ecx的值为edi所指向的C字符串的长度。我们可以看到,只有当用户名和注册码都不为空的时候,程序才会跳到0x40A285处继续执行,否则会弹出提示框并不再往下执行。
我们到0x40A285处继续往下看
0040A285 > \8D85 F0FCFFFF lea eax, dword ptr [ebp-310]
0040A28B . 6A 00 push 0
0040A28D . 50 push eax
0040A28E . 8D8D F0FDFFFF lea ecx, dword ptr [ebp-210]
0040A294 . 68 68B44200 push 0042B468 ; ASCII "WindRand"
0040A299 . 51 push ecx
0040A29A . C745 FC 00000>mov dword ptr [ebp-4], 0
0040A2A1 . E8 FAFCFFFF call 00409FA0
0040A2A6 . 83C4 10 add esp, 10
0040A2A9 . 8D95 F0FCFFFF lea edx, dword ptr [ebp-310]
0040A2AF . 8D85 F0FEFFFF lea eax, dword ptr [ebp-110]
0040A2B5 . 52 push edx
0040A2B6 . 50 push eax
0040A2B7 . E8 A4FDFFFF call 0040A060
0040A2BC . 83C4 08 add esp, 8
[COLOR="red"]0040A2BF . 83F8 01 cmp eax, 1[/COLOR]
0040A2C2 . 6A 40 push 40 ; /Style = MB_OK|MB_ICONASTERISK|MB_APPLMODAL
0040A2C4 . 68 A4B44200 push 0042B4A4 ; |Title = ""D7,"",A2,"",B2,"崽崾?
[COLOR="Red"]0040A2C9 . 75 1F jnz short 0040A2EA ; |[/COLOR]
0040A2CB . 68 54B44200 push 0042B454 ; |Text = "恭?,B2,"你?,AC,"",D7,"",A2,"",B2,"崧胝",B7,"!"
0040A2D0 . 56 push esi ; |hOwner
0040A2D1 . FF15 04514200 call dword ptr [<&USER32.MessageBoxA>>; \MessageBoxA
0040A2D7 . 33C0 xor eax, eax
0040A2D9 . 8B4D F4 mov ecx, dword ptr [ebp-C]
0040A2DC . 64:890D 00000>mov dword ptr fs:[0], ecx
0040A2E3 . 5F pop edi
0040A2E4 . 5E pop esi
0040A2E5 . 5B pop ebx
0040A2E6 . 8BE5 mov esp, ebp
0040A2E8 . 5D pop ebp
0040A2E9 . C3 retn
0040A2EA > \68 3CB44200 push 0042B43C ; |Text = ""D7,"",A2,"",B2,"崧?,B4,"砦螅",AC,"继续加油!"
0040A2EF . 56 push esi ; |hOwner
0040A2F0 . FF15 04514200 call dword ptr [<&USER32.MessageBoxA>>; \MessageBoxA
0040A2F6 . 33C0 xor eax, eax
0040A2F8 . 8B4D F4 mov ecx, dword ptr [ebp-C]
0040A2FB . 64:890D 00000>mov dword ptr fs:[0], ecx
0040A302 . 5F pop edi
0040A303 . 5E pop esi
0040A304 . 5B pop ebx
0040A305 . 8BE5 mov esp, ebp
0040A307 . 5D pop ebp
0040A308 . C3 retn
很明显0x40A2C9处的JNZ指令就是关键跳,而在这个跳转上面,程序只调用了两个函数地址分别为0x409FA0和0x40A060,说明这两个函数都很重要,我们一个一个来仔细看。
跟进函数0x409FA0到0x409FE0处的一个call,发现存放注册码的内存地址被作为参数传递进去了,见图二
(图二)
这里一定是在对注册码进行什么操作,跟进去看一下
00409E40 /$ 53 push ebx
00409E41 |. 55 push ebp
00409E42 |. 8B6C24 14 mov ebp, dword ptr [esp+14] ; 取第三个参数,也就是注册码长度的一半
00409E46 |. 56 push esi
00409E47 |. 33F6 xor esi, esi
00409E49 |. 57 push edi
00409E4A |. 85ED test ebp, ebp
00409E4C |. 7E 29 jle short 00409E77 ; 判断第三个参数是否小于等于0,是则跳走
00409E4E |. 8B7C24 18 mov edi, dword ptr [esp+18]
00409E52 |. 8B5C24 14 mov ebx, dword ptr [esp+14]
00409E56 |> 8D4424 1C /lea eax, dword ptr [esp+1C]
00409E5A |. 50 |push eax
00409E5B |. 53 |push ebx
00409E5C |. E8 7FFFFFFF |call 00409DE0 ; 需要跟进查看
00409E61 |. 83C4 08 |add esp, 8
00409E64 |. 84C0 |test al, al
00409E66 |. 74 16 |je short 00409E7E
00409E68 |. 8A4C24 1C |mov cl, byte ptr [esp+1C]
00409E6C |. 46 |inc esi
00409E6D |. 880F |mov byte ptr [edi], cl
00409E6F |. 83C3 02 |add ebx, 2
00409E72 |. 47 |inc edi
00409E73 |. 3BF5 |cmp esi, ebp
00409E75 |.^ 7C DF \jl short 00409E56
00409E77 |> 5F pop edi
00409E78 |. 5E pop esi
00409E79 |. 5D pop ebp
00409E7A |. B0 01 mov al, 1
00409E7C |. 5B pop ebx
00409E7D |. C3 retn
继续跟进到函数0x409DE0里查看
00409DE0 /$ 8B5424 04 mov edx, dword ptr [esp+4] ; 取注册码的地址
00409DE4 |. 8A02 mov al, byte ptr [edx] ; 取第一位字符
00409DE6 |. 3C 30 cmp al, 30
00409DE8 |. 7C 08 jl short 00409DF2
00409DEA |. 3C 39 cmp al, 39
00409DEC |. 7F 04 jg short 00409DF2
00409DEE |. 2C 30 sub al, 30 ; 如果字符是'0'-'9'之一,则把它们的ascii码减去0x30
00409DF0 |. EB 0A jmp short 00409DFC
00409DF2 |> 3C 41 cmp al, 41
00409DF4 |. 7C 45 jl short 00409E3B
00409DF6 |. 3C 46 cmp al, 46
00409DF8 |. 7F 41 jg short 00409E3B
00409DFA |. 2C 37 sub al, 37 ; 如果字符是'A'-'F'之一,则把它们的ascii码减去0x37
00409DFC |> 8B4C24 08 mov ecx, dword ptr [esp+8] ; ecx=第二个参数
00409E00 |. 42 inc edx ; edx=edx+1
00409E01 |. 8801 mov byte ptr [ecx], al ; 将转好好的第一位16进制数存入第二个参数指定的地址中
00409E03 |. 8A02 mov al, byte ptr [edx] ; 取第二位字符
00409E05 |. 3C 30 cmp al, 30
00409E07 |. 7C 17 jl short 00409E20
00409E09 |. 3C 39 cmp al, 39
00409E0B |. 7F 13 jg short 00409E20
00409E0D |. 8A01 mov al, byte ptr [ecx] ; 如果第二位字符是'0'-'9'之一则来到这里
00409E0F |. C0E0 04 shl al, 4
00409E12 |. 8801 mov byte ptr [ecx], al ; 将刚才存放的第一个16进制数左移到高四位
00409E14 |. 8A12 mov dl, byte ptr [edx] ; 实际上上面这个mov语句是多余的,下面转换好之后会再存
00409E16 |. 02D0 add dl, al ; 加上第二个字符的ascii码
00409E18 |. B0 01 mov al, 1
00409E1A |. 80EA 30 sub dl, 30 ; 因为刚才是直接加了第二个字符的ascii码,所以减去0x30
00409E1D |. 8811 mov byte ptr [ecx], dl ; 将转换好的16进制数存入目标地址
00409E1F |. C3 retn
00409E20 |> 3C 41 cmp al, 41
00409E22 |. 7C 17 jl short 00409E3B
00409E24 |. 3C 46 cmp al, 46
00409E26 |. 7F 13 jg short 00409E3B
00409E28 |. 8A01 mov al, byte ptr [ecx] ; 如果第二位字符是'A'-'F'之一则来到这里,否则跳走
00409E2A |. C0E0 04 shl al, 4
00409E2D |. 8801 mov byte ptr [ecx], al
00409E2F |. 8A12 mov dl, byte ptr [edx] ; 将刚才存放的第一个16进制数左移到高四位
00409E31 |. 02D0 add dl, al ; 加上第二个字符的ascii码
00409E33 |. B0 01 mov al, 1
00409E35 |. 80EA 37 sub dl, 37 ; 因为刚才是直接加了第二个字符的ascii码,所以减去0x37
00409E38 |. 8811 mov byte ptr [ecx], dl ; 将转换好的16进制数存入目标地址
00409E3A |. C3 retn
00409E3B |> 32C0 xor al, al ; 字符不是16进制字符,返回0
00409E3D \. C3 retn
相信我的注释已经写得非常清楚了,函数0x409DE0的功能是,将第一个参数指向的两个16进制字符转换为对应的16进制数(例如"8A"转换成0x8A),并存放到第二个参数指定的内存地址中并且返回1。如果字符非法(不是'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'之一)则返回0,转换不成功,程序就不再继续往下验证了。
这个函数分析清楚之后,函数0x409E40(什么?!你已经想不起来这个函数在哪里出现过?赶紧往回看看,它就是刚刚分析的函数0x409DE0的调用者)的功能也就一目了然了,其作用就是将第一个参数指向的16进制字符串转换为16进制数并存放到第二个参数指定的地址中,而第三个参数正好则是转换后的16进制数所占字节数(字符串"8A"的长度为2,而0x8A只占用一个字节,这就是为什么刚才我们看到第三个参数是注册码长度一半的原因)。
需要注意的一点是,因为注册码的长度被直接除以2(参见图二),如果注册码的长度是奇数的话,多余的那1位就被自动舍弃了,并不会被转换。到目前为止,我们已经搞清楚这个CrackMe的
第一点限制,注册码必须是一个16进制字符串!革命尚未成功,继续走
00406F57 . 8B4D 08 mov ecx, dword ptr [ebp+8] ; ecx=8
00406F5A . 8BC3 mov eax, ebx ; eax赋值为注册码长度除以2
00406F5C . 33D2 xor edx, edx ; 将edx清空,因为div指令将edx:eax作为被除数
00406F5E . F7F1 div ecx ; 如果edx的值不清空的话,eax可能存不下商,会有溢出
00406F60 . 85D2 test edx, edx ; 除以ecx之后判断余数是否为0
00406F62 . 0F85 7A010000 jnz 004070E2 ; 余数不为0则跳走
一直F7单步走到0x406F57会发现,这里是对注册码长度的一个校验,假设注册码长度为k,如果(k / 2) % 8 != 0为真的话,就不再继续往下验证。
也就是说注册码的长度至少为16个字节且必须为16的倍数(也可以是16的倍数加1,因为前面我们已经说了,如果注册码的长度为奇数,多余的一位并不会参与转换,相当于被舍弃了)。
到这里我们已经知道了注册码的两点限制,现在我们在刚才跟到的地址0x406F57处下一个断点,然后直接F9让CrackMe弹出继续努力的对话框,然后重新输入一个符合要求的注册码,点击注册认证后会断在刚才我们跟到的位置0x406F57,从这里再继续往下走
(图三)
很快我们就来到了这个CrackMe最为关键的地方,相信熟悉密码学常用算法的同学已经看出来这个就是XTEA算法的解密过程
00406A70 /$ 83EC 08 sub esp, 8
00406A73 |. 8D4424 00 lea eax, dword ptr [esp]
00406A77 |. 53 push ebx
00406A78 |. 55 push ebp
00406A79 |. 56 push esi
00406A7A |. 8B7424 18 mov esi, dword ptr [esp+18]
00406A7E |. 57 push edi
00406A7F |. 50 push eax
00406A80 |. 8BF9 mov edi, ecx
00406A82 |. 56 push esi
00406A83 |. E8 D8BFFFFF call 00402A60 ; 将参数1指向的DWORD中的数据反向存入到参数2指向的DWORD中
00406A88 |. 8D4C24 1C lea ecx, dword ptr [esp+1C]
00406A8C |. 83C6 04 add esi, 4
00406A8F |. 51 push ecx
00406A90 |. 56 push esi
00406A91 |. E8 CABFFFFF call 00402A60
00406A96 |. 8B4C24 20 mov ecx, dword ptr [esp+20] ; 取第一个DWORD的密文
00406A9A |. 8B7424 24 mov esi, dword ptr [esp+24] ; 取第二个DWORD的密文
00406A9E |. 83C4 10 add esp, 10 ; 在下面我们将第一个DWORD的密文称为v0,第二个DWORD的密文称为v1
00406AA1 |. B8 2037EFC6 mov eax, [COLOR="red"]C6EF3720[/COLOR] ; eax=delta<<5
00406AA6 |. BA 20000000 mov edx, 20 ; rounds=0x20
00406AAB |> 8BD9 /mov ebx, ecx ; ebp=v0
00406AAD |. 8BE9 |mov ebp, ecx ; ebp=v0
00406AAF |. C1EB 05 |shr ebx, 5 ; ebx右移5位
00406AB2 |. C1E5 04 |shl ebp, 4 ; ebp左移4位
00406AB5 |. 33DD |xor ebx, ebp ; 上面4句和这一句代码连起来看就是ebx=(v0>>5)^(v0<<4)
00406AB7 |. 8BE8 |mov ebp, eax ; ebp=eax,也就是ebp=delta<<5,下一次循环的时候就不是这个值了
00406AB9 |. C1ED 0B |shr ebp, 0B ; ebp=ebp>>0x0b
00406ABC |. 83E5 03 |and ebp, 3 ; ebp=ebp&03
00406ABF |. 03D9 |add ebx, ecx ; ebx=ebx+v0
00406AC1 |. 8B6CAF 20 |mov ebp, dword ptr [edi+ebp*4+20] ; 取一个DWORD大小的密钥,ebp*4是索引,跟到这里在OD命令行执行db edi+20就可以看到密钥了
00406AC5 |. 03E8 |add ebp, eax ; ebp=ebp+(delta<<5)
00406AC7 |. 05 4786C861 |add eax, 61C88647 ; eax=eax+0x61c88647,相当于eax=eax-delta
00406ACC |. 33DD |xor ebx, ebp
00406ACE |. 2BF3 |sub esi, ebx ; v1 = v1-ebx
00406AD0 |. 8BDE |mov ebx, esi
00406AD2 |. 8BEE |mov ebp, esi
00406AD4 |. C1EB 05 |shr ebx, 5
00406AD7 |. C1E5 04 |shl ebp, 4
00406ADA |. 33DD |xor ebx, ebp
00406ADC |. 8BE8 |mov ebp, eax
00406ADE |. 83E5 03 |and ebp, 3
00406AE1 |. 03DE |add ebx, esi
00406AE3 |. 8B6CAF 20 |mov ebp, dword ptr [edi+ebp*4+20] ; 再取一个DWORD的密钥
00406AE7 |. 03E8 |add ebp, eax
00406AE9 |. 33DD |xor ebx, ebp
00406AEB |. 2BCB |sub ecx, ebx
00406AED |. 4A |dec edx ; rounds减1
00406AEE |.^ 75 BB \jnz short 00406AAB
00406AF0 |. 8B7C24 20 mov edi, dword ptr [esp+20]
00406AF4 |. 57 push edi
00406AF5 |. 51 push ecx
00406AF6 |. E8 A5BFFFFF call 00402AA0 ; 参数1是解密后的一个DWORD,把它存到到参数二指向的地址中
00406AFB |. 83C7 04 add edi, 4
00406AFE |. 57 push edi
00406AFF |. 56 push esi
00406B00 |. E8 9BBFFFFF call 00402AA0 ; 这个函数被调用了两次,也就是总共存放了两个DWORD
00406B05 |. 83C4 10 add esp, 10
00406B08 |. 5F pop edi
00406B09 |. 5E pop esi
00406B0A |. 5D pop ebp
00406B0B |. 5B pop ebx
00406B0C |. 83C4 08 add esp, 8
00406B0F \. C2 0800 retn 8
上面有几个常数在这里稍微解释下,注释中的delta=0x9E3779B9,它是由
计算而来的,而代码中的0xC6EF3720恰好等于delta<<5。语句add eax, 61C88647和语句
sub eax,9E3779B9的运算结果是相同的。0x406A70这个函数的功能就是将我们的注册码转换成的16进制数据解密,而解密所使用的密钥是BYTE keystream[16] = {0x64,0x6E,0x69,0x57,0x64,0x6E,0x61,0x52,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
细心的朋友可能已经发现0x64,0x6E,0x69,0x57,0x64,0x6E,0x61,0x52正好是字符串“dniWdnaR”中字符的ascii码(不包含0结束符),看来windrand有点自恋噢^_^
最后,程序会把注册码对应的16进制数据解密后的数据与用户名进行比较,如果相同就提示“注册码正确”,否则就是叫你“继续加油”。
(图四)
0x40A060就是一个山寨版的strcmp函数,我们不再在这里分析它,它的实现很简单,感兴趣的读者可以自己跟进去详细看看。这个CrackMe的分析过程到这里就算是完了,总结一下它都有哪些限制,1.注册码中的字符只能是16进制字符。2.注册码的长度至少为16个字节并且只能为16的倍数(16的倍数+1也可以,前面已经说过)。3.用户名的长度=注册码的长度/2。其实这些限制都是XTEA算法的要求,因为XTEA算法的分组长度是64位(每次加密或者解密的数据的长度必须是64位=8字节),因此用户名的长度至少为8字节(或者8的倍数),注册码的长度至少为16个字节(因为字符串转换成16进制数据之后,所占的内存大小刚好是16/2也就是8字节)。搞清楚算法之后,很容易我们就可以写出注册机了,也就是把用户名变换一下(详见注册机代码)然后再使用XTEA算法加密(密钥在前面已经说过了),得到的密文就是对应的注册码了,注册机代码如下
#include <stdio.h>
#include <stdlib.h>
void encipher(unsigned int *v,unsigned int *key);
void reverse_dword(unsigned int a);
unsigned char keystream[16] = {0x64,0x6E,0x69,0x57,0x64,0x6E,0x61,0x52,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
char username[9];//为了简单起见,我们设定用户名的长度只能为位
char regcode[17];//存放最后生成的注册码字符串
int main()
{
char backup[9];//备份用户名
int i = 0;
printf("Please enter username:");
scanf("%08s",&username);
memset(backup,0,sizeof(backup));
strcpy(backup,username);
reverse_dword(&username);//这里之所以要做两次reverse,是因为CrackMe在解密注册码时,也做了两次reverse DWORD的操作
reverse_dword((unsigned int)(&username) + 4);
printf("%08x\n",&username);
printf("%08x\n",(unsigned int)(&username) + 4);
encipher(&username,&keystream);
memset(regcode,0,sizeof(regcode));
sprintf(regcode,"%08X%08X",*(unsigned int*)(&username),*(unsigned int*)(&username[4]));
printf("Username:%s\n",backup);
printf("RegCode:%s\n",regcode);
system("pause");
return 0;
}
void reverse_dword(unsigned int *n)
{
unsigned int a;
unsigned int k;
a = *n;
k = a & 0x000000ff;
a = (a & 0xffffff00) | ((a & 0xff000000) >> 24);
a = (a & 0x00ffffff) | (k << 24);
k = a & 0x0000ff00;
a = (a & 0xffff00ff) | ((a & 0x00ff0000) >> 8);
a = (a & 0xff00ffff) | (k << 8);
*n = a;
}
void encipher(unsigned int *v,unsigned int *key)
{
int i = 0;
unsigned int v0=v[0], v1=v[1], sum=0, delta=0x9E3779B9;
for (;i < 0x20;i++)
{
v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
sum += delta;
v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]);
}
v[0]=v0; v[1]=v1;
}
大功告成,上一张胜利的截图^_^