这两个exe都是“加密与解密”书中“调试篇”的例子,CrackMe很简单,TraceMe稍微复杂一点。
分析CrackMe无需用OD打断点跟进,该程序逻辑非常简单,仅仅从汇编代码的调用上即可判断出其内部实现。
先用IDA进行CrackMe的反汇编,在最下方可以看到关于序列号的判断:
注意刚开始是无法看到右侧“序列号不对,重新再试一次”的字样的,需要查看内存地址00403000,发现是一个字符串数组,手动转化为数组后才能看到这些汉字。
很显然,关于判断序列号是否正确的代码就是:
.text:004010BD push edx ; lpString2
.text:004010BE push eax ; lpString1
.text:004010BF call ds:lstrcmpA
.text:004010C5 test eax, eax
.text:004010C7 push 0 ; uType
.text:004010C9 jnz short loc_4010E8
.text:004010CB push offset Caption ; "OK!"
.text:004010D0 push offset Text ; "恭喜你!"
.text:004010D5 push 0 ; hWnd
.text:004010D7 call ds:MessageBoxA
这里的序列号比对是将用户输入的序列号与预设的一个值进行比较,调用lstrcmpA函数。
如果eax = 1,则test语句执行后ZF = 0,会进入飘黄的跳转部分
如果eax = 0,则test语句执行后ZF = 1,会进入下方的恭喜提示
事实上,这里的序列号是以明文的方式写在程序中。
dx中的“9981”就是正确的序列号。这个9981是直接写死在CrackMe.exe的数据区中,运行的时候再被加载到栈里边。
.data:00403034 byte_403034 db 39h ; 9
.data:00403035 db 39h ; 9
.data:00403036 db 38h ; 8
.data:00403037 db 31h ; 1
.data:00403038 byte_403038 db 0
crack的办法已经很清楚了,可以将lstrcmpA的两个参数置为相同,比如都是edx,也可以将JNZ一句替换成NOP指令。
用OD给TraceMe.exe下断,刚开始的时候下的GetWindowTextA,后来几经ctrl-F9,才回到程序领空。
这里发现一个原来还不知道的调用,实际上在GetDlgItemTextA的内部,会调用GetWindowTextA。
程序内部 ----> GetDlgItemTextA ---> GetWindowTextA
因此可以直接对GetDlgItemTextA下断。
这里的两个GetDlgItemTextA调用即是获取用户名、序列号。
004011CA . 8A4424 4C mov al, byte ptr [esp+4C] ;取首字节判断用户名是否为空
004011CE . 84C0 test al, al
004011D0 . 74 76 je short 00401248
004011D2 . 83FB 05 cmp ebx, 5 ;判断用户名长度是否>=5
004011D5 . 7C 71 jl short 00401248
004011D7 . 8D5424 4C lea edx, dword ptr [esp+4C]
004011DB . 53 push ebx
004011DC . 8D8424 A00000>lea eax, dword ptr [esp+A0]
004011E3 . 52 push edx
004011E4 . 50 push eax
004011E5 . E8 56010000 call 00401340 ;判断序列号是否真确
最关键的部分是call 00401340,这个函数用来验证用户输入的用户名与序列号是否匹配。
该函数调用之前需要3次压栈,可以大概看出来,这3个参数是(密码,用户名,用户名长度)
下面来看call 00401340的具体实现:
ecx 相当于用户名byte数组的下标
eax 相当于密钥byte数组 的下标
可以查看内存得知密钥是: 【 0C 0A 13 09 0C 0B 0A 08 】
看懂这段代码即可以很轻松的写出针对TraceMe的注册机。
大体上的加密算法为:
function validation(passWord,userName,length){ var keyArray = [0x0c,0x0A,0x13,0x09,0x0c,0x0B,0x0A,0x08] var keyArrayIndex = 0 var userNameIndex = 3 var passWord2 = 0; while(length > userNameIndex) { if (keyArrayIndex > 7) { keyArrayIndex = 0 } passWord2 += userName[userNameIndex]*keyArray[keyArrayIndex] userNameIndex++ keyArrayIndex++ } ToString(passWord2) if(passWord == passWord2){ return true } else return false }
举个例子:如果用户名为 “abcde”,则序列号为
“d”×0C + “e”×0A = 100 × 12 + 101 ×10 = 2210
注意最后的几句:
0040138F |. FF15 04404000 call dword ptr [<&KERNEL32.lstrcmpA>] ; \lstrcmpA
00401395 |. F7D8 neg eax
00401397 |. 1BC0 sbb eax, eax
00401399 |. 5F pop edi ; USER32.GetDlgItemTextA
0040139A |. 5E pop esi
0040139B |. 40 inc eax
0040139C |. 5D pop ebp
0040139D \. C3 retn
strcmp的结果存放在eax中
只有eax为0的时候,才可以将eax设置为1(因为最后的返回结果也要放在eax中);如果strcmp的结果eax≠0,则要将eax设置为0。
这里用了
neg eax //求补 ,0->0 (CF=0) ,1-> -1(CF=1) ,-1>1(CF=1)
sbb eax,eax // SBB结果是 -CF
inc eax
这三句来完成,精妙无比。