了解RPG Maker MV的文件建构
上一篇文章,我们已经成功在PC上运行了游戏,那我们如何对游戏进行逆向呢?
首先要了解正常的RPG Maker MV制作的游戏应该具有哪些文件,以及他的结构
那如何了解他的结构呢,很简单,我们只需要用RPG Maker MV创建一个默认工程,来看看一个游戏的最简结构是怎么样的
创建新项目
项目创建完成
查看项目目录结构
是不是和之前解包出来的很像呢?
通过目录名字可以知道
目录 | 用途 |
---|---|
audio | 音频资源 |
data | 数据资源 |
fonts | 字体资源 |
icon | 图标资源 |
img | 图片资源 |
js | 脚本资源 |
movies | 动画资源 |
我们进入data目录看看数据资源长什么样
都是json文件(一种资源交换的文件格式)而且命名都很规范,我们打开Weapons.json来看看都有什么武器
1 2 3 4 5 6 7 | [ null, {"id":1,"animationId":6,"description":"","etypeId":1,"traits":[{"code":31,"dataId":1,"value":0},{"code":22,"dataId":0,"value":0}],"iconIndex":97,"name":"剑","note":"","params":[0,0,10,0,0,0,0,0],"price":500,"wtypeId":2}, {"id":2,"animationId":6,"description":"","etypeId":1,"traits":[{"code":31,"dataId":1,"value":0},{"code":22,"dataId":0,"value":0}],"iconIndex":99,"name":"斧","note":"","params":[0,0,10,0,0,0,0,0],"price":500,"wtypeId":4}, {"id":3,"animationId":1,"description":"","etypeId":1,"traits":[{"code":31,"dataId":1,"value":0},{"code":22,"dataId":0,"value":0}],"iconIndex":101,"name":"杖","note":"","params":[0,0,10,0,0,0,0,0],"price":500,"wtypeId":6}, {"id":4,"animationId":11,"description":"","etypeId":1,"traits":[{"code":31,"dataId":1,"value":0},{"code":22,"dataId":0,"value":0}],"iconIndex":102,"name":"弓","note":"","params":[0,0,10,0,0,0,0,0],"price":500,"wtypeId":7} ] |
嗯,四种基本的武器
从RPG Maker MV里来看一看是怎么样的形式
选择数据库
与我们刚刚看到的json文件完全吻合
之前解包出来的文件具有相同的目录结构,那我们是不是可以直接将刚才解包的数据拷贝到当前目录下,然后用RPG Maker MV来打开,这样整个游戏我们不是可以为所欲为了吗
心动不如行动,将解压出来的文件全部拷贝到我们新建的项目目录下,并选择替换已存在的文件
使用RPG Maker MV重新打开项目(资源重加载)
一打开心就凉了,居然还是默认初始工程的资源文件
重新来观察一下游戏的目录结构
看到有一个
我们是否就这样束手无策了呢?
我们不妨来思考一下,既然游戏能在本地运行,那数据必然是在启动游戏后解密加载的,一个单机游戏的解密过程必然是在本地进行的
那我们如何寻找解密逻辑呢?
思路一
之前有提过所有的逻辑都是由JavaScript编写的,回忆一下有一个叫
我们在
可以说是开幕雷击了,压缩成一行的代码,外加毫无意义的函数名,摆明了就是告诉你代码经过混淆了(注意:混淆不是加密,混淆是指替换变量名变为人不能直接理解的,并且调整代码顺序,最终的结果是PC看得懂,人看不懂。顺便一提,复杂的混淆是以消耗运行效率为代价的,而且被解析是必然的,无非是花多少心思,所以也不是越复杂的混淆越好,要兼顾程序性能)
我们难道要止步于此了吗,不,还不能放弃,使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | DataManager.loadDataFile = function (a, d) { var h = new XMLHttpRequest, k = "data/" + d; Decrypter.hasEncryptedData && !Decrypter.checkImgIgnore(k) ? (k = Decrypter.extToEncryptExt(k), h.open("GET", k), h.responseType = "arraybuffer", h.onload = function () { 400 > h.status && (window[a] = JSON.parse(Decrypter.decryptText(h.response)), DataManager.onLoad(window[a])) }) : (h.open("GET", k), h.overrideMimeType("application/json"), h.onload = function () { 400 > h.status && (window[a] = JSON.parse(h.responseText), DataManager.onLoad(window[a])) }); h.onerror = function () { DataManager._errorUrl = DataManager._errorUrl || k }; window[a] = null; h.send() }; |
嗯,似乎这里是在加载数据文件,但这代码乱七八糟,如何下手呢
在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | DataManager.loadDataFile = function(name, src) { var xhr = new XMLHttpRequest(); var url = 'data/' + src; xhr.open('GET', url); xhr.overrideMimeType('application/json'); xhr.onload = function() { if (xhr.status < 400) { window[name] = JSON.parse(xhr.responseText); DataManager.onLoad(window[name]); } }; xhr.onerror = this._mapLoader || function() { DataManager._errorUrl = DataManager._errorUrl || url; }; window[name] = null; xhr.send(); }; |
虽然没有学过
对比看看上面的加密版本
很明显了,这个
在
得到如下结果
1 2 3 | Decrypter.decryptText = function (a) { return this.decrypt(a, 1, "t") }; |
那也就是说应该有一个
继续搜索,但遗憾的是我们这次的搜索没有任何结果,至此我们没有线索了,怎么办?
注意到
尝试搜索
找到
看作者描述
1 2 3 4 5 | 大概功能: * 加入本插件,并设置为on * 进入游戏,f8,使用 Decrypter.startEncrypt() 生成加密文件夹, * 使用 Decrypter.saveMY("test","miyao") //参数可更改 * 即可生成 以"test"加密的miyao.js |
嗯,运气不错,这个游戏大概率是使用这个加密的,作者提供了附件,我们下载下来看看
压缩包里有一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | /** * 读取数据文件 * @param {string} name 名称 * @param {string} src 地址 */ DataManager.loadDataFile = function(name, src) { var xhr = new XMLHttpRequest(); var url = 'data/' + src; if (Decrypter.hasEncryptedData && !Decrypter.checkImgIgnore(url)) { var url = Decrypter.extToEncryptExt(url) xhr.open('GET', url); xhr.responseType = "arraybuffer" xhr.onload = function() { if (xhr.status < 400) { window[name] = JSON.parse(Decrypter.decryptText(xhr.response)); DataManager.onLoad(window[name]); } }; } else { xhr.open('GET', url); xhr.overrideMimeType('application/json'); xhr.onload = function() { if (xhr.status < 400) { window[name] = JSON.parse(xhr.responseText); DataManager.onLoad(window[name]); } }; } xhr.onerror = function() { DataManager._errorUrl = DataManager._errorUrl || url; }; window[name] = null; xhr.send(); }; |
嗯,未经混淆的
但在这个文件中我们依然搜索不到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | * 使用: * 加入本插件,并设置为on * 进入游戏,f8,使用 Decrypter.startEncrypt() 生成加密文件夹, * 使用 Decrypter.saveMY("test","miyao") //参数可更改 * 即可生成 以"test"加密的miyao.js * * * 发布时, * 将本插件删除, * 将DecrypterPlayer插件(可以改名)加入并设置好 * 将上面生成的miyao插件加入 * 将本插件从游戏文件中删除,将已经加密的文件从游戏文件中删除 * 进入游戏时将提示输入密钥,如上例则输入 test * 即可进入游戏, |
果不其然,我们又有了下一条线索,加密后会生成一个miyao,那么这个解密函数相比是存在这个miyao里了,继续去
经过不懈努力,我们发现一个文件
豁,一样是经过了压缩和混淆的,那看来就是这个文件了,不然也没必要混淆
继续,先格式化,然后搜索
得到如下结果
1 2 3 4 5 6 7 8 9 10 11 12 13 | b.decrypt = function (a, e, c, d, g) { if (!a) return null; c = b.rm(c); a = b.ab(a); if (c && (a = b.d.use(a, c, d, g), !a)) throw Error("Decrypt is wrong"); return e ? 1 == e ? b.d.tu(b.bt(a)) : a : b.ba(a) }; b.load = function () { b.m = JSON.parse(b.m2); b.h = b.uh ? b.mh(b.h2) : b.t2b(b.h2); b.k = b.t2b(b.k2) }; Decrypter.decrypt = b.decrypt.bind(b); |
很好,一切都如我们所料,果然找到了关键的地方,可是,居然是混淆过的,这可让人如何是好…
其实我们大可转换思路,我们的目的是解密文件,不是搞清楚他的加解密算法,所以,我们大可直接调用这里的解密函数,对每一个加密文件进行解密
给出部分代码,大致思路:遍历加密文件夹,每个文件根据对应的类型调用对应的解密函数
写的很丑,凑活看吧,冗余的地方很多,可以精简
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | function readDir(path) { fs.readdir(path, function (err, menu) { if (!menu) return; menu.forEach(function (ele) { fs.stat(path + "/" + ele, function (err, info) { if (info.isDirectory()) { readDir(path + "/" + ele); } else { var extname = pathm.extname(ele) var encryptData = null fs.readFile(path + '/' + ele, function (err, data) { if (err) { return console.error(err) } encryptData = data if (extname == '.rmd') { var decryptData = decryptText(encryptData) fs.writeFile(path + '/' + pathm.basename(ele, '.rmd') + '.json', decryptData, function (err) { if (err) { console.error(err) } }) } if (extname == '.rmp') { var decryptData = decryptArrayBuffer(encryptData) fs.writeFile(path + '/' + pathm.basename(ele, '.rmp') + '.png', decryptData, function (err) { if (err) { console.error(err) } }) } if (extname == '.rmm') { var decryptData = decryptArrayBuffer(encryptData) fs.writeFile(path + '/' + pathm.basename(ele, '.rmm') + '.m4a', decryptData, function (err) { if (err) { console.error(err) } }) } if (extname == '.rmo') { var decryptData = decryptArrayBuffer(encryptData) fs.writeFile(path + '/' + pathm.basename(ele, '.rmo') + '.ogg', decryptData, function (err) { if (err) { console.error(err) } }) } }) } }) }) }) } |
执行这个代码,所有的文件就被解密了,要求有node.js运行环境(让js可以本地运行,不依托浏览器)
将解密的文件拷贝到项目目录下,重新用RPG Maker MV加载项目
打开数据库,能看到所有游戏数据,至此,整个逆向完成
想必你听说过顶尖黑客必修社会工程学,通过阅读本篇文章,想必你也对这句话有了自己的理解,有时候技术只是细枝末节,而黑客往往能通过蛛丝马迹,摸索着一点点的线索,获得想要的信息。加密插件的作者想必也不会想到因为一句注释,顺藤摸瓜,还原了整个加解密过程,如果你对加密过程感兴趣,可以去研究我们下载到的js文件
下篇我们讲一讲如果没有通过注释去搜索,没有找到原版加密文件,我们如何继续逆向,是否就束手无策了呢?