一、前言

本文是逆向分析CM4系列的最后一篇,我会将该游戏的序列号验证机制分析完毕,进而编写出注册码生成器。

二、分析第二个验证循环

延续上一篇文章的内容,来到如下代码处:

图1

上述代码并没有特别需要注意的地方,只是知道了接下来的循环需要执行4次。下面就是重要的验证部分:

图2

这是注册码中第二组四个字符的生成代码,主要是利用[ebp+var_20]进行运算,将结果作为字符串的偏移值,从而得到注册码。回顾一下,这里的[ebp+var_20]是之前运算所得到的余数,可见这个游戏的验证过程中的取余运算还是比较多的。接下来的两段代码,与图2代码较为类似:

图3

图4

上述两段代码在取余并获取相应字符的同时,还更改了[ebp+var_20]、[ebp+var_2C]与[ebp+var_38]中的值,用于接下来的运算,由于比较简单,这里就不再赘述。

        至此,CM4的注册码验证机制彻底分析完毕,那么接下来就可以开始注册码生成器的编写了。

三、编写注册码生成器

结合之前的分析,我们很容易就可以编写出注册机。但是要注意,我们在生成注册码的时候,是需要利用“cm4.epe”这个文件的,需要将二者放置在同一目录,让注册机程序便于读取“密码本”中的内容以进行运算,代码如下:

  1. #include<stdio.h>
  2. #include<windows.h>
  3. //////////////////////////////////////////////////////////////
  4. // GetNum函数用于计算cm4.epe文件中相应偏移值处的DWORD大小的
  5. // 十六进制数值,用于接下来的运算,该函数有一个参数var,保存
  6. // 有偏移值
  7. //////////////////////////////////////////////////////////////
  8. DWORD GetNum( DWORD dwOffset )
  9. {
  10. HANDLE hFile = NULL;
  11. DWORD dwSigNum = 0; // 用于保存位于偏移位置的DWORD字节的内容
  12. DWORD dwNum = 0; // 恒为0,用作ReadFile的参数
  13. // 打开名为cm4.epe的文件,该文件与本程序应处于同一目录下
  14. hFile = CreateFile("cm4.epe",
  15. GENERIC_READ,
  16. 0,
  17. NULL,
  18. OPEN_EXISTING,
  19. FILE_ATTRIBUTE_NORMAL,
  20. NULL
  21. );
  22. // 如果文件打开失败,则提示出错信息并退出
  23. if (hFile == INVALID_HANDLE_VALUE)
  24. {
  25. printf("Could not open cm4.epe\n");
  26. return 0;
  27. }
  28. // 设置文件指针到指定的位置
  29. SetFilePointer(hFile, dwOffset, 0, FILE_BEGIN);
  30. // 读取起始于文件指针位置的十六进制代码,读取长度为4个字节(DWORD)
  31. ReadFile(hFile, &dwSigNum, sizeof(DWORD), &dwNum, NULL);
  32. CloseHandle(hFile);
  33. return dwSigNum;
  34. }
  35. int main()
  36. {
  37. int a, b, c; // 用于控制循环次数
  38. int i, j, m, n; // 用于保存第一组验证码的四个ASCII码值
  39. int count = 10; // 用于保存生成的注册码的组数
  40. int tmp[4]; // 用于临时保存前四位验证码的ASCII码减去0x30或0x37后的值
  41. int temp; // 用于保存临时的运算结果
  42. int edx; // 用于保存运算的余数
  43. DWORD Num; // 用于保存位于cm4.epe相应偏移处的十六进制代码
  44. DWORD var_9C = 0x800000; // 这是一个固定的值,作为之后验证中的除数
  45. DWORD var_14; // 用于保存第一循环算法最终运算的结果
  46. DWORD var_20 = 0;
  47. DWORD var_2C = 0;
  48. DWORD var_38 = 0; // 这三个变量用于保存第二循环算法中的运算结果
  49. char Reg[4][4] = { "0" };// 这个二维数组保存最终得出的注册码
  50. char letter[37] = { "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" };// 字母表,用于生成注册码
  51. //////////////////////////////////////////////////////////////
  52. // 这里是注册码算法的第一处验证循环,这里通过四重循环,来不断
  53. // 验证各种不同的ASCII码值的组合,也就是从0000到ZZZZ,从而生成
  54. // 第一组的验证码(4个字符)
  55. //////////////////////////////////////////////////////////////
  56. // 此处循环生成第一组验证码的第一个字符
  57. for ( i = 48; i <= 90; i++ )
  58. {
  59. if( i >= 58 && i <= 64 ) continue;
  60. // 如果注册码是数字,则减去48
  61. if( i >= 48 && i <= 57 )
  62. {
  63. tmp[0] = i - 48;
  64. }
  65. // 如果注册码是大写字母,则减去55
  66. else
  67. {
  68. tmp[0] = i - 55;
  69. }
  70. // 此处循环生成第一组验证码的第二个字符
  71. for ( j = 48; j <= 90; j++ )
  72. {
  73. if( j >= 58 && j <= 64 ) continue;
  74. // 如果注册码是数字,则减去48
  75. if( j >= 48 && j <= 57 )
  76. {
  77. tmp[1] = j - 48;
  78. }
  79. // 如果注册码是大写字母,则减去55
  80. else
  81. {
  82. tmp[1] = j - 55;
  83. }
  84. // 此处循环生成第一组验证码的第三个字符
  85. for ( m = 48; m <= 90; m++ )
  86. {
  87. if( m >= 58 && m <= 64 ) continue;
  88. // 如果注册码是数字,则减去48
  89. if( m >= 48 && m <= 57 )
  90. {
  91. tmp[2] = m - 48;
  92. }
  93. // 如果注册码是大写字母,则减去55
  94. else
  95. {
  96. tmp[2] = m - 55;
  97. }
  98. // 此处循环生成第一组验证码的第四个字符
  99. for ( n = 48; n <= 90; n++ )
  100. {
  101. if( n >= 58 && n <= 64 ) continue;
  102. // 如果注册码是数字,则减去48
  103. if( n >= 48 && n <= 57 )
  104. {
  105. tmp[3] = n - 48;
  106. }
  107. // 如果注册码是大写字母,则减去55
  108. else
  109. {
  110. tmp[3] = n - 55;
  111. }
  112. var_14 = 0;
  113. // 按照算法进行运算
  114. for( a = 3; a >= 0; a-- )
  115. {
  116. var_14 *= 36;
  117. var_14 += tmp[a];
  118. }
  119. if (var_14 % 36 != 0 )
  120. {
  121. // loc_4132CB
  122. Num = GetNum(var_14);
  123. var_20 = Num % var_9C;
  124. temp = var_20;
  125. temp %= 36;
  126. if ( temp == 0 )
  127. {
  128. continue;
  129. }
  130. else
  131. {
  132. // loc_41330F
  133. Num = GetNum(var_20);
  134. var_2C = Num % var_9C;
  135. temp = var_2C;
  136. temp %= 36;
  137. if ( temp == 0 )
  138. {
  139. continue;
  140. }
  141. else
  142. {
  143. // loc_413353
  144. Num = GetNum(var_2C);
  145. var_38 = Num % var_9C;
  146. temp = var_38;
  147. temp %= 36;
  148. if ( temp == 0 )
  149. {
  150. continue;
  151. }
  152. else
  153. {
  154. // 第一组(前四个)注册码验证完毕并赋值
  155. Reg[0][0] = i;
  156. Reg[0][1] = j;
  157. Reg[0][2] = m;
  158. Reg[0][3] = n;
  159. //////////////////////////////////////////////////////////////
  160. // 这里是注册码算法的第二处验证循环,这里通过之前运算的结果,
  161. // 经过运算得到余数(edx),作为letter[]字母表的偏移,从而生成
  162. // 注册码字符
  163. //////////////////////////////////////////////////////////////
  164. // loc_4133C5,第二处循环算法
  165. for( b = 0; b < 4; b++ )
  166. {
  167. c = 1;
  168. edx = var_20 % 36;
  169. Reg[c][b] = letter[edx];
  170. c += 1;
  171. temp = var_20 / 36;
  172. var_20 = temp;
  173. edx = var_2C % 36;
  174. Reg[c][b] = letter[edx];
  175. c += 1;
  176. temp = var_2C / 36;
  177. var_2C = temp;
  178. edx = var_38 % 36;
  179. Reg[c][b] = letter[edx];
  180. temp = var_38 / 36;
  181. var_38 = temp;
  182. }
  183. // 输出已经运算完毕的注册码
  184. for ( a = 0; a <= 3; a++)
  185. {
  186. for( b = 0; b <= 3; b++ )
  187. {
  188. printf("%c", Reg[a][b]);
  189. }
  190. if(a != 3)
  191. {
  192. printf("-");
  193. }
  194. }
  195. printf("\n");
  196. }
  197. }
  198. }
  199. count--;
  200. }
  201. if ( count == 0 )
  202. {
  203. getchar();
  204. return 0;
  205. }
  206. }
  207. }
  208. }
  209. }
  210. return 0;
  211. }

结合之前的分析,代码并不难理解,只是各种验证与循环比较多。这里我只在乎实现,而不考虑代码的优化等问题。

四、程序测试

这里我先生成10个注册码。由于后三组注册码字符是严格取决于第一组注册码字符的取值的,而第一组注册码字符的取值范围是在0000到FFFF之间,那么我这里生成的10个注册码其实也就所有注册码中的前十个,运行结果如下:

图5 所生成的前10个注册码

为了测试这些注册码,我们无需重新安装游戏,因为游戏在安装时会在注册表中建立相应的键值,用于保存注册码,而游戏每次启动又会查询注册表获取该注册码,所以我们只需修改该键值即可:

图6

我不可能验证所有注册码,但是验证这十个,结果是可行的,那么可以认为上面的程序是可行的,这里不再赘述。

五、后记

这三篇关于CM4逆向分析的文章,可以说是我到目前为止写作时间跨度最长的了。本系列的上篇,我是在研究完FIFA07《仙剑奇侠传》之后就打算写作的,大概是在今年6月中旬的时候。那时写了个初稿,也就是通过“爆破”的方式来绕过验证机制,想继续写注册机的编写部分,无奈水平有限,一直拖到现在才完成。这大半年的时间也是我的知识水平增长最快的半年,要不然我依旧不能分析出CM4的验证机制。这三篇文章的中篇和下篇,是利用了一个多星期的时间突击完成的,这段时间每天只有很少的时间用于分析,而且分析过程中也走了非常多的弯路,也正是因为这些弯路,我才能够在文章中始终展示出一片坦途。因为是第一次分析注册码验证机制,没有经验,耗时也比较长。但是我的收获却是巨大的。我也希望各位读者也能够从这些文章中有所收获,多多练习,多多思考,不断尝试,将自己所学,真正运用于自己的身边以及自己的工作中。

逆向工程第005篇:跨越CM4验证机制的鸿沟(下)的更多相关文章

  1. 逆向工程第004篇:跨越CM4验证机制的鸿沟(中)

    一.前言 在上一篇文章的最后,我已经找出了关键的CALL语句,那么这篇文章我就带领大家来一步一步地分析这个CALL.我会将我的思路完整地展现给大家,因此分析过程可能略显冗长,我会分为两篇文章进行讨论. ...

  2. 逆向project第005篇:跨越CM4验证机制的鸿沟(下)

    一.前言 本文是逆向分析CM4系列的最后一篇,我会将该游戏的序列号验证机制分析完成,进而编写出注冊码生成器. 二.分析第二个验证循环 延续上一篇文章的内容,来到例如以下代码处: 图1 上述代码并没有特 ...

  3. 逆向工程第003篇:跨越CM4验证机制的鸿沟(上)

    一.前言 <冠军足球经理>系列作为一款拟真度极高的足球经营类游戏,赢得过无数赞誉,而CM4可以说是这个传奇的起点.但是在游戏安装过程中,当用户输入完序列号之后,程序并不会对用户的输入进行真 ...

  4. 逆向project第003篇:跨越CM4验证机制的鸿沟(上)

    一.前言 <冠军足球经理>系列作为一款拟真度极高的足球经营类游戏.赢得过无数赞誉,而CM4可以说是这个传奇的起点. 可是在游戏安装过程中.当用户输入完序列号之后.程序并不会对用户的输入进行 ...

  5. 黑客攻防技术宝典web实战篇:攻击验证机制习题

    猫宁!!! 参考链接:http://www.ituring.com.cn/book/885 随书答案. 1. 在测试一个使用joe和pass证书登录的Web应用程序的过程中,在登录阶段,在拦截代理服务 ...

  6. 通过扩展改善ASP.NET MVC的验证机制[实现篇]

    原文:通过扩展改善ASP.NET MVC的验证机制[实现篇] 在<使用篇>中我们谈到扩展的验证编程方式,并且演示了本解决方案的三大特性:消息提供机制的分离.多语言的支持和多验证规则的支持, ...

  7. 通过扩展改善ASP.NET MVC的验证机制[使用篇]

    原文:通过扩展改善ASP.NET MVC的验证机制[使用篇] ASP.NET MVC提供一种基于元数据的验证方式是我们可以将相应的验证特性应用到作为Model实体的类型或者属性/字段上,但是这依然具有 ...

  8. 本版本延续MVC中的统一验证机制~续的这篇文章,本篇主要是对验证基类的扩展和改善(转)

    本版本延续MVC中的统一验证机制~续的这篇文章,本篇主要是对验证基类的扩展和改善 namespace Web.Mvc.Extensions { #region 验证基类 /// <summary ...

  9. ASP.NET 身份验证机制

    ASP.NET提供了3种认证方式:windows身份验证:IIS根据应用程序的设置执行身份验证.要使用这种验证方式,在IIS中必须禁用匿名访问.Forms验证          :用Cookie来保存 ...

随机推荐

  1. Shiro反序列化漏洞检测、dnslog

    目录 信息收集 poc 参考 信息收集 poc # pip install pycrypto import sys import base64 import uuid from random impo ...

  2. 剑指 Offer 32 - I. 从上到下打印二叉树 + 层次遍历二叉树

    剑指 Offer 32 - I. 从上到下打印二叉树 Offer_32_1 题目描述 解题思路 这题属于简单题,考察的是我们对二叉树以及层次遍历的方法. 这里只需要使用简单的队列即可完成二叉树的层次遍 ...

  3. CodeBlocks的安装配置以及使用教程

    CodeBlocks的安装配置以及使用教程 教程写的很啰嗦,本来几句话就能搞定的,但为了照顾到那部分真正的小白还请大家见谅! 一.下载 前往CodeBlocks官网下载带编译器的版本,目前的最新版本为 ...

  4. FreeBSD pkg基础教程1

    pkg 基础教程1装上系统默认没有pkg,先获取pkg:#pkg 回车即可输入y 确认下载------------------------------------pkg使用https,先安装ssl 证 ...

  5. ArrayList 、Vector 和 LinkedList 有什么区别?

    ArrayList.Vector .LinkedList 类均在java.util 包中,均为可伸缩数组,即可以动态改变长度的数组. ArrayList 和 Vector 都是基于存储元素的 Obje ...

  6. 京东数科面试真题:常见的 IO 模型有哪些?Java 中的 BIO、NIO、AIO 有啥区别?

    本文节选自<Java面试进阶指北 打造个人的技术竞争力> 面试中经常喜欢问的一个问题,因为通过这个问题,面试官可以顺便了解一下你的操作系统的水平. IO 模型这块确实挺难理解的,需要太多计 ...

  7. gtk中构件添加背景图

    在gtk中我们总想要去给构件添加背景图,具体函数代码如下 void chang_background(GtkWidget *widget, int w, int h, const gcha r *pa ...

  8. 攻防世界 reverse elrond32

    tinyctf-2014 elrond32 1 int __cdecl main(int a1, char **arg_input) 2 { 3 if ( a1 > 1 && c ...

  9. 攻防世界 reverse debug

    debug  XCTF 3rd-GCTF-2017 .net程序,这里我用的dnspy,当然.net Reflector也很好用. 查看程序,发现是明文比较,下断,debug,完成. flag{967 ...

  10. springboot源码解析-管中窥豹系列之BeanDefine如何加载(十三)

    一.前言 Springboot源码解析是一件大工程,逐行逐句的去研究代码,会很枯燥,也不容易坚持下去. 我们不追求大而全,而是试着每次去研究一个小知识点,最终聚沙成塔,这就是我们的springboot ...