题目来自于如下网址:
第13篇 论坛活动 \ 金山杯2007逆向分析挑战赛 \ 第一阶段 \ 第一题 \ 题目 \ [第一阶段 第一题];
现将此题目概述粘贴如下:
CrackMe.exe 是一个简单的注册程序,见附件,请写一个注册机,要求:
1. 注册机是KeyGen,不是内存注册机或文件Patch;
2. 注册机可以使用 ASM,VC,BC,VB,Delphi等语言书写,其他谢绝使用;
3. 注册机必须可以运行在 Windows 系统上;
......
昨天我偶然浏览到这里,看到这个题目,于是看了下,题目早就已经是过去时了,但今天依然可以看看,大概用了半天时间求解此题目(本来以为挺简单,但实际上还是少有点难度的)。把附件下载下来可以看到,CrackMe.exe 是一个仅有 1.85 KB 的 Windows 对话框程序。界面也非常简单,只有两个文本框用于获取用户名,注册码,和一个注册按钮,当点击注册按钮时,如果注册码正确,会弹出 MessageBox 显示 "OK!!",否则会显示 "Fail!" 。
很显然,如果我们直接修改这个 exe,跳过其注册码检测或者修改里面的跳转逻辑,修改调用 DecryptText 的参数,修改栈上的密文数据等等,有 无数种方法可以直接令这个程序显示 “OK!!” 消息框。但是,这显然不是这个题目的目的(因为这太简单了,也无需理解那些验证用的汇编代码)。根据题目要求,可以看出出题者的要求是,原程序是不能修改的,解题的人通过阅读和分析验证注册码的汇编代码,得出其验证思路,然后根据此写出注册机(KeyGen),实现给出任意的 UserName,得出 SerialNo = f (UserName) ,也就是要求找出 f 关系,并用编程语言实现注册机。
用 IDA 反汇编这个极小的程序,可以看到它的组成非常简单,下面给出一些主要的汇编代码,并部分的翻译成 C 语言。
首先是,当点击“注册"按钮,程序将会检测注册码是否正确。在对话框的窗口过程[参考补充说明2]中,可以找到一段重要代码,现在把它提取出来作为一个注册机要用到的重要函数,即根据用户名字符串得到一个整数特征值,汇编代码省略,这里把这个函数翻译为 C 语言如下:
//根据用户名,计算出用户特征值 int GetUserValue(const char* pUserName) { int nUserVal= 0x13572468; int i; int len = strlen(pUserName); for(i = 0; i < len; i++) { nUserVal = nUserVal + pUserName[i]; nUserVal = nUserVal * 0x03721273; nUserVal = nUserVal + 0x24681357; nUserVal = (nUserVal << 25) | (nUserVal >> 7); } return nUserVal; }
接下来,程序将会调用一个校验注册码的函数,在这个函数中同时会弹出 MessageBox,这个函数由几块功能组成,也是此题目必须要分析的重点,这个函数的原型可以推测出为形如:
void CheckSerialNo(int nUserVal, char* pSerialNo);
通过这个函数,我们可以很容易看到它是如何弹出显示着 “Fail!!" 的消息框的,该函数首先在栈上放置成功和失败的文本密文,然后通过检测结果,把相应的密文地址传送到解密函数(这里称之为 DecryptText),解密函数把解密后的明文放入栈上的缓冲区,然后以此调用 MessageBox,非常明显,这是模拟软件的常规保护方法,在实际软件中都会将关键和敏感信息进行隐藏,当然这也不是这道题目的重点,因为很容易就找到密文的位置,为紧靠 EBP 附近的两个字节数组。
在代码段(.text) 的第一个函数就是解密函数,其原型形如:void DecrptText(const BYTE* pSecret, char* pPlainTextBuffer); 这个加解密非常简单,只是把一个数组用另一个事先拟定好的 key 数组线性的异或了一下而已,所以这不是本题重点,在此暂时省略不提(其 C 语言代码版本见本文结尾 [1] )。
下面给出的是这个程序的关键汇编代码,也就是 CheckSerialNo 的完整代码,此题目的本意正是要求读懂这个函数的逻辑,并找出注册机算法。这个函数较长,但分开割裂不太好,所以完整粘贴如下(注:主体来自于 IDA 反汇编结果,为了更好的显示和更好的可读性,我整理和调整了反汇编结果的部分细节,以及对部分地址标号进行了重命名)(前面有一大段花里胡哨的稀奇古怪的指令,一些变量赋值操作也通过隔开少许的 PUSH / POP 来完成,最恶劣的是 DWORD 指针竟然地址不对齐,仿佛是人为故意设置的障碍):
.text:004002CC CheckSerialNo proc near .text:004002CC .text:004002CC Text = byte ptr -128h ; char Text[260]; .text:004002CC var_24 = byte ptr -24h ; BYTE var_24[12]; //nUserBits .text:004002CC var_18 = byte ptr -18h ; BYTE var_18[12]; //Secret_OK .text:004002CC var_C = byte ptr -0Ch ; BYTE var_C[12]; //Secret_Fail .text:004002CC CurSerialChar = byte ptr -1 .text:004002CC nUserValue = dword ptr 8 ; (uint nUserVal, .text:004002CC szSerialNo = dword ptr 0Ch ; const char *pSerialNo) .text:004002CC .text:004002CC push ebp .text:004002CD mov ebp, esp .text:004002CF sub esp, 128h ; alloc 296 bytes on stack .text:004002D5 and byte ptr [ebp+var_24], 0 ; var_24[0] = 0; .text:004002D9 push ebx .text:004002DA push esi .text:004002DB push edi .text:004002DC xor eax, eax .text:004002DE lea edi, [ebp+var_24+1] .text:004002E1 stosd ;var_24[1~4]=0 .text:004002E2 and [ebp+Text], 0 ; Text[0] = 0; .text:004002E9 push 40h .text:004002EB stosd ;var_24[5~8]=0 .text:004002EC stosb ;var_24[9]=0 .text:004002ED pop ecx .text:004002EE xor eax, eax .text:004002F0 lea edi, [ebp-127h] ; 非常古怪,edi未对齐到 DWORD .text:004002F6 or [ebp+var_C], 0FFh ; Secret_Fail[0] = 0xFF .text:004002FA rep stosd ;Text[1~256]=0 .text:004002FC or [ebp+var_18], 0FFh ; Secret_OK[0] = 0xFF .text:00400300 or ecx, 0FFFFFFFFh ; 计算序列号长度 .text:00400303 stosw .text:00400305 stosb .text:00400306 mov edi, [ebp+szSerialNo] .text:00400309 xor eax, eax .text:0040030B repne scasb .text:0040030D not ecx .text:0040030F push 1 .text:00400311 dec ecx ; ecx = strlen(szSerialNo) .text:00400312 pop ebx ; ebx = 1 (此函数中 EBX 恒为 1) .text:00400313 mov byte ptr [ebp-0Bh], 63h ; Init Secret_Fail[1~10] .text:00400317 mov [ebp-0Ah], 0FBh ; "Fail!"的密文: var_C .text:0040031B mov [ebp-09h], 9Ah .text:0040031F mov [ebp-08h], 3 .text:00400323 mov [ebp-07h], 0A3h .text:00400327 mov [ebp-06h], 0DAh .text:0040032B mov [ebp-05h], 72h .text:0040032F mov [ebp-04h], 0FEh .text:00400333 mov [ebp-03h], 0C9h .text:00400337 mov [ebp-02h], 0B7h .text:0040033B mov byte ptr [ebp-17h], 6Ah ; Init Secret_OK[1~10] .text:0040033F mov [ebp-16h], 0D1h ; "OK!!"的密文: var_18 .text:00400343 mov [ebp-15h], 0D2h .text:00400347 mov [ebp-14h], 4Eh .text:0040034B mov [ebp-13h], 82h .text:0040034F mov [ebp-12h], 0DAh .text:00400353 mov [ebp-11h], 72h .text:00400357 mov [ebp-10h], 0FEh .text:0040035B mov [ebp-0Fh], 0C9h .text:0040035F mov [ebp-0Eh], 0B7h .text:00400363 mov esi, ecx ; esi = strlen(szSerialNo) .text:00400365 mov edi, ebx ; EDI = 1; 循环起始值 .text:00400367 .text:00400367 loc_400367: .text:00400367 mov eax, [ebp+nUserValue] ; ----初始化 var_24[] 内容----- .text:00400367 ; for (edi = 1; edi < 9; ++edi) { .text:0040036A mov ecx, edi ; ecx = edi; .text:0040036C shr eax, cl ; eax = ((uint)nUserVal >> edi); .text:0040036E and al, bl ; al = al & 1 .text:00400370 mov byte ptr [ebp+edi+var_24], al ; var_24[edi] = (nUserVal >> edi) & 1; .text:00400374 inc edi .text:00400375 cmp edi, 9 .text:00400378 jl short loc_400367 ; }---循环尾部---- .text:0040037A xor edi, edi ; edi = 0 .text:0040037C mov [ebp+var_1B], bl ; var_24[9] = 1 .text:0040037F test esi, esi ; esi = strlen(szSerial) .text:00400381 jle short loc_4003EE ; for(edi=0; edi<strlen(szSerialNo);edi++) { .text:00400383 .text:00400383 loc_400383: .text:00400383 mov eax, [ebp+szSerialNo] .text:00400386 mov al, [edi+eax] ;al=szSerialNo[edi]; .text:00400389 cmp al, 30h .text:0040038B mov [ebp+CurSerialChar], al .text:0040038E jl AlertFail .text:00400394 cmp al, 39h .text:00400396 jg AlertFail ; if(szSerial[edi] < '0' || szSerial[edi] > '9') .text:00400396 ; goto AlertFail; .text:0040039C mov eax, edi ; 准备计算 edi % 31 .text:0040039E push 1Fh .text:004003A0 cdq .text:004003A1 pop ecx ; .text:004003A2 idiv ecx ; edx = edi % 31; .text:004003A4 mov eax, [ebp+nUserValue] .text:004003A7 push 0Ah .text:004003A9 mov ecx, edx ; ecx = edi % 31; .text:004003AB xor edx, edx .text:004003AD shr eax, cl ; eax = nUserVal >> (EDI % 31); .text:004003AF pop ecx ; ecx = 10; .text:004003B0 div ecx ; edx = (nUserVal >> (EDI % 31)) % 10; .text:004003B2 movsx eax, [ebp+CurSerialChar] ; eax = szSerial[edi] .text:004003B6 lea eax, [edx+eax-30h] ; eax = EDX + (szSerial[i] - '0'); .text:004003BA xor edx, edx .text:004003BC div ecx ; edx = (edx + szSerial[i] - '0') % 10; .text:004003BE cmp edx, ebx ; if(ebx == 1) { .text:004003C0 jnz short EdxIsNotOne_ ; else goto ... .text:004003C2 xor byte ptr [ebp+var_24+1], bl ; var_24[1] ^ = 1; switching var_24[1] .text:004003C5 jmp short ContinueLoop ; continue; } .text:004003C7 ; -------------------- .text:004003C7 .text:004003C7 EdxIsNotOne_: .text:004003C7 cmp [ebp+edx+var_24-1], bl ; else { .text:004003C7 ; if(var_24[edx-1] != 1) .text:004003C7 ; goto AlertFail; .text:004003C7 ; var_24[edx-1] 必须为1 .text:004003CB jnz short AlertFail .text:004003CD lea eax, [edx-2] ; //检查 [1, edx-2] 元素是否都是 0. .text:004003D0 mov ecx, ebx ; for(ecx = 1; .text:004003D2 cmp eax, ebx ; ecx <= edx-2; ecx++) { .text:004003D4 jl short SwitchingBit_ .text:004003D6 .text:004003D6 loc_4003D6: .text:004003D6 cmp byte ptr [ebp+ecx+var_24], bl .text:004003D6 ; if(var_24[ecx] == 1) .text:004003D6 ; goto AlertFail; .text:004003DA jz short AlertFail .text:004003DC inc ecx ; } .text:004003DD cmp ecx, eax .text:004003DF jle short loc_4003D6 .text:004003E1 .text:004003E1 SwitchingBit_: .text:004003E1 xor byte ptr [ebp+edx+var_24], bl ; var_24[edx]^=1; switching var_24[edx] .text:004003E5 lea eax, [ebp+edx+var_24] ; .text:004003E9 .text:004003E9 ContinueLoop: .text:004003E9 inc edi ; for(_;_; ++edi) .text:004003EA cmp edi, esi .text:004003EC jl short loc_400383 ; } --序列号循环的尾部-- .text:004003EE .text:004003EE loc_4003EE: .text:004003EE mov eax, ebx ; for(eax = 1; eax < 10; eax++) { .text:004003F0 .text:004003F0 loc_4003F0: .text:004003F0 cmp byte ptr [ebp+eax+var_24], bl .text:004003F0 ; if(var_24[eax] == 1) .text:004003F4 jz short AlertFail ; goto AlertFail; .text:004003F6 inc eax .text:004003F7 cmp eax, 0Ah .text:004003FA jl short loc_4003F0 ; } --检测序列号是否正确 .text:004003FC lea eax, [ebp+Text] .text:00400402 push eax .text:00400403 lea eax, [ebp+var_18] ; DecryptText(Secret_OK, Text); .text:00400406 .text:00400406 loc_400406: .text:00400406 push eax .text:00400407 call DecryptText ; 把密文解密存放到 Text 中 .text:0040040C pop ecx .text:0040040D lea eax, [ebp+Text] .text:00400413 pop ecx .text:00400414 push 0 ; uType .text:00400416 push offset Caption ; lpCaption .text:0040041B push eax ; lpText .text:0040041C push 0 ; hWnd .text:0040041E call MessageBoxA ; MessageBox(NULL, Text, "", MB_OK); .text:00400424 pop edi .text:00400425 mov eax, ebx ; return 1; .text:00400427 pop esi .text:00400428 pop ebx .text:00400429 leave .text:0040042A retn .text:0040042B ;-------------------- .text:0040042B .text:0040042B AlertFail: .text:0040042B .text:0040042B lea eax, [ebp+Text] .text:00400431 push eax .text:00400432 lea eax, [ebp+var_C] ; DecryptText(Secret_Fail, Text); .text:00400435 jmp short loc_400406 .text:00400435 CheckSerialNo endp
在汇编代码右侧,我已经大致写了一定的注释,下面把这个校验注册码的函数翻译到 C 语言如下(为了使代码的文本紧凑,部分使用了起始大括号不换行的代码风格),然后根据代码推断注册机算法。
int CheckSerialNo(UINT uintUserVal, char* pSerialNo) { char CurrentSerialChar; //Secret_Fail[11]: 当前字符 BYTE Secret_Fail[] = { 0xFF, 0x63, 0xFB, 0x9A, 0x03, 0xA3, 0xDA, 0x72, 0xFE, 0xC9, 0xB7 }; //"Fail!" 密文 (EBP-0Ch) BYTE Secret_OK[] = { 0xFF, 0x6A, 0xD1, 0xD2, 0x4E, 0x82, 0xDA, 0x72, 0xFE, 0xC9, 0xB7 }; //"OK!!" 密文 (EBP-18h) BYTE nUserBits[10] = { 0 }; //即上面汇编代码中的 var_24 char Text[260] = { 0 }; //存放解密后的明文,送给 MessageBox int i; //EDI: 序列号索引 int k; //ECX / EAX: nUserBits 索引 int nWhichBit; // EDX: 要修改的 nUserBits 索引 nUserBits[0] = 0; //初始化 BYTE nUserBits[10] for(i = 1; i < 9; i++) { nUserBits[i] = (uintUserVal >> i ) & 1; } nUserBits[9] = 1; //最高位手工设1,保证序列号具有较大长度 BOOL bShowFail = FALSE; // 是否显示 "Fail!" int SerialLen = strlen(pSerialNo); //线性遍历注册码字符串 for(i = 0; i < SerialLen; i++) { CurrentSerialChar = pSerialNo[i]; //注册码必须是数字 if(CurrentSerialChar < '0' || CurrentSerialChar > '9') { bShowFail = TRUE; break; } //最初发表时此处遗漏了 break,2014-4-29 补充 nWhichBit = ((uintUserVal >> (i % 31)) + CurrentSerialChar - '0') % 10; //修改最低位时,无须做任何校验。否则需检测是否满足可修改条件 if(nWhichBit == 1) { nUserBits[1] ^= 1; } else { //紧邻的低位是 1 吗?如果不是,则注册失败 if(nUserBits[nWhichBit - 1] != 1) { bShowFail = TRUE; break; } //其余低位都是 0 吗?如果不是,则注册失败 for(k = 1; k <= nWhichBit - 2; k++) { if(nUserBits[k] != 0) { bShowFail = TRUE; break; } } if(bShowFail) break; //对当前位取反(0-1 切换) nUserBits[ nWhichBit ] ^= 1; } } //查验 nUserBits 是否都为 0,是则成功,否则失败 if( !bShowFail ) { for(k = 1; k < 10; k++) { if(nUserBits[k] == 1) { bShowFail = TRUE; break; } } } //解密密文到 Text 中 DecryptText(bShowFail? Secret_Fail : Secret_OK, Text); MessageBox(NULL, Text, "", MB_OK); return 1; }
在高级语言版本中,我使用了一些语言方法,来避免在高级语言代码把汇编中的那些相对跳转直译为 goto,大体上将是等效的。从上面的代码中可以看出检查注册码的重要标准,如果我们把注册码看做输入,实际上在扫描注册码的过程中,就是 nUserBits 这个元素为二元的数组变化的过程,nUserBits 是根据用户填写的用户名计算得到的数组,所以它的元素和用户名相关,是不确定的,注册码扫描结束后,这个数组必须所有元素都为 0。因此,相当于以注册码为驱动。
从上面的代码逻辑中我们还能看到很重要的一点,要修改 nUserBits 的某一位(假设为 nWhichBit),那么必须满足以下条件,nUserBits 必须处于如下状态:
Index: | 1 | 2 | 3 | ... | 5 | 6 | 7 | 8 | 9 | ... |
---|---|---|---|---|---|---|---|---|---|---|
Value: | 0 | 0 | 0 | ... | 0 | 1 | nWhichBit | x | x | ... |
也就是说,当我们要切换某一位的状态时,从这一位向低位方向(左侧)看去,应该是 【0】* N + 【1】 的组合(紧邻的低位为 1, 其余均为 0)。最低位(索引 1 )可以随时修改,因为左边已经没有位了,当然就没必要向左看了。这里把上述逻辑用数学语言表述如下:
有一个数组x,可以把它理解为一个二进制数,每个数组元素表示一个bit。x = f (UserName) 。能够切换 x[i] (i = 1, 2, 3, ..., n )的准许条件是:
(1)对任何 1 <= j <= i - 2,有 x [ j ] = 0; 并且
(2)如果 i > 1, 有 x [ i - 1 ] = 1;
这时可以执行对 x [ i ] 的切换动作(对该位取反)。注册码正确的标准是,根据注册码执行一系列动作(注册码的每一位对应于切换 x 的哪一位)后,x 的所有元素为0。在 x 初始化时,已经将 x 的最高位固定设置为1,以防止 x 的初始值恰好都是 0 的特殊情况。
所以这样就提示注册机算法,可以用递归函数 ( 下面代码中的 SetBit 函数 ) 来求解。注册码是由一系列的 nWhichBit (要切换状态的位索引)组成,这个索引是根据注册码的当前位得到的。换句话说,求注册码相当于找出这样一个有序序列,{ b1, b2, b3, ..., } ,每一步都是合法切换,处理后 nUserBits 所有位都为 0。
这样注册机算法就算呼之欲出了,还有一个简单问题是,我们的目标是把 nUserBits 中的每一位都变为 0,那么应该先从哪一位入手呢,应该按照从左(低位)向右(高位)的顺序依次清零,还是从右(高位)向左(低位)?不难得出答案应该采用后者,因为在设置某一位为 0 时,其所有低位都会动态变化,而高位则不受影响可以保持静态不动。所以我们应该先把高位清零,然后在逐一向低位方向推进。程序中显式的把最高位(索引为9)设置为 1,也暗示了这一求解顺序。
这里给出一个例子来说明,以校验的索引范围为从 1 到 4 ,假设 nUserBits 的初始状态为 { 0, 0, 0, 1 } (索引从 1 开始),则如何经过一系列上述规则允许的元素切换,把它变为 { 0, 0, 0, 0 },参考下表(整个过程让我想起了汉诺塔,可以很容易看出,整体解中包含了形式相同的规模更小的子问题的解):
Index (1-based) | [1] | [2] | [3] | [4] | nWhichBit to be switched |
---|---|---|---|---|---|
nUserBits[]: | 0 | 0 | 0 | 1 | Initial State |
1 | 0 | 0 | 1 | [1] | |
1 | 1 | 0 | 1 | [2] | |
0 | 1 | 0 | 1 | [1] | |
0 | 1 | 1 | 1 | [3] | |
1 | 1 | 1 | 1 | [1] | |
1 | 0 | 1 | 1 | [2] | |
0 | 0 | 1 | 1 | [1] | |
0 | 0 | 1 | 0 | [4] | |
1 | 0 | 1 | 0 | [1] | |
1 | 1 | 1 | 0 | [2] | |
0 | 1 | 1 | 0 | [1] | |
0 | 1 | 0 | 0 | [3] | |
1 | 1 | 0 | 0 | [1] | |
1 | 0 | 0 | 0 | [2] | |
0 | 0 | 0 | 0 | [1] |
把最右侧一列的索引值连在一起,就是一个我们需要设计的位置序列 { 121312141213121 },依次对这些位置进行开关切换(把 nUserBits 每一个元素想象成一个开关)后,就可以让 nUserBits 数组变为全为 0 的状态(注册码正确的条件)。如果不需要和用户特征值关联到一起的话,那这这个序列就是注册码。但原程序中是通过注册码和用户特征值(nUserVal)混编后得到这个调整序列,所以我们只需要对这个调整序列做个逆运算,“剔除”其中的用户特征值成分,即可得到实际注册码。
为此我们给出注册机的如下多个函数,即为题目要求的注册机,显然,注册码如果不限长度,可以有无数多个,但我们当然选择生成简短的。
//用户特征值(注意有时候需要使用其无符号形式,根据CrackMe的指令而定) int g_UserVal; //用户特征位数组,由9个位组成 [1,... 9] 仅索引1~9有效 int g_UserBits[10]; //序列号 TCHAR g_SerialNo[4096]; ////如果把序列号看着stack,这是栈顶 int g_Top; //注册机算法: void Push(int index); BOOL CanModify(int index); void SetBit(int index, int nDesiredVal); int GetUserValue(LPCTSTR szUser); void InitUserBits(); //index 即需要修改的 UserBits 索引。这个索引变换后,追加到序列号 void Push(int index) { //注意汇编中的右移指令是 SHR,所以需要转为无符号数字(否则为 SAR) UINT uintUserVal = g_UserVal; int nMapped = index - (uintUserVal >> (g_Top % 31)) % 10; if(nMapped < 0) nMapped += 10; //把数字转换成字符,这样 SerialNo 就是字符串。 g_SerialNo[g_Top] = nMapped + _T('0'); ++g_Top; } //是否可以直接修改 Bits[ index ] //要求必须满足 [0 0 0 ... 0 1 Index... BOOL CanModify(int index) { int i; if(index > 1) { if(g_UserBits[index - 1] != 1) return FALSE; } for(i = 1; i <= index - 2; i++) { if(g_UserBits[i] != 0) return FALSE; } return TRUE; } //递归函数,把索引为index的位设置为 nDesiredVal。 void SetBit(int index, int nDesiredVal) { int i; if(g_UserBits[index] != nDesiredVal) { //能立即修改吗?如果不能,调整低位。 if(!CanModify(index)) { SetBit(index - 1, 1); for(i = index - 2; i >= 1; i--) { SetBit(i, 0); } } //现在可以修改这一位了! g_UserBits[index] = nDesiredVal; Push(index); } } //根据用户名,计算出用户特征值 int GetUserValue(LPCTSTR szUser) { int nUserVal= 0x13572468; int i; int len = _tcslen(szUser); for(i = 0; i < len; i++) { nUserVal = nUserVal + szUser[i]; nUserVal = nUserVal * 0x03721273; nUserVal = nUserVal + 0x24681357; nUserVal = (nUserVal << 25) | (nUserVal >> 7); } return nUserVal; } // void InitUserBits() { //下面指令中用到 SHR (逻辑右移),所以需要无符号数 UINT uintUserVal = g_UserVal; int i; for(i = 1; i < 9; i++) { g_UserBits[i] = (uintUserVal >> i) & 1; } //最高位被置为1,保证注册码必然具有相当长度。 g_UserBits[9] = 1; } BOOL GetSerialNo(LPCTSTR szUserName) { int i; if(_tcslen(szUserName) == 0) return FALSE; g_UserVal = GetUserValue(szUserName); memset(g_UserBits, 0, sizeof(g_UserBits)); InitUserBits(); g_Top = 0; for(i = 9; i >= 1; i--) SetBit(i, 0); //字符串的 null-terminator; g_SerialNo[g_Top] = 0; ++g_Top; }
这里,附上原题目程序,我写的注册机的源码以及可执行文件的压缩包下载:
https://files.cnblogs.com/hoodlum1980/CrackMe_1_1.zip
注册机截图,用 VC 写的一个对话框程序(写成 Console 程序更容易,但 Windows 程序对用户来说更熟悉):
最后,随便给出一个注册机计算出的注册码作为结束(由于注册码太长,所以插入了换行和缩进):
User: hoodlum1980 Serial:
3299167863423793371021417400742237107415
4242840289212146582658628926588332475377
0031219490849267008495623294423911312678
1668321916686352376337402131741074023730
7405425284528981213658365842894658733257
5347006121849094924700049552320442991171
2668167832991688634237733710216174007412
3710742542428462892121865826585289265893
3247535700312114908492570084957232944209
1131260816683209166863623763372021317430
740237207405426284528931213658
【1】关于 DecryptText 函数,用于加/解密字符串(对称),其等效 C 语言代码如下:
void DecryptText(const BYTE *pSecret, char *pTextBuf) { int i; BYTE key[] = { 0x25, 0x9A, 0xF3, 0x6F, 0x82, 0xDA, 0x72, 0xFE, 0xC9, 0xB7 }; if(pSecret[0] == 0xFF) { for(i = 0; i < 10; i++) pTextBuf[i] = pSecret[i + 1] ^ key[i]; } else { strcpy(pTextBuf, (const char*)pSecret); } }
【2】关于对话框的窗口过程,是非常简单的。但它在栈上为临时变量分配空间使用的不是常规的( sub esp, ....) 语句,而是调用了函数(loc_400540)来完成分配临时空间,这个函数可以总结为等效于( sub esp, eax ),只需要把这个函数理解成分配栈上空间即可。(2014年5月4日 补充 --hoodlum1980)