【技术教程】关于RPG Maker mv 游戏逆向思路
”末法时代,没有汉化,最可靠的还是自己”
1.背景
大家好??,我又来了
本来想直接把我Blog的RMMV逆向的文章链接粘贴复制的,但是考虑到那边排版太乱了,就重新写一篇吧。
事出有因,看到评论区【RPG/PC/本人汉化(试作)】煌星のヴィクトリアーズ【秒传/1.07G】有人也想动手汉化rpgmaker系列的游戏。所以就发了这篇类似于教人开坑指南的玩意儿,顺便给站里的冷宫技术教程区添篇文章。
授人以鱼不如授人以渔,如果营造出站里有那么几个能够做个人汉化然后站内分享出来的环境,那还挺不错的。但是我能力有限,写的东西也很有限,希望如果有大佬也能多多交流,欢迎交流。
需要注意的是:
其实大部分RPGMAKER游戏都没加密,直接就能去data文件夹下找到文件进行汉化即可,一般都是对json文件进行汉化或者csv。
有的RPGMaker游戏不一定适用于本文的逆向方法,而是使用网络上现成的RPGMAKER游戏的解包软件。所以本文适用有一定范围,当那些软件无法起作用时(比如上文的游戏)便可以试着自己去逆向
好了,不废话,让我们开始吧
本文所需前置技能:一定的编码语言基础
2.信息搜集
- RPGMAKER
首先我们来看看维基坤上对RPGMAKER的描述(此处引用的是RPGMAKER MZ)
RPG Maker MZ是一款角色扮演遊戲的製作工具,為RPG Maker MV的後繼版本。於2020年8月20日發售
可以看出是MV后继版本,而且在介绍中没有发现有关MZ版本对MV大修的内容,那我们继续来看RPGMAKER MV在维坤上的介绍
名稱中的「MV」是指「Multi-View」,即為「多觀點」的意思。遊戲可以發布在Windows、Mac、Android、iOS、網頁瀏覽器等平台。
基本型態是由HTML5與JavaScript組成,專為在網頁瀏覽器上遊玩所設計,經過轉換也能夠發布在各個平台
Windows與Mac版使用NW.js格式發布。Android與iOS版使用Crosswalk與Apache Cordova格式發布。Linux與Windows商店版雖然尚未支援,但如果開發環境符合,是可以自行發布的。
在不同平台上,被讀取的音樂格式是不同的。Windows版使用ogg格式、手機板使用m4a格式。如果沒有必要的文件,遊戲會強制停止。但在測試時難以察覺,這點要注意。
”专门为在网页浏览器”,”HTML5”,”JS”,这段文字告诉我们RPGMAKER做的的游戏看来是可以到浏览器上直接玩的
- RPGMAKER 工程目录
RPGMAKER系列大多数工程目录结构大致像是这样(这边拿的上面提到的游戏举例,忽视data_encrypted)
audio放音频相关的|css样式表|data一般存游戏数据相关的(比如敌人数据,物品数据,武器数据等)|img是图像|js是游戏逻辑代码
3.欲善其事必利其器
- 工具
对于一般软件去逆向有很多工具比如ollydbg什么的,但是对于RPGMAKER系列的游戏用ollydbg首先是无法用的,而且很复杂,所以不推荐。回到2.中我们提到过游戏由js组成并且可以在浏览器运行,同时还发现游戏目录下有个index.html(如果有web开发经验的估计很熟悉这个结构)。
所以现在的任务是让游戏在浏览器上运行,同时配以浏览器F12开发人员工具,那么后续逆向游戏的基础地基就有了,我们可以直接在浏览器上进行调代码
*使用工具VSCODE、某一款浏览器
- 配置
我们把index.html直接点开康康,发生了问题
我们按F12看看控制台报错
index.html:28 Access to XMLHttpRequest at ‘file:///D:/Game/victorias_official_v100/package.nw/js/main.js’ from origin ‘null’ has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, isolated-app, chrome-extension, chrome-untrusted, https, edge.
这里这个报错指的是跨域错误,CORS是一种安全机制,限制浏览器跨域访问资源的能力。简单来说就是浏览器并没有权限去读你的index.html
网上搜一下,解除跨域限制访问本地文件,很快找到了适配EDGE浏览器的解法(注意不同浏览器由于内核不同,所以参数可能不同,请使用自己浏览器对应的参数)
- 找到C:/下你的edge浏览器的exe位置(注意,这里是exe而不是快捷方式),msedge.exe
- 右键发送快捷方式到桌面
- 右键桌面上我们创建的快捷方式 -> ‘属性’ ->‘目标’ 后加上
(空格)–disable-web-security –user-data-dir=(路径)
//(注意:前面有个空格和前面原有的字符串隔开)
//(注意:路径处你可以随便换一个你创建的临时文件夹的路径)
//比如我的后面是这样 –disable-web-security –user-data-dir=C:UsersADMINDesktopSandbox
- 然后将我们的index.html拖到这个快捷方式上来打开,游戏成功运行于浏览器
按F12打开控制台看看
好了,现在环境OK。剩下的下个VSCODE然后随意装个JAVASCRIPT插件就行,VSCODE用于我们编辑代码用(不会有人想用记事本吧,你用你喜欢的编辑器也可以)
4.顺藤摸瓜
现在开始的步骤也只能是提供类似的大致流程和思路,不同的游戏加密细节可能不同,但是主要思路还是一样的。下面是以【RPG/PC/本人汉化(试作)】煌星のヴィクトリアーズ【秒传/1.07G】 为例的逆向过程,仅供参考。
现在问题是我们知道哪些文件被加密了,既然游戏运行时能个正常输出文本,那说明游戏在加载时一定有某个函数将密文解密了,然后加载解密资源。所以我们的目标先是找到这个函数可能是在哪。
- 顺藤摸瓜1-寻找 csv 文件对应的解密函数
关于一般的逆向可以有火绒剑等工具找到进程所用文件,很幸运浏览器很方便:F12打开控制台->网络->浏览器F5刷新 便能够看到哪些文件被加载了以及加载顺序。我们目前目标是目录下那个叫 ExternMessage.csv 的那个文件,所以控制台上面有个搜索框,我们键入Ex搜索发现它出现了,我们选中它,右侧出现了涉及到这个文件的函数调用堆栈(这里涉及了点计算机知识)
现在有人问看哪个函数?我们从上往下看层层递进直到找到我们的目标,我们先看第一个 DataEncryption.js 名字很可疑啊,点右边同行蓝字可以找到这个文件在哪一行与我们的这个 ExternMessage.csv 产生了关系,然后我们在VSCODE打开同名的这个js找到这边对应的行数 128 行 t.send() 感觉没啥用,我们将这个函数往前看
(DataManager.loadCSVFile = function (e, n) {var t = new XMLHttpRequest(),i = “data/” + n;t.open(“GET”, i),t.overrideMimeType(`text/plain; charset=${$externMessage.CsvFileEncode}`),(t.data-filter = function () {if (t.status < 400)if (o(e)) {if (r) var n = performance.now();
console.log(t) 添加输出看看var i = a(t.responseText);r && performance.now() – n, (window[e] = i);} else window[e] = t.responseText;}),(t.data-filter =this._mapLoader ||function () {var a = “Test_”;0 === n.indexOf(a)? DataManager.loadDataFile(e, n.substring(a.length)): (DataManager._errorUrl = DataManager._errorUrl || i);}),(window[e] = null),t.send();});
大致看一眼,发现这个代码块大概是通过输入的参数e和n然后发起了某个请求,在请求得到响应后又干了其他的活,我们console.log(t) 输出t这个对象的内容到控制台看看 F12打开控制台-> F5刷新游戏页面,此时控制台将会出现一个 XMLHttpRequest对象的输出,点开内部有一个response属性后跟了一长串不明字符,如果你记得清楚的话,这个字符就是csv文件里面的乱码内容。
我们再审视下调用堆栈,这里是栈顶,没有后续函数来接手参与解码工作了,证明密文一定是在这个 DataEncryption.js 中处理得到明文,我们看下这个 DataEncryption.js 文件,此时文件顶部一个 26行 奇怪的函数引起注意,这种涉及到移位运算的函数要么是涉及到随机数运算要么涉及到加密解密
var s,l = null,c = 146103486,d = 950312395,f = 720541948,p = Number(a.seed),u = 0;function g(a) {if (!l || u <= a – 1)for (l = l || []; u <= a – 1; u = (u + 1) | 0) {var e = c ^ (c << 11);(c = d),(d = f),(f = p),(p = p ^ (p >>> 19) ^ e ^ (e >>> 8)),(l[u] = (Math.abs(p) % 1e5) / 1e5);}return l;}
文件内匹配搜索 g 函数,发现就在我们先前console.log(t)上面的一个a函数里出现了g函数
我们 console.log(t) 输出t得到 t.responseText属性包含的是乱码,本文件一定有一个解码函数接受了乱码然后解码,而g函数可能与解码函数相关,g被a使用,而a恰好也是奇怪运算。重要的是
var i = a(t.responseText);
出现了以上调用,所以推测 a与g都是涉及解密的工作,g为a提供服务,a是最上层,然后a(t.responseText)可能做的就是解码工作,OK,得到这点,我们输出一下a(t.responseText)
(DataManager.loadCSVFile = function (e, n) {var t = new XMLHttpRequest(),i = “data/” + n;t.open(“GET”, i),t.overrideMimeType(`text/plain; charset=${$externMessage.CsvFileEncode}`),(t.data-filter = function () {if (t.status < 400)if (o(e)) {if (r) var n = performance.now();var i = a(t.responseText);console.log(a(t.responseText)) 输出r && performance.now() – n, (window[e] = i);} else window[e] = t.responseText;}),(t.data-filter =this._mapLoader ||function () {var a = “Test_”;0 === n.indexOf(a)? DataManager.loadDataFile(e, n.substring(a.length)): (DataManager._errorUrl = DataManager._errorUrl || i);}),(window[e] = null),t.send();});
F12打开控制台->F5刷新游戏
紧皱的眉头舒展开来,我们的推测完全没有错,正确的文本验证了我们的猜想。
- 顺藤摸瓜2-脚本处理文件、反解码
我们有了a函数和g函数,我们只需要通过python重现这个函数逻辑然后完成一个批处理脚本即可。脚本编写不是重点所以不做过多叙述(但是需要注意这里g函数使用了 p = Number(a.seed) 这个p变量,其实这里p是个定值,只需要console.log(p)在控制台得到这个值,然后再脚本重现时把p换成这个值即可,千万不要直接这样用,pyhton和js随机数逻辑可能不一样,python执行这个方法先不说能不能用,即使能用,产生的p值都可能不一样)
….一段时间解码完成后….好,我们得到了data_encrypted文件夹下所有文件的解密结果
现在我们如何实现我们的解密然后汉化方案呢??
思路1: 将密文解密后拿去汉化,然后将汉化文本通过原本加密函数加密,使程序读取密文,然后自己解密
思路2: 将密文解密后拿去汉化,然后将程序中解密函数去掉,使程序直接读取明文
显然在没有加密函数情况下思路二是最方便的,同时我也没能找到加密函数,所以选取思路2
修改游戏源码,只要在那个块中涉及到a调用的都换掉直接读取明文
(DataManager.onXhrLoad = function (e, t, i, s) {if (o(t)) {if (r) var l = performance.now();// var c = a(e.responseText);var c = e.responseText;r && performance.now() – l,(window[t] = JSON.parse(c)),this.data-filter(window[t]);} else n.call(this, …arguments);}),(DataManager.loadCSVFile = function (e, n) {var t = new XMLHttpRequest(),i = “data/” + n;t.open(“GET”, i),
`text/plain; charset=${$externMessage.CsvFileEncode}`),(t.data-filter = function () {if (t.status < 400)if (o(e)) {if (r) var n = performance.now();// var i = a(t.responseText);var i = t.responseText;r && performance.now() – n, (window[e] = i);} else window[e] = t.responseText;}),(t.data-filter =this._mapLoader ||function () {var a = “Test_”;0 === n.indexOf(a)? DataManager.loadDataFile(e, n.substring(a.length)): (DataManager._errorUrl = DataManager._errorUrl || i);}),(window[e] = null),t.send();});
大功告成,把我们解码的文件将游戏原来的乱码文件替换后,重新打开网页运行游戏,正常运行(我不想贴图了)
5.总结
其实这一个思路还能用于本游戏的图片解码和音频解码,涉及到的解密函数可能不一样,但是整个流程思路还是一样,即通过观察函数调用栈从栈顶往前推,有需要console.log的地方就consloe.log一下
大概内容就这样,如果有人还有兴趣的话&我有空的话,我也把后面解码图片的过程也可以做出来。
第一次逆向RPGMaker mz系列的游戏以及汉化的教程,有不清楚的地方欢迎交流,也请大家多多指点
?晚安
2023.07.24 深夜
observer