本文是逆向分析CM4系列的最后一篇,我会将该游戏的序列号验证机制分析完毕,进而编写出注册码生成器。
延续上一篇文章的内容,来到如下代码处:
图1
上述代码并没有特别需要注意的地方,只是知道了接下来的循环需要执行4次。下面就是重要的验证部分:
图2
这是注册码中第二组四个字符的生成代码,主要是利用[ebp+var_20]进行运算,将结果作为字符串的偏移值,从而得到注册码。回顾一下,这里的[ebp+var_20]是之前运算所得到的余数,可见这个游戏的验证过程中的取余运算还是比较多的。接下来的两段代码,与图2代码较为类似:
图3
图4
上述两段代码在取余并获取相应字符的同时,还更改了[ebp+var_20]、[ebp+var_2C]与[ebp+var_38]中的值,用于接下来的运算,由于比较简单,这里就不再赘述。
#include<stdio.h> #include<windows.h> ////////////////////////////////////////////////////////////// // GetNum函数用于计算cm4.epe文件中相应偏移值处的DWORD大小的 // 十六进制数值,用于接下来的运算,该函数有一个参数var,保存 // 有偏移值 ////////////////////////////////////////////////////////////// DWORD GetNum( DWORD dwOffset ) { HANDLE hFile = NULL; DWORD dwSigNum = 0; // 用于保存位于偏移位置的DWORD字节的内容 DWORD dwNum = 0; // 恒为0,用作ReadFile的参数 // 打开名为cm4.epe的文件,该文件与本程序应处于同一目录下 hFile = CreateFile("cm4.epe", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); // 如果文件打开失败,则提示出错信息并退出 if (hFile == INVALID_HANDLE_VALUE) { printf("Could not open cm4.epe\n"); return 0; } // 设置文件指针到指定的位置 SetFilePointer(hFile, dwOffset, 0, FILE_BEGIN); // 读取起始于文件指针位置的十六进制代码,读取长度为4个字节(DWORD) ReadFile(hFile, &dwSigNum, sizeof(DWORD), &dwNum, NULL); CloseHandle(hFile); return dwSigNum; } int main() { int a, b, c; // 用于控制循环次数 int i, j, m, n; // 用于保存第一组验证码的四个ASCII码值 int count = 10; // 用于保存生成的注册码的组数 int tmp[4]; // 用于临时保存前四位验证码的ASCII码减去0x30或0x37后的值 int temp; // 用于保存临时的运算结果 int edx; // 用于保存运算的余数 DWORD Num; // 用于保存位于cm4.epe相应偏移处的十六进制代码 DWORD var_9C = 0x800000; // 这是一个固定的值,作为之后验证中的除数 DWORD var_14; // 用于保存第一循环算法最终运算的结果 DWORD var_20 = 0; DWORD var_2C = 0; DWORD var_38 = 0; // 这三个变量用于保存第二循环算法中的运算结果 char Reg[4][4] = { "0" };// 这个二维数组保存最终得出的注册码 char letter[37] = { "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" };// 字母表,用于生成注册码 ////////////////////////////////////////////////////////////// // 这里是注册码算法的第一处验证循环,这里通过四重循环,来不断 // 验证各种不同的ASCII码值的组合,也就是从0000到ZZZZ,从而生成 // 第一组的验证码(4个字符) ////////////////////////////////////////////////////////////// // 此处循环生成第一组验证码的第一个字符 for ( i = 48; i <= 90; i++ ) { if( i >= 58 && i <= 64 ) continue; // 如果注册码是数字,则减去48 if( i >= 48 && i <= 57 ) { tmp[0] = i - 48; } // 如果注册码是大写字母,则减去55 else { tmp[0] = i - 55; } // 此处循环生成第一组验证码的第二个字符 for ( j = 48; j <= 90; j++ ) { if( j >= 58 && j <= 64 ) continue; // 如果注册码是数字,则减去48 if( j >= 48 && j <= 57 ) { tmp[1] = j - 48; } // 如果注册码是大写字母,则减去55 else { tmp[1] = j - 55; } // 此处循环生成第一组验证码的第三个字符 for ( m = 48; m <= 90; m++ ) { if( m >= 58 && m <= 64 ) continue; // 如果注册码是数字,则减去48 if( m >= 48 && m <= 57 ) { tmp[2] = m - 48; } // 如果注册码是大写字母,则减去55 else { tmp[2] = m - 55; } // 此处循环生成第一组验证码的第四个字符 for ( n = 48; n <= 90; n++ ) { if( n >= 58 && n <= 64 ) continue; // 如果注册码是数字,则减去48 if( n >= 48 && n <= 57 ) { tmp[3] = n - 48; } // 如果注册码是大写字母,则减去55 else { tmp[3] = n - 55; } var_14 = 0; // 按照算法进行运算 for( a = 3; a >= 0; a-- ) { var_14 *= 36; var_14 += tmp[a]; } if (var_14 % 36 != 0 ) { // loc_4132CB Num = GetNum(var_14); var_20 = Num % var_9C; temp = var_20; temp %= 36; if ( temp == 0 ) { continue; } else { // loc_41330F Num = GetNum(var_20); var_2C = Num % var_9C; temp = var_2C; temp %= 36; if ( temp == 0 ) { continue; } else { // loc_413353 Num = GetNum(var_2C); var_38 = Num % var_9C; temp = var_38; temp %= 36; if ( temp == 0 ) { continue; } else { // 第一组(前四个)注册码验证完毕并赋值 Reg[0][0] = i; Reg[0][1] = j; Reg[0][2] = m; Reg[0][3] = n; ////////////////////////////////////////////////////////////// // 这里是注册码算法的第二处验证循环,这里通过之前运算的结果, // 经过运算得到余数(edx),作为letter[]字母表的偏移,从而生成 // 注册码字符 ////////////////////////////////////////////////////////////// // loc_4133C5,第二处循环算法 for( b = 0; b < 4; b++ ) { c = 1; edx = var_20 % 36; Reg[c][b] = letter[edx]; c += 1; temp = var_20 / 36; var_20 = temp; edx = var_2C % 36; Reg[c][b] = letter[edx]; c += 1; temp = var_2C / 36; var_2C = temp; edx = var_38 % 36; Reg[c][b] = letter[edx]; temp = var_38 / 36; var_38 = temp; } // 输出已经运算完毕的注册码 for ( a = 0; a <= 3; a++) { for( b = 0; b <= 3; b++ ) { printf("%c", Reg[a][b]); } if(a != 3) { printf("-"); } } printf("\n"); } } } count--; } if ( count == 0 ) { getchar(); return 0; } } } } } return 0; }
结合之前的分析,代码并不难理解,只是各种验证与循环比较多。这里我只在乎实现,而不考虑代码的优化等问题。
图5 所生成的前10个注册码
为了测试这些注册码,我们无需重新安装游戏,因为游戏在安装时会在注册表中建立相应的键值,用于保存注册码,而游戏每次启动又会查询注册表获取该注册码,所以我们只需修改该键值即可:
图6
我不可能验证所有注册码,但是验证这十个,结果是可行的,那么可以认为上面的程序是可行的,这里不再赘述。
原文地址:http://blog.csdn.net/ioio_jy/article/details/41994047