000000000000
现在很多分享RM游戏的网站除了给游戏做一些机翻汉化,还会给游戏添加礼包码功能。
礼包码这东西需要你支付一点点小钱来获得。本来嘛,赚钱这东西,不寒碜。但是现在有些人会给游戏上强度,除了会把游戏本身自带的CG屋功能给删掉,还会给礼包码功能做加密。这样会严重影响mtools的使用。
这里我们以某催眠洗脳游戏探讨一下怎么解密出来礼包码到底是多少。
RPGMAKER MV的加载流程
RPGMAKER MV为了适应网页游戏,游戏本体本质上就是一个精简的浏览器。游戏的加载流程基本就是下面这样:
graph TB A[启动Game.exe] --> B[加载index.html]; B --> C[加载js目录下的js文件]; C --> D[根据plugins.js配置加载插件]; D --> E[加载CommonEvents.json等数据资源]; E --> G[启动游戏];
开始解密
一般来说,游戏的交互内容都是放在CommonEvents.json呢,我们打开游戏的CommonEvents.json看看里面都是什么

看上去就是加密过的字符串。RPGMV的引擎是肯定无法读取加密字符串的,只能是在通过插件在读取CommonEvents.json的过程中做解密。
类似的项目还有一个叫jsono的插件
现在我们去plugins文件夹下看看有么有什么可疑的插件。

发现有两个插件是做过混肴的。
1ActorCommand.js, 765,689 字节, 2025/10/15 22:36:57
2UTA_Commone.js, 162,306 字节, 2025/9/13 23:49:32
谁家好好的代码没事做混肴啊,看函数命名方式明显就是用JS Obfuscator进行混肴的。现在我们只需要找一个网站把这些代码还原。
这两个插件都经过多轮混肴,经过反混肴之后能发现,ActorCommand.js是插入游戏开始时推广广告用的,UTA_Commone.js是解密json用的。
1DataManager.loadDataFile = function (_0x29c38f, _0x1d2b10) {
2 var _0x36b42d = new XMLHttpRequest();
3 var _0x5b61ee = "data/" + _0x1d2b10;
4 _0x36b42d.open("GET", _0x5b61ee);
5 _0x36b42d.overrideMimeType("application/json");
6 _0x36b42d.onload = function () {
7 if (_0x36b42d.status < 400) {
8 if (_0x1d2b10 === "CommonEvents.json") {
9 window[_0x29c38f] = JSON.parse(decryptData(_0x36b42d.responseText, "_!=Yxy@F/SC?ngT-"));
10 } else {
11 window[_0x29c38f] = JSON.parse(_0x36b42d.responseText);
12 }
13 DataManager.onLoad(window[_0x29c38f]);
14 }
15 };
16 _0x36b42d.onerror = this._mapLoader || function () {
17 DataManager._errorUrl = DataManager._errorUrl || _0x5b61ee;
18 };
19 window[_0x29c38f] = null;
20 _0x36b42d.send();
21};
22function decryptData(_0x27b969, _0xe70ece) {
23 var _0x136a9a = CryptoJS.enc.Utf8.parse(_0xe70ece);
24 var _0x35fd2e = CryptoJS.AES.decrypt(_0x27b969, _0x136a9a, {
25 mode: CryptoJS.mode.ECB,
26 padding: CryptoJS.pad.Pkcs7
27 });
28 return CryptoJS.enc.Utf8.stringify(_0x35fd2e).toString(CryptoJS.enc.Utf8);
29}
30function _0x4b7682(_0x21a2c7) {
31 function _0x3ad32b(_0x6a3f48) {
32 if (typeof _0x6a3f48 === "string") {
33 return function (_0x277c2c) {}.constructor("while (true) {}").apply("counter");
34 } else if (("" + _0x6a3f48 / _0x6a3f48).length !== 1 || _0x6a3f48 % 20 === 0) {
35 (function () {
36 return true;
37 }).constructor("debuggergger").call("action");
38 } else {
39 (function () {
40 return false;
41 }).constructor("debuggergger").apply("stateObject");
42 }
43 _0x3ad32b(++_0x6a3f48);
44 }
45 try {
46 if (_0x21a2c7) {
47 return _0x3ad32b;
48 } else {
49 _0x3ad32b(0);
50 }
51 } catch (_0x41298f) {}
52}
53function _0x460038(_0x4b0d11) {
54 function _0x39826c(_0x4ab110) {
55 if (typeof _0x4ab110 === "string") {
56 return function (_0x5809f9) {}.constructor("while (true) {}").apply("counter");
57 } else if (("" + _0x4ab110 / _0x4ab110).length !== 1 || _0x4ab110 % 20 === 0) {
58 (function () {
59 return true;
60 }).constructor("debuggergger").call("action");
61 } else {
62 (function () {
63 return false;
64 }).constructor("debuggergger").apply("stateObject");
65 }
66 _0x39826c(++_0x4ab110);
67 }
68 try {
69 if (_0x4b0d11) {
70 return _0x39826c;
71 } else {
72 _0x39826c(0);
73 }
74 } catch (_0x47716a) {}
75}从上面的代码中我们能发现,如果游戏读取了CommonEvents.json,游戏就会调用decryptData函数使用密钥_!=Yxy@F/SC?ngT-对数据进行解密。
观察decryptData函数,能看到这里使用的就是AES加密。
这就简单了,随便找一个在线AES解密网站使用对应的参数解密json文件就可以了
因为国内的一些网站对解密文件的尺寸有限制,这里直接用一段代码解密json文件
1const CryptoJS = require("crypto-js");
2const fs = require("fs");
3
4function decryptData(encryptedData, key) {
5 var decKey = CryptoJS.enc.Utf8.parse(key);
6 var decryptedData = CryptoJS.AES.decrypt(encryptedData, decKey, {
7 mode: CryptoJS.mode.ECB,
8 padding: CryptoJS.pad.Pkcs7,
9 });
10 return CryptoJS.enc.Utf8.stringify(decryptedData).toString(CryptoJS.enc.Utf8);
11}
12
13try {
14 const encryptedData = fs.readFileSync("./CommonEvents.json", "utf8");
15
16 const decryptedData = decryptData(encryptedData, "_!=Yxy@F/SC?ngT-");
17
18 fs.writeFileSync("./dec_CommonEvents.json", decryptedData);
19
20 console.log("Success")
21} catch (err) {
22 console.error("Error", error.message);
23}
这里我们已经完成了json的解密。
继续找到礼包码的地方
一般情况,礼包码都存在CommonEvents.json的最后一个节点中。我们稍微格式化一下看看:

这里能看到最终礼包码判断位置,但是XYOU2这个变量是哪里来的?
我们往上翻一下就能看到,这里依然使用了JS Obfuscator进行了混肴:

我们继续使用老办法还原一下代码。
1$customVariables.setValue("XYOU2", ($gameVariables.value($customVariables.getValue("XYOU1")) ^ $gameVariables.value($customVariables.getValue("XYOU1")) * 8 + 6) % 1000000);这里能看到 XYOU2这个变量是XYOU1计算出来的
1$customVariables.setValue("XYOU1", _0x3029a4.length - _0x57a0e3 - 1);XYOU1就是玩家输入的变量。
现在我们知道XYOU2最后的值为891882,这里我们直接用穷举法穷举出来XYOU1的值就好了
1for (var i = 0; i < 999999; i++) {
2 var k = (i ^ (i * 8 + 6)) % 1000000;
3 if (k === 891882) {
4 console.log(i, k);
5 break;
6 }
7}最后我们得到礼包码为:335884 。
在游戏内验证成功

总结一下
实际上,我们还能继续改上面的各种代码来提升游戏的运行效率,这些就交给有爱的人去做吧。
