简单メ传说 发表于 2020-5-1 19:08

记一次完整的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

zhangbaocs 发表于 2020-5-1 20:01

好厉害啊~~~~~~~~~~~~

呱呱生 发表于 2020-5-1 22:48

分析很准确

ashi876 发表于 2020-5-1 19:57

历史很久的东东了

crb8331 发表于 2020-5-1 21:03

这个就是得顶一个。

duan898 发表于 2020-5-1 22:00

顺便学学下

涛之雨 发表于 2020-5-2 00:24

之前分析7k7k的一个h5游戏,就一堆混淆的代码。。。。分析了两三天没搞出个头绪,弃之
人生苦短,何必荒废时间。。。

心病 发表于 2020-5-2 09:03

看不懂js

庄寰 发表于 2020-5-2 09:04

强啊。你是做开发的吗?

角招 发表于 2020-5-2 09:42

本帖最后由 角招 于 2020-5-2 09:47 编辑

很久以前玩过XP,有好多开源的Mod,后来出了VX,游戏语言从ruby改成了JavaScript,就没玩了
页: [1] 2 3
查看完整版本: 记一次完整的RPG Maker MV游戏逆向过程(思路一)