了解RPG Maker MV的文件建构
上一篇文章,我们已经成功在PC上运行了游戏,那我们如何对游戏进行逆向呢?
首先要了解正常的RPG Maker MV制作的游戏应该具有哪些文件,以及他的结构
那如何了解他的结构呢,很简单,我们只需要用RPG Maker MV创建一个默认工程,来看看一个游戏的最简结构是怎么样的
创建新项目
项目创建完成
查看项目目录结构
是不是和之前解包出来的很像呢?
通过目录名字可以知道
目录 | 用途 |
---|---|
audio | 音频资源 |
data | 数据资源 |
fonts | 字体资源 |
icon | 图标资源 |
img | 图片资源 |
js | 脚本资源 |
movies | 动画资源 |
我们进入data目录看看数据资源长什么样
都是json文件(一种资源交换的文件格式)而且命名都很规范,我们打开Weapons.json来看看都有什么武器
[
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重新打开项目(资源重加载)
一打开心就凉了,居然还是默认初始工程的资源文件
重新来观察一下游戏的目录结构
看到有一个encrypt(加密)
的文件夹,很是可疑,进入目录观察,果然,数据都被加密成为了.rmd
后缀的文件,直接编辑器打开发现文件时乱码
我们是否就这样束手无策了呢?
我们不妨来思考一下,既然游戏能在本地运行,那数据必然是在启动游戏后解密加载的,一个单机游戏的解密过程必然是在本地进行的
那我们如何寻找解密逻辑呢?
思路一
之前有提过所有的逻辑都是由JavaScript编写的,回忆一下有一个叫js
的目录就是专门存放游戏逻辑的,那么去看一看目录里的文件,是否有一些线索
我们在js\plugins
这个目录下面发现有两个文件非常可疑
DecrypterPlayer.js
这名字就让人很是怀疑,打开看看
可以说是开幕雷击了,压缩成一行的代码,外加毫无意义的函数名,摆明了就是告诉你代码经过混淆了(注意:混淆不是加密,混淆是指替换变量名变为人不能直接理解的,并且调整代码顺序,最终的结果是PC看得懂,人看不懂。顺便一提,复杂的混淆是以消耗运行效率为代价的,而且被解析是必然的,无非是花多少心思,所以也不是越复杂的混淆越好,要兼顾程序性能)
我们难道要止步于此了吗,不,还不能放弃,使用Shift + Alt + F
格式化代码,浏览整个代码,寻找线索
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()
};
嗯,似乎这里是在加载数据文件,但这代码乱七八糟,如何下手呢
DataManager.loadDataFile
这个清晰的函数名似乎是我们的突破口,游戏本身必然是有读取数据的函数的,这个DecrypterPlayer.js
文件肯定是重写了读文件函数,在读取前进行解密,我们在文件中寻找原始的数据读取函数
在js\rpg_managers.js
中可以找到如下代码段
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();
};
虽然没有学过JavaScript
但是我们可以大致猜测,这里是读取文件的函数最终读取的内容是window[name]
,而这个变量的值来自JSON.parse(xhr.responseText)
对比看看上面的加密版本
JSON.parse(Decrypter.decryptText(h.response))
很明显了,这个Decrypter.decryptText(h.response)
就是解密函数了
在DecrypterPlayer.js
中搜索关键字Decrypter.decryptText
得到如下结果
Decrypter.decryptText = function (a) {
return this.decrypt(a, 1, "t")
};
那也就是说应该有一个Decrypter.decrypt(pram1, pram2, pram3)
的函数来进行解密
继续搜索,但遗憾的是我们这次的搜索没有任何结果,至此我们没有线索了,怎么办?
注意到DecrypterPlayer.js
这个文件有注释信息,也许我们能得到什么线索
尝试谷歌(百度也没问题)搜索Decrypter 仿mv加密解密
找到https://rpg.blue/thread-405389-1-1.html
这个链接
看作者描述
大概功能:
* 加入本插件,并设置为on
* 进入游戏,f8,使用 Decrypter.startEncrypt() 生成加密文件夹,
* 使用 Decrypter.saveMY("test","miyao") //参数可更改
* 即可生成 以"test"加密的miyao.js
嗯,运气不错,这个游戏大概率是使用这个加密的,作者提供了附件,我们下载下来看看
压缩包里有一个Decrypter.js
,哇,难道是加密源码,赶紧打开来看看
/**
* 读取数据文件
* @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();
};
嗯,未经混淆的DataManager.loadDataFile
可以看到这与我们之前的分析一致,确实是调用了Decrypter.decryptText()
这个函数来解密游戏数据的
但在这个文件中我们依然搜索不到Decrypter.decryptText()
这个函数,线索再次中断,留意到有很长的注释,继续看注释,也许有意外之喜
* 使用:
* 加入本插件,并设置为on
* 进入游戏,f8,使用 Decrypter.startEncrypt() 生成加密文件夹,
* 使用 Decrypter.saveMY("test","miyao") //参数可更改
* 即可生成 以"test"加密的miyao.js
*
*
* 发布时,
* 将本插件删除,
* 将DecrypterPlayer插件(可以改名)加入并设置好
* 将上面生成的miyao插件加入
* 将本插件从游戏文件中删除,将已经加密的文件从游戏文件中删除
* 进入游戏时将提示输入密钥,如上例则输入 test
* 即可进入游戏,
果不其然,我们又有了下一条线索,加密后会生成一个miyao,那么这个解密函数相比是存在这个miyao里了,继续去js\plugins
这个目录下面寻找
经过不懈努力,我们发现一个文件YEP_KeyCore.js
,嗯,KeyCore很是可疑,打开看看
豁,一样是经过了压缩和混淆的,那看来就是这个文件了,不然也没必要混淆
继续,先格式化,然后搜索Decrypter.decrypt
得到如下结果
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);
很好,一切都如我们所料,果然找到了关键的地方,可是,居然是混淆过的,这可让人如何是好...
其实我们大可转换思路,我们的目的是解密文件,不是搞清楚他的加解密算法,所以,我们大可直接调用这里的解密函数,对每一个加密文件进行解密
给出部分代码,大致思路:遍历加密文件夹,每个文件根据对应的类型调用对应的解密函数
写的很丑,凑活看吧,冗余的地方很多,可以精简
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文件
下篇我们讲一讲如果没有通过注释去搜索,没有找到原版加密文件,我们如何继续逆向,是否就束手无策了呢?