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

了解RPG Maker MV的文件建构

上一篇文章,我们已经成功在PC上运行了游戏,那我们如何对游戏进行逆向呢?

首先要了解正常的RPG Maker MV制作的游戏应该具有哪些文件,以及他的结构

那如何了解他的结构呢,很简单,我们只需要用RPG Maker MV创建一个默认工程,来看看一个游戏的最简结构是怎么样的

创建新项目

记一次完整的RPG Maker MV游戏逆向过程(思路一)_第1张图片

项目创建完成

记一次完整的RPG Maker MV游戏逆向过程(思路一)_第2张图片

查看项目目录结构

记一次完整的RPG Maker MV游戏逆向过程(思路一)_第3张图片

是不是和之前解包出来的很像呢?

通过目录名字可以知道

目录 用途
audio 音频资源
data 数据资源
fonts 字体资源
icon 图标资源
img 图片资源
js 脚本资源
movies 动画资源

我们进入data目录看看数据资源长什么样

记一次完整的RPG Maker MV游戏逆向过程(思路一)_第4张图片

都是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里来看一看是怎么样的形式

记一次完整的RPG Maker MV游戏逆向过程(思路一)_第5张图片

选择数据库

记一次完整的RPG Maker MV游戏逆向过程(思路一)_第6张图片

与我们刚刚看到的json文件完全吻合

之前解包出来的文件具有相同的目录结构,那我们是不是可以直接将刚才解包的数据拷贝到当前目录下,然后用RPG Maker MV来打开,这样整个游戏我们不是可以为所欲为了吗

心动不如行动,将解压出来的文件全部拷贝到我们新建的项目目录下,并选择替换已存在的文件

使用RPG Maker MV重新打开项目(资源重加载)

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

记一次完整的RPG Maker MV游戏逆向过程(思路一)_第7张图片

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

记一次完整的RPG Maker MV游戏逆向过程(思路一)_第8张图片

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

记一次完整的RPG Maker MV游戏逆向过程(思路一)_第9张图片

记一次完整的RPG Maker MV游戏逆向过程(思路一)_第10张图片

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

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

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

思路一

之前有提过所有的逻辑都是由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加载项目

记一次完整的RPG Maker MV游戏逆向过程(思路一)_第11张图片

打开数据库,能看到所有游戏数据,至此,整个逆向完成

想必你听说过顶尖黑客必修社会工程学,通过阅读本篇文章,想必你也对这句话有了自己的理解,有时候技术只是细枝末节,而黑客往往能通过蛛丝马迹,摸索着一点点的线索,获得想要的信息。加密插件的作者想必也不会想到因为一句注释,顺藤摸瓜,还原了整个加解密过程,如果你对加密过程感兴趣,可以去研究我们下载到的js文件

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

你可能感兴趣的:(逆向)