记一次完整的RPG Maker MV游戏逆向过程(思路一)


了解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重新打开项目(资源重加载)

一打开心就凉了,居然还是默认初始工程的资源文件

在这里插入图片描述

重新来观察一下游戏的目录结构

在这里插入图片描述

看到有一个encrypt(加密)的文件夹,很是可疑,进入目录观察,果然,数据都被加密成为了.rmd后缀的文件,直接编辑器打开发现文件时乱码

在这里插入图片描述

在这里插入图片描述

我们是否就这样束手无策了呢?

我们不妨来思考一下,既然游戏能在本地运行,那数据必然是在启动游戏后解密加载的,一个单机游戏的解密过程必然是在本地进行的

那我们如何寻找解密逻辑呢?

思路一

之前有提过所有的逻辑都是由JavaScript编写的,回忆一下有一个叫js的目录就是专门存放游戏逻辑的,那么去看一看目录里的文件,是否有一些线索

我们在js\plugins这个目录下面发现有两个文件非常可疑

在这里插入图片描述

DecrypterPlayer.js这名字就让人很是怀疑,打开看看

在这里插入图片描述

可以说是开幕雷击了,压缩成一行的代码,外加毫无意义的函数名,摆明了就是告诉你代码经过混淆了(注意:混淆不是加密,混淆是指替换变量名变为人不能直接理解的,并且调整代码顺序,最终的结果是PC看得懂,人看不懂。顺便一提,复杂的混淆是以消耗运行效率为代价的,而且被解析是必然的,无非是花多少心思,所以也不是越复杂的混淆越好,要兼顾程序性能)

我们难道要止步于此了吗,不,还不能放弃,使用Shift + Alt + F格式化代码,浏览整个代码,寻找线索

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()
};

嗯,似乎这里是在加载数据文件,但这代码乱七八糟,如何下手呢

DataManager.loadDataFile这个清晰的函数名似乎是我们的突破口,游戏本身必然是有读取数据的函数的,这个DecrypterPlayer.js文件肯定是重写了读文件函数,在读取先进行解密,我们在文件中寻找原始的数据读取函数

js\rpg_managers.js中可以找到如下代码段

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();
};

虽然没有学过JavaScript但是我们可以大致猜测,这里是读取文件的函数最终读取的内容是window[name],而这个变量的值来自JSON.parse(xhr.responseText)

对比看看上面的加密版本

JSON.parse(Decrypter.decryptText(h.response))

很明显了,这个Decrypter.decryptText(h.response)就是解密函数了

DecrypterPlayer.js中搜索关键字Decrypter.decryptText

得到如下结果

1
2
3
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这个链接

看作者描述

1
2
3
4
5
大概功能:
* 加入本插件,并设置为on
* 进入游戏,f8,使用 Decrypter.startEncrypt() 生成加密文件夹,
* 使用 Decrypter.saveMY("test","miyao")  //参数可更改
* 即可生成 以"test"加密的miyao.js

嗯,运气不错,这个游戏大概率是使用这个加密的,作者提供了附件,我们下载下来看看

压缩包里有一个Decrypter.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();
};

嗯,未经混淆的DataManager.loadDataFile可以看到这与我们之前的分析一致,确实是调用了Decrypter.decryptText()这个函数来解密游戏数据的

但在这个文件中我们依然搜索不到Decrypter.decryptText()这个函数,线索再次中断,留意到有很长的注释,继续看注释,也许有意外之喜

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里了,继续去js\plugins这个目录下面寻找

经过不懈努力,我们发现一个文件YEP_KeyCore.js,嗯,KeyCore很是可疑,打开看看

豁,一样是经过了压缩和混淆的,那看来就是这个文件了,不然也没必要混淆

继续,先格式化,然后搜索Decrypter.decrypt

得到如下结果

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文件

下篇我们讲一讲如果没有通过注释去搜索,没有找到原版加密文件,我们如何继续逆向,是否就束手无策了呢?