记一次完整的RPG Maker MV游戏逆向过程(思路一)
本帖最后由 简单メ传说 于 2021-2-27 21:03 编辑优秀评论:
回复;
Hi~ o(* ̄▽ ̄*)ブ 这位小伙伴
机智的你已经知道我思路二要讲什么了
但是这种方式最终还是要依赖于node.js从新写一个脚本出来,毕竟js本身是不支持对系统文件读写的,要依赖于node.js的读写库。
当然我本人不熟悉js,我只查到使用node.js可以读写文件,所以可能有其他思路,也可以和大家分享一下。
这是第二篇文章
第一篇传送门:https://www.52pojie.cn/thread-1169825-1-1.html
第三篇传送门:https://www.52pojie.cn/thread-1170415-1-1.html 强烈推荐,有很多干货
第四篇传送门:https://www.52pojie.cn/thread-1379159-1-1.html
## 了解RPG Maker MV的文件建构
上一篇文章,我们已经成功在PC上运行了游戏,那我们如何对游戏进行逆向呢?
首先要了解正常的RPG Maker MV制作的游戏应该具有哪些文件,以及他的结构
那如何了解他的结构呢,很简单,我们只需要用RPG Maker MV创建一个默认工程,来看看一个游戏的最简结构是怎么样的
### 创建新项目
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200501160023427.png)
项目创建完成
![在这里插入图片描述](https://img-blog.csdnimg.cn/2020050116014471.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NpaWlpaWluZw==,size_16,color_FFFFFF,t_70)
### 查看项目目录结构
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200501160237340.png)
是不是和之前解包出来的很像呢?
通过目录名字可以知道
|目录|用途|
|--|--|
|audio|音频资源|
|data|数据资源|
|fonts|字体资源|
|icon|图标资源|
|img|图片资源|
|js|脚本资源|
|movies|动画资源|
我们进入data目录看看数据资源长什么样
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200501160803445.png)
都是json文件(一种资源交换的文件格式)而且命名都很规范,我们打开Weapons.json来看看都有什么武器
```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":,"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":,"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":,"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":,"price":500,"wtypeId":7}
]
```
嗯,四种基本的武器
从RPG Maker MV里来看一看是怎么样的形式
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200501161104644.png)
选择数据库
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200501161122467.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NpaWlpaWluZw==,size_16,color_FFFFFF,t_70)
与我们刚刚看到的json文件完全吻合
之前解包出来的文件具有相同的目录结构,那我们是不是可以直接将刚才解包的数据拷贝到当前目录下,然后用RPG Maker MV来打开,这样整个游戏我们不是可以为所欲为了吗
心动不如行动,将解压出来的文件全部拷贝到我们新建的项目目录下,并选择替换已存在的文件
使用RPG Maker MV重新打开项目(资源重加载)
一打开心就凉了,居然还是默认初始工程的资源文件
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200501161817562.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NpaWlpaWluZw==,size_16,color_FFFFFF,t_70)
重新来观察一下游戏的目录结构
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200501161858266.png)
看到有一个`encrypt(加密)`的文件夹,很是可疑,进入目录观察,果然,数据都被加密成为了`.rmd`后缀的文件,直接编辑器打开发现文件时乱码
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200501162150596.png)
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200501162207636.png)
我们是否就这样束手无策了呢?
我们不妨来思考一下,既然游戏能在本地运行,那数据必然是在启动游戏后解密加载的,一个单机游戏的解密过程必然是在本地进行的
那我们如何寻找解密逻辑呢?
### 思路一
之前有提过所有的逻辑都是由JavaScript编写的,回忆一下有一个叫`js`的目录就是专门存放游戏逻辑的,那么去看一看目录里的文件,是否有一些线索
我们在`js\plugins`这个目录下面发现有两个文件非常可疑
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200501162740565.png)
`DecrypterPlayer.js`这名字就让人很是怀疑,打开看看
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200501163117699.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NpaWlpaWluZw==,size_16,color_FFFFFF,t_70)
可以说是开幕雷击了,压缩成一行的代码,外加毫无意义的函数名,摆明了就是告诉你代码经过混淆了(注意:混淆不是加密,混淆是指替换变量名变为人不能直接理解的,并且调整代码顺序,最终的结果是PC看得懂,人看不懂。顺便一提,复杂的混淆是以消耗运行效率为代价的,而且被解析是必然的,无非是花多少心思,所以也不是越复杂的混淆越好,要兼顾程序性能)
我们难道要止步于此了吗,不,还不能放弃,使用`Shift + Alt + F`格式化代码,浏览整个代码,寻找线索
```javascript
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 = JSON.parse(Decrypter.decryptText(h.response)), DataManager.onLoad(window))
}) : (h.open("GET", k), h.overrideMimeType("application/json"), h.onload = function () {
400 > h.status && (window = JSON.parse(h.responseText), DataManager.onLoad(window))
});
h.onerror = function () {
DataManager._errorUrl = DataManager._errorUrl || k
};
window = null;
h.send()
};
```
嗯,似乎这里是在加载数据文件,但这代码乱七八糟,如何下手呢
`DataManager.loadDataFile`这个清晰的函数名似乎是我们的突破口,游戏本身必然是有读取数据的函数的,这个`DecrypterPlayer.js`文件肯定是重写了读文件函数,在读取前进行解密,我们在文件中寻找原始的数据读取函数
在`js\rpg_managers.js`中可以找到如下代码段
```javascript
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 = JSON.parse(xhr.responseText);
DataManager.onLoad(window);
}
};
xhr.onerror = this._mapLoader || function() {
DataManager._errorUrl = DataManager._errorUrl || url;
};
window = null;
xhr.send();
};
```
虽然没有学过`JavaScript`但是我们可以大致猜测,这里是读取文件的函数最终读取的内容是`window`,而这个变量的值来自`JSON.parse(xhr.responseText)`
对比看看上面的加密版本
`JSON.parse(Decrypter.decryptText(h.response))`
很明显了,这个`Decrypter.decryptText(h.response)`就是解密函数了
在`DecrypterPlayer.js`中搜索关键字`Decrypter.decryptText`
得到如下结果
```javascript
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`,哇,难道是加密源码,赶紧打开来看看
```javascript
/**
* 读取数据文件
* @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 = JSON.parse(Decrypter.decryptText(xhr.response));
DataManager.onLoad(window);
}
};
} else {
xhr.open('GET', url);
xhr.overrideMimeType('application/json');
xhr.onload = function() {
if (xhr.status < 400) {
window = JSON.parse(xhr.responseText);
DataManager.onLoad(window);
}
};
}
xhr.onerror = function() {
DataManager._errorUrl = DataManager._errorUrl || url;
};
window = 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`
得到如下结果
```javascript
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);
```
很好,一切都如我们所料,果然找到了关键的地方,可是,居然是混淆过的,这可让人如何是好...
其实我们大可转换思路,我们的目的是解密文件,不是搞清楚他的加解密算法,所以,我们大可直接调用这里的解密函数,对每一个加密文件进行解密
给出部分代码,大致思路:遍历加密文件夹,每个文件根据对应的类型调用对应的解密函数
写的很丑,凑活看吧,冗余的地方很多,可以精简
```javascript
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加载项目
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200501175018719.png)
打开数据库,能看到所有游戏数据,至此,整个逆向完成
想必你听说过顶尖黑客必修社会工程学,通过阅读本篇文章,想必你也对这句话有了自己的理解,有时候技术只是细枝末节,而黑客往往能通过蛛丝马迹,摸索着一点点的线索,获得想要的信息。加密插件的作者想必也不会想到因为一句注释,顺藤摸瓜,还原了整个加解密过程,如果你对加密过程感兴趣,可以去研究我们下载到的js文件
下篇我们讲一讲如果没有通过注释去搜索,没有找到原版加密文件,我们如何继续逆向,是否就束手无策了呢?
下一篇传送门:https://www.52pojie.cn/thread-1170415-1-1.html 好厉害啊~~~~~~~~~~~~ 分析很准确 历史很久的东东了 这个就是得顶一个。 顺便学学下 之前分析7k7k的一个h5游戏,就一堆混淆的代码。。。。分析了两三天没搞出个头绪,弃之
人生苦短,何必荒废时间。。。 看不懂js 强啊。你是做开发的吗? 本帖最后由 角招 于 2020-5-2 09:47 编辑
很久以前玩过XP,有好多开源的Mod,后来出了VX,游戏语言从ruby改成了JavaScript,就没玩了